[Django]-Where/how is Django ManyToManyField represented in the database?

1👍

The many-to-many field is represented in the database in exactly the same way as your original FavouriteSongs model – as a linking table with ForeignKeys to both Song and User. The only benefit of getting rid of FavouriteSongs is that you’re now using an automatically-defined through table, rather than a manual one.

I don’t understand your example query, since you don’t say what model you are actually calling it on, or what self.logged_in_user is. However, you can’t use extra like this: you are trying to put Django query syntax there, complete with double-underscore names to traverse relationships, but extra is passed directly to the SQL, and that doesn’t know anything about that syntax.

I would not attempt to do this in one query. Instead I would do two queries, one to get all the albums and one to get the user’s favourites. get_queryset would just return the full album list, and then you can use get_context_data to get an additional set of objects representing the IDs of the favourites:

favorites = self.logged_in_user.album_set.all().values_list('id', flat=True)
context['favorites'] = set(favorites)

The values_list just gets the IDs of the albums only, since that’s all we need, and we then put them into a set to make lookups quicker.

Now, in the template, you can just do:

{% for album in albums %}
    ...
    <td>{% if album.id in favorites %}Yes{% else %}No{% endif %}</td>
{% endfor %}

8👍

M2M relationships are brand-new tables created. They get an unique name, and have two foreign keys. A composite key is created so the direct and the related model combinations are unique.

When you do:

class Topping(models.Model):
    name = ...

class Pizza(models.Model):
    name = ...
    toppings = models.ManyToManyField(Topping, related_name="pizzas")
    #not including a related_name will generate a "pizza_set" related name.

A new table appears describing the relationship, with an internal name. This table has a pizza_id and a topping_id foreign key, and a composite unique key including both fields. You cannot, and should not, predict the name of such table.

On the other side, if you want to access the relationship and, possible, declare more fields, you can:

class Topping(models.Model):
    name = ...

class Pizza(models.Model):
    name = ...
    toppings = models.ManyToManyField(Topping, related_name="pizzas", through="PizzaAndTopping")
    #not including a related_name will generate a "pizza_set" related name.

class PizzaAndTopping(models.Model):
    more_data = models.TextField()
    pizza = models.ForeignKey(Pizza, null=False)
    topping = models.ForeignKey(Topping, null=False)

    class Meta:
        unique_together = (('pizza','topping'),)

Notice how I added a through parameter. Now you have control of the middle-table BUT you cannot add or delete models from the relationship. This means, with this approach you cannot:

Pizza.objects.get(pk=1).toppings.append(Topping.objects.get(pk=2))

Nor you can remove, nor you can do these operations in the Topping side of the life.

If you want to add or delete toppin-pizza relationships, you must do it directly in the PizzaAndTopping relationship.

If you want to know whether the current user has marked any song as their favorite, you should prefetch the relationship. In Django 1.7, you can prefetch a M2M related field using a custom filter: you can fetch all the albums, and only a query getting the current user, using a Prefetch object. See the official docs about prefetching here.

Antoher solution would involve:

  • fetching the current user’s favorite albums list: ulist = user.album_set.all()
  • fetching the current page of alboms: _list = Album.objects.all()[0:20]
  • fetch values of user albums: ulist = ulist.values_list('id', flat=True)

    [1, 2, 4, 5, 10, ...] #you'll get a list of ids
    
  • when iterating over each album in the page, you test currentAlbum.id in ulist and print a different message (either yes or no).

Leave a comment