Ao desenvolver ou oferecer suporte a um aplicativo do Google App Engine, muitas vezes é útil poder manipular o armazenamento de dados de maneira não tão adequada para o modelo de solicitação/resposta que funciona tão bem para servir aplicativos da web. Antes, fazer operações desse tipo implicava em soluções alternativas, como o app3 ou o App Rocket. A partir da versão 1.1.9 do SDK do Google App Engine, no entanto, há uma maneira nova de interagir com o armazenamento de dados: o módulo remote_api. Esse módulo permite o acesso remoto ao armazenamento de dados do Google App Engine, usando as mesmas APIs que você conhece e adora usar para criar aplicativos do Google App Engine.
Neste artigo, vamos apresentar o módulo remote_api, descrever as suas funcionalidades básicas e mostrar como obter um console interativo com acesso ao armazenamento de dados do seu aplicativo. Em seguida, forneceremos uma visão geral das limitações do módulo remote_api. Por último, analisaremos um exemplo mais sofisticado: uma implementação de parte de uma operação de mapeamento/redução do "Map", permitindo executar uma função em todas as entidades de um tipo.
O módulo remote_api consiste em duas partes: um "manipulador", que você instala no servidor para manipular as solicitações do armazenamento de dados remoto, e um "stub", que você configura no cliente para traduzir as solicitações do armazenamento de dados em chamadas para o manipulador remoto. Como o remote_api trabalha no nível mais baixo do armazenamento de dados, uma vez configurado o stub, não é preciso se preocupar com o fato de estar operando em um armazenamento de dados remoto: com alguns avisos, ele funciona exatamente como se você estivesse acessando diretamente o armazenamento de dados.
É muito fácil instalar o manipulador. Basta adicionar as linhas a seguir ao app.yaml na chave "handlers":
handlers:
- url: /remote_api
script: $PYTHON_LIB/google/appengine/ext/remote_api/handler.py
login: admin
Isso instala o manipulador remote_api no URL "/remote_api ". Se quiser, você pode escolher uma extremidade diferente, mas será preciso alterar os exemplos abaixo. Observe que o manipulador especifica "login:admin". Isso é extremamente importante pois não queremos dar a ninguém acesso irrestrito ao nosso armazenamento de dados!
Depois de atualizar o arquivo app.yaml, você terá que executar appcfg.py update para que o seu aplicativo envie o mapeamento novo.
Uma das coisas mais simples e úteis a se fazer com o remote_api é configurar um console Python interativo que tem acesso ao seu armazenamento de dados de produção. Crie um novo arquivo em Python chamado "appengine_console.py" e acrescente o seguinte texto a ele:
#!/usr/bin/python
import code
import getpass
import sys
sys.path.append("~/google_appengine")
from google.appengine.ext.remote_api import remote_api_stub
from google.appengine.ext import db
def auth_func():
return raw_input('Username:'), getpass.getpass('Password:')
if len(sys.argv) < 2:
print "Usage: %s app_id [host]" % (sys.argv[0],)
app_id = sys.argv[1]
if len(sys.argv) > 2:
host = sys.argv[2]
else:
host = '%s.appspot.com' % app_id
remote_api_stub.ConfigureRemoteDatastore(app_id, '/remote_api', auth_func, host)
code.interact('App Engine interactive console for %s' % (app_id,), None, locals())
O código aqui deve ser bem autoexplanatório. Na linha 6, adicionamos o diretório do SDK ao caminho do Python. O exemplo dado aqui funcionará sem problemas se estiver usando Linux ou OS X e se o SDK estiver instalado na pasta de início. No Windows, altere-o para o caminho onde você instalo o SDK, como "c:\google_appengine". Se estiver usando o iniciador no OS X, defina o caminho para "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine".
Na linha 11, definimos uma função para adquirir credenciais do usuário quando elas forem solicitadas. No exemplo, nós simplesmente as solicitamos pelo console, mas isso pode ser feito por qualquer outro método. Começando na linha 14, extraímos o ID do aplicativo e o nome do host opcional da linha de comando, e na linha 22, usamos uma função auxiliar fornecida pelo remote_api para configurar o ambiente para enviar todas as solicitações do armazenamento de dados para o armazenamento de dados remoto. Por último, chamamos code.Interact, que inicia um interpretador interativo de Python.
Como você provavelmente irá querer acessar os módulos definidos pelo seu aplicativo (como as definições de modelo, por exemplo), precisamos nos certificar de que o seu aplicativo esteja no caminho do Python. A forma mais fácil de fazer isso é alterando o diretório para o diretório raiz do seu aplicativo (aquele que possui o app.yaml) antes de executar o nosso comando novo. Em seguida, execute:
python appengine_console.py myapp
Substitua "myapp" pelo ID do seu aplicativo e você receberá uma solicitação do console interativo Python.
Para fins de demonstração, usaremos o aplicativo Livro de visitas da documentação Primeiros passos. Imaginando que estamos no diretório raiz do aplicativo do livro de visitas, emita:
>>> import helloworld >>> from google.appengine.ext import db
Agora que temos acesso ao conteúdo do aplicativo, podemos emitir comandos como faríamos se estivéssemos gravando um código para ser executado no servidor:
>>> # Fetch the most recent 10 guestbook entries
>>> entries = helloworld.Greeting.all().order("-date").fetch(10)
>>>
>>> # Create our own guestbook entry
>>> helloworld.Greeting(content="A greeting").put()
Em geral, o console irá atuar exatamente como se você estivesse acessando o armazenamento de dados diretamente, mas como o script está sendo executado no seu próprio computador, não é preciso se preocupar sobre quanto tempo ele levará para ser executado, e você pode acessar normalmente todos os arquivos e recursos no computador local.
O módulo remote_api se esforça muito para garantir que, até onde for possível, ele se comporte exatamente como o armazenamento de dados nativo do Google App Engine. Em alguns casos, isso significa fazer as coisas de forma menos eficiente do ocorreria em outra situação. Ao usar o remote_api, lembre-se:
Como você está acessando o armazenamento de dados por meio de HTTP, há um pouco mais de sobrecarga e latência do que quando ele é acessado localmente. Para agilizar as coisas e diminuir a carga, tente limitar o número de percursos completos por meio de operações em lote de gets e puts, e pela obtenção de lotes de entidades nas consultas. Esse é um bom conselho não somente para o remote_api, mas também para usar o armazenamento de dados em geral, pois uma operação em lote é considerada apenas uma única operação do armazenamento de dados. Por exemplo, em vez disto:
for key in keys: rec = MyModel.get(key) rec.foo = bar rec.put()
faça isto:
records = MyModel.get(keys) for rec in records: rec.foo = bar db.Put(records)
Os dois exemplos têm o mesmo efeito, mas o último requer apenas dois percursos completos no total, enquanto o primeiro exige dois para cada entidade.
Como o remote_api opera por HTTP, cada chamada feita ao armazenamento de dados causa o uso da cota para as solicitações HTTP, bytes de entrada e de saída, bem como a cota normal do armazenamento de dados. Lembre-se de que se você estiver usando o remote_api, faça atualizações em massa.
Assim como quando sendo executadas nativamente, o limite de 1 MB para solicitações e respostas da API ainda é aplicável. No caso de entidades grandes, é preciso limitar o número de obtenções ou colocações (put) por vez abaixo desse limite. Infelizmente, isso conflita com os percursos completos minimizadores e o melhor conselho é usar os maiores lotes possíveis sem passar pelas limitações de tamanho de solicitação ou resposta. No entanto, isso provavelmente não será problema para a maioria das entidades.
Um padrão comum no acesso do armazenamento de dados é o seguinte:
q = MyModel.all() for entity in q: # Do something with entity
Ao fazer isso, o SDK obtém entidades do armazenamento de dados em lotes de 20, obtendo um lote novo após a utilização dos existentes. Como o remote_api deve obter cada lote em uma solicitação separada, ele não consegue fazer isso com tanta eficácia. Em vez disso, o remote_api executa uma consulta totalmente nova para cada lote, usando a funcionalidade de deslocamento para se aprofundar ainda mais nos resultados.
Se você sabe de quantas entidades precisa, faça a obtenção inteira em uma solicitação solicitando o número de que você precisa:
entities = MyModel.all().fetch(100) for entity in entities: # Do something with entity
Se você não sabe quantas entidades deseja, use a pseudopropriedade __key__ para acessar eficientemente os conjuntos grandes de resultados. Isso também permite evitar o limite de 1.000 entidades imposto nas consultas normais ao armazenamento de dados:
entities = MyModel.all().fetch(100)
while entities:
for entity in entities:
# Do something with entity
entities = MyModel.all().filter('__key__ >', entities[-1].key()).fetch(100)
Para implementar as transações por meio do remote_api, as informações são acumuladas em entidades obtidas dentro da transação, juntamente com cópias das entidades que foram colocadas ou excluídas na transação. Quando executada, a transação envia todas essas informações para o servidor do Google App Engine, onde tem que obter novamente todas as entidades usadas na transação, verificar se não foram modificadas e, em seguida, colocar e excluir todas as alterações feitas pela transação e executá-la. Se houver um conflito, o servidor reverte a transação e notifica o sistema do cliente, que deve repetir o processo.
Essa abordagem funciona e duplica exatamente a funcionalidade fornecida pelas transações no armazenamento de dados local, mas é ineficiente. Sem dúvida, use as transações onde forem necessárias, mas tente limitar o número e a complexidade das que forem executadas visando a eficiência.
Depois de demonstrarmos o poder do remote_api e descrevermos as suas limitações, é hora de colocar em funcionamento o que aprendemos com uma ferramenta prática. Muitas vezes seria útil poder acessar cada entidade de um determinado tipo, seja para extrair dados ou para modificá-los e armazenar as entidades atualizadas de volta no armazenamento de dados.
Para isso, vamos implementar uma estrutura simples de "mapeamento". Iremos definir uma classe, Mapper, que expõe um método map() para subclasses a serem estendidas e dois campos, KIND e FILTERS, para que definam qual tipo será mapeado e os filtros a serem aplicados.
class Mapper(object):
# Subclasses should replace this with a model class (eg, model.Person).
KIND = None
# Subclasses can replace this with a list of (property, value) tuples to filter by.
FILTERS = []
def map(self, entity):
"""Updates a single entity.
Implementers should return a tuple containing two iterables (to_update, to_delete).
"""
return ([], [])
def get_query(self):
"""Returns a query over the specified kind, with any appropriate filters applied."""
q = self.KIND.all()
for prop, value in self.FILTERS:
q.filter("%s =" % prop, value)
q.order("__key__")
return q
def run(self, batch_size=100):
"""Executes the map procedure over all matching entities."""
q = self.get_query()
entities = q.fetch(batch_size)
while entities:
to_put = []
to_delete = []
for entity in entities:
map_updates, map_deletes = self.map(entity):
to_put.extend(map_updates)
to_delete.extend(map_deletes)
if to_put:
db.put(to_put)
if to_delete:
db.delete(to_delete)
q = self.get_query()
q.filter("__key__ >", entities[-1].key())
entities = q.fetch(batch_size)
Como você pode ver, é muito simples. Primeiramente, definimos um método de conveniência, get_query(), que retorna uma consulta correspondente ao tipo e aos filtros especificados na definição da classe, classificados pela pseudopropriedade __key__. Esse método poderia opcionalmente ser substituído por uma subclasse, por exemplo, para suportar a variação de filtros na execução, contanto que utilize apenas filtros de igualdade, e ordene a consulta por __key__. Em seguida, definimos um método de instância, run(), que acessa cada entidade correspondente em lotes, chamando a função map() em cada uma e atualizando ou excluindo a entidade conforme apropriado.
Um aviso para o nosso mapeador: o processo de mapeamento não funciona a partir de um instantâneo do armazenamento de dados. Portanto, se você retornar entidades novas do map() que atendem aos critérios do mapeamento, elas poderão ser passadas para a função map() posteriormente no processo. Se elas fazem isso ou não, depende de onde as chaves classificam em comparação à chave do registro atual. Geralmente, quando se cria entidades novas do mesmo tipo em uma função map(), é preciso diferenciá-las das entidades originais de alguma maneira para que elas não sejam processas uma segunda vez.
Para usar essa classe, definimos uma subclasse que implementa a função map(). Neste exemplo, vamos adicionar a frase "Bar!" a qualquer entrada do livro de visitas que contenha a frase "foo":
class GuestbookUpdater(Mapper):
KIND = "Greeting"
def map(self, entity):
if entity.content.lower().find('foo') != -1:
entity.content += ' Bar!'
return ([entity], [])
return ([], [])
Em seguida, instanciamos a nossa classe e chamamos run():
mapper = MyMapper() mapper.run()
Experimente você mesmo: insira o código no console interativo que configuramos anteriormente.
Por último, aqui está um exemplo prático, embora trivial, de onde a nossa estrutura nova pode ser útil: para excluir todas as entidades de um determinado tipo.
class MyModelDeleter(Mapper):
KIND = "MyModel"
def map(self, entity):
return ([], [entity])
Simples! Como o mapeador tem o cuidado de acessar sempre KIND e FILTERS como variáveis de instâncias, podemos até generalizar isso para permitir que você selecione o tipo e os filtros na execução:
class BulkDeleter(Mapper):
def __init__(self, kind, filters=None):
self.KIND = kind
if filters:
self.FILTERS = filters
def map(self, entity):
return ([], [entity])
É claro, isso é apenas um pouco do que pode ser feito com o remote_api e a estrutura do mapeador. Se você descobrir um uso novo, poste uma entrada no grupo. Vamos adorar saber mais sobre ela.