如何为Django模型创建一个具有多对多字段的对象?

175

我的模型:

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

我想要同时保存user1user2到该模型中:

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

我知道那样是不对的,但我相信你明白我的意思。你会怎么做呢?

6个回答

286

您无法从未保存的对象创建多对多关系。如果您有主键pk,请尝试以下方法:

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

更新:在阅读saverio的回答后,我决定更深入地调查这个问题。以下是我的发现。

这是我的原始建议。它可以工作,但不是最佳选择。(注意:我使用的是BarFoo,而不是UserSample,但你可以理解这个想法)。

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

它生成了整整 7 个查询:

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)

我相信我们可以做得更好。你可以将多个对象传递给add()方法:

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

我们可以看到,传递多个对象可以节省一个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)

我不知道您也可以分配一个对象列表:

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

不幸的是,这会创建额外的一个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)

我们尝试按照saverio的建议,给一个pk列表进行赋值:

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

由于我们没有获取这两个Bar,我们节省了两个SELECT语句,总共只有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)

胜者是:

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

通过向add()传递pk,我们总共进行了4次查询:

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)

31
我想补充一下,你可以使用 * 传递一个列表,像这样:foo.bars.add(*list),它会将列表展开为参数:D - Guillermo Siliceo Trueba
1
这应该是关于Django ManyToMany的文档!比https://docs.djangoproject.com/en/1.10/topics/db/examples/many_to_many/或https://docs.djangoproject.com/en/1.10/ref/models/fields/更清晰,并且包括不同方法的性能惩罚。也许您可以更新它以适用于Django 1.9?(set方法) - gabn88
我想用单个ID保存具有多个项目和数量的模型。 这可能吗?类购物车(models.Model): 项目= models.ForeignKey(Item,verbose_name =“item”) 数量= models.PositiveIntegerField(default = 1) - Nitesh
1
哇。我感到惊讶。:D - М.Б.

134

对于未来的访问者,您可以使用 Django 1.4 中的新 bulk_create 方法,在 2 个查询 中创建对象及其所有多对多关系。请注意,只有在不需要使用 save() 方法或信号对数据进行任何预处理或后处理时才能使用此方法。插入到数据库中的内容会与您插入的完全一致。

您可以在字段上不指定 "through" 模型的情况下执行此操作。为了完整起见,下面的示例创建了一个空的 Users 模型,以模仿原始帖子中的问题。

from django.db import models

class Users(models.Model):
    pass

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

现在,在一个 shell 或其他代码中,创建 2 个用户,创建一个示例对象,并将这些用户大量添加到该示例对象中。

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)
])

1
我正在尝试使用您在这里的答案(http://stackoverflow.com/q/28437208/1075247),但我卡住了。有什么想法吗? - AncientSwordRage
1
绝妙的解决方案! - pymarco
嗯,这对我起作用了,但我必须将属性设置为模型实例而不是ID才行。 - Aaron

20

Django 1.9
一个快速的例子:

sample_object = Sample()
sample_object.save()

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

9

RelatedObjectManagers是模型中不同于字段的“属性”。实现您想要的最简单的方法是:

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

这与分配用户列表相同,不需要额外的查询和模型构建。

如果您担心查询次数(而不是简单性),那么最优解决方案需要三个查询:

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)

这将起作用,因为我们已经知道“用户”列表为空,所以我们可以不加思考地创建。

3
您可以通过以下方式更换相关对象的集合(Django 1.9中新增):
new_list = [user1, user2, user3]
sample_object.related_set.set(new_list)

-1
如果有人想在自引用的ManyToMany字段上执行David Marbles的回答。通过模型的ID称为: “to_'model_name_id”和 “from_'model_name'_id”。
如果这不起作用,您可以检查django连接。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接