Como criar um objeto para um modelo do Django com um campo muitos para muitos?

138

Meu modelo:

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

Quero salvar os dois user1e user2nesse modelo:

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

Sei que isso está errado, mas tenho certeza que você conseguiu o que eu quero fazer. Como você faria ?

Sai Krishna
fonte

Respostas:

241

Você não pode criar relações m2m a partir de objetos não salvos. Se você possui pks, tente o seguinte:

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

Atualização: Depois de ler a resposta do saverio , decidi investigar o problema um pouco mais profundamente. Aqui estão minhas descobertas.

Esta foi a minha sugestão original. Funciona, mas não é o ideal. (Nota: estou usando Bars e a em Foovez de Users e a Sample, mas você entendeu).

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

Ele gera um total impressionante de 7 consultas:

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)

Tenho certeza que podemos fazer melhor. Você pode passar vários objetos para o add()método:

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

Como podemos ver, passar vários objetos salva um 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)

Eu não sabia que você também pode atribuir uma lista de objetos:

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

Infelizmente, isso cria um adicional 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)

Vamos tentar atribuir uma lista de pks, como sugerido pela saverio:

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

Como não buscamos os dois Bars, salvamos duas SELECTinstruções, resultando em um total de 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)

E o vencedor é:

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

Passar pks para add()nos dá um total de 4 consultas:

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)
Daniel Hepper
fonte
25
Eu gostaria de adicionar, você pode passar uma lista com * assim: foo.bars.add (* list) e ela explodirá a lista em argumentos: D
Guillermo Siliceo Trueba
1
Este deve ser o Django Docs sobre o ManyToMany! muito mais claro, docs.djangoproject.com/en/1.10/topics/db/examples/many_to_many ou docs.djangoproject.com/en/1.10/ref/models/fields e também com as penalidades de desempenho para os diferentes métodos incluídos. Talvez você possa atualizá-lo para o Django 1.9? (o método set) #
1178 Gabn88
Quero salvar o modelo com identificação única com mais de um item e quantidade. Será possível ?? classe Cart (models.Model): item = models.ForeignKey (Item, verbose_name = "item") quantidade = models.PositiveIntegerField (padrão = 1)
Nitesh
Uau. Estou impressionado. : D
М.Б.
111

Para futuros visitantes, você pode criar um objeto e todos os seus objetos m2m em 2 consultas usando o novo bulk_create no django 1.4. Observe que isso só é utilizável se você não precisar de nenhum pré ou pós-processamento nos dados com métodos ou sinais save (). O que você insere é exatamente o que estará no banco de dados

Você pode fazer isso sem especificar um modelo "através" no campo. Para ser completo, o exemplo abaixo cria um modelo de Usuários em branco para imitar o que o pôster original estava perguntando.

from django.db import models

class Users(models.Model):
    pass

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

Agora, em um shell ou outro código, crie 2 usuários, crie um objeto de amostra e adicione os usuários em massa a esse objeto de amostra.

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)
])
David Marble
fonte
Estou tentando usar sua resposta aqui, mas estou ficando paralisada. Pensamentos?
Pureferret 10/02
certamente é informativo saber que se pode fazer isso sem uma classe intermediária (direta) explicitamente definida, no entanto, é recomendável a legibilidade do código usando uma classe intermediária. Veja aqui .
Colm.anseo
solução incrível!
Pymarco 11/08/19
19

Django 1.9
Um exemplo rápido:

sample_object = Sample()
sample_object.save()

list_of_users = DestinationRate.objects.all()
sample_object.users.set(list_of_users)
Slipstream
fonte
8

RelatedObjectManagers são "atributos" diferentes dos campos em um Modelo. A maneira mais simples de alcançar o que você está procurando é

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

É o mesmo que atribuir uma lista de usuários, sem as consultas adicionais e a construção do modelo.

Se o número de consultas é o que o incomoda (em vez da simplicidade), a solução ideal exige três consultas:

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)

Isso funcionará porque já sabemos que a lista de 'usuários' está vazia, para que possamos criar sem pensar.

reescrito
fonte
2

Você pode substituir o conjunto de objetos relacionados desta maneira (novo no Django 1.9):

new_list = [user1, user2, user3]
sample_object.related_set.set(new_list)
iraj jelodari
fonte
0

Se alguém quer fazer David Marbles, responda por um campo ManyToMany auto-referente. Os IDs do modelo through são chamados: "to_'model_name_id" e "from_'model_name'_id".

Se isso não funcionar, você pode verificar a conexão do django.

jopjk
fonte