Djangoでnullを許可する一意のフィールド


135

フィールドバーがあるモデルFooを持っています。バーフィールドは一意であるが、私はバーフィールドがある場合、複数のレコードを許可するという意味、それにNULLを許可する必要がありnullますが、そうでない場合nullの値は一意である必要があります。

これが私のモデルです:

class Foo(models.Model):
    name = models.CharField(max_length=40)
    bar = models.CharField(max_length=40, unique=True, blank=True, null=True, default=None)

そして、これはテーブルに対応するSQLです:

CREATE TABLE appl_foo
(
    id serial NOT NULL,
     "name" character varying(40) NOT NULL,
    bar character varying(40),
    CONSTRAINT appl_foo_pkey PRIMARY KEY (id),
    CONSTRAINT appl_foo_bar_key UNIQUE (bar)
)   

管理インターフェイスを使用して、barがnullである複数のfooオブジェクトを作成すると、「このBarを持つFooはすでに存在します」というエラーが発生します。

ただし、データベース(PostgreSQL)に挿入すると:

insert into appl_foo ("name", bar) values ('test1', null)
insert into appl_foo ("name", bar) values ('test2', null)

これは問題なく機能し、barがnullの場合に複数のレコードを挿入できるため、データベースでは、私がやりたいことを実行できます。これは、Djangoモデルの問題点です。何か案は?

編集

DBが問題でない限り、ソリューションの移植性は、Postgresに満足しています。callableに固有の設定を試みました。これは、barの特定の値に対してTrue / Falseを返す関数でしたが、エラーは発生しませんでしたが、まったく影響がないように継ぎ合わせられました。

これまでのところ、barプロパティから一意の指定子を削除し、アプリケーションでバーの一意性を処理していますが、よりエレガントなソリューションを探しています。何かお勧めですか?


まだコメントできないので、mighthalに少し追加します。Django1.4以降はdef get_db_prep_value(self, value, connection, prepared=False)、メソッド呼び出しとして必要になります。チェックgroups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJをより情報のため。次のメソッドも私にとっては機能します:def get_prep_value(self、value):if value == "":#if Django試行s to '' string、send the db None(NULL)return None else:return value #otherwise、just値を渡す
イェンス

このためにDjangoチケットをオープンしました。サポートを追加します。code.djangoproject.com/ticket/30210#ticket
Carl Brubaker

回答:


154

チケット#9039が修正されて以来、Djangoは一意性検査の目的でNULLはNULLと等しいとは見なしていません。以下を参照してください:

http://code.djangoproject.com/ticket/9039

ここでの問題は、フォームCharFieldの正規化された「空白」値がNoneではなく空の文字列であることです。したがって、フィールドを空白のままにすると、DBに格納されているNULLではなく空の文字列が取得されます。空の文字列は、Djangoとデータベースのルールの両方で、一意性チェックの空の文字列と同じです。

空の文字列をNoneに変換するclean_barメソッドを使用してFooの独自のカスタマイズされたモデルフォームを提供することにより、管理インターフェイスに空の文字列のNULLを強制的に格納できます。

class FooForm(forms.ModelForm):
    class Meta:
        model = Foo
    def clean_bar(self):
        return self.cleaned_data['bar'] or None

class FooAdmin(admin.ModelAdmin):
    form = FooForm

2
バーが空白の場合、pre_saveメソッドでそれをNoneに置き換えます。コードは私が推測するよりドライになります。
Ashish Gupta

6
この回答はフォームベースのデータ入力にのみ役立ちますが、実際にデータの整合性を保護することはありません。データは、APIやその他の手段を介して、シェルからインポートスクリプトを介して入力できます。データに触れる可能性のあるすべてのフォームに対してカスタムケースを作成するよりも、save()メソッドをオーバーライドする方がはるかに優れています。
ハッカー2016

Django 1.9以降では、インスタンスにfieldsor exclude属性が必要ModelFormです。この問題を回避Metaするには、adminで使用するModelFormから内部クラスを省略します。参照:docs.djangoproject.com/en/1.10/ref/contrib/admin/…– user85461
1

62

** 2015年11月30日編集:Python 3では、モジュールグローバル__metaclass__変数はサポートされなくなりました。以下のようAdditionaly、クラスがされた非推奨しますDjango 1.10SubfieldBase

