Altering a Django project to migrate from a simple ManyToMany relation to one with extra information
There are still quite a few features which could be useful on Clusterify. One I thought could help in structuring projects is the concept of Roles: a way to let users joining a project specify which role they want to take in it.
Now, most common tasks are made amazingly easy with Django. Changing models, while probably common, seems very tricky to automate right, though (and anyway would you trust the automation logic to make the right changes?). Yet there’s dmigrations which I need to try one day.
Back to the original problem: Roles. Formerly we had a ManyToManyField in the Project model linked to the User model for users joining a project. In the background, Django creates a table to hold the relationship named after the field (projects_project_joined_users).
The way to add extra information to a relationship, in the Django ORM, is with the “through” argument to the ManyToManyField, which lets you specify a model which will hold the relationship. In this case, we’d get:
class Membership(models.Model): user = models.ForeignKey(User) project = models.ForeignKey(Project) role = models.CharField(max_length=120) approved = models.BooleanField(default=False)
and in Project we now add the field:
members = models.ManyToManyField(User, through='Membership')
This would create a table named projects_membership. It still doesn’t hold any data, though. Now I see two ways of making this change in the DB:
- Renaming or copying the old table holding the relationship, and ALTERing it until it looks like that new one.
- Creating the new table (by looking at the output of sqlall) and filling it by copying over the data with a mapping of fields.
I chose the later, which seemed more straightforward (and anyway I have other manual copying operations to perform). There’s an easy way to copy over the data, the INSERT … SELECT syntax in SQL. The old table was named projects_project_joined_users, so the syntax becomes:
INSERT INTO projects_membership (user_id, project_id, role, approved) SELECT projects_project_joined_users.user_id, projects_project_joined_users.project_id, '', 1 FROM projects_project_joined_users;
Running this copied over the old data, and now the code may be updated. Note that I had first created the new table by checking the output of “sqlall” and running the relevant statements (CREATE TABLE, ALTERs for foreign key constraints, and index creation).
It’s now more complicated to handle the relationship this way, though, as one can’t use add() or remove() as before. Take a look at the doc for more info on this.