django mptt 中 treeforeignkey 为 null 时重复记录

Duplicate records when treeforeignkey is null in django mptt

我有这个型号:

class Genre(MPTTModel):
    id = models.CharField(max_length=100)
    name = models.CharField(max_length=100)
    parent = TreeForeignKey(
        'self', 
        null=True, 
        blank=True, 
        related_name='subgenre'
    )

    def __str__(self):
        return self.name

    class Meta:
        unique_together = (('id', 'parent'),)

我不想有重复的记录,所以我将 unique_together 与 id 和 TreeForeignKey 一起使用。

即使使用 unique_together,当我将父级设置为 null 时,我仍然能够添加重复项。我怎样才能避免这种情况?

这是一个 SQL 设计决定。

SQL 2011 draft,第 474 页显示:

If there are no two rows in T such that the value of each column in one row is non-null and is not distinct from the value of the corresponding column in the other row, then the result of the is True; otherwise, the result of the is False.

这意味着当涉及唯一约束时,两个 NULL 值被认为是不同的。这与第 41 页中的 NULL 数据类型定义相矛盾:

Two null values are not distinct.

A null value and a non-null value are distinct.

Two non-null values are distinct if the General Rules of Subclause 8.15, “”, return True.

第 8.15 款的一般规则说:

If both V1 and V2 are the null value, then the result is False.

总结:

说到数据类型,两个null的“区别”是False,意思是NULL == NULL。

但是 table 级别的唯一约束另有说明:NULL != NULL。 table 的字段中可以有许多 NULL,表示它们应该是唯一的。

跟踪此的 Django 票证是 #1751 unique_together does not work when any of the listed fields contains a FK. The workaround is to define your own .validate_unique model method as mentioned in the documentation

from django.core.exceptions import ValidationError
from django.db import transaction

def validate_unique(self, exclude=None):
    with transaction.atomic():
        if Genre.objects.select_for_update().filter(parent=self.parent, id=self.id).exists():
            params = {
                'model_name': _('Genre'),
                'field_labels': _('Parent and ID')
            }
            raise ValidationError(
                message=_(
                    '%(model_name)s with this %(field_labels)s already exists.'
                ), code='unique_together', params=params,
            )

select_for_update 创建一个锁以避免竞争条件。

此解决方案适用于表单提交,直接访问 Genre.objects.create() 方法时无效。在这种情况下,您需要分三步创建 Genre 实例:

genre = Genre(id='id1')
genre.validate_unique()
genre.save()