Bidang unik yang membolehkan null di Django

139

Saya memiliki model Foo yang memiliki kolom bar. Bidang batang harus unik, tetapi memungkinkan null di dalamnya, yang berarti saya ingin mengizinkan lebih dari satu catatan jika bidang batang adalah null, tetapi jika tidak null, nilainya harus unik.

Ini model saya:

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

Dan berikut adalah SQL yang sesuai untuk tabel:

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

Saat menggunakan antarmuka admin untuk membuat lebih dari 1 objek foo dengan bilah nol, ini memberi saya kesalahan: "Foo dengan bilah ini sudah ada."

Namun ketika saya memasukkan ke dalam database (PostgreSQL):

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

Ini berfungsi, baik-baik saja, ini mengijinkan saya untuk memasukkan lebih dari 1 catatan dengan bar menjadi null, jadi database memperbolehkan saya untuk melakukan apa yang saya inginkan, itu hanya sesuatu yang salah dengan model Django. Ada ide?

EDIT

Portabilitas solusi sejauh DB tidak menjadi masalah, kami senang dengan Postgres. Saya sudah mencoba mengatur unique to a callable, yaitu fungsi saya mengembalikan True / False untuk nilai tertentu dari bar , itu tidak memberikan kesalahan apa pun, namun seamed seperti itu tidak berpengaruh sama sekali.

Sejauh ini, saya telah menghapus specifier unik dari properti bar dan menangani keunikan bar dalam aplikasi, namun tetap mencari solusi yang lebih elegan. Ada rekomendasi?

Sergey Golovchenko
sumber
Saya belum bisa berkomentar jadi di sini sedikit tambahan untuk mightyhal: Sejak Django 1.4 anda akan membutuhkan def get_db_prep_value(self, value, connection, prepared=False)sebagai pemanggilan metode. Periksa groups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ untuk informasi lebih lanjut. Metode berikut juga bekerja untuk saya: def get_prep_value (self, value): if value == "": # jika Django mencoba menyimpan '' string, kirim db None (NULL) return None else: return value #otherwise, just lulus nilai
J
Saya membuka tiket Django untuk ini. Tambahkan dukungan Anda. code.djangoproject.com/ticket/30210#ticket
Carl Brubaker

Jawaban:

155

Django belum menganggap NULL sama dengan NULL untuk tujuan pemeriksaan keunikan sejak tiket # 9039 diperbaiki, lihat:

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

Masalahnya di sini adalah bahwa nilai "kosong" yang dinormalisasi untuk formulir CharField adalah string kosong, bukan None. Jadi jika Anda membiarkan field kosong, Anda mendapatkan string kosong, bukan NULL, yang disimpan di DB. String kosong sama dengan string kosong untuk pemeriksaan keunikan, di bawah aturan Django dan database.

Anda dapat memaksa antarmuka admin untuk menyimpan NULL untuk string kosong dengan menyediakan formulir model kustom Anda sendiri untuk Foo dengan metode clean_bar yang mengubah string kosong menjadi None:

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
Karen Tracey
sumber
2
Jika bar kosong maka gantilah dengan None dalam metode pre_save. Kode akan lebih KERING kurasa.
Ashish Gupta
6
Jawaban ini hanya membantu untuk input data berbasis formulir, tetapi tidak melakukan apa pun untuk benar-benar melindungi integritas data. Data dapat dimasukkan melalui skrip impor, dari shell, melalui API atau cara lain apa pun. Jauh lebih baik mengganti metode save () daripada membuat kasus khusus untuk setiap formulir yang mungkin menyentuh data.
shacker
Django 1.9+ membutuhkan sebuah fieldsatau excludeatribut dalam ModelFormcontoh. Anda dapat mengatasi ini dengan menghilangkan Metakelas dalam dari ModelForm untuk digunakan di admin. Referensi: docs.djangoproject.com/en/1.10/ref/contrib/admin/…
user85461
63

