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を使用して次のテストを実行しました。
私の質問は、null許容のForeignKeyにunique_togetherを適用するにはどうすればよいですか?
この問題について私が見つけた最新の回答は5年です...私はDjangoが進化していて、問題は同じではないかもしれないと思います。
[〜#〜] 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
を呼び出す必要があります。
ただし、競合状態の処理には注意してください。
@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)
]
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)