Django에서 널을 허용하는 고유 필드


135

필드 바가있는 Foo 모델이 있습니다. 막대 필드는 고유해야하지만 그 안에 널을 허용합니다. 즉, 막대 필드가 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)

이것은 잘 작동합니다 .bar가 null 인 상태에서 둘 이상의 레코드를 삽입 할 수 있으므로 데이터베이스는 내가 원하는 것을 수행 할 수있게 해줍니다 .Django 모델에는 문제가 있습니다. 어떤 아이디어?

편집하다

DB가 문제가되지 않는 한 솔루션의 이식성은 Postgres에 만족합니다. bar의 특정 값에 대해 True / False를 반환하는 함수 인 callable에 고유 한 설정을 시도했지만 오류가 발생하지 않았지만 전혀 영향을 미치지 않는 것처럼 이음새가 없었습니다.

지금까지 막대 속성 에서 고유 지정자를 제거하고 응용 프로그램에서 막대 고유성을 처리 했지만 여전히 더 우아한 솔루션을 찾고 있습니다. 어떤 추천?


나는 아직 여기에 의견을 말할 수 없으므로 mightyhal에 약간의 추가 사항이 있습니다. Django 1.4부터 def get_db_prep_value(self, value, connection, prepared=False)메소드 호출로 필요할 것 입니다. 자세한 내용은 groups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ 를 확인 하십시오. 다음 방법도 나에게 효과적입니다. 값을 전달
Jens

나는 이것을 위해 장고 티켓을 열었다. 지원을 추가하십시오. code.djangoproject.com/ticket/30210#ticket
Carl Brubaker

답변:


154

Django는 티켓 # 9039가 수정 된 이후 고유성 검사를 위해 NULL을 NULL과 동일하다고 간주하지 않았습니다.

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

여기서 문제는 CharField 양식의 정규화 된 "공백"값이 없음이 아니라 빈 문자열이라는 것입니다. 따라서 필드를 비워두면 NULL이 아닌 빈 문자열이 DB에 저장됩니다. 빈 문자열은 Django 및 데이터베이스 규칙 모두에서 고유성 검사를 위해 빈 문자열과 같습니다.

빈 문자열을 없음으로 바꾸는 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
bar가 비어 있으면 pre_save 메소드에서 None으로 바꾸십시오. 코드는 내가 생각하기에 더 건조 할 것입니다.
Ashish Gupta

6
이 답변은 양식 기반 데이터 입력에만 도움이되지만 실제로 데이터 무결성을 보호하는 것은 아닙니다. 셸에서 가져 오기 스크립트를 통해 API 또는 다른 방법으로 데이터를 입력 할 수 있습니다. 데이터를 건드릴 수있는 모든 양식에 대해 사용자 정의 케이스를 작성하는 것보다 save () 메소드를 대체하는 것이 훨씬 좋습니다.
shacker

Django 1.9+는 인스턴스에 fields또는 exclude속성이 필요 ModelForm합니다. Meta관리자에서 사용하기 위해 ModelForm에서 내부 클래스를 생략하여이 문제를 해결할 수 있습니다 . 참조 : docs.djangoproject.com/en/1.10/ref/contrib/admin/…
user85461

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

관리자에서 cleaned_data를 재정의하는 것보다 더 나은 방법은 charfield를 서브 클래 싱하는 것입니다. 어떤 방식으로 필드에 액세스하든 "그냥 작동합니다." 당신은 잡을 수 ''가 데이터베이스로 전송되기 직전에, 그리고 데이터베이스에서 나온 직후 NULL을 잡아, 그리고 장고의 나머지 / 치료를 알 수 없습니다. 빠르고 더러운 예 :

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

1
여기에 업데이트 된 답변은 Django 1.10과 EmailField를 사용하여 2016 년에 잘 작동합니다.
k0nG

4
를 a CharField로 업데이트하는 경우 CharNullField세 단계로 수행해야합니다. 먼저 null=True필드에 추가 하고 마이그레이션하십시오. 그런 다음 데이터 마이그레이션을 수행하여 빈 값을 업데이트하여 널이되도록하십시오. 마지막으로 필드를 CharNullField로 변환하십시오. 데이터 마이그레이션을 수행하기 전에 필드를 변환하면 데이터 마이그레이션이 수행되지 않습니다.
mlissner

3
업데이트 된 솔루션 from_db_value()에는 추가 contex매개 변수 가 없어야합니다 . 그것은해야한다def from_db_value(self, value, expression, connection):
필 Gyford

1
@PhilGyford의 의견은 2.0부터 적용됩니다.
Shaheed Haque

16

나는 stackoverflow를 처음 사용하기 때문에 아직 답변에 답변을 할 수는 없지만 철학적 관점 에서이 질문에 대한 가장 인기있는 답변에 동의 할 수는 없다는 것을 지적하고 싶습니다. (카렌 트레이시)

OP는 막대 필드에 값이 있으면 고유해야하며, 그렇지 않으면 널입니다. 그런 다음 모델 자체가 이것이 사실인지 확인해야합니다. 이를 확인하기 위해 외부 코드에 맡길 수는 없습니다. 이는 무시할 수 있기 때문입니다. (또는 나중에 새로운 견해를 쓰면 확인하지 않아도됩니다)

따라서 코드를 실제로 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

@ 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)

불필요한 관계를 만들고 있기 때문에 이것은 좋은 해결책이 아닙니다.
Burak Ö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은 고유성과 모순되지 않습니다


1

이 필드를 목록 에 포함하지 않는 UniqueConstraint조건으로 추가 할 수 있습니다 . 당신도 함께 제약이 필요하면nullable_field=nullfieldsnullable_fieldwich value가 not null인 추가 추가 할 수 있습니다.

참고 : django 2.2 이후 UniqueConstraint가 추가되었습니다.

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')
        ]

작동합니다! 하지만 양식 유효성 검사 오류 대신 IntegrityError 예외가 발생합니다. 어떻게 처리합니까? 그것을 잡아서 작성 + 업데이트보기에서 ValidationError를 발생 시킵니까?
gek

0

장고는 더 좋든 나쁘 든 고유성 검사의 목적과 NULL동등한 것으로 간주 NULL합니다. 고려할 고유성 검사의 구현을 직접 작성하는 방법은 없습니다.NULL테이블에서 발생하는 횟수에 관계없이 고유 한 없습니다.

(일부 DB 솔루션은 동일한 관점을 취 NULL하므로 한 DB의 아이디어에 의존하는 코드 NULL는 다른 사람들에게 이식성이 없을 수 있음 을 명심하십시오 )


6
이것은 정답이 아닙니다. 설명이 답변을 참조하십시오 .
Carl G

2
동의하지 않았습니다. Django 1.4에서 IntegerField (blank = True, null = True, unique = True)를 테스트했으며 null 값을 가진 여러 행을 허용합니다.
slacy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.