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.