Campos únicos que permiten nulos en Django


135

Tengo el modelo Foo que tiene barra de campo. El campo de la barra debe ser único, pero permitir nulos en él, lo que significa que quiero permitir más de un registro si el campo de la barra es null, pero si no lo es, nulllos valores deben ser únicos.

Aquí está mi modelo:

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

Y aquí está el SQL correspondiente para la tabla:

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

Cuando uso la interfaz de administrador para crear más de 1 objetos foo donde la barra es nula, me da un error: "Foo con esta barra ya existe".

Sin embargo, cuando inserto en la base de datos (PostgreSQL):

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

Esto funciona, está bien, me permite insertar más de 1 registro con la barra nula, por lo que la base de datos me permite hacer lo que quiero, es simplemente algo mal con el modelo Django. ¿Algunas ideas?

EDITAR

La portabilidad de la solución en lo que respecta a DB no es un problema, estamos contentos con Postgres. Intenté establecer un valor único para un invocable, que era mi función que devolvía Verdadero / Falso para valores específicos de barra , no daba ningún error, sin embargo, parecía que no tenía ningún efecto.

Hasta ahora, eliminé el especificador único de la propiedad de la barra y manejé la singularidad de la barra en la aplicación, sin embargo, todavía busco una solución más elegante. ¿Alguna recomendación?


No puedo comentar todavía, así que aquí una pequeña adición a mightyhal: desde Django 1.4 necesitarías def get_db_prep_value(self, value, connection, prepared=False)como método de llamada. Visite groups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ para obtener más información. El siguiente método también funciona para mí: def get_prep_value (self, value): if value == "": #si Django intenta guardar la cadena '', envíe el db None (NULL) return Ninguno más: valor de retorno #otherwise, solo pasar el valor
Jens

Abrí un boleto de Django para esto. Agrega tu apoyo. code.djangoproject.com/ticket/30210#ticket
Carl Brubaker

Respuestas:


154

Django no ha considerado que NULL sea igual a NULL para fines de comprobaciones de unicidad ya que el ticket # 9039 fue corregido, vea:

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

El problema aquí es que el valor "en blanco" normalizado para un formulario CharField es una cadena vacía, no None. Entonces, si deja el campo en blanco, obtendrá una cadena vacía, no NULL, almacenada en la base de datos. Las cadenas vacías son iguales a las cadenas vacías para las comprobaciones de unicidad, tanto en Django como en las reglas de la base de datos.

Puede forzar a la interfaz de administración a almacenar NULL para una cadena vacía proporcionando su propio formulario de modelo personalizado para Foo con un método clean_bar que convierte la cadena vacía en Ninguno:

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
Si la barra está en blanco, reemplácela por Ninguna en el método pre_save. El código será más SECO, supongo.
Ashish Gupta

66
Esta respuesta solo ayuda para la entrada de datos basada en formularios, pero no hace nada para proteger realmente la integridad de los datos. Los datos pueden ingresarse a través de scripts de importación, desde el shell, a través de una API o cualquier otro medio. Mucho mejor anular el método save () que crear casos personalizados para cada formulario que pueda tocar los datos.
shacker

Django 1.9+ requiere un atributo fieldso excludeen ModelForminstancias. Puede Metasolucionar esto omitiendo la clase interna de ModelForm para usarla en admin. Referencia: docs.djangoproject.com/en/1.10/ref/contrib/admin/…
user85461

62

** editar 30/11/2015 : en python 3, la __metaclass__variable global del módulo ya no es compatible . Además, a partir de Django 1.10la SubfieldBaseclase quedó en desuso :

de los documentos :

django.db.models.fields.subclassing.SubfieldBaseha quedado en desuso y se eliminará en Django 1.10. Históricamente, se usaba para manejar campos donde se necesitaba la conversión de tipos al cargar desde la base de datos, pero no se usaba en .values()llamadas ni en agregados. Ha sido reemplazado por from_db_value(). Tenga en cuenta que el nuevo enfoque no llama al to_python()método en la asignación como fue el caso SubfieldBase.

Por lo tanto, como lo sugiere la from_db_value() documentación y este ejemplo , esta solución debe cambiarse a:

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

Creo que una mejor manera de anular el clean_data en el administrador sería subclasificar el charfield; de esta manera, no importa de qué forma acceda al campo, "simplemente funcionará". Puede capturar ''justo antes de que se envíe a la base de datos, y capturar el NULL justo después de que salga de la base de datos, y el resto de Django no lo sabrá / no le importará. Un ejemplo rápido y sucio:

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  

Para mi proyecto, volqué esto en un extras.pyarchivo que vive en la raíz de mi sitio, luego puedo simplemente from mysite.extras import CharNullFielden el models.pyarchivo de mi aplicación . El campo actúa como un CharField: solo recuerde configurarlo blank=True, null=Trueal declarar el campo, o de lo contrario Django arrojará un error de validación (campo requerido) o creará una columna db que no acepta NULL.


3
en get_prep_value, debe quitar el valor, en caso de que el valor tenga varios espacios.
ax003d

1
La respuesta actualizada aquí funciona bien en 2016 con Django 1.10 y con EmailField.
k0nG

44
Si está actualizando a CharFieldpara que sea a CharNullField, deberá hacerlo en tres pasos. Primero, agregue null=Trueal campo y migre eso. Luego, realice una migración de datos para actualizar los valores en blanco para que sean nulos. Finalmente, convierta el campo a CharNullField. Si convierte el campo antes de realizar la migración de datos, su migración de datos no hará nada.
mlissner

3
Tenga en cuenta que en la solución actualizada, from_db_value()no debe tener ese contexparámetro adicional . Debería serdef from_db_value(self, value, expression, connection):
Phil Gyford

1
El comentario de @PhilGyford se aplica a partir de 2.0.
Shaheed Haque

16

Debido a que soy nuevo en stackoverflow, todavía no puedo responder a las respuestas, pero me gustaría señalar que desde un punto de vista filosófico, no puedo estar de acuerdo con la respuesta más popular a esta pregunta. (por Karen Tracey)

El OP requiere que su campo de barra sea único si tiene un valor, y nulo de lo contrario. Entonces debe ser que el modelo mismo se asegura de que este sea el caso. No se puede dejar que el código externo verifique esto, porque eso significaría que se puede omitir. (O puede olvidarse de verificarlo si escribe una nueva vista en el futuro)

Por lo tanto, para mantener su código verdaderamente OOP, debe usar un método interno de su modelo Foo. Modificar el método save () o el campo son buenas opciones, pero usar un formulario para hacerlo ciertamente no lo es.

Personalmente prefiero usar el CharNullField sugerido, para la portabilidad a los modelos que pueda definir en el futuro.


13

La solución rápida es hacer:

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

    if not self.bar:
        self.bar = None

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

2
tenga en cuenta que el uso MyModel.objects.bulk_create()evitaría este método.
BenjaminGolder

¿Se llama a este método cuando guardamos desde el panel de administración? Lo intenté pero no.
Kishan Mehta

1
@Kishan django-admin panel omitirá estos ganchos desafortunadamente
Vincent Buscarello

@ e-satis su lógica es sólida, así que implementé esto, pero el error sigue siendo un problema. Me dicen que nulo es un duplicado.
Vincent Buscarello

6

Otra posible solución

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

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

Esta no es una buena solución ya que está creando una relación innecesaria.
Burak Özdemir


1

Recientemente tuve el mismo requisito. En lugar de subclasificar diferentes campos, elegí anular el método save () en mi modelo (llamado 'MyModel' a continuación) de la siguiente manera:

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

Si tiene un modelo MyModel y desea que my_field sea Nulo o único, puede anular el método de guardado del modelo:

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 esta manera, el campo no puede estar en blanco solo será no vacío o nulo. los nulos no contradicen la unicidad


1

Puede agregar UniqueConstraintcon la condición de nullable_field=nully no incluir este campo en la fieldslista. Si también necesita una restricción cuyo nullable_fieldvalor no es null, puede agregar uno adicional.

Nota: UniqueConstraint se agregó desde 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')
        ]

¡Eso funciona! pero obtengo una excepción IntegrityError en lugar del error de validación del formulario. ¿Cómo manejas eso? ¿Capturarlo y aumentar ValidationError en las vistas crear + actualizar?
gek

0

Para bien o para mal, Django considera NULLque es equivalente a los NULLfines de las comprobaciones de unicidad. Realmente no hay otra forma de escribir su propia implementación de la verificación de unicidad que se considera NULLúnica, sin importar cuántas veces ocurra en una tabla.

(y tenga en cuenta que algunas soluciones de DB tienen la misma visión NULL, por lo que el código que se basa en las ideas de un DB NULLpuede no ser portátil para otros)


66
Esta no es la respuesta correcta. Vea esta respuesta para una explicación .
Carl G

2
De acuerdo, esto no es correcto. Acabo de probar IntegerField (en blanco = verdadero, nulo = verdadero, único = verdadero) en Django 1.4 y permite varias filas con valores nulos.
Slacy
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.