149 votes

Champs uniques autorisant les valeurs nulles dans Django

J'ai un modèle Foo qui a un champ bar. Le champ bar doit être unique, mais il peut contenir des nuls, ce qui signifie que je veux autoriser plus d'un enregistrement si le champ bar a la valeur suivante null mais si ce n'est pas le cas 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 j'utilise l'interface d'administration pour créer plus d'un objet foo où bar est nul, je reçois 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 la barre étant nulle, donc la base de données me permet de faire ce que je veux, c'est juste quelque chose qui ne va pas avec le modèle Django. Avez-vous une idée ?

EDIT

La portabilité de la solution en ce qui concerne la base de données n'est pas un problème, nous sommes satisfaits de Postgres. J'ai essayé de mettre unique à un callable, qui était ma fonction retournant Vrai/Faux pour des valeurs spécifiques de bar Il n'y a pas eu d'erreur, mais il semble que cela n'ait pas eu d'effet du tout.

Jusqu'à présent, j'ai supprimé le spécificateur unique de l'option bar et le traitement de la propriété bar l'unicité de l'application, mais je suis toujours à la recherche d'une solution plus élégante. Des recommandations ?

0 votes

Je ne peux pas encore faire de commentaires, alors voici un petit complément à l'original : Depuis Django 1.4, vous devez avoir def get_db_prep_value(self, value, connection, prepared=False) comme appel de méthode. Vérifiez groups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ pour plus d'informations. La méthode suivante fonctionne aussi pour moi : def get_prep_value(self, value) : if value=="" : #Si Django essaie de sauvegarder la chaîne '', envoyer à la base de données None (NULL) return None else : return value #autrement, passer simplement la valeur

0 votes

J'ai ouvert un ticket Django pour cela. Ajoutez votre soutien. code.djangoproject.com/ticket/30210#ticket

168voto

Karen Tracey Points 760

Django ne considère pas que NULL est égal à NULL dans le cadre des contrôles d'unicité depuis la correction du ticket #9039, voir :

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

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

Vous pouvez forcer l'interface d'administration à enregistrer 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

2 votes

Si la barre est vide, remplacez-la par None dans la méthode pre_save. Le code sera plus DRY je suppose.

7 votes

Cette réponse n'est utile que pour la saisie de données à l'aide de formulaires, mais ne protège en rien l'intégrité des données. Les données peuvent être saisies via des scripts d'importation, à partir du shell, via une API ou par tout autre moyen. Il vaut mieux remplacer la méthode save() que de créer des cas personnalisés pour chaque formulaire qui pourrait toucher les données.

0 votes

Django 1.9+ nécessite un fields ou exclude l'attribut dans ModelForm instances. Vous pouvez contourner ce problème en omettant l'option Meta classe interne du ModelForm à utiliser dans l'administration. Référence : docs.djangoproject.com/fr/1.10/ref/contrib/admin/

64voto

mightyhal Points 432

** modifier 11/30/2015 : Dans python 3, le module-global __metaclass__ La variable est n'est plus supporté . En outre, à partir de Django 1.10 le site SubfieldBase La classe était déprécié :

de la docs :

django.db.models.fields.subclassing.SubfieldBase a été déprécié et sera supprimé dans Django 1.10. Historiquement, il était utilisé pour gérer les champs pour lesquels une conversion de type était nécessaire lors du chargement depuis la base de données, mais il n'était pas utilisé dans .values() des appels ou des agrégats. Il a été remplacé par from_db_value() . Notez que la nouvelle approche n'appelle pas le to_python() méthode en mission, comme ce fut le cas pour SubfieldBase .

Par conséquent, comme le suggère le from_db_value() documentation et ceci exemple cette solution doit être modifié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'un meilleur moyen que de surcharger le cleaned_data dans l'administration serait de sous-classer le champ de caractères - de cette façon, quel que soit le formulaire accédant au champ, il va "juste fonctionner". Vous pouvez attraper le '' juste avant qu'il soit envoyé à la base de données, et attraper le NULL juste après qu'il soit sorti de la base de données, et le reste de Django n'en saura rien. 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 placé dans un fichier extras.py qui se trouve dans la racine de mon site, alors je peux juste from mysite.extras import CharNullField dans l'application models.py fichier. Le champ se comporte comme un CharField - n'oubliez pas de définir le paramètre blank=True, null=True lors de la déclaration du champ, sinon Django lancera une erreur de validation (champ requis) ou créera une colonne de base de données qui n'accepte pas NULL.

3 votes

dans get_prep_value, vous devez dépouiller la valeur, au cas où la valeur comporte plusieurs espaces.

1 votes

La réponse mise à jour ici fonctionne bien en 2016 avec Django 1.10 et en utilisant EmailField.

4 votes

Si vous mettez à jour un CharField pour être un CharNullField vous devrez le faire en trois étapes. Tout d'abord, ajoutez null=True au champ, et migrer ça. Ensuite, faites une migration de données pour mettre à jour toutes les valeurs vides afin qu'elles soient nulles. Enfin, convertissez le champ en CharNullField. Si vous convertissez le champ avant d'effectuer la migration des données, cette dernière n'aura aucun effet.

14voto

e-satis Points 146299

La solution rapide est de faire :

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

    if not self.bar:
        self.bar = None

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

2 votes

Sachez que l'utilisation de MyModel.objects.bulk_create() contournerait cette méthode.

0 votes

Cette méthode est-elle appelée lorsque nous sauvegardons à partir du panneau d'administration ? J'ai essayé mais ça ne marche pas.

1 votes

@Kishan le panneau django-admin ignorera ces crochets malheureusement.

6voto

Radagast Points 761

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)

0 votes

Ce n'est pas une bonne solution car vous créez une relation inutile.

0voto

James Bennett Points 6318

Pour le meilleur ou pour le pire, Django considère NULL pour être équivalent à NULL pour les besoins des contrôles d'unicité. Il n'y a pas vraiment de moyen de contourner ce problème, à moins d'écrire votre propre implémentation de la vérification d'unicité qui considère NULL pour être unique, quel que soit le nombre de fois où il apparaît dans une table.

(et gardez à l'esprit que certaines solutions de DB adoptent le même point de vue sur les NULL Ainsi, un code reposant sur les idées d'un DB concernant NULL peut ne pas être transférable à d'autres)

6 votes

Ce n'est pas la bonne réponse. Voir cette réponse pour explication .

2 votes

Je suis d'accord, ce n'est pas correct. Je viens de tester IntegerField(blank=True, null=True, unique=True) dans Django 1.4 et il autorise plusieurs lignes avec des valeurs nulles.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X