Search code examples
pythondjangodjango-modelsdjango-orm

How to create an object for a Django model with a many to many field?


My model:

class Sample(models.Model):
    users = models.ManyToManyField(User)

I want to save both user1 and user2 in that model:

user1 = User.objects.get(pk=1)
user2 = User.objects.get(pk=2)
sample_object = Sample(users=user1, users=user2)
sample_object.save()

I know that's wrong, but I'm sure you get what I want to do. How would you do it ?


Solution

  • You cannot create m2m relations from unsaved objects. If you have the pks, try this:

    sample_object = Sample()
    sample_object.save()
    sample_object.users.add(1,2)
    

    Update: After reading the saverio's answer, I decided to investigate the issue a bit more in depth. Here are my findings.

    This was my original suggestion. It works, but isn't optimal. (Note: I'm using Bars and a Foo instead of Users and a Sample, but you get the idea).

    bar1 = Bar.objects.get(pk=1)
    bar2 = Bar.objects.get(pk=2)
    foo = Foo()
    foo.save()
    foo.bars.add(bar1)
    foo.bars.add(bar2)
    

    It generates a whopping total of 7 queries:

    SELECT "app_bar"."id", "app_bar"."name" FROM "app_bar" WHERE "app_bar"."id" = 1
    SELECT "app_bar"."id", "app_bar"."name" FROM "app_bar" WHERE "app_bar"."id" = 2
    INSERT INTO "app_foo" ("name") VALUES ()
    SELECT "app_foo_bars"."bar_id" FROM "app_foo_bars" WHERE ("app_foo_bars"."foo_id" = 1  AND "app_foo_bars"."bar_id" IN (1))
    INSERT INTO "app_foo_bars" ("foo_id", "bar_id") VALUES (1, 1)
    SELECT "app_foo_bars"."bar_id" FROM "app_foo_bars" WHERE ("app_foo_bars"."foo_id" = 1  AND "app_foo_bars"."bar_id" IN (2))
    INSERT INTO "app_foo_bars" ("foo_id", "bar_id") VALUES (1, 2)
    

    I'm sure we can do better. You can pass multiple objects to the add() method:

    bar1 = Bar.objects.get(pk=1)
    bar2 = Bar.objects.get(pk=2)
    foo = Foo()
    foo.save()
    foo.bars.add(bar1, bar2)
    

    As we can see, passing multiple objects saves one SELECT:

    SELECT "app_bar"."id", "app_bar"."name" FROM "app_bar" WHERE "app_bar"."id" = 1
    SELECT "app_bar"."id", "app_bar"."name" FROM "app_bar" WHERE "app_bar"."id" = 2
    INSERT INTO "app_foo" ("name") VALUES ()
    SELECT "app_foo_bars"."bar_id" FROM "app_foo_bars" WHERE ("app_foo_bars"."foo_id" = 1  AND "app_foo_bars"."bar_id" IN (1, 2))
    INSERT INTO "app_foo_bars" ("foo_id", "bar_id") VALUES (1, 1)
    INSERT INTO "app_foo_bars" ("foo_id", "bar_id") VALUES (1, 2)
    

    I wasn't aware that you can also assign a list of objects:

    bar1 = Bar.objects.get(pk=1)
    bar2 = Bar.objects.get(pk=2)
    foo = Foo()
    foo.save()
    foo.bars = [bar1, bar2]
    

    Unfortunately, that creates one additional SELECT:

    SELECT "app_bar"."id", "app_bar"."name" FROM "app_bar" WHERE "app_bar"."id" = 1
    SELECT "app_bar"."id", "app_bar"."name" FROM "app_bar" WHERE "app_bar"."id" = 2
    INSERT INTO "app_foo" ("name") VALUES ()
    SELECT "app_foo_bars"."id", "app_foo_bars"."foo_id", "app_foo_bars"."bar_id" FROM "app_foo_bars" WHERE "app_foo_bars"."foo_id" = 1
    SELECT "app_foo_bars"."bar_id" FROM "app_foo_bars" WHERE ("app_foo_bars"."foo_id" = 1  AND "app_foo_bars"."bar_id" IN (1, 2))
    INSERT INTO "app_foo_bars" ("foo_id", "bar_id") VALUES (1, 1)
    INSERT INTO "app_foo_bars" ("foo_id", "bar_id") VALUES (1, 2)
    

    Let's try to assign a list of pks, as saverio suggested:

    foo = Foo()
    foo.save()
    foo.bars = [1,2]
    

    As we don't fetch the two Bars, we save two SELECT statements, resulting in a total of 5:

    INSERT INTO "app_foo" ("name") VALUES ()
    SELECT "app_foo_bars"."id", "app_foo_bars"."foo_id", "app_foo_bars"."bar_id" FROM "app_foo_bars" WHERE "app_foo_bars"."foo_id" = 1
    SELECT "app_foo_bars"."bar_id" FROM "app_foo_bars" WHERE ("app_foo_bars"."foo_id" = 1  AND "app_foo_bars"."bar_id" IN (1, 2))
    INSERT INTO "app_foo_bars" ("foo_id", "bar_id") VALUES (1, 1)
    INSERT INTO "app_foo_bars" ("foo_id", "bar_id") VALUES (1, 2)
    

    And the winner is:

    foo = Foo()
    foo.save()
    foo.bars.add(1,2)
    

    Passing pks to add() gives us a total of 4 queries:

    INSERT INTO "app_foo" ("name") VALUES ()
    SELECT "app_foo_bars"."bar_id" FROM "app_foo_bars" WHERE ("app_foo_bars"."foo_id" = 1  AND "app_foo_bars"."bar_id" IN (1, 2))
    INSERT INTO "app_foo_bars" ("foo_id", "bar_id") VALUES (1, 1)
    INSERT INTO "app_foo_bars" ("foo_id", "bar_id") VALUES (1, 2)