web-dev-qa-db-fra.com

Champs uniques qui autorisent les valeurs nulles dans Django

J'ai le modèle Foo qui a une barre de champ. Le champ de barre doit être unique, mais autoriser des valeurs nulles, ce qui signifie que je souhaite autoriser plusieurs enregistrements si le champ de barre est null, mais s'il ne l'est pas null, les valeurs doivent être uniques.

Voici mon modèle:

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

Et voici le SQL correspondant pour la table:

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

Lorsque vous utilisez l'interface d'administration pour créer plus de 1 objets foo où la barre est nulle, cela me donne une erreur: "Foo avec cette barre existe déjà."

Cependant, lorsque j'insère dans la base de données (PostgreSQL):

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

Cela fonctionne, très bien, cela me permet d'insérer plus d'un enregistrement avec une barre nulle, donc la base de données me permet de faire ce que je veux, c'est juste quelque chose de mal avec le modèle Django. Tout des idées?

MODIFIER

La portabilité de la solution dans la mesure où DB n'est pas un problème, nous sommes satisfaits de Postgres. J'ai essayé de définir l'unique à un appelable, ce qui était ma fonction renvoyant True/False pour des valeurs spécifiques de bar, cela ne donnait aucune erreur, mais comme si cela n'avait aucun effet.

Jusqu'à présent, j'ai supprimé le spécificateur unique de la propriété bar et géré l'unicité bar dans l'application, mais je cherche toujours une solution plus élégante. Des recommandations?

118
Sergey Golovchenko

Django n'a pas considéré NULL comme égal à NULL aux fins des vérifications d'unicité depuis que le ticket # 9039 a été corrigé, voir:

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

Le problème ici est que la valeur "vide" normalisée pour un formulaire CharField est une chaîne vide, pas None. Donc, si vous laissez le champ vide, vous obtenez une chaîne vide, pas NULL, stockée dans la base de données. Les chaînes vides sont égales aux chaînes vides pour les vérifications d'unicité, sous les deux règles Django et base de données.

Vous pouvez forcer l'interface d'administration à stocker NULL pour une chaîne vide en fournissant votre propre formulaire de modèle personnalisé pour Foo avec une méthode clean_bar qui transforme la chaîne vide en 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
137
Karen Tracey

** edit 30/11/2015 : Dans python 3, le module global __metaclass__ variable est n'est plus prise en charge . De plus, à partir de Django 1.10 la classe SubfieldBase était obsolète :

à partir de docs :

Django.db.models.fields.subclassing.SubfieldBase A été déconseillé et sera supprimé dans Django 1.10. Historiquement, il était utilisé pour gérer les champs où une conversion de type était nécessaire lors du chargement à partir de la base de données, mais il n'était pas utilisé dans les appels .values() ou dans les agrégats. Il a été remplacé par from_db_value() . Notez que la nouvelle approche n'appelle pas la méthode to_python() lors de l'affectation comme c'était le cas avec SubfieldBase.

Par conséquent, comme suggéré par la from_db_value()documentation et ceci exemple , cette solution doit être changée en:

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

Je pense qu'une meilleure façon que de remplacer les données nettoyées dans l'administrateur serait de sous-classer le champ de char - de cette façon, peu importe la forme qui accède au champ, cela "fonctionnera tout simplement". Vous pouvez attraper le '' Juste avant qu'il ne soit envoyé à la base de données, et attraper le NULL juste après sa sortie de la base de données, et le reste de Django ne sait pas/care. Un exemple rapide et sale:

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  

Pour mon projet, je l'ai vidé dans un fichier extras.py Qui vit à la racine de mon site, puis je peux simplement from mysite.extras import CharNullField Dans le fichier models.py De mon application. Le champ agit comme un CharField - n'oubliez pas de définir blank=True, null=True Lors de la déclaration du champ, ou sinon Django générera une erreur de validation (champ obligatoire) ou créera une colonne db qui n'accepte pas NULL.

60
mightyhal

Parce que je suis nouveau sur stackoverflow, je ne suis pas encore autorisé à répondre aux réponses, mais je voudrais souligner que d'un point de vue philosophique, je ne suis pas d'accord avec la réponse la plus populaire à cette question. (par Karen Tracey)

L'OP requiert que son champ de barre soit unique s'il a une valeur, et null sinon. Ensuite, il faut que le modèle lui-même s'assure que c'est le cas. Il ne peut pas être laissé à un code externe pour vérifier cela, car cela signifierait qu'il peut être contourné. (Ou vous pouvez oublier de le vérifier si vous écrivez une nouvelle vue à l'avenir)

Par conséquent, pour garder votre code véritablement OOP, vous devez utiliser une méthode interne de votre modèle Foo. La modification de la méthode save () ou du champ sont de bonnes options, mais l'utilisation d'un formulaire pour ce faire ne l'est certainement pas.

Personnellement, je préfère utiliser le CharNullField suggéré, pour la portabilité aux modèles que je pourrais définir à l'avenir.

12
tBuLi

La solution rapide consiste à faire:

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

    if not self.bar:
        self.bar = None

    super(Foo, self).save(*args, **kwargs)
12
e-satis

Une autre solution possible

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

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

Pour le meilleur ou pour le pire, Django considère NULL comme équivalent à NULL à des fins de vérification de l'unicité. Il n'y a vraiment aucun moyen de contourner cela avant d'écrire votre propre implémentation du contrôle d'unicité qui considère que NULL est unique quel que soit le nombre de fois où il se produit dans une table.

(et gardez à l'esprit que certaines solutions de base de données adoptent la même vue que NULL, donc le code reposant sur les idées d'une base de données sur NULL peut ne pas être portable pour les autres)

2
James Bennett

J'ai récemment eu la même exigence. Au lieu de sous-classer différents champs, j'ai choisi de remplacer le métode save () sur mon modèle (nommé ci-dessous "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
captnswing

Si vous avez un modèle MyModel et souhaitez que my_field soit Null ou unique, vous pouvez remplacer la méthode de sauvegarde du modèle:

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)

De cette façon, le champ ne peut pas être vide sera uniquement non vide ou nul. les nuls ne contredisent pas l'unicité

0
Joseph Bani