ช่องเฉพาะที่อนุญาตให้มี null ใน Django


139

ฉันมีโมเดล 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)
)   

เมื่อใช้อินเทอร์เฟซผู้ดูแลระบบเพื่อสร้างออบเจ็กต์ foo มากกว่า 1 ชิ้นโดยที่ bar เป็นค่าว่างมันทำให้ฉันมีข้อผิดพลาด: "Foo with this Bar มีอยู่แล้ว"

อย่างไรก็ตามเมื่อฉันแทรกลงในฐานข้อมูล (PostgreSQL):

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

มันใช้งานได้ดีมันช่วยให้ฉันสามารถแทรกมากกว่า 1 เรคคอร์ดโดยที่บาร์เป็นโมฆะดังนั้นฐานข้อมูลจึงอนุญาตให้ฉันทำในสิ่งที่ฉันต้องการมันมีบางอย่างผิดปกติกับโมเดล Django ความคิดใด ๆ ?

แก้ไข

ความสามารถในการพกพาของโซลูชันตราบเท่าที่ DB ไม่ใช่ปัญหาเราพอใจกับ Postgres ฉันได้ลองตั้งค่าเฉพาะให้กับ callable ซึ่งเป็นฟังก์ชันของฉันที่คืนค่า True / False สำหรับค่าเฉพาะของbarแต่ก็ไม่ได้ให้ข้อผิดพลาดใด ๆ แต่ดูเหมือนว่าจะไม่มีผลเลย

จนถึงตอนนี้ฉันได้ลบตัวระบุที่ไม่ซ้ำกันออกจากคุณสมบัติบาร์และจัดการความเป็นเอกลักษณ์ของบาร์ในแอปพลิเคชัน แต่ก็ยังคงมองหาโซลูชันที่หรูหรากว่านี้ ข้อเสนอแนะใด ๆ


ฉันยังไม่สามารถแสดงความคิดเห็นได้ดังนั้นที่นี่จึงมีการเพิ่มพลังเล็กน้อย: เนื่องจาก Django 1.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 พยายามบันทึกสตริง '' ส่ง db None (NULL) ส่งคืน None else: return value # ไม่เช่นนั้น ผ่านค่า
Jens

ฉันเปิดตั๋ว Django สำหรับสิ่งนี้ เพิ่มการสนับสนุนของคุณ code.djangoproject.com/ticket/30210#ticket
Carl Brubaker

คำตอบ:


155

Django ไม่ได้ถือว่า NULL เท่ากับ NULL เพื่อจุดประสงค์ในการตรวจสอบความเป็นเอกลักษณ์เนื่องจากตั๋ว # 9039 ได้รับการแก้ไขโปรดดู:

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

ปัญหาที่นี่คือค่า "ว่าง" ที่ทำให้เป็นมาตรฐานสำหรับฟอร์ม CharField เป็นสตริงว่างไม่ใช่ไม่มี ดังนั้นหากคุณปล่อยฟิลด์ว่างไว้คุณจะได้รับสตริงว่างไม่ใช่ NULL เก็บไว้ในฐานข้อมูล สตริงว่างจะเท่ากับสตริงว่างสำหรับการตรวจสอบความเป็นเอกลักษณ์ภายใต้กฎ Django และฐานข้อมูล

คุณสามารถบังคับให้อินเทอร์เฟซผู้ดูแลระบบจัดเก็บค่า NULL สำหรับสตริงว่างโดยการจัดเตรียมแบบจำลองของคุณเองสำหรับ Foo ด้วยเมธอด clean_bar ที่เปลี่ยนสตริงว่างให้เป็นไม่มี:

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
หากแถบว่างให้แทนที่ด้วย None ในวิธีการบันทึกล่วงหน้า ฉันคิดว่าโค้ดจะแห้งมากขึ้น
Ashish Gupta

6
คำตอบนี้ช่วยสำหรับการป้อนข้อมูลตามฟอร์มเท่านั้น แต่ไม่ได้ช่วยปกป้องความสมบูรณ์ของข้อมูล ข้อมูลอาจถูกป้อนผ่านสคริปต์นำเข้าจากเชลล์ผ่าน API หรือวิธีการอื่นใด การแทนที่เมธอด save () ดีกว่าการสร้างกรณีแบบกำหนดเองสำหรับทุกรูปแบบที่อาจสัมผัสกับข้อมูล
shacker

Django 1.9+ ต้องการ a fieldsหรือexcludeแอตทริบิวต์ในModelFormอินสแตนซ์ คุณสามารถแก้ไขปัญหานี้ได้โดยการละเว้นMetaคลาสภายในจาก ModelForm เพื่อใช้ในผู้ดูแลระบบ อ้างอิง: docs.djangoproject.com/th/1.10/ref/contrib/admin/…
user85461

63

** แก้ไข 2015/11/30 : ในหลาม 3 โมดูลทั่วโลก__metaclass__ตัวแปรไม่สนับสนุน นอกจากนี้ในขณะDjango 1.10ที่SubfieldBaseคลาสถูกเลิกใช้ :

จากเอกสาร :

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

ฉันคิดว่าวิธีที่ดีกว่าการลบล้าง clean_data ในผู้ดูแลระบบคือการซับคลาสชาร์ฟิลด์ - วิธีนี้ไม่ว่าจะเข้าถึงฟิลด์ในรูปแบบใดก็ตามมันจะ "ใช้ได้" คุณสามารถตรวจจับ''ก่อนที่จะถูกส่งไปยังฐานข้อมูลและจับ 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 จะแสดงข้อผิดพลาดในการตรวจสอบความถูกต้อง (ต้องระบุฟิลด์) หรือสร้างคอลัมน์ db ที่ไม่ยอมรับ NULL


3
ใน get_prep_value คุณควรตัดค่าออกในกรณีที่ค่ามีช่องว่างหลายช่อง
ax003d

1
คำตอบที่อัปเดตที่นี่ใช้งานได้ดีในปี 2559 กับ Django 1.10 และใช้ EmailField
k0nG

4
หากคุณกำลังอัปเดต a CharFieldเป็น a CharNullFieldคุณจะต้องดำเนินการในสามขั้นตอน ขั้นแรกเพิ่มลงnull=Trueในฟิลด์และย้ายข้อมูลนั้น จากนั้นทำการย้ายข้อมูลเพื่ออัปเดตค่าว่างให้เป็นโมฆะ สุดท้ายแปลงฟิลด์เป็น CharNullField หากคุณแปลงฟิลด์ก่อนที่จะทำการย้ายข้อมูลการย้ายข้อมูลของคุณจะไม่ทำอะไรเลย
mlissner

3
โปรดทราบว่าในโซลูชันที่อัปเดตfrom_db_value()ไม่ควรมีcontexพารามิเตอร์เพิ่มเติมนั้น ควรจะเป็นdef from_db_value(self, value, expression, connection):
Phil Gyford

1
ความคิดเห็นจาก @PhilGyford มีผลตั้งแต่ 2.0
Shaheed Haque

17

เนื่องจากฉันยังใหม่กับ stackoverflow ฉันจึงยังไม่ได้รับอนุญาตให้ตอบกลับคำตอบ แต่ฉันอยากจะชี้ให้เห็นว่าจากมุมมองทางปรัชญาฉันไม่เห็นด้วยกับคำตอบที่เป็นที่นิยมมากที่สุดสำหรับคำถามนี้ (โดย Karen Tracey)

OP ต้องการให้ฟิลด์บาร์ของเขาไม่ซ้ำกันหากมีค่าและเป็นโมฆะ จากนั้นจะต้องเป็นแบบจำลองที่ทำให้แน่ใจว่าเป็นเช่นนั้น ไม่สามารถปล่อยให้รหัสภายนอกตรวจสอบสิ่งนี้ได้เพราะนั่นหมายความว่าสามารถข้ามได้ (หรือคุณอาจลืมตรวจสอบหากคุณเขียนมุมมองใหม่ในอนาคต)

ดังนั้นเพื่อให้รหัสของคุณ OOP อย่างแท้จริงคุณต้องใช้วิธีการภายในของแบบจำลอง Foo ของคุณ การแก้ไขเมธอด save () หรือฟิลด์เป็นตัวเลือกที่ดี แต่การใช้แบบฟอร์มเพื่อทำสิ่งนี้ไม่แน่นอนที่สุด

โดยส่วนตัวแล้วฉันชอบใช้ CharNullField ที่แนะนำสำหรับการพกพาไปยังรุ่นที่ฉันอาจกำหนดในอนาคต


14

การแก้ไขด่วนคือการทำ:

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 panel จะข้ามตะขอเหล่านี้ไปอย่างน่าเสียดาย
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

5

ตอนนี้ได้รับการแก้ไขแล้วว่าhttps://code.djangoproject.com/ticket/4136ได้รับการแก้ไขแล้ว ใน Django 1.11+ คุณสามารถใช้ได้models.CharField(unique=True, null=True, blank=True)โดยไม่ต้องแปลงค่าว่างเป็นNoneไฟล์.


สิ่งนี้ใช้ได้ผลกับฉันใน Django 3.1 ด้วยCharFieldแต่ไม่ใช่ด้วยTextField- ข้อ จำกัด ล้มเหลวเนื่องจากสตริงว่างยังคงถูกส่งผ่านแบบฟอร์มผู้ดูแลระบบ
gz

2

คุณสามารถเพิ่มUniqueConstraintโดยมีเงื่อนไขnullable_field=nullและไม่รวมฟิลด์นี้ในfieldsรายการ หากคุณไม่ต้องการข้อ จำกัด ที่มีnullable_fieldค่าวิชnullคุณสามารถเพิ่มได้

หมายเหตุ: 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')
        ]

ได้ผล! แต่ฉันได้รับข้อยกเว้น IntegrityError แทนที่จะเป็นข้อผิดพลาดในการตรวจสอบแบบฟอร์ม คุณจัดการอย่างไร? จับมันและเพิ่ม ValidationError ในมุมมองสร้าง + อัปเดต?
เก็

พระเจ้าทำไมฉันต้องเลื่อนลงมาเพื่อสิ่งนี้? ขอบคุณที่ช่วยฉัน
Yokhen

1

ฉันเพิ่งมีข้อกำหนดเดียวกันนี้ แทนที่จะใช้ subclassing ฟิลด์อื่นฉันเลือกที่จะแทนที่ save () metod บนโมเดลของฉัน (ชื่อ 'MyModel' ด้านล่าง) ดังนี้:

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 หรือไม่ซ้ำกันคุณสามารถแทนที่วิธีการบันทึกของโมเดลได้:

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)

ด้วยวิธีนี้ช่องต้องไม่ว่างจะเป็นเพียงช่องว่างหรือว่างเท่านั้น ค่าว่างไม่ขัดแย้งกับความเป็นเอกลักษณ์


0

เพื่อให้ดีขึ้นหรือแย่ลง Django ถือว่าNULLเทียบเท่ากับNULLวัตถุประสงค์ในการตรวจสอบความเป็นเอกลักษณ์ ไม่มีทางที่จะเขียนการดำเนินการตรวจสอบความเป็นเอกลักษณ์ของคุณเองซึ่งถือว่าNULLไม่ซ้ำกันไม่ว่าจะเกิดขึ้นกี่ครั้งในตาราง

(และโปรดทราบว่าโซลูชัน DB บางตัวมีมุมมองเดียวกันNULLดังนั้นโค้ดที่อาศัยแนวคิดของ DB หนึ่งเกี่ยวกับNULLอาจไม่สามารถพกพาไปยังผู้อื่นได้)


6
นี่ไม่ใช่คำตอบที่ถูกต้อง ดูคำตอบนี้สำหรับคำอธิบาย
Carl G

2
ตกลงนี้ไม่ถูกต้อง ฉันเพิ่งทดสอบ IntegerField (blank = True, null = True, unique = True) ใน Django 1.4 และอนุญาตให้มีหลายแถวที่มีค่า null
slacy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.