** edit 30/11/2015 : Di python 3, __metaclass__variabel module-global tidak lagi didukung . Ditambahkannya, sebagai dari Django 1.10yang SubfieldBasekelas itu ditinggalkan :

dari dokumen :

django.db.models.fields.subclassing.SubfieldBasetidak digunakan lagi dan akan dihapus di Django 1.10. Secara historis, ini digunakan untuk menangani bidang yang memerlukan konversi jenis saat memuat dari database, tetapi tidak digunakan dalam .values()panggilan atau secara agregat. Sudah diganti dengan from_db_value(). Perhatikan bahwa pendekatan baru tidak memanggil to_python()metode pada penugasan seperti yang terjadi pada SubfieldBase.

Oleh karena itu, seperti yang disarankan oleh from_db_value() dokumentasi dan contoh ini, solusi ini harus diubah menjadi:

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

Saya pikir cara yang lebih baik daripada menimpa clean_data di admin adalah dengan membuat subclass charfield - cara ini tidak peduli bentuk apa pun yang mengakses field, itu akan "berfungsi." Anda dapat menangkap ''tepat sebelum itu dikirim ke basis data, dan menangkap NULL tepat setelah keluar dari basis data, dan sisa Django tidak akan tahu / peduli. Contoh cepat dan kotor:

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  

Untuk proyek saya, saya membuang ini ke dalam extras.pyfile yang berada di root situs saya, lalu saya bisa langsung from mysite.extras import CharNullFieldke models.pyfile aplikasi saya . Bidang bertindak seperti CharField - ingatlah untuk menyetel blank=True, null=Trueketika mendeklarasikan bidang, atau jika tidak Django akan melontarkan kesalahan validasi (diperlukan bidang) atau membuat kolom db yang tidak menerima NULL.

perkasa
sumber
3
di get_prep_value, Anda harus menghapus nilainya, jika nilainya memiliki beberapa spasi.
ax003d
1
Jawaban yang diperbarui di sini bekerja dengan baik di 2016 dengan Django 1.10 dan menggunakan EmailField.
k0nG
4
Jika Anda memperbarui a CharFieldmenjadi a CharNullField, Anda harus melakukannya dalam tiga langkah. Pertama, tambahkan null=Trueke bidang, dan pindahkan itu. Kemudian, lakukan migrasi data untuk memperbarui nilai kosong apa pun sehingga nilainya menjadi null. Terakhir, ubah bidang menjadi CharNullField. Jika Anda mengonversi bidang sebelum Anda melakukan migrasi data, migrasi data Anda tidak akan melakukan apa pun.
mlissner
3
Perhatikan bahwa dalam solusi yang diperbarui, from_db_value()tidak boleh memiliki contexparameter ekstra itu . Seharusnyadef from_db_value(self, value, expression, connection):
Phil Gyford
1
Komentar dari @PhilGyford berlaku mulai 2.0.
Shaheed Haque
17

Karena saya baru mengenal stackoverflow, saya belum diizinkan untuk membalas jawaban, tetapi saya ingin menunjukkan bahwa dari sudut pandang filosofis, saya tidak dapat setuju dengan jawaban paling populer untuk pertanyaan ini. (oleh Karen Tracey)

OP mengharuskan kolom barnya unik jika memiliki nilai, dan null jika tidak. Maka harus model itu sendiri memastikan ini masalahnya. Ini tidak dapat diserahkan ke kode eksternal untuk memeriksa ini, karena itu berarti dapat dilewati. (Atau Anda bisa lupa untuk memeriksanya jika Anda menulis pandangan baru di masa mendatang)

Oleh karena itu, untuk menjaga kode Anda benar-benar OOP, Anda harus menggunakan metode internal model Foo Anda. Memodifikasi metode save () atau bidang adalah opsi yang bagus, tetapi menggunakan formulir untuk melakukan ini pasti tidak.

