如何将一个 ManyToManyField 赋值给另一个
结论
先保存 refund,确保它已经有主键,再通过 Many-to-Many 管理器的 set() 方法复制关联:
refund.save()
refund.invoices.set(payment_to_refund.invoices.all())
set() 会把 refund.invoices 的关联设置为与 payment_to_refund.invoices 相同。它只复制关联关系,不会复制或新建 Invoice 记录。
为什么不能直接赋值
invoices 不是普通字段,而是 Django 提供的 Many-to-Many 关系管理器。关联数据实际保存在中间表中,因此不能直接赋值:
refund.invoices = payment_to_refund.invoices
all() 返回的是 QuerySet,也就是查询结果,不能作为赋值目标。所以下面的写法同样无效:
refund.invoices.all() = payment_to_refund.invoices.all()
修改 Many-to-Many 关系时,需要使用 set()、add()、remove() 或 clear() 等管理器方法。
完整示例
from django.db import transaction
with transaction.atomic():
refund = PaymentOrRefund.objects.create(
amount=-payment_to_refund.amount
)
refund.invoices.set(
payment_to_refund.invoices.all()
)
如果 refund 已经创建并保存,直接执行以下代码即可:
refund.invoices.set(payment_to_refund.invoices.all())
set() 和 add() 的区别
set() 会替换 refund 当前的所有 invoice 关联:
refund.invoices.set(payment_to_refund.invoices.all())
如果需要保留 refund 原有的关联,只追加新的关联,可以使用 add():
refund.invoices.add(*payment_to_refund.invoices.all())
这里需要让退款关联的 invoices 与原 payment 相同,所以通常使用 set()。
注意事项
refund 必须先保存
Many-to-Many 中间表需要用 refund 的主键建立关联。实例尚未保存时没有主键,调用 set() 会报错:
refund = PaymentOrRefund(amount=-100)
refund.invoices.set(payment_to_refund.invoices.all()) # 无法执行
正确顺序如下:
refund.save()
refund.invoices.set(payment_to_refund.invoices.all())
使用表单的 commit=False 时
如果 refund 来自 ModelForm:
refund = form.save(commit=False)
refund.amount = -abs(refund.amount)
refund.save()
refund.invoices.set(payment_to_refund.invoices.all())
设置完成后,通常不要再调用 form.save_m2m() 写入客户端提交的 invoices,否则可能覆盖服务器端设置的关联。是否需要调用它,要看表单中是否还有其他 Many-to-Many 字段。
只复制关联,不复制 Invoice
下面的代码复制的是关联关系:
refund.invoices.set(payment_to_refund.invoices.all())
payment 和 refund 最终会指向同一批 Invoice 对象,不会创建新的 invoice 数据,适合退款沿用原付款 invoices 的场景。
备注:内容仅供参考。
