PAYATHON 2026

如何将一个 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 的场景。

备注:内容仅供参考。