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




6 comentários:

  1. Este comentário foi removido pelo autor.

    ResponderExcluir
  2. Na verdade o queryset precisa ter mais intruções para se comportar melhor com sua necessidade. Para isso vc deve usar select_related, only e prefetch_related

    ResponderExcluir
  3. Valeu valder, vou testar sua dica e coloco o feedback.

    ResponderExcluir
  4. Oi Elton!!
    Parabéns pela iniciativa do blog! Estou iniciando no Python/Django e percebi que os campos ChoiceField são muito lentos (carregam tudo no HTML). O Select2 é melhor pra FKs com grande quantidade de registros.
    Uma das coisas que estão pegando aqui são os ChoiceFields "aninhados". Exemplo:
    - No models.py tenho uma classe Endereco que tem um atributo cidade (FK).
    - Para montar meu form, gostaria que o usuário selecionasse primeiro o país, depois o estado e daí então a cidade, filtrando os resultados de cada escolha.
    Porém, não tenho o país e estado no modelo... como faço isso?
    Abraços!

    ResponderExcluir
    Respostas
    1. Tudo bom Junior?

      Eu faria o seguinte:

      No forms.py defino um ModelForm, e acrescento dois ModelChoiceField, um com o pais e outro com o estado.

      Depois via javascript você vai fazendo a lógica que você explicou.

      Espero ter ajudado.

      Excluir
    2. Vou ter que aprender javascript também! Kkkk :/

      Excluir