ドキュメントから:

django.db.models.fields.subclassing.SubfieldBaseは非推奨であり、Django 1.10で削除されます。歴史的には、データベースからのロード時に型変換が必要なフィールドを処理するために使用されていましたが、.values()呼び出しや集計では使用されていませんでした。に置き換えられましたfrom_db_value()新しいアプローチでto_python()は、の場合のように、割り当て時にメソッド呼び出されないことに注意してくださいSubfieldBase

したがって、from_db_value() ドキュメントとこので示唆されているように、このソリューションは次のように変更する必要があります。

class CharNullField(models.CharField):

    """
    Subclass of the CharField that allows empty strings to be stored as NULL.
    """

    description = "CharField that stores NULL but returns ''."

    def from_db_value(self, value, expression, connection, contex):
        """
        Gets value right out of the db and changes it if its ``None``.
        """
        if value is None:
            return ''
        else:
            return value


    def to_python(self, value):
        """
        Gets value right out of the db or an instance, and changes it if its ``None``.
        """
        if isinstance(value, models.CharField):
            # If an instance, just return the instance.
            return value
        if value is None:
            # If db has NULL, convert it to ''.
            return ''

        # Otherwise, just return the value.
        return value

    def get_prep_value(self, value):
        """
        Catches value right before sending to db.
        """
        if value == '':
            # If Django tries to save an empty string, send the db None (NULL).
            return None
        else:
            # Otherwise, just pass the value.
            return value

adminでcleaned_dataをオーバーライドするよりも良い方法は、charfieldをサブクラス化することだと思います。この方法は、フィールドにアクセスするフォームに関係なく、「そのまま機能する」でしょう。''データベースに送信される直前をキャッチし、データベースから送信された直後にNULLをキャッチすることができます。それ以外のDjangoは知りません/気にしません。簡単で汚い例:

from django.db import models


class CharNullField(models.CharField):  # subclass the CharField
    description = "CharField that stores NULL but returns ''"
    __metaclass__ = models.SubfieldBase  # this ensures to_python will be called

    def to_python(self, value):
        # this is the value right out of the db, or an instance
        # if an instance, just return the instance
        if isinstance(value, models.CharField):
            return value 
        if value is None:  # if the db has a NULL (None in Python)
            return ''      # convert it into an empty string
        else:
            return value   # otherwise, just return the value

    def get_prep_value(self, value):  # catches value right before sending to db
        if value == '':   
            # if Django tries to save an empty string, send the db None (NULL)
            return None
        else:
            # otherwise, just pass the value
            return value  

私のプロジェクトでは、これをextras.py自分のサイトのルートにあるファイルにダンプしました。その後from mysite.extras import CharNullField、アプリのmodels.pyファイルだけに入れることができます。フィールドはCharFieldと同じように機能します。フィールドをblank=True, null=True宣言するときに設定することを忘れないでください。そうしないと、Djangoが検証エラー(フィールドが必要)をスローするか、NULLを受け入れないdb列を作成します。


3
get_prep_valueでは、値に複数のスペースがある場合は、値を取り除く必要があります。
ax003d 2015年

1
ここで更新された回答は、Django 1.10とEmailFieldを使用して2016年にうまく機能します。
k0nG 2016年

4
更新している場合CharFieldであることをCharNullField、次の3つのステップでそれを行う必要があります。まず、null=Trueフィールドに追加し、それを移行します。次に、データ移行を実行して、空白値を更新し、nullにします。最後に、フィールドをCharNullFieldに変換します。データ移行を行う前にフィールドを変換すると、データ移行は何もしません。
mlissner 2017

3
更新されたソリューションでfrom_db_value()は、追加のcontexパラメーターを使用しないでください。それはする必要がありますdef from_db_value(self, value, expression, connection):
フィルギフォード2018年

1
@PhilGyfordからのコメントは2.0以降に適用されます。
Shaheed Haque

16

私はStackoverflowを初めて使用するため、まだ回答に答えることはできませんが、哲学的な観点から、この質問に対する最も一般的な回答に同意することはできません。(カレン・トレーシー)

