首页 > 解决方案 > 使 ArrayField 的 base_field 对 Django 中的表是唯一的

问题描述

目标

以下应该提出一个ValidationError

>>> m1 = MyModel(names=['name1'])
>>> m2 = MyModel(names=['name1', 'name2'])
>>> m1.save()
>>> m2.save()
django.core.exceptions.ValidationError: ...

用简单的英语来说,如果模型ArrayField中的元素与数据库表中的元素匹配,则ValidationError应该引发

失败的解决方案

ArrayField 文档没有提到unique关键字,所以我尝试了几种方法(这些是最小的代码示例)。

添加unique=True到运行后base_field似乎什么都没有makemigrationsmigrate

# models.py
... 
class MyModel(models.Model)
    title = ArrayField(
        models.CharField(max_length=255, unique=True),
    )
...
# shell_plus from Django-extensions
>>> m1 = MyModel(names=['name1'])
>>> m2 = MyModel(names=['name1'])
>>> m1.save()
>>> m2.save()
# no errors raised

我尝试unique=True仅添加到ArrayField. 如果数组完全相同,Django 会引发错误。

# models.py
...
class MyModel(models.Model)
    title = ArrayField(
        models.CharField(max_length=255),
        unique=True,
    )
# shell_plus from Django-extensions
>>> m1 = MyModel(names=['name1'])
>>> m2 = MyModel(names=['name1'])
>>> m1.save()
>>> m2.save()
django.core.exceptions.ValidationError: ...
>>> m3 = MyModel(names=['name1', 'name2'])
>>> m3.save()
# no error raised

当我想到时,以上是有道理的。然后我尝试添加unique=Truebase_field和中,ArrayField但行为没有改变。我跑makemigrationsmigrate

class MyModel(models.Model)
    title = ArrayField(
        models.CharField(max_length=255, unique=True),
        unique=True,
    )
# shell_plus from Django-extensions
>>> m1 = MyModel(names=['name1'])
>>> m2 = MyModel(names=['name1'])
>>> m1.save()
>>> m2.save()
django.core.exceptions.ValidationError: ...
>>> m3 = MyModel(names=['name1', 'name2'])
>>> m3.save()
# no error raised

TL;博士

我可以使以下引发错误unique=True还是我需要编写自己的验证器?

>>> m1 = MyModel(names=['name1'])
>>> m2 = MyModel(names=['name1', 'name2'])
>>> m1.save()
>>> m2.save()
django.core.exceptions.ValidationError: ...

标签: pythondjango

解决方案


这是我最终做的一个最小的例子

# models.py
...
UNIQUE_ARRAY_FIELDS = ('name',)

class MyManager(models.Manager):
    def prevent_duplicates_in_array_fields(self, model, array_field):
        def duplicate_check(_lookup_params):
            fields = self.model._meta.get_fields()
            for unique_field in UNIQUE_ARRAY_FIELDS:
                unique_field_index = [getattr(field, 'name', '') for field in fields]
                try:
                    # if model doesn't have the unique field, then proceed to the next loop iteration
                    unique_field_index = unique_field_index.index(unique_field)
                except ValueError:
                    continue
            all_items_in_db = [item for sublist in self.values_list(fields[unique_field_index].name).exclude(**_lookup_params) for item in sublist]
            all_items_in_db = [item for sublist in all_items_in_db for item in sublist]
            if not set(array_field).isdisjoint(all_items_in_db):
                raise ValidationError('{} contains items already in the database'.format(array_field))
        if model.id:
            lookup_params = {'id': model.id}
        else:   
            lookup_params = {}
        duplicate_check(lookup_params)

...

class MyModel(models.Model):
    name = ArrayField(
        models.CharField(max_length=255),
        default=list
    )
    objects = MyManager()
    ...
    def save(self, *args, **kwargs):
        self.full_clean()
        MyModel.objects.prevent_duplicates_in_array_fields(self, self.name)
        super().save(*args, **kwargs)
    ...

推荐阅读