campos exclusivos que permitem valores nulos em Django

votos
92

Tenho modelo Foo que tem bar campo. O campo de bar deve ser original, mas permite nulos nele, ou seja, eu quero permitir que mais de um registro se o campo bar é null, mas se não for nullos valores devem ser exclusivos.

Aqui é 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 objetos foo onde bar é nulo dá-me um erro: Foo com este Bar já existe.

No entanto, quando eu inserir na base 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, que me permite inserir mais de 1 registro com bar sendo nula, então o banco de dados permite que eu faça o que eu quero, é apenas algo de errado com o modelo de Django. Alguma ideia?

EDITAR

A portabilidade da solução, tanto quanto DB não é um problema, estamos felizes com Postgres. Eu tentei configuração única de um exigível, que era a minha função retornando verdadeiro / falso para valores específicos de bar , não deu quaisquer erros, no entanto cosido como não teve nenhum efeito em tudo.

Até agora, eu removi o especificador único do bar de propriedade e manusear o bar singularidade na aplicação, no entanto, ainda à procura de uma solução mais elegante. Quaisquer recomendações?

Publicado 18/01/2009 em 02:13
fonte usuário
Em outras línguas...                            


7 respostas

votos
115

Django não considerou NULL para ser igual a NULL com a finalidade de verificações de exclusividade desde ticket # 9039 foi fixado, veja:

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

A questão aqui é que o valor normalizado "em branco" para uma forma CharField é uma cadeia vazia, não Nenhum. Então, se você deixar o campo em branco, você tem uma cadeia vazia, não NULL, armazenado no DB. strings vazias são iguais para esvaziar cordas para verificações de exclusividade, tanto sob Django e regras de banco de dados.

Você pode forçar a interface de administração para armazenar NULL para uma cadeia vazia, fornecendo o seu próprio modelo de formulário personalizado para Foo com um método clean_bar que transforma a cadeia vazia em Nenhum:

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
Respondeu 09/09/2009 em 15:26
fonte usuário

votos
52

** edição 2015/11/30 : em Python 3, o módulo global __metaclass__variável é não mais suportada . Adicionalmente, a partir Django 1.10da SubfieldBaseclasse foi preterido :

dos docs :

django.db.models.fields.subclassing.SubfieldBasefoi descontinuado e vai ser removido no Django 1,10. Historicamente, foi utilizado para lidar com os campos em que a conversão foi tipo necessários durante o carregamento a partir da base de dados, mas não foi utilizado em .values()chamadas ou em agregados. Ele foi substituído por from_db_value(). Note-se que a nova abordagem não chama o to_python()método de atribuição como foi o caso com SubfieldBase.

Portanto, como sugerido pela from_db_value() documentação e este 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 substituindo o cleaned_data na administração seria a subclasse o CharField - "só trabalho" Desta forma, não importa qual a forma acessa o campo, ele vai Você pode pegar o ''pouco antes de ser enviado para o banco de dados, e pegar o NULL logo após ele sai do banco de dados, eo resto do Django não vai saber / cuidado. 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  

Para o meu projeto, eu larguei isso em um extras.pyarquivo que vive na raiz do meu site, então eu posso apenas from mysite.extras import CharNullFieldno do meu aplicativo models.pyarquivo. O campo age como um CharField - basta lembrar para definir blank=True, null=Truequando declarar o campo, ou de outra forma Django lançará um erro de validação (campo obrigatório) ou criar uma coluna db que não aceita NULL.

Respondeu 20/12/2009 em 04:40
fonte usuário

votos
10

Porque eu sou novo para stackoverflow Eu ainda não estou autorizado a responder a respostas, mas eu gostaria de salientar que a partir de um ponto de vista filosófico, não posso concordar com a resposta mais popular tot esta pergunta. (Por Karen Tracey)

O OP exige seu campo bar de ser único, se tiver um valor, e NULL contrário. Em seguida, ele deve ser que o próprio modelo fizer certo de que este é o caso. Ele não pode ser deixado ao código externo para verificar isso, porque isso significaria que ele pode ser ignorada. (Ou você pode se esqueça de verificar se você escrever uma nova visão no futuro)

Portanto, para manter seu código realmente OOP, você deve usar um método interno do seu modelo Foo. Modificando o método save () ou o campo são boas opções, mas usando uma forma de fazer isso certamente não é.

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

Respondeu 09/08/2012 em 12:52
fonte usuário

votos
10

A correção rápida é fazer:

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

    if not self.bar:
        self.bar = None

    super(Foo, self).save(*args, **kwargs)
Respondeu 26/06/2010 em 17:24
fonte usuário

votos
4

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)
Respondeu 07/07/2011 em 09:10
fonte usuário

votos
2

Para melhor ou pior, Django considera NULLser equivalente a NULL, para fins de verificações de exclusividade. Não há realmente nenhuma maneira de contornar isso curto de escrever sua própria implementação da verificação de exclusividade que considera NULLser único, não importa quantas vezes ela ocorre em uma tabela.

(e ter em mente que algumas soluções DB ter a mesma visão do NULL, então código depender de uma ideias da DB sobre NULLpode não ser portátil para os outros)

Respondeu 18/01/2009 em 04:49
fonte usuário

votos
1

Recentemente, tive a mesma exigência. Em vez de subclassificação campos diferentes, eu escolhi para substituir o metod save () no meu modelo (chamado 'MyModel' abaixo) como 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()    
Respondeu 22/06/2010 em 12:50
fonte usuário

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more