OPでは、値がある場合はバーフィールドが一意であり、それ以外の場合はnullである必要があります。次に、モデル自体がこれに該当することを確認する必要があります。これをチェックするのは外部コードに任せることはできません。これはバイパスされる可能性があるためです。(または、将来新しいビューを作成する場合は、チェックするのを忘れることがあります)

したがって、コードを真にOOPに保つには、Fooモデルの内部メソッドを使用する必要があります。save()メソッドまたはフィールドを変更することは適切なオプションですが、フォームを使用してこれを行うことは、間違いなくそうではありません。

個人的には、将来定義するモデルへの移植性のために、提案されたCharNullFieldを使用することを好みます。


13

迅速な修正は次のことです:

def save(self, *args, **kwargs):

    if not self.bar:
        self.bar = None

    super(Foo, self).save(*args, **kwargs)

2
を使用MyModel.objects.bulk_create()すると、このメソッドがバイパスされることに注意してください。
BenjaminGolder

このメソッドは、管理パネルから保存するときに呼び出されますか?私は試しましたが、しません。
Kishan Mehta

1
@Kishan django-adminパネルは残念ながらこれらのフックをスキップします
Vincent Buscarello 2018

@ e-satisロジックは正しいので実装しましたが、エラーはまだ問題です。私はnullと言われるのは重複しています。
Vincent Buscarello

6

別の可能な解決策

class Foo(models.Model):
    value = models.CharField(max_length=255, unique=True)

class Bar(models.Model):
    foo = models.OneToOneField(Foo, null=True)

不要な関係を作成しているため、これは良い解決策ではありません。
ブラクÖzdemir


1

最近同じ要件がありました。さまざまなフィールドをサブクラス化する代わりに、次のようにモデル(以下では「MyModel」という名前)のsave()メソッドをオーバーライドすることを選択しました。

def save(self):
        """overriding save method so that we can save Null to database, instead of empty string (project requirement)"""
        # get a list of all model fields (i.e. self._meta.fields)...
        emptystringfields = [ field for field in self._meta.fields \
                # ...that are of type CharField or Textfield...
                if ((type(field) == django.db.models.fields.CharField) or (type(field) == django.db.models.fields.TextField)) \
                # ...and that contain the empty string
                and (getattr(self, field.name) == "") ]
        # set each of these fields to None (which tells Django to save Null)
        for field in emptystringfields:
            setattr(self, field.name, None)
        # call the super.save() method
        super(MyModel, self).save()    

1

モデルMyModelがあり、my_fieldをNullまたは一意にする場合は、モデルのsaveメソッドをオーバーライドできます。

class MyModel(models.Model):
    my_field = models.TextField(unique=True, default=None, null=True, blank=True) 

    def save(self, **kwargs):
        self.my_field = self.my_field or None
        super().save(**kwargs)

この方法では、フィールドを空白にすることはできず、空白以外またはnullになるだけです。nullは一意性と矛盾しません


0

良くも悪くも、Django は一意性チェックの目的ではNULL同等と見なしNULLます。NULLテーブルで何回発生しても一意であると見なされる一意性チェックの独自の実装を記述すること以外に、実際に回避する方法はありません。

(また、一部のDBソリューションはと同じビューを取るため、NULLあるDBのアイデアに依存するコードは他のDB NULLに移植できない場合があることに注意してください)


6
これは正解ではありません。説明については、この回答を参照してください。
Carl G

2
これは正しくないことに同意しました。Django 1.4でIntegerField(blank = True、null = True、unique = True)をテストしたところ、null値を持つ複数の行が許可されました。
12

0

このフィールドをリストに含めないようにUniqueConstraint条件を追加できます。値がでない制約も必要な場合は、さらに追加できます。nullable_field=nullfieldsnullable_fieldnull

注:UniqueConstraintはdjango 2.2以降に追加されました

class Foo(models.Model):
    name = models.CharField(max_length=40)
    bar = models.CharField(max_length=40, unique=True, blank=True, null=True, default=None)
    
    class Meta:
        constraints = [
            # For bar == null only
            models.UniqueConstraint(fields=['name'], name='unique__name__when__bar__null',
                                    condition=Q(bar__isnull=True)),
            # For bar != null only
            models.UniqueConstraint(fields=['name', 'bar'], name='unique__name__when__bar__not_null')
        ]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.