terça-feira, 4 de dezembro de 2012

Django: Managers e Querysets

Uma das coisas que eu mais uso nos models do Django são os Managers, aquelas classes que adicionam funções aos `.objects.sua_funcao_aqui` (ou até mesmo, criar outro "objects").

O único problema, era que essa função estava restrita à chamadas via `.objects`, ou seja, enquanto é lindo fazer:

Post.objects.da_semana()

Não era possível fazer:

Comment.objects.da_semana().aprovados()

Não era. (Mentira, sempre foi, eu é quem não sabia fazer)

A solução surgiu em um post no DjangoSnippets.org. E era muito mais simples do que eu esperava.

Da mesma forma que criamos novos Managers para Models, podemos criar Querysets para Managers. Simples assim:

# -*- coding: utf-8 -*-
from django.db import models
from django.db.models.query import QuerySet

class BlogQueryset(QuerySet):
    u'''Aqui nós definimos as funções que retornarão o que 
    queremos

    Você pode retornar qualquer coisa da função, mas tenha 
    em mente que se você não retornar um QuerySet, não será
    possível continuar o encadeamento de filter/order_by/etc
    '''
    def da_semana(self):
        return self.filter(created_at__gt=...)
    def aprovados(self):
        return self.filter(published=True)


class BlogManager(models.Manager):
    u'''Aqui nós precisamos repetir as funções, mas apenas 
    suas definições.
    
    O corpo da função é apenas a chamada para a função no 
    QuerySet, é exatamente assim que as funções filter, 
    exclude, order_by, etc, são feitas no Manager padrão 
    do Django
    
https://github.com/django/django/blob/master/django/db/models/manager.py
    '''
    def da_semana(self):
        return self.get_query_set().da_semana()
    def aprovados(self):
        return self.get_query_set().aprovados()
    def get_query_set(self):
        '''É aqui onde a mágica acontece, pra que toda essa 
        história funcione, você precisa que seja retornada 
        uma instancia do QuerySet que nós criamos'''
        return BlogQuerySet(self.model, using=self._db)
        # Particularmente eu acho que seria mais interessante 
        # usar self.queryset_class, assim não seria preciso 
        # ficar reescrevendo essa função desnecessariamente.
        # Vou tentar alterar no branch de desenvolvimento e 
        # se passar em todos os testes eu faço um pull request

class Post(models.Model):
    # Aqui vem seus campos como geralmente faria
    # ... 
    published = models.BooleanField(db_index=True)
    created_at = models.DateTimeField(db_index=True, auto_now_add=True)
    objects = BlogManager()

Com isso você já pode encadear suas chamadas aos seus filtros :)

pony powered

quinta-feira, 7 de junho de 2012

Snippet: Gerar string aleatória em Python

A cada 2 meses eu preciso, por algum motivo, gerar uma string aleatória, e a cada 2 meses eu abro um terminal do python, importo random, string e fico brincando por alguns minutos até chegar a uma solução que me deixe feliz (ou até cansar de brincar mesmo).

Mas dessa vez resolvi achar alguma solução que me parecesse suficientemente aleatória e que eu não precisasse abrir o terminal de novo e achar mais uma solução, pesquisando nas internets, achei um tópico no stackoverflow que me pareceu interessante pois ele usava a função os.urandom que de acordo com a documentação é aleatória o suficiente para uso criptográfico.

Com base no que eu vi no stackoverflow, fiz a seguinte solução:

import os
import string

def generate_random_string(length, stringset=string.ascii_letters+string.digits+string.punctuation):
    '''
    Returns a string with `length` characters chosen from `stringset`
    >>> len(generate_random_string(20) == 20 
    '''
    return ''.join([stringset[i%len(stringset)] \
        for i in [ord(x) for x in os.urandom(length)]])

quarta-feira, 6 de junho de 2012

Pare de usar a função `.exclude` do Django!

Atenção: Se você é DBA, pare de ler por aqui, esse post vai te magoar!

Ta bom, não esse tipo de DBA, mas mesmo assim...

Pra você que pode não saber o que é, a função queryset.exclude serve para você remover objetos da queryset que não lhe interessam, por exemplo, se você tem uma lista de noticias agrupadas por categoria, você pode usar o exclude para remover as noticias de uma determinada categoria. Mas isso tem um custo.

Quando você usa a função exclude, a query gerada pelo Django passa a usar uma condição negativa em algum ponto, seja usando um NOT, seja usando um <>, e isso tem um problema sério: o indice do banco de dados pode não ser usado corretamente, especialmente se você pega um código como o que eu já peguei assim: Model.objects.exclude(id__in=[1,2,3,...,20]).

Existem situações legitimas em que usar o exclude é aceitável (ao menos eu acho que deve existir, senão essa função nem existiria), mas no meu caso, onde uma das tabelas em que era feito o JOIN tinha mais de 1 milhão de registros, isso tava levando mais de 3 segundos pra executar uma query. UMA QUERY, ainda tinha o site todo pra carregar e ele já estava me ferrando ali.

