Campi univoci che consentono null in Django


135

Ho il modello Foo che ha la barra di campo. Il campo della barra dovrebbe essere univoco, ma consentire valori nulli al suo interno, il che significa che voglio consentire più di un record se il campo della barra lo è null, ma se non lo è nulli valori devono essere univoci.

Ecco il mio modello:

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

Ed ecco l'SQL corrispondente per la tabella:

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

Quando si utilizza l'interfaccia di amministrazione per creare più di 1 oggetti foo in cui la barra è nulla mi dà un errore: "Foo con questa barra esiste già".

Tuttavia, quando inserisco nel database (PostgreSQL):

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

Funziona bene, mi permette di inserire più di 1 record con la barra nulla, quindi il database mi permette di fare quello che voglio, è solo qualcosa di sbagliato nel modello Django. Qualche idea?

MODIFICARE

La portabilità della soluzione per quanto DB non sia un problema, siamo contenti di Postgres. Ho provato a impostare l'esclusivo su un callable, che era la mia funzione che restituiva True / False per valori specifici di bar , non dava alcun errore, tuttavia era aggraffato come se non avesse alcun effetto.

Finora, ho rimosso l'identificatore univoco dalla proprietà bar e gestendo l' unicità della barra nell'applicazione, tuttavia sto ancora cercando una soluzione più elegante. Qualche consiglio?


Non posso ancora commentare, quindi ecco una piccola aggiunta a mightyhal: da Django 1.4 avresti bisogno def get_db_prep_value(self, value, connection, prepared=False)come metodo di chiamata. Controlla gruppi.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ per ulteriori informazioni. Il seguente metodo funziona anche per me: def get_prep_value (self, value): if value == "": #if Django prova a salvare '' stringa, invia il db None (NULL) return Nient'altro: valore restituito # altrimenti, solo passare il valore
Jens,

Ho aperto un biglietto Django per questo. Aggiungi il tuo supporto. code.djangoproject.com/ticket/30210#ticket
Carl Brubaker,

Risposte:


154

Django non ha considerato NULL uguale a NULL ai fini dei controlli di unicità poiché il biglietto n. 9039 è stato corretto, vedere:

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

Il problema qui è che il valore "vuoto" normalizzato per un modulo CharField è una stringa vuota, non nessuna. Quindi, se si lascia il campo vuoto, si ottiene una stringa vuota, non NULL, memorizzata nel DB. Le stringhe vuote sono uguali alle stringhe vuote per i controlli di unicità, in base alle regole di Django e del database.

Puoi forzare l'interfaccia di amministrazione a memorizzare NULL per una stringa vuota fornendo il tuo modello personalizzato per Foo con un metodo clean_bar che trasforma la stringa vuota in 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

2
Se la barra è vuota, sostituirla con Nessuno nel metodo pre_save. Il codice sarà più ASCIUTTO, suppongo.
Ashish Gupta,

6
Questa risposta aiuta solo per l'immissione di dati basata su form, ma non fa nulla per proteggere effettivamente l'integrità dei dati. I dati possono essere inseriti tramite script di importazione, dalla shell, tramite un'API o qualsiasi altro mezzo. Molto meglio sostituire il metodo save () piuttosto che creare casi personalizzati per ogni modulo che potrebbe toccare i dati.
shacker,

Django 1.9+ richiede un attributo fieldso excludenelle ModelFormistanze. È possibile aggirare questo problema omettendo la Metaclasse interna da ModelForm per l'uso in admin. Riferimento: docs.djangoproject.com/en/1.10/ref/contrib/admin/…
user85461

62

** modifica 30/11/2015 : In python 3, la __metaclass__variabile modulo-globale non è più supportata . Inoltre, a partire Django 1.10dalla SubfieldBaseclasse è stato deprecato :

dai documenti :

django.db.models.fields.subclassing.SubfieldBaseè stato deprecato e verrà rimosso in Django 1.10. Storicamente, veniva utilizzato per gestire i campi in cui era necessaria la conversione del tipo durante il caricamento dal database, ma non veniva utilizzato nelle .values()chiamate o negli aggregati. È stato sostituito con from_db_value(). Si noti che il nuovo approccio non chiama il to_python()metodo sull'assegnazione come nel caso SubfieldBase.

Pertanto, come suggerito dalla from_db_value() documentazione e da questo esempio , questa soluzione deve essere modificata in:

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

Penso che un modo migliore di sovrascrivere il clean_data nell'amministratore sarebbe sottoclassare il campo di caratteri - in questo modo, indipendentemente dalla forma che accede al campo, "funzionerà". Puoi catturare il ''poco prima che venga inviato al database e catturare il NULL subito dopo che esce dal database, e il resto di Django non lo saprà. Un esempio veloce e sporco:

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  

