web-dev-qa-db-ja.com

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

フィールドバーのあるモデル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オブジェクトを作成すると、「このバーのフーは既に存在します」というエラーが表示されます。

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

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

これはうまく機能し、バーがnullの状態で複数のレコードを挿入できるため、データベースで必要な処理を実行できます。Djangoモデルの問題です。アイデア?

編集

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

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

118

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

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

ここでの問題は、フォームCharFieldの正規化された「空白」値がNoneではなく空の文字列であることです。そのため、フィールドを空白のままにすると、NULLではなく空の文字列がDBに保存されます。 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
137
Karen Tracey

**edit 11/30/2015:python 3では、モジュールグローバルの___metaclass___変数は サポートされなくなりました 。さらに、_Django 1.10_現在、SubfieldBaseクラスは 非推奨 です。

docs から:

_Django.db.models.fields.subclassing.SubfieldBase_は廃止され、Django 1.10で削除されます。歴史的には、データベースからロードするときに型変換が必要なフィールドを処理するために使用されていましたが、.values()呼び出しまたは集約では使用されていませんでした。 from_db_value() に置き換えられました。 新しいアプローチでは、SubfieldBaseの場合のように、割り当て時にto_python() methodを呼び出しません。

したがって、from_db_value()documentation およびthis example で示唆されているように、このソリューションは次のように変更する必要があります。

_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
_

管理者で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列を作成します。

60
mightyhal

私はstackoverflowに慣れていないので、まだ答えに答えることはできませんが、哲学的な観点から、この質問に対する最も人気のある答えに同意できないことを指摘したいと思います。 (カレン・トレーシー作)

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

したがって、コードを本当にOOPに保つには、Fooモデルの内部メソッドを使用する必要があります。 save()メソッドまたはフィールドを変更するのは良いオプションですが、これを行うためにフォームを使用することは最も確実ではありません。

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

12
tBuLi

簡単な修正方法は次のとおりです。

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

    if not self.bar:
        self.bar = None

    super(Foo, self).save(*args, **kwargs)
12
e-satis

別の可能な解決策

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

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

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

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

2
James Bennett

最近、同じ要件がありました。さまざまなフィールドをサブクラス化する代わりに、次のようにモデル(下記の「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
captnswing

モデル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は一意性と矛盾しません

0
Joseph Bani