Django'da null değerlere izin veren benzersiz alanlar


135

Alan çubuğu olan Foo modelim var. Bar alanı benzersiz olmalıdır, ancak buna null değerlerine izin verin, yani bar alanı ise birden fazla kayda izin vermek istiyorum null, ancak değilse nulldeğerler benzersiz olmalıdır.

İşte benim modelim:

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

Ve tablo için karşılık gelen 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)
)   

Çubuğun boş olduğu 1'den fazla foo nesnesi oluşturmak için admin arabirimini kullanırken bana bir hata veriyor: "Bu Çubuğa sahip Foo zaten var."

Ancak veritabanına (PostgreSQL) eklediğimde:

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

Bu işe yarıyor, iyi, bar null olan 1'den fazla kayıt eklememe izin veriyor, bu yüzden veritabanı istediğimi yapmama izin veriyor, bu Django modelinde yanlış bir şey. Herhangi bir fikir?

DÜZENLE

DB'nin bir çözüm olmadığı sürece çözümün taşınabilirliği, Postgres'den memnunuz. Çubuğun belirli değerleri için True / False döndüren işlevim olan bir callable için benzersiz ayar yapmayı denedim , ancak herhangi bir etkisi olmadığı gibi eklenmiş, herhangi bir hata vermedi.

Şimdiye kadar, benzersiz özellikli bar özelliğinden çıktım ve uygulamadaki bar benzersizliğini ele aldım , ancak yine de daha zarif bir çözüm arıyorum. Herhangi bir tavsiye?


Henüz yorum yapamam bu yüzden burada güçlü bir ek: Django 1.4 beri def get_db_prep_value(self, value, connection, prepared=False)yöntem çağrısı olarak gerekir . Daha fazla bilgi için groups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ adresini kontrol edin . Aşağıdaki yöntem de benim için çalışıyor: def get_prep_value (self, value): if value == "": #if Django '' dizesini kaydetmeye çalışırsa, db'yi gönderin None (NULL) return Hiçbiri: return value #otherwise, just değeri geçmek
Jens

Bunun için bir Django bileti açtım. Desteğinizi ekleyin. code.djangoproject.com/ticket/30210#ticket
Carl Brubaker

Yanıtlar:


154

Django, 9039 numaralı bilet düzeltildiğinden teklik kontrolleri için NULL değerinin NULL değerine eşit olduğunu düşünmedi, bkz:

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

Buradaki sorun, bir form CharField için normalleştirilmiş "boş" değerin Boş değil, boş bir dize olmasıdır. Alanı boş bırakırsanız, DB'de NULL değil, boş bir dize alırsınız. Boş dizeler, hem Django hem de veritabanı kuralları altında benzersizlik denetimleri için boş dizelere eşittir.

Yönetici arayüzünü, boş dizeyi Yok'a dönüştüren bir clean_bar yöntemiyle Foo için kendi özelleştirilmiş model formunuzu sağlayarak boş bir dize için NULL depolamaya zorlayabilirsiniz:

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
Çubuk boşsa, pre_save yönteminde bunu None ile değiştirin. Sanırım kod daha KURU olacak.
Ashish Gupta

6
Bu yanıt yalnızca form tabanlı veri girişine yardımcı olur, ancak veri bütünlüğünü gerçekten korumak için hiçbir şey yapmaz. Veriler, içe aktarma komut dosyaları aracılığıyla, kabuktan, bir API veya başka bir yolla girilebilir. Save () yöntemini geçersiz kılmak, verilere dokunabilecek her form için özel durumlar yapmaktan çok daha iyidir.
shacker

Django 1.9+ , örneklerde bir fieldsveya excludeniteliği gerektirir ModelForm. MetaYönetici içinde kullanılmak üzere iç sınıfı ModelForm'dan çıkararak bu sorunu çözebilirsiniz. Referans: docs.djangoproject.com/tr/1.10/ref/contrib/admin/…
user85461

62

** değiştir 11/30/2015 : Python 3'te modül global __metaclass__değişkeni artık desteklenmiyor . Ek olarak, itibariyle sınıfına edildi kaldırılmış :Django 1.10SubfieldBase

dan docs :

django.db.models.fields.subclassing.SubfieldBasekullanımdan kaldırıldı ve Django 1.10'da kaldırılacak. Tarihsel olarak, veritabanından yüklenirken tip dönüşümünün gerekli olduğu alanları işlemek için kullanılırdı, ancak .values()çağrılarda veya toplu olarak kullanılmadı. İle değiştirildi from_db_value(). Yeni yaklaşımın,to_python() olduğu gibi atama yöntemini çağırmadığını unutmayınSubfieldBase .

Bu nedenle, from_db_value() belgelerde ve bu örnekte önerildiği gibi , bu çözüm şu şekilde değiştirilmelidir:

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

Ben yöneticinin clean_data geçersiz kılmak daha iyi bir yol charfield alt sınıf için olacağını düşünüyorum - bu şekilde alan hangi formu erişir olursa olsun, "sadece işe yarayacak". ''Veritabanına gönderilmeden hemen önce yakalayabilir ve veritabanından çıktıktan hemen sonra NULL'u yakalayabilirsiniz ve Django'nun geri kalanı bilmez / bakım yapmaz. Hızlı ve kirli bir örnek:

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  

