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

PythonDjangoDjango ModelsDjango Orm

Python Problem Overview


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 ?

Python Solutions


Solution 1 - Python

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)

Solution 2 - Python

For future visitors, you can create an object and all of its m2m objects in 2 queries using the new bulk_create in django 1.4. Note that this is only usable if you don't require any pre or post-processing on the data with save() methods or signals. What you insert is exactly what will be in the DB

You can do this without specifying a "through" model on the field. For completeness, the example below creates a blank Users model to mimic what the original poster was asking.

from django.db import models

class Users(models.Model):
    pass

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

Now, in a shell or other code, create 2 users, create a sample object, and bulk add the users to that sample object.

Users().save()
Users().save()

# Access the through model directly
ThroughModel = Sample.users.through

users = Users.objects.filter(pk__in=[1,2])

sample_object = Sample()
sample_object.save()

ThroughModel.objects.bulk_create([
    ThroughModel(users_id=users[0].pk, sample_id=sample_object.pk),
    ThroughModel(users_id=users[1].pk, sample_id=sample_object.pk)
])

Solution 3 - Python

Django 1.9
A quick example:

sample_object = Sample()
sample_object.save()

list_of_users = DestinationRate.objects.all()
sample_object.users.set(list_of_users)

Solution 4 - Python

RelatedObjectManagers are different "attributes" than fields in a Model. The simplest way to achieve what you are looking for is

sample_object = Sample.objects.create()
sample_object.users = [1, 2]

That's the same as assigning a User list, without the additional queries and the model building.

If the number of queries is what bothers you (instead of simplicity), then the optimal solution requires three queries:

sample_object = Sample.objects.create()
sample_id = sample_object.id
sample_object.users.through.objects.create(user_id=1, sample_id=sample_id)
sample_object.users.through.objects.create(user_id=2, sample_id=sample_id)

This will work because we already know that the 'users' list is empty, so we can create mindlessly.

Solution 5 - Python

You could replace the set of related objects in this way (new in Django 1.9):

new_list = [user1, user2, user3]
sample_object.related_set.set(new_list)

Solution 6 - Python

If someone is looking to do David Marbles answer on a self referring ManyToMany field. The ids of the through model are called: "to_'model_name_id" and "from_'model_name'_id".

If that doesn't work you can check the django connection.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSai KrishnaView Question on Stackoverflow
Solution 1 - PythonDaniel HepperView Answer on Stackoverflow
Solution 2 - PythonDavid MarbleView Answer on Stackoverflow
Solution 3 - PythonSlipstreamView Answer on Stackoverflow
Solution 4 - PythonrewrittenView Answer on Stackoverflow
Solution 5 - Pythoniraj jelodariView Answer on Stackoverflow
Solution 6 - PythonjopjkView Answer on Stackoverflow