[Felipe Santiago] SQLAlchemy e memcached

Quando falamos de aplicações de alta perfomance, falamos basicamente em cache, cache e mais cache. E então, nos deparamos com a seguinte pergunta: como integrar uma ferramenta de mapeamento de objetos relacionais (ORM), que mantém associação e relacionamentos entre objetos em sessão, com uma solução de cache, como o memcached ? Complicou ? Então acalme-se, pois iremos explicar como fazer de uma forma limpa e elegante.

Para começar vamos falar um pouco do ORM SQLAlchemy, que estamos utilizando em um projeto. O SQLAlchemy é um framework python responsável por transformar suas tabelas em objetos relacionais, a fim de abstrair todo acesso ao banco, dando total flexibilidade ao desenvolvedor. Está atualmente na versão 0.5.6 e entre suas principais features estão, pool de conexões e gerencia de sessão. Nosso foco aqui não é se aprofundar no SQLAlchemy, mas sim em como integrá-lo com o memcached.

O grande gargalo das aplicações atualmente é, sem dúvida, o acesso ao banco, e para resolver esse problema nós utilizamos cache em memória, a fim de evitar acessar o banco toda vez que se quer ter acesso a alguma informação. O problema é que quando utilizamos um ORM, ele fica responsável pelo acesso ao banco e você perde o controle sobre as operações realizadas. O que nós queremos é ter o poder de um ORM, aliado ao poder do cache, e a solução passa por criar uma abstração no SQLAlchemy, que verifique se um determinado objeto está no cache, e ir ao banco somente se o ele não existir em cache.

Isso pode parecer simples, mas o SQLAlchemy na atual versão, mantém os objetos em sessão, para aumentar a performance, evitando ir ao banco sempre que um objeto é requisitado pela aplicação, e somente ele tem o controle desses objetos. Imagine que tenhamos uma associação simples entre jogador e clube, onde um jogador pertence a um único clube. Vejamos o exemplo abaixo:

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy import create_engine
 
Base = declarative_base()
class Jogador(Base):
 
    __tablename__ = 'jogador'
 
    id = Column('jogador_id', Integer, primary_key=True)
    nome = Column('nome_txt', String)
    clube_id = Column('clube_id', Integer, ForeignKey("clube.clube_id"))
    clube = relation(Clube, backref="jogadores")
 
# cria uma sessão com o banco
engine = create_engine('mysql://root:@localhost/teste', echo=True)
session = scoped_session(sessionmaker(bind=engine, autocommit=True, autoflush=True))
 
jogador = session.query(Jogador).get(id) #recupera o jogador de id=1
clube = jogador.clube #recupera o clube do jogador
print "o nome do jogador e %s" %clube.nome

Ao procurar por um jogador, o ORM verifica se ele está na sessão, se não estiver ele irá executar um SELECT no banco, o mesmo acontecendo com o clube. Entretanto, nós desejamos que antes dele fazer acesso ao banco verifique se o mesmo está em cache. Para isso precisamos sobreescrever o objeto query da sessão do SQLAlchemy, criando uma classe, CachedQuery, que extende sqlalchemy.orm.query.Query e implementa essa lógica.

import memcache
from sqlalchemy.orm.query import Query
 
cache = memcache.Client(['127.0.0.1:11211'], debug=0)
 
class CachedQuery(Query):
 
    def get(self, ident, **kw):
 
        mapper = self._mapper_zero()
        session = self.session
 
        # gera uma chave para o objeto
        key = mapper.identity_key_from_primary_key(ident)
 
        # pega o objeto da sessão, se existir
        cacheobj = session.identity_map.get(key)
 
        # gerando uma chave para o memcached module.Classe(id)
        cache_key = "%s.%s%s" % (key[0].__module__,key[0].__name__,key[1])
 
        if not cacheobj:
 
            # pega o objeto do memcached, se existir
            cacheobj = cache.get(cache_key)
 
            if cacheobj is not None:
                # recuperando do cache e setando na sessao
                cacheobj.__dict__["_sa_instance_state"] = attributes.instance_state(cacheobj)
                session.add(cacheobj)
            else:
                # nao existe no cache, pega do banco
                cacheobj = super(CachedQuery, self).get(ident)
                if cacheobj is None:
                    return None
                # setando objeto no cache
                cache.set(cache_key, cacheobj)
        else:
            # recuperando da sessao
            pass
 
        return cacheobj

Perceba que ao extender Query e sobreescrever o método get, estamos alterando apenas o seu comportamento, não interferindo no resto da classe. Poderíamos sobreescrever também os metodos save, update e delete, para salvar, atualizar e remover o objeto também do cache. Depois de criada a classe CachedQuery basta instanciar a sessão do SQLALchemy, utilizando essa classe. voltando ao nosso exemplo do jogador a create_session ficaria da seguinte forma

 
..
 
# cria uma sessão com o banco
engine = create_engine('mysql://root:@localhost/teste', echo=True)
session = scoped_session(sessionmaker(bind=engine, autocommit=True, autoflush=True, query_cls=CachedQuery))
 
...

A partir daqui todo acesso ao banco passará antes pelo memcached aumentando de forma exponencial a performance de sua aplicação.