158

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

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

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

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

編集

DBが問題にならない限り、ソリューションの移植性はPostgresに満足しています。呼び出し可能オブジェクトに固有の設定を試みました。これは、barの特定の値に対してTrue / Falseを返す関数でしたが、エラーは発生しませんでしたが、まったく効果がないように継ぎ目がありました。

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

4

10 に答える 10

65

** 2015 年 11 月 30 日編集: Python 3 では、module-global__metaclass__変数はサポートされなくなりました。さらに、クラスの時点で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

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

于 2009-12-20T03:40:23.593 に答える
17

私はstackoverflowを初めて使用するため、回答に返信することはまだ許可されていませんが、哲学的な観点から、この質問に対する最も人気のある回答には同意できないことを指摘したいと思います. (カレン・トレーシーによる)

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

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

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

于 2012-08-09T11:52:37.253 に答える
14

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

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

    if not self.bar:
        self.bar = None

    super(Foo, self).save(*args, **kwargs)
于 2010-06-26T16:24:25.553 に答える
13

このフィールドをリストに含めるかどうかUniqueConstraintの条件付きで追加できます。値が notの制約も必要な場合は、さらに制約を追加できます。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')
        ]
于 2020-07-07T11:25:21.670 に答える
6

別の可能な解決策

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

class Bar(models.Model):
    foo = models.OneToOneField(Foo, null=True)
于 2011-07-07T08:10:33.523 に答える
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()    
于 2010-06-22T11:50:20.800 に答える
0

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

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

于 2009-01-18T03:49:52.673 に答える