Per il mio progetto, l'ho scaricato in un extras.pyfile che si trova nella radice del mio sito, quindi posso solo from mysite.extras import CharNullFieldnel models.pyfile della mia app . Il campo si comporta come un CharField: ricorda di impostare blank=True, null=Truequando dichiari il campo, altrimenti Django genererà un errore di convalida (campo obbligatorio) o creerà una colonna db che non accetta NULL.


3
in get_prep_value, è necessario rimuovere il valore, nel caso in cui il valore abbia più spazi.
ax003d

1
La risposta aggiornata qui funziona bene nel 2016 con Django 1.10 e utilizzando EmailField.
k0nG

4
Se stai aggiornando a CharFieldper essere un CharNullField, dovrai farlo in tre passaggi. Innanzitutto, aggiungi null=Trueal campo ed esegui la migrazione. Quindi, eseguire una migrazione dei dati per aggiornare eventuali valori vuoti in modo che siano nulli. Infine, converti il ​​campo in CharNullField. Se converti il ​​campo prima di eseguire la migrazione dei dati, la migrazione dei dati non farà nulla.
mlissner,

3
Si noti che nella soluzione aggiornata, from_db_value()non dovrebbe avere quel contexparametro aggiuntivo . Dovrebbe esseredef from_db_value(self, value, expression, connection):
Phil Gyford il

1
Il commento di @PhilGyford si applica a partire dalla 2.0.
Shaheed Haque,

16

Dato che sono nuovo di StackOverflow non mi è ancora consentito rispondere alle risposte, ma vorrei sottolineare che da un punto di vista filosofico, non posso essere d'accordo con la risposta più popolare a questa domanda. (di Karen Tracey)

L'OP richiede che il suo campo a barre sia univoco se ha un valore e null altrimenti. Quindi deve essere che il modello stesso si assicuri che sia così. Non può essere lasciato a codice esterno per verificarlo, perché ciò significherebbe che può essere ignorato. (O puoi dimenticare di controllarlo se scrivi una nuova vista in futuro)

Pertanto, per mantenere il codice veramente OOP, è necessario utilizzare un metodo interno del modello Foo. Modificare il metodo save () o il campo sono buone opzioni, ma sicuramente non lo è usare un modulo per farlo.

Personalmente preferisco usare il CharNullField suggerito, per la portabilità ai modelli che potrei definire in futuro.


13

La soluzione rapida è fare:

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

    if not self.bar:
        self.bar = None

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

2
tenere presente che l'utilizzo MyModel.objects.bulk_create()aggirerebbe questo metodo.
BenjaminGolder,

Questo metodo viene chiamato quando salviamo dal pannello di amministrazione? Ho provato ma non lo fa.
Kishan Mehta,

1
Il pannello di @Kishan django-admin salterà purtroppo questi ganci
Vincent Buscarello,

@ e-soddisfa la tua logica è sana, quindi l'ho implementata, ma l'errore è ancora un problema. Mi viene detto che null è un duplicato.
Vincent Buscarello,

6

Un'altra possibile soluzione

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

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

Questa non è una buona soluzione poiché stai creando una relazione non necessaria.
Burak Özdemir,


1

Di recente ho avuto lo stesso requisito. Invece di sottoclassare campi diversi, ho scelto di sovrascrivere il metodo save () sul mio modello (di seguito "MyModel") come segue:

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

Se hai un modello MyModel e desideri che my_field sia Null o unico, puoi ignorare il metodo di salvataggio del modello:

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)

In questo modo, il campo non può essere vuoto sarà solo non vuoto o nullo. i null non contraddicono l'unicità


1

È possibile aggiungere UniqueConstraintcon condizione di nullable_field=nulle non includere questo campo fieldsnell'elenco. Se è necessario anche un vincolo con nullable_fieldvalore diverso null, è possibile aggiungerne uno aggiuntivo.

Nota: UniqueConstraint è stato aggiunto da 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')
        ]

Che funzioni! ma ottengo un'eccezione IntegrityError invece dell'errore di convalida del modulo. Come lo gestisci? Catturalo e solleva ValidationError nelle viste create + update?
Gek

0

Nel bene o nel male, Django considera NULLequivalente ai NULLfini dei controlli di unicità. Non c'è davvero modo di evitarlo a meno di scrivere la propria implementazione del controllo di unicità che considera NULLunico, non importa quante volte si verifica in una tabella.

(e tieni presente che alcune soluzioni DB hanno la stessa visione NULL, quindi il codice basato sulle idee di un DB su NULLpotrebbe non essere portabile ad altri)


6
Questa non è la risposta corretta Vedi questa risposta per una spiegazione .
Carl G,

2
D'accordo, questo non è corretto. Ho appena testato IntegerField (vuoto = True, null = True, unique = True) in Django 1.4 e consente più righe con valori null.
slacy,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.