O que fazer então?

No meu caso, existia uma variável que era uma lista de ID's dos objetos que eu não queria e fiz o que poderia ser impensável pra você DBA teimoso:

# Quero 5 objetos do banco de dados
limit = 5 

# Esses são os IDs dos objetos que eu não quero
excluded = [1,2,3,4,5,6] 

queryset = Model.objects.filter(foo=bar).order_by('-data')

objetos = []

# Pegamos uma quantidade de objetos que garanta 
# que eu va conseguir exatamente `limit` objetos
for objeto in queryset[:limit+len(excluded)]: 
    if objeto.id not in excluded:
        objetos.append(objeto)
    if len(objetos) >= limit:
        break

E pronto, tenho uma lista com apenas os objetos que eu quero exibir.

Existem (varias) situações em que essa solução não serve, por exemplo se você precisa usar o Paginator, mas ainda assim se é o caso que você precisa apenas de uma quantidade pequena de objetos, pode ser mais vantajoso adotar uma solução como essa.

Depois que fiz essa alteração no código, o carregamento inteiro da página caiu pra 0,7 segundos de execução de query, ainda tem muito o que melhorar porque o código ta f0d@, mas já é alguma coisa :)

PS: Esses tempos de execução de SQL eu peguei pelo django debug toolbar.

segunda-feira, 12 de março de 2012

quinta-feira, 8 de março de 2012

Melhorando a escrita dos testes em Django

Esses dias eu estava discutindo com o Avelino que é um saco escrever testes porque em toda maldita classe de testes eu preciso recriar os objetos, tipo assim:

class TesteDaMorte(TestCase):
    def setUp(self):
        # Aqui vem um bilhao de linhas pra criar
        # algumas instancias de objetos.
        # E nenhuma dessas linhas testa alguma 
        # coisa de fato

Eu argumentei que isso é um porre, e se repete várias vezes (código repetido nunca é bom), ai um belo dia o model recebe um novo campo e pronto, la vai você retardadamente reescrever todos os testes pra adicionar o novo campo na criação do objeto e não pra escrever os testes em cima dele. Eu acho esse esforço um desperdicio, mas ao mesmo tempo, sei que testes são extremamente importantes.

Aah, a documentação, nada como ler a documentação...

Bom, como meu índice de preguiça costuma ser representado em gráficos com o texto "MAX" no topo do eixo Y, eu decidi que não ia mais ficar escrevendo essa caralhada de código desnecessário em testes e saí em busca de alternativas.

Eu já tinha pensado em alguma hipoteses como heranças ou chamar setUp's de outros testes, mas não me agradou muito essas opções pelo que o Avelino vivia falando: "ah, mas os testes precisam ser independentes" (ou algo assim, não lembro exatamente o termo, mas era algo assim), e de fato, eu concordo, os testes precisam rodar sozinhos e serem capazes de se testarem a si mesmos isoladamente.

Depois pensei em outras alternativas que provavelmente ainda não existem implementadas, como definir dependencias de testes. Ou seja, não adianta você testar B, se A não passar. Mas como eu disse, não encontrei nenhuma documentação falando se isso existe, então ignorei logo (não tenho experiencia com testes o suficiente pra poder escrever uma solução desse porte).

Cansado de pensar em soluções, resolvi fazer o óbvio: fui procurar a solução na internet. Naturalmente a solução veio direto do site do Django na página de testes: Fixtures!

Quando definindo uma classe de testes, adicione um atributo na classe chamado "fixtures" e indicando quais fixtures você gostaria de importar antes de iniciar os testes. Assim:

class TesteDaMorte(TestCase):
    fixtures = ['tests/usuarios.json']
    def setUp(self):
        # Voce vai poder apagar essa funcao em 90% 
        # dos seus testes e ir direto ao que interessa

Dessa forma, antes de iniciar os testes (e antes mesmo de chamar a função setUp), o Django vai importar as fixtures do arquivo usuarios.json e vai criar os dados pra você.

Pra criar o arquivo de fixtures, você pode iniciar um novo syncdb em um banco de dados temporário, em sqlite mesmo se for o caso, inserir os dados la pelo admin (ou pelo shell) e executar o commando dumpdata do Django, mais ou menos assim:

python manage.py --indent=2 dumpdata auth.User profile.Profile > myapp/fixtures/tests/usuarios.json

Isso vai gerar o arquivo usuarios.json (desde que o diretório exista, claro) com os dados de User e Profile (supondo que seja assim que você o chamou). A ordem em que você adiciona eles no comando é relevante, é importante que as classes sejam especificadas por ordem de parentesco, ou seja, as classes pai aparecem antes das classes filhas (sendo pai=classe que tem o ID e filho=classe que tem a ForeignKey).

Com isso você pode focar em escrever o que importa de verdade: testes.

quinta-feira, 23 de fevereiro de 2012

commit messages

Você sabe que é verdade.

UPDATE: Obrigado Blogger por remover minhas imagens por algum motivo...