Campos exclusivos que permitem nulos no Django


135

Eu tenho o modelo Foo que tem barra de campo. O campo da barra deve ser exclusivo, mas permitir nulos, o que significa que desejo permitir mais de um registro se o campo da barra for null, mas se não for, nullos valores deverão ser exclusivos.

Aqui está o meu 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)

E aqui está o SQL correspondente para a tabela:

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

Ao usar a interface de administração para criar mais de 1 objeto foo em que a barra é nula, ocorre um erro: "O foo com esta barra já existe".

No entanto, quando insiro no banco de dados (PostgreSQL):

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

Isso funciona, muito bem, me permite inserir mais de 1 registro com a barra sendo nula, então o banco de dados permite que eu faça o que eu quero, é apenas algo errado com o modelo do Django. Alguma ideia?

EDITAR

A portabilidade da solução no que diz respeito ao DB não é um problema, estamos felizes com o Postgres. Eu tentei definir exclusivo para um callable, que era minha função retornando True / False para valores específicos de bar , não dava nenhum erro, por mais costurado como se não tivesse nenhum efeito.

Até agora, removi o especificador exclusivo da propriedade da barra e lidei com a exclusividade da barra no aplicativo, mas ainda estou procurando uma solução mais elegante. Alguma recomendação?


Eu não posso comentar ainda, então aqui está uma pequena adição ao mightyhal: Desde o Django 1.4, você precisaria def get_db_prep_value(self, value, connection, prepared=False)como chamada de método. Consulte groups.google.com/d/msg/django-users/Z_AXgg2GCqs/zKEsfu33OZMJ para obter mais informações. O método a seguir também funciona para mim: def get_prep_value (self, value): if value == "": #if Django tenta salvar '' string, envie o retorno db None (NULL) passa o valor
Jens

Abri um ingresso do Django para isso. Adicione seu suporte. code.djangoproject.com/ticket/30210#ticket
Carl Brubaker

Respostas:


154

O Django não considerou NULL igual a NULL para fins de verificação de exclusividade desde que o ticket # 9039 foi corrigido, consulte:

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

O problema aqui é que o valor "em branco" normalizado de um formulário CharField é uma sequência vazia, não Nenhuma. Portanto, se você deixar o campo em branco, obterá uma string vazia, não NULL, armazenada no banco de dados. Strings vazias são iguais a strings vazias para verificações de exclusividade, sob as regras do Django e do banco de dados.

Você pode forçar a interface de administração a armazenar NULL para uma cadeia vazia, fornecendo seu próprio formulário de modelo personalizado para Foo com um método clean_bar que transforma a cadeia vazia em 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 a barra estiver em branco, substitua-a por Nenhuma no método pre_save. Código será mais seco, suponho.
Ashish Gupta

6
Esta resposta ajuda apenas na entrada de dados com base em formulário, mas não faz nada para realmente proteger a integridade dos dados. Os dados podem ser inseridos por meio de scripts de importação, a partir do shell, por meio de uma API ou qualquer outro meio. É muito melhor substituir o método save () do que criar casos personalizados para todos os formulários que possam tocar nos dados.
shacker 27/07

O Django 1.9+ requer um atributo fieldsou excludeem ModelForminstâncias. Você pode contornar isso, omitindo a Metaclasse interna do ModelForm para uso em admin. Referência: docs.djangoproject.com/en/1.10/ref/contrib/admin/…
user85461 15/17

62

** editar 30/11/2015 : No python 3, a __metaclass__variável global do módulo não é mais suportada . Adicionalmente, a partir Django 1.10da SubfieldBaseclasse foi preterido :

dos documentos :

django.db.models.fields.subclassing.SubfieldBasefoi descontinuado e será removido no Django 1.10. Historicamente, era usado para manipular campos onde a conversão de tipo era necessária ao carregar do banco de dados, mas não era usada em .values()chamadas ou em agregados. Foi substituído por from_db_value(). Observe que a nova abordagem não chama o to_python()método na atribuição, como foi o caso SubfieldBase.

Portanto, conforme sugerido pela from_db_value() documentação e neste exemplo , esta solução deve ser alterada para:

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

Eu acho que uma maneira melhor do que substituir os dados limpos no administrador seria subclassificar o charfield - dessa forma, não importa qual formulário acesse o campo, ele "funcionará". Você pode pegar o ''pouco antes de ele ser enviado ao banco de dados, e pegar o NULL logo após sair do banco de dados, e o resto do Django não saberá / se importará. Um exemplo rápido e sujo:

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  

No meu projeto, coloquei isso em um extras.pyarquivo que fica from mysite.extras import CharNullFieldna raiz do meu site, e posso apenas no models.pyarquivo do meu aplicativo . O campo age como um CharField - lembre-se de definir blank=True, null=Trueao declarar o campo, ou o Django lançará um erro de validação (campo obrigatório) ou criará uma coluna db que não aceita NULL.


3
em get_prep_value, você deve retirar o valor, caso o valor tenha vários espaços.
ax003d

1
A resposta atualizada aqui funciona bem em 2016 com o Django 1.10 e usando o EmailField.
k0nG

4
Se você estiver atualizando a CharFieldpara ser um CharNullField, precisará fazer isso em três etapas. Primeiro, adicione null=Trueao campo e migre isso. Em seguida, faça uma migração de dados para atualizar quaisquer valores em branco para que sejam nulos. Por fim, converta o campo em CharNullField. Se você converter o campo antes de fazer a migração de dados, sua migração de dados não fará nada.
mlissner

3
Observe que na solução atualizada, from_db_value()não deve ter esse contexparâmetro extra . Deveria serdef from_db_value(self, value, expression, connection):
Phil Gyford 15/0318

1
O comentário de @PhilGyford se aplica a partir de 2.0.
Shaheed Haque

16

Como sou iniciante no stackoverflow, ainda não tenho permissão para responder às respostas, mas gostaria de salientar que, de um ponto de vista filosófico, não posso concordar com a resposta mais popular para esta pergunta. (de Karen Tracey)

O OP exige que seu campo de barra seja único se tiver um valor e nulo caso contrário. Então deve ser que o próprio modelo verifique se é esse o caso. Não pode ser deixado para o código externo verificar isso, porque isso significa que pode ser ignorado. (Ou você pode esquecer de verificar se escrever uma nova exibição no futuro)

Portanto, para manter seu código verdadeiramente OOP, você deve usar um método interno do seu modelo Foo. Modificar o método save () ou o campo são boas opções, mas o uso de um formulário para isso certamente não é.

Pessoalmente, prefiro usar o CharNullField sugerido, para portabilidade para modelos que eu possa definir no futuro.


13

A solução rápida é:

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

    if not self.bar:
        self.bar = None

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

2
esteja ciente de que o uso MyModel.objects.bulk_create()ignoraria esse método.
BenjaminGolder

Esse método é chamado quando salvamos no painel de administração? Eu tentei, mas não.
Kishan Mehta

1
Painel django-admin @Kishan irão ignorar estes ganchos infelizmente
Vincent Buscarello

@ e-satisf sua lógica é boa, então eu implementei isso, mas o erro ainda é um problema. Estou sendo informado que nulo é uma duplicata.
Vincent Buscarello

6

Outra solução possível

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

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

Esta não é uma boa solução, pois você está criando uma relação desnecessária.
Burak Özdemir


1

Recentemente, tive o mesmo requisito. Em vez de subclassificar campos diferentes, optei por substituir o método save () no meu modelo (chamado 'MyModel' abaixo) da seguinte maneira:

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 você possui um modelo MyModel e deseja que meu_campo seja Nulo ou exclusivo, você pode substituir o método de salvamento do 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)

Dessa forma, o campo não pode ficar em branco, apenas ficará em branco ou nulo. nulos não contradizem a exclusividade


1

Você pode adicionar UniqueConstraintcom a condição de nullable_field=nulle não incluir esse campo na fieldslista. Se você também precisar de restrição cujo nullable_fieldvalor não seja null, poderá adicionar uma adicional.

Nota: UniqueConstraint foi adicionado desde o 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')
        ]

Isso funciona! mas recebo uma exceção IntegrityError em vez do erro de validação do formulário. Como você lida com isso? Pegá-lo e aumentar ValidationError em criar + atualizar visualizações?
gek

0

Para o bem ou para o mal, o Django considera NULLequivalente NULLpara fins de checagem de exclusividade. Realmente, não há como evitar escrever sua própria implementação da verificação de exclusividade, que considera NULLúnica, não importa quantas vezes ocorra em uma tabela.

(e lembre-se de que algumas soluções de banco de dados têm a mesma visão NULL, portanto, o código que se baseia nas idéias de um banco de dados NULLpode não ser portátil para outros)


6
Esta não é a resposta correta. Veja esta resposta para explicação .
Carl G

2
Concordou que isso não está correto. Acabei de testar o IntegerField (em branco = True, null = True, exclusivo = True) no Django 1.4 e permite várias linhas com valores nulos.
slacy 6/09/12
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.