Search code examples
djangodatabaseormmany-to-many

Django - ManyToMany where I could select the same foreign model multiple times


Apologies if this has been answered, I'm uncertain how to word it so couldn't find an existing solution.

Let's say I have a basic ManyToMany relationship as shown here:

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

class Pizza(models.Model):
    toppings = models.ManyToMany(Topping)

That's simple and great, however in the project I'm working on, I want to be able to select the same Topping more than once. For example, ["pepperoni", "mushroom", "onion"] would be a perfectly good result, but I need to allow something like ["pepperoni", "pepperoni", "pepperoni"]

I've tried using a intermediate class, where Pizza has a ManyToMany to Topping, which is just a foreignkey to ToppingType, which is the Charfield -

class ToppingType(models.Model):
    name = models.Charfield()

class Topping(models.Model):
    type = models.ForeignKey(ToppingType)

class Pizza(models.Model):
    toppings = models.ManyToMany(Topping)

and that works, however it means if one day I create something with five "pepperoni" selections, I then permanently have five copies of pepperoni as a Topping in my database.

As I mentioned at the top, I'm sure there's a fairly clean solution, but I've had trouble figuring out how to phrase my searches.


Solution

  • You don't need a ManyToManyField to the Topping, you can let the Topping act as a junction table [wiki] and thus specify this as through=… model [Django-doc]:

    class ToppingType(models.Model):
        name = models.Charfield(max_length=255, unique=True)
    
    class Pizza(models.Model):
        toppings = models.ManyToMany(
            ToppingType,
            through='Topping'
        )
    
    class Topping(models.Model):
        type = models.ForeignKey(ToppingType, on_delete=models.CASCADE)
        pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE)

    Here you thus create a Topping object per pizza per topping. But you do not duplicate the ToppingTypes. In fact if you do not specify a through=…, then Django creates a (hidden) model that acts as the junction model.

    You thus can create for example as ToppingTypes pepperoni and onion:

    pepperoni = ToppingType.objects.create(name='pepperoni')
    onion = ToppingType.objects.create(name='onion')

    and then for a pizza add pepperoni, onion, and pepperoni:

    my_pizza = Pizza.objects.create()
    Topping.objects.create(pizza=my_pizza, type=pepperoni)
    Topping.objects.create(pizza=my_pizza, type=onion)
    Topping.objects.create(pizza=my_pizza, type=pepperoni)