web-dev-qa-db-ja.com

Django unique_together with nullable ForeignKey

Sqliteを使用する開発マシンでDjango 1.8.4を使用しており、次のモデルがあります。

class ModelA(Model):
    field_a = CharField(verbose_name='a', max_length=20)
    field_b = CharField(verbose_name='b', max_length=20)

    class Meta:
        unique_together = ('field_a', 'field_b',)


class ModelB(Model):
    field_c = CharField(verbose_name='c', max_length=20)
    field_d = ForeignKey(ModelA, verbose_name='d', null=True, blank=True)

    class Meta:
        unique_together = ('field_c', 'field_d',)

適切な移行を実行し、それらをDjango Adminに登録しました。したがって、Adminを使用して次のテストを実行しました。

  • ModelAレコードを作成することができ、Djangoは、予想どおり、重複レコードを作成することを禁止しています!
  • Field_bが空でない場合、同一のModelBレコードを作成できません
  • ただし、field_dを空として使用すると、同一のModelBレコードを作成できます。

私の質問は、null許容のForeignKeyにunique_togetherを適用するにはどうすればよいですか?

この問題について私が見つけた最新の回答は5年です...私はDjangoが進化していて、問題は同じではないかもしれないと思います。

21
MatheusJardimB

[〜#〜] update [〜#〜]:以前のバージョンの回答は機能していましたが、デザインが悪かったため、コメントやその他の回答の一部が考慮されています。

SQLでは、NULLはNULLと等しくありません。これは、field_d == None and field_c == "somestring"が等しくない2つのオブジェクトがある場合、両方を作成できることを意味します。

Model.cleanをオーバーライドして、チェックを追加できます。

class ModelB(Model):
    #...
    def validate_unique(self, exclude=None):
        if ModelB.objects.exclude(id=self.id).filter(field_c=self.field_c, \
                                 field_d__isnull=True).exists():
            raise ValidationError("Duplicate ModelB")
        super(ModelB, self).validate_unique(exclude)

フォームの外部で使用する場合は、full_cleanまたはvalidate_uniqueを呼び出す必要があります。

ただし、競合状態の処理には注意してください。

17
Ivan

@ivan、Djangoがこの状況を管理する簡単な方法があるとは思いません。必ずしもフォームから来るとは限らない、すべての作成および更新操作について考える必要があります。また、 、競合状態について考える必要があります。

また、このロジックをDBレベルで強制しないため、実際には2倍のレコードが存在する可能性があり、結果をクエリするときに確認する必要があります。

そして、あなたの解決策については、それはフォームには良いかもしれませんが、saveメソッドがValidationErrorを引き起こすことはないと思います。

可能であれば、このロジックをDBに委任することをお勧めします。この特定のケースでは、2つの部分インデックスを使用できます。 StackOverflowにも同様の質問があります- null列で一意の制約を作成します

したがって、Django移行を作成できます。これにより、DBに2つの部分インデックスが追加されます。

例:

# Assume that app name is just `example`

CREATE_TWO_PARTIAL_INDEX = """
    CREATE UNIQUE INDEX model_b_2col_uni_idx ON example_model_b (field_c, field_d)
    WHERE field_d IS NOT NULL;

    CREATE UNIQUE INDEX model_b_1col_uni_idx ON example_model_b (field_c)
    WHERE field_d IS NULL;
"""

DROP_TWO_PARTIAL_INDEX = """
    DROP INDEX model_b_2col_uni_idx;
    DROP INDEX model_b_1col_uni_idx;
"""


class Migration(migrations.Migration):

    dependencies = [
        ('example', 'PREVIOUS MIGRATION NAME'),
    ]

    operations = [
        migrations.RunSQL(CREATE_TWO_PARTIAL_INDEX, DROP_TWO_PARTIAL_INDEX)
    ]
9
vvkuznetsov

Django 1.2+の場合、これはより明確な方法だと思います

フォームでは、500エラーなしでnon_field_errorとして発生します。他の場合、DRFのように、500エラーになるため、このケースマニュアルを確認する必要があります。ただし、常にunique_togetherをチェックします。

class BaseModelExt(models.Model):
is_cleaned = False

def clean(self):
    for field_Tuple in self._meta.unique_together[:]:
        unique_filter = {}
        unique_fields = []
        null_found = False
        for field_name in field_Tuple:
            field_value = getattr(self, field_name)
            if getattr(self, field_name) is None:
                unique_filter['%s__isnull' % field_name] = True
                null_found = True
            else:
                unique_filter['%s' % field_name] = field_value
                unique_fields.append(field_name)
        if null_found:
            unique_queryset = self.__class__.objects.filter(**unique_filter)
            if self.pk:
                unique_queryset = unique_queryset.exclude(pk=self.pk)
            if unique_queryset.exists():
                msg = self.unique_error_message(self.__class__, Tuple(unique_fields))

                raise ValidationError(msg)

    self.is_cleaned = True

def save(self, *args, **kwargs):
    if not self.is_cleaned:
        self.clean()

    super().save(*args, **kwargs)
0
megajoe