Projem için, bunu extras.pysitemin kökünde yaşayan bir dosyaya döktüm , sonra sadece from mysite.extras import CharNullFielduygulamamın models.pydosyasında yapabilirim. Alan tıpkı bir CharField gibi davranır; yalnızca alanı blank=True, null=Truebildirirken ayarlamayı unutmayın , aksi takdirde Django bir doğrulama hatası atar (alan gerekir) veya NULL kabul etmeyen bir db sütunu oluşturur.


3
get_prep_value değerinde, değerin birden fazla boşluğu olması durumunda değeri kaldırmanız gerekir.
ax003d

1
Güncellenmiş cevap burada 2016 yılında Django 1.10 ve EmailField kullanarak iyi çalışıyor.
k0nG

4
Eğer bir güncelleme varsa CharFieldbir olmaya CharNullField, üç adımda bunu yapmak gerekir. İlk null=Trueolarak, alana ekleyin ve taşıyın. Ardından, boş değerleri boş olacak şekilde güncelleştirmek için veri taşıma işlemi yapın. Son olarak, alanı CharNullField'e dönüştürün. Veri taşıma işlemini gerçekleştirmeden önce alanı dönüştürürseniz, veri taşıma işleminiz hiçbir şey yapmaz.
mlissner

3
Güncellenmiş çözümde, from_db_value()bu ekstra contexparametreye sahip olmamalıdır . O olmalıdef from_db_value(self, value, expression, connection):
Phil Gyford

1
@PhilGyford'un yorumu 2.0 itibariyle geçerlidir.
Shaheed Haque

16

Stackoverflow'da yeni olduğum için henüz cevaplara cevap vermeme izin verilmiyor, ancak felsefi bir bakış açısıyla, bu soruya en popüler cevabı kabul edemediğimi belirtmek isterim. (Karen Tracey tarafından)

OP, çubuk alanının bir değeri varsa benzersiz olmasını ve aksi halde null olmasını gerektirir. O zaman modelin kendisinin durumun bu olduğundan emin olması gerekir. Bunu kontrol etmek için harici koda bırakılamaz, çünkü bu atlanabileceği anlamına gelir. (Ya da gelecekte yeni bir görünüm yazarsanız kontrol etmeyi unutabilirsiniz)

Bu nedenle, kodunuzu gerçekten OOP olarak tutmak için Foo modelinizin dahili bir yöntemini kullanmalısınız. Save () yöntemini veya alanı değiştirmek iyi seçeneklerdir, ancak bunu yapmak için bir form kullanmak kesinlikle mümkün değildir.

Şahsen, gelecekte tanımlayabileceğim modellere taşınabilirlik için önerilen CharNullField öğesini kullanmayı tercih ediyorum.


13

Hızlı düzeltme şunları yapmaktır:

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

    if not self.bar:
        self.bar = None

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

2
kullanımın MyModel.objects.bulk_create()bu yöntemi atlayacağını unutmayın.
BenjaminGolder

Yönetici panelinden kaydettiğimizde bu yöntem çağrılıyor mu? Denedim ama öyle değil.
Kishan Mehta

1
@Kishan django-admin panel bu kancaları maalesef atlayacak
Vincent Buscarello

@ e-satis mantığınız sağlam, bu yüzden bunu uyguladım, ancak hata hala bir sorun. Ben null bir kopya olduğunu söylüyorum.
Vincent Buscarello

6

Başka bir olası çözüm

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

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

Gereksiz bir ilişki oluşturduğunuz için bu iyi bir çözüm değil.
Burak Özdemir


1

Son zamanlarda aynı gereksinime sahiptim. Farklı alanları alt sınıflamak yerine, modelimdeki save () yöntemini (aşağıda 'MyModel' olarak adlandırılır) aşağıdaki gibi geçersiz kılmayı seçtim:

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

Bir MyModel modeliniz varsa ve alanımın Boş veya benzersiz olmasını istiyorsanız, modelin kaydetme yöntemini geçersiz kılabilirsiniz:

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)

Bu şekilde alan boş bırakılamaz, yalnızca boş veya boş olur. null'lar benzersizlikle çelişmez


1

Bu alanı listeye dahil edip etmemek UniqueConstraintkoşuluyla ekleyebilirsiniz . Değeri olmayan kısıtlamaya da ihtiyacınız varsa , bir tane daha ekleyebilirsiniz.nullable_field=nullfieldsnullable_fieldnull

Not: django 2.2'den beri UniqueConstraint eklendi

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

Bu işe yarıyor! ancak form doğrulama hatası yerine bir IntegrityError istisnası alıyorum. Bununla nasıl başa çıkarsın? Yakala ve oluşturma + güncelleme görünümlerinde ValidationError'ı yükseltilsin mi?
gek

0

Daha iyi ya da kötü için, Django benzersizlik kontrolleri için NULLeşdeğer kabul NULLeder. NULLBir tabloda kaç kez olursa olsun benzersiz olduğunu düşünen benzersizlik kontrolünü kendi uygulamanızı yazmanın kısa bir yolu yoktur.

(ve bazı DB çözümlerinin aynı görüşü aldığını unutmayın NULL, bu nedenle bir DB'nin fikirlerine dayanan kod NULLbaşkaları için taşınabilir olmayabilir)


6
Bu doğru cevap değil. Açıklama için bu cevaba bakınız .
Carl G

2
Bu doğru değil kabul etti. Ben sadece Django 1.4 IntegerField (blank = True, null = True, unique = True) test ve null değerleri ile birden çok satır sağlar.
slacy
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.