Secara pribadi saya lebih suka menggunakan CharNullField yang disarankan, untuk portabilitas ke model yang mungkin saya tentukan di masa depan.

tBuLi
sumber
14

Perbaikan cepatnya adalah dengan melakukan:

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

    if not self.bar:
        self.bar = None

    super(Foo, self).save(*args, **kwargs)
e-satis
sumber
2
ketahuilah bahwa menggunakan MyModel.objects.bulk_create()akan melewati metode ini.
BenjaminGolder
Apakah metode ini dipanggil saat kita menyimpan dari panel admin? Saya mencoba tetapi tidak.
Kishan Mehta
1
Sayangnya, panel @Kishan django-admin akan melewatkan pengait ini
Vincent Buscarello
@ e-satis logika Anda terdengar jadi saya menerapkan ini, tetapi kesalahan masih menjadi masalah. Saya diberi tahu bahwa null adalah duplikat.
Vincent Buscarello
6

Solusi lain yang mungkin

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

class Bar(models.Model):
    foo = models.OneToOneField(Foo, null=True)
Radagast
sumber
Ini bukan solusi yang baik karena Anda sedang membuat relasi yang tidak perlu.
Burak Özdemir
5

Ini diperbaiki sekarang setelah https://code.djangoproject.com/ticket/4136 diselesaikan. Dalam Django 1.11+ anda dapat menggunakan models.CharField(unique=True, null=True, blank=True)tanpa harus mengubah nilai kosong secara manual menjadi None.

praseodym
sumber
Ini bekerja untuk saya pada Django 3.1 dengan CharFieldtetapi tidak dengan TextField- batasan gagal karena string kosong masih dilewatkan oleh formulir admin.
gz.
2

Anda dapat menambahkan UniqueConstraintdengan kondisi nullable_field=nulldan tidak menyertakan bidang ini dalam fieldsdaftar. Jika Anda membutuhkan juga kendala dengan nullable_fieldnilai yang tidak null, Anda dapat menambahkan satu tambahan.

Catatan: UniqueConstraint ditambahkan sejak 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')
        ]
Андрей Лебедев
sumber
Itu bekerja! tetapi saya mendapatkan pengecualian IntegrityError alih-alih kesalahan validasi formulir. Bagaimana Anda mengatasinya? Tangkap dan naikkan ValidationError dalam tampilan buat + pembaruan?
gek
Ya Tuhan, kenapa aku harus menggulung ke bawah hanya untuk ini? Terima kasih telah menyelamatkan saya.
Yokhen
1

Saya baru-baru ini memiliki persyaratan yang sama. Alih-alih subclassing bidang yang berbeda, saya memilih untuk mengganti metode save () pada model saya (bernama 'MyModel' di bawah) sebagai berikut:

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()    
captnswing
sumber
1

Jika Anda memiliki model MyModel dan ingin my_field menjadi Null atau unik, Anda dapat mengganti metode penyimpanan model:

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)

Dengan cara ini, bidang tidak boleh kosong hanya akan menjadi tidak kosong atau nol. nulls tidak bertentangan dengan keunikan

Joseph Bani
sumber
0

Baik atau buruk, Django menganggap NULLsetara dengan NULLuntuk tujuan pemeriksaan keunikan. Benar-benar tidak ada jalan lain selain menulis implementasi Anda sendiri dari pemeriksaan keunikan yang dianggap NULLunik tidak peduli berapa kali itu terjadi dalam tabel.

(dan perlu diingat bahwa beberapa solusi DB memiliki pandangan yang sama NULL, jadi kode yang bergantung pada ide satu DB tentang NULLmungkin tidak portabel bagi orang lain)

James Bennett
sumber
6
Ini bukanlah jawaban yang benar. Lihat jawaban ini untuk penjelasannya .
Carl G
2
Setuju ini tidak benar. Saya baru saja menguji IntegerField (blank = True, null = True, unique = True) di Django 1.4 dan ini mengizinkan beberapa baris dengan nilai null.
slacy