Creating many-to-many relationships with an intermediary model

In previous chapters, you created many-to-many relationships by adding ManyToManyField to one of the related models and letting Django create the database table for the relationship. This is suitable for most of the cases, but sometimes you may need to create an intermediate model for the relation. Creating an intermediary model is necessary when you want to store additional information for the relationship, for example, the date when the relation was created, or a field that describes the nature of the relationship.

We will create an intermediary model to build relationships between users. There are two reasons why we want to use an intermediate model:

  • We are using the User model provided by Django, and we want to avoid altering it
  • We want to store the time when the relation is created

Edit the models.py file of your account application and add the following code to it:

class Contact(models.Model):
user_from = models.ForeignKey('auth.User',
related_name='rel_from_set',
on_delete=models.CASCADE)
user_to = models.ForeignKey('auth.User',
related_name='rel_to_set',
on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True,
db_index=True)

class Meta:
ordering = ('-created',)

def __str__(self):
return '{} follows {}'.format(self.user_from,
self.user_to)

The preceding code shows the Contact model we will use for user relationships. It contains the following fields:

  • user_from: ForeignKey for the user that creates the relationship
  • user_to: ForeignKey for the user being followed
  • created: A DateTimeField field with auto_now_add=True to store the time when the relationship was created

A database index is automatically created on the ForeignKey fields. We use db_index=True to create a database index for the created field. This will improve query performance when ordering QuerySets by this field.

Using the ORM, we could create a relationship for a user—user1following another user, user2, like this:

user1 = User.objects.get(id=1)
user2 = User.objects.get(id=2)
Contact.objects.create(user_from=user1, user_to=user2)

The related managers rel_from_set and rel_to_set will return a QuerySet for the Contact model. In order to access the end side of the relationship from the User model, it would be desirable that User contained ManyToManyField, as follows:

following = models.ManyToManyField('self',
through=Contact,
related_name='followers',
symmetrical=False)

In the preceding example, we tell Django to use our custom intermediary model for the relationship by adding through=Contact to the ManyToManyField. This is a many-to-many relationship from the User model to itself: we refer to 'self' in the ManyToManyField field to create a relationship to the same model.

When you need additional fields in a many-to-many relationship, create a custom model with ForeignKey for each side of the relationship. Add ManyToManyField in one of the related models and indicate to Django that your intermediary model should be used by including it in the through parameter.

If the User model was part of our application, we could add the previous field to the model. However, we cannot alter the User class directly because it belongs to the django.contrib.auth application. We will take a slightly different approach by adding this field dynamically to the User model. Edit the models.py file of the account application and add the following lines:

from django.contrib.auth.models import User

# Add following field to User dynamically
User.add_to_class('following',
models.ManyToManyField('self',
through=Contact,
related_name='followers',
symmetrical=False))

In the preceding code, we use the add_to_class() method of Django models to monkey patch the User model. Be aware that using add_to_class() is not the recommended way of adding fields to models. However, we take advantage of using it in this case because of the following reasons:

  • We simplify the way we retrieve related objects using the Django ORM with user.followers.all() and user.following.all(). We use the intermediary Contact model and avoid complex queries that would involve additional database joins, as it would have been, had we defined the relationship in our custom Profile model.
  • The table for this many-to-many relationship will be created using the Contact model. Thus, the ManyToManyField added dynamically will not imply any database changes for the Django User model.
  • We avoid creating a custom user model, keeping all the advantages of Django's built-in User.

Keep in mind that, in most cases, it is preferable to add fields to the Profile model we created before, instead of monkey-patching the User model. Django also allows you to use custom user models. If you want to use your custom user model, take a look at the documentation at https://docs.djangoproject.com/en/2.0/topics/auth/customizing/#specifying-a-custom-user-model.

You can note that the relationship includes symmetrical=False. When you define a ManyToManyField to the model itself, Django forces the relationship to be symmetrical. In this case, we are setting symmetrical=False to define a non-symmetric relation. This is, if I follow you, it doesn't mean that you automatically follow me.

When you use an intermediate model for many-to-many relationships, some of the related manager's methods are disabled, such as add(), create(), or remove(). You need to create or delete instances of the intermediate model instead.

Run the following command to generate the initial migrations for the account application:

python manage.py makemigrations account

You will obtain the following output:

Migrations for 'account':
account/migrations/0002_contact.py
- Create model Contact

Now, run the following command to sync the application with the database:

python manage.py migrate account

You should see an output that includes the following line:

Applying account.0002_contact... OK

The Contact model is now synced to the database, and we are able to create relationships between users. However, our site doesn't offer a way to browse users or see a particular user profile yet. Let's build list and detail views for the User model.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset