quarta-feira, 30 de outubro de 2013

Overload com ForeignKey no Django

No Django os campos do tipo ForeignKey são preenchidos por dados que vem de uma QuerySet, até ai nenhuma novidade, mas você sabia que as QuerySets são implementadas utilizando o recurso de avaliação preguiçosa?

Mas o que é avaliação preguiçosa e o que isso tem a ver com o tema?

Quando você executa algo como ClasseX.objects.all(), você recebe um QuerySet com todos os registros, nenhuma novidade. Mas você sabia que somente quando você tentar acessar algum atributo é que um comando SQL será enviado para o banco para recuperar o valor deste atributo? É por isso que é chamada de avaliação preguiçosa, pois o ORM só vai enviar um comando SQL quando realmente for necessário. E isso poupa bastante recurso e tempo.

Mas cuidado... esse comportamento é uma faca de dois gumes.

Imagine que você tem uma ForeignKey que aponta para uma tabela com 2000 registros, na melhor das hipóteses você terá 2000 consultas ao banco, e se a classe para qual aponta o ForeignKey usar herança, a situação fica ainda pior, pois não teremos uma consulta simples, mas um join com outra(s) tabela(s), que aumentará ainda mais o tempo de resposta.

Então como faço para contornar este "problema"?

Eu consigo contornar este problema usando um Form, na verdade um ModelForm:

class ClienteForm(forms.ModelForm):
    choices = PessoaBase.objects.values_list("id", "nome").all()
    """ Precisamos executar a etapa abaixo, senão o admin vai
    ficar pegando o primeiro registro, e não queremos isso.

    Aqui eu usei -------- para seguir o que o Django adota, mas
    você pode colocar qualquer mensagem.

    Se você conseguir aperfeiçoar este código, compartilhe conosco.

    ;)
    """
    choices = [('','--------')] + [i for i in choices]

    pessoa_base = forms.ChoiceField(choices=choices)
    def __init__(self, *args, **kwargs):
        super(ClienteForm, self).__init__(*args, **kwargs)

    class Meta:
        model = Cliente

    def clean(self):
        """ É preciso sobrescrever o método clean porque o Django
        vai estar esperando um objeto do tipo PessoaBase, e caso 
        não façamos isso ele vai receber um inteiro e levantar uma
        exceção
        """
        cleaned_data = self.cleaned_data
        pessoa_base_id = cleaned_data.get('pessoa_base')
        try:
            pessoa_base = PessoaBase.objects.get(id=pessoa_base_id)
        except PessoaBase.DoesNotExist:
            raise forms.ValidationError('Escolha inválida!')

        cleaned_data["pessoa_base"] = pessoa_base
        return cleaned_data

Para finalizar só precisamos vincular nosso form ao admin(caso você esteja usando):

class ClienteAdmin(admin.ModelAdmin):
    model = Cliente
    form = ClienteForm

Apenas para vocês terem uma noção da diferença que essa técnica simples causa no desempenho, vejam os prints que fiz antes e depois de usar código acima(observem os campos hora e SQL):




Problemas com ordenção no Django usando PostgreSQL

Hoje perdi algumas horas por causa de um problema que tinha uma aparência simples, ordenação de campos CharField.

O que estava aconcetecendo era que as letras minúsculas ou acentuadas iam para o fim da fila, ou seja, ao invés de ter A,a,á,b,c,d o retorno era A,a,b,c,d,á.

Após muito pesquisar, encontrei o aparente problema, e não é culpa do Django. O problema é que quando fui criar o banco de dados, utilizei o encoding UTF-8, e não LATIN1, que é o padrão.

Como solucionar?

Faça um backup do seu banco de dados, apague-o, e crie novamente deixando o enconding como LATIN1. Depois é só restaurar o backup e voltar a programar.