Các trường duy nhất cho phép null trong Django


135

Tôi có mô hình Foo có thanh trường. Trường thanh phải là duy nhất, nhưng cho phép null trong đó, nghĩa là tôi muốn cho phép nhiều hơn một bản ghi nếu trường thanh là null, nhưng nếu nó không phải là nullcác giá trị phải là duy nhất.

Đây là mô hình của tôi:

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

Và đây là SQL tương ứng cho bảng:

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

Khi sử dụng giao diện quản trị để tạo nhiều hơn 1 đối tượng foo trong đó thanh là null, nó sẽ báo lỗi: "Foo với thanh này đã tồn tại."

Tuy nhiên khi tôi chèn vào cơ sở dữ liệu (PostgreSQL):

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

Điều này hoạt động tốt, nó cho phép tôi chèn hơn 1 bản ghi với thanh là null, vì vậy cơ sở dữ liệu cho phép tôi làm những gì tôi muốn, đó chỉ là một cái gì đó sai với mô hình Django. Có ý kiến ​​gì không?

BIÊN TẬP

Tính di động của giải pháp đối với DB không phải là vấn đề, chúng tôi hài lòng với Postgres. Tôi đã thử cài đặt duy nhất cho một cuộc gọi, đó là chức năng của tôi trả về True / false cho các giá trị cụ thể của thanh , nó không đưa ra bất kỳ lỗi nào, tuy nhiên được xử lý như không có tác dụng gì cả.

Cho đến nay, tôi đã xóa trình xác định duy nhất khỏi thuộc tính thanh và xử lý tính duy nhất của thanh trong ứng dụng, tuy nhiên vẫn đang tìm kiếm một giải pháp thanh lịch hơn. Có khuyến nghị nào không?


Tôi chưa thể bình luận vì vậy đây là một bổ sung nhỏ cho mightyhal: Vì Django 1.4 bạn sẽ cần def get_db_prep_value(self, value, connection, prepared=False)gọi phương thức. Kiểm tra nhóm.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ để biết thêm thông tin. Phương thức sau cũng hoạt động với tôi: def get_prep_value (self, value): if value == "": #if Django cố lưu chuỗi '', gửi db Không (NULL) trả về Không ai khác: trả về giá trị #therwise, chỉ vượt qua giá trị
Jens

Tôi đã mở một vé Django cho việc này. Thêm hỗ trợ của bạn. code.djangoproject.com/ticket/30210#ticket
Carl Brubaker

Câu trả lời:


154

Django đã không coi NULL bằng với NULL cho mục đích kiểm tra tính duy nhất vì vé # 9039 đã được sửa, xem:

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

Vấn đề ở đây là giá trị "trống" được chuẩn hóa cho biểu mẫu CharField là một chuỗi rỗng, không phải là Không có. Vì vậy, nếu bạn để trống trường, bạn nhận được một chuỗi trống, không phải NULL, được lưu trữ trong DB. Chuỗi rỗng bằng với chuỗi rỗng để kiểm tra tính duy nhất, theo cả quy tắc cơ sở dữ liệu và Django.

Bạn có thể buộc giao diện quản trị lưu trữ NULL cho một chuỗi trống bằng cách cung cấp biểu mẫu mô hình tùy chỉnh của riêng bạn cho Foo bằng phương thức Clean_bar để biến chuỗi trống thành Không có:

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
Nếu thanh trống thì thay thế nó bằng Không trong phương thức pre_save. Mã tôi sẽ DRY nhiều hơn tôi cho rằng.
Ashish Gupta

6
Câu trả lời này chỉ giúp cho việc nhập dữ liệu dựa trên biểu mẫu, nhưng không thực sự bảo vệ tính toàn vẹn dữ liệu. Dữ liệu có thể được nhập thông qua các tập lệnh nhập, từ trình bao, thông qua API hoặc bất kỳ phương tiện nào khác. Tốt hơn nhiều là ghi đè phương thức save () hơn là tạo các trường hợp tùy chỉnh cho mọi dạng có thể chạm vào dữ liệu.
Shacker

Django 1.9+ yêu cầu một fieldshoặc excludethuộc tính trong các ModelFormtrường hợp. Bạn có thể giải quyết vấn đề này bằng cách bỏ Metalớp bên trong khỏi ModelForm để sử dụng trong quản trị viên. Tham khảo: docs.djangoproject.com/en/1.10/ref/contrib/admin/iêu
user85461

62

** chỉnh sửa 30/11/2015 : Trong python 3, __metaclass__biến toàn cầu mô-đun không còn được hỗ trợ . Ngoài ra, kể từ Django 1.10khi SubfieldBaselớp học không được chấp nhận :

từ các tài liệu :

django.db.models.fields.subclassing.SubfieldBaseđã không được chấp nhận và sẽ bị xóa trong Django 1.10. Trước đây, nó được sử dụng để xử lý các trường trong đó cần chuyển đổi loại khi tải từ cơ sở dữ liệu, nhưng nó không được sử dụng trong .values()các cuộc gọi hoặc tổng hợp. Nó đã được thay thế bằng from_db_value(). Lưu ý rằng cách tiếp cận mới không gọi to_python()phương thức trên bài tập như trường hợp với SubfieldBase.

Do đó, như được đề xuất bởi from_db_value() tài liệuví dụ này, giải pháp này phải được thay đổi thành:

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

Tôi nghĩ rằng một cách tốt hơn so với ghi đè clean_data trong quản trị viên sẽ là phân lớp trường charfield - cách này cho dù hình thức nào truy cập vào trường, nó sẽ "chỉ hoạt động". Bạn có thể bắt ''ngay trước khi nó được gửi đến cơ sở dữ liệu và bắt NULL ngay sau khi nó ra khỏi cơ sở dữ liệu và phần còn lại của Django sẽ không biết / quan tâm. Một ví dụ nhanh và bẩn:

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  

Đối với dự án của tôi, tôi đã đổ nó vào một extras.pytệp nằm trong thư mục gốc của trang web của mình, sau đó tôi có thể chỉ from mysite.extras import CharNullFieldtrong models.pytệp của ứng dụng . Trường hoạt động giống như CharField - chỉ cần nhớ đặt blank=True, null=Truekhi khai báo trường hoặc nếu không Django sẽ đưa ra lỗi xác thực (trường bắt buộc) hoặc tạo cột db không chấp nhận NULL.


3
trong get_prep_value, bạn nên loại bỏ giá trị, trong trường hợp giá trị có nhiều khoảng trắng.
ax003d

1
Câu trả lời cập nhật ở đây hoạt động tốt trong năm 2016 với Django 1.10 và sử dụng EmailField.
k0nG

4
Nếu bạn đang cập nhật CharFieldđể trở thành một CharNullField, bạn sẽ cần phải thực hiện theo ba bước. Đầu tiên, thêm null=Truevào trường và di chuyển nó. Sau đó, thực hiện di chuyển dữ liệu để cập nhật bất kỳ giá trị trống nào để chúng là null. Cuối cùng, chuyển đổi trường thành CharNullField. Nếu bạn chuyển đổi trường trước khi bạn thực hiện di chuyển dữ liệu, việc di chuyển dữ liệu của bạn sẽ không làm gì cả.
mlissner

3
Lưu ý rằng trong giải pháp cập nhật, from_db_value()không nên có contextham số bổ sung đó . Nó nên làdef from_db_value(self, value, expression, connection):
Phil Gyford 15/03/18

1
Nhận xét từ @PhilGyford áp dụng kể từ 2.0.
Shaheed Haque

16

Bởi vì tôi chưa quen với stackoverflow nên tôi chưa được phép trả lời câu trả lời, nhưng tôi muốn chỉ ra rằng theo quan điểm triết học, tôi không thể đồng ý với câu trả lời phổ biến nhất cho câu hỏi này. (bởi Karen Tracey)

OP yêu cầu trường thanh của anh ta là duy nhất nếu nó có giá trị và khác. Sau đó, phải là mô hình chính nó đảm bảo đây là trường hợp. Không thể để mã bên ngoài để kiểm tra điều này, vì điều đó có nghĩa là nó có thể được bỏ qua. (Hoặc bạn có thể quên kiểm tra nếu bạn viết một chế độ xem mới trong tương lai)

Do đó, để giữ cho mã của bạn thực sự OOP, bạn phải sử dụng một phương thức nội bộ của mô hình Foo của bạn. Sửa đổi phương thức save () hoặc trường là các tùy chọn tốt, nhưng sử dụng một biểu mẫu để làm điều này chắc chắn là không.

Cá nhân tôi thích sử dụng CharNullField được đề xuất, cho tính di động đối với các mô hình mà tôi có thể xác định trong tương lai.


13

Cách khắc phục nhanh là làm:

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

    if not self.bar:
        self.bar = None

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

2
lưu ý rằng việc sử dụng MyModel.objects.bulk_create()sẽ bỏ qua phương pháp này.
BenjaminGolder

Phương thức này có được gọi khi chúng ta lưu từ bảng quản trị không? Tôi đã thử nhưng không được.
Kishan Mehta

1
Thật không may bảng điều khiển django-admin sẽ bỏ qua những cái móc này
Vincent Buscarello

@ e-satis logic của bạn là âm thanh nên tôi đã thực hiện điều này, nhưng lỗi vẫn là một vấn đề. Tôi nhận được nói null là một bản sao.
Vincent Buscarello

6

Một giải pháp khả thi khác

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

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

Đây không phải là một giải pháp tốt vì bạn đang tạo ra một mối quan hệ không cần thiết.
Burak zdemir

3

Điều này hiện đã được khắc phục khi https://code.djangoproject.com/ticket/4136 được giải quyết. Trong Django 1.11+, bạn có thể sử dụng models.CharField(unique=True, null=True, blank=True)mà không phải chuyển đổi thủ công các giá trị trống sang None.


1

Gần đây tôi có yêu cầu tương tự. Thay vì phân lớp các trường khác nhau, tôi đã chọn ghi đè lên metod save () trên mô hình của mình (có tên 'MyModel' bên dưới) như sau:

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

Nếu bạn có một mô hình MyModel và muốn my_field là Null hoặc duy nhất, bạn có thể ghi đè phương thức lưu của mô hình:

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)

Theo cách này, trường không thể để trống sẽ chỉ là không trống hoặc null. nulls không mâu thuẫn với tính độc đáo


0

Dù tốt hay xấu, Django coi NULLlà tương đương NULLvới mục đích kiểm tra tính duy nhất. Thực sự không có cách nào khác để viết ra việc thực hiện kiểm tra tính duy nhất của riêng bạn, được coi NULLlà duy nhất cho dù có xảy ra bao nhiêu lần trong một bảng.

(và lưu ý rằng một số giải pháp DB có cùng quan điểm NULL, vì vậy mã dựa trên một ý tưởng của DB về NULLcó thể không khả chuyển đối với các giải pháp khác)


6
Đây không phải là câu trả lời chính xác. Xem câu trả lời này để giải thích .
Carl G

2
Đồng ý điều này là không chính xác. Tôi vừa thử nghiệm IntegerField (blank = True, null = True, unique = True) trong Django 1.4 và nó cho phép nhiều hàng có giá trị null.
Slacy

0

Bạn có thể thêm UniqueConstraintvới điều kiện nullable_field=nullvà không bao gồm trường này trong fieldsdanh sách. Nếu bạn cũng không cần ràng buộc với nullable_fieldgiá trị wich null, bạn có thể thêm một giá trị bổ sung.

Lưu ý: UniqueConstraint đã được thêm từ 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')
        ]
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.