Conteúdo
- Funciona!
- Importando
- Definição básica
- Criando um banco de dados e um armazém (store)
- Criando um objeto
- Armazenagem de um objeto
- Procurando um objeto
- Cacheando comportamento
- Sincronizando
- Alterando objetos com Store
- Efetivando
- ''Rolagem de volta''
- Construtores
- Referências e Subclasses
- Relacionamentos muitos-para-um
- Relacionamentos muitos-para-muitos e chaves compostas
- Junções
- Sub-seleções
- Ordenando e limitando resultados
- Múltiplos tipos com uma consulta
- A classe base de Storm
- Carregando hook
- Executando expressões
- Auto-recarregar valores
- Valores de expressão
- Codinomes (''Aliases'')
- Muito mais!
Funciona!
O tutorial original em inglês de Storm está incluso no código-fonte em tests/tutorial.txt, para que ele possa ser testado e atualizado.
Importando
Vamos começar importando alguns nomes para o namespace.
Definição básica
Agora definiremos um tipo com algumas propriedades descrevendo a informação que estamos para mapear.
Perceba que não tem definição-storm de classe base ou construtora.
Criando um banco de dados e um armazém (store)
Ainda não temos ninguém com quem conversar, então vamos definir na memória um banco de dados SQLite para usar e um armazém (store) utilizando aquela banco de dados.
Suportam-se três banco de dados até o momento: SQLite, MySQL e PostgreSQL. O parâmetro passado para create_database() é um URI, como o seguinte:
database = create_database("scheme://nomeusario:senha@nomehost:porta/nome_banco")
O scheme pode ser "sqlite", "postgres", ou "mysql".
Agora temos que criar a tabela que realmente irá guardar os dados para nossa classe.
Recebemos um resultado de volta, mas não vamos nos preocupar com isso agora. Poderíamos também usar noresult=True para evitar o resultado inteiro.
Criando um objeto
Vamos criar um objeto da classe definida.
Até agora esse objeto não tem conexão com o banco de dados. Vamos adicioná-lo ao armazém que criamos acima.
Repare que o objeto não foi alterado, mesmo depois de ser adicionado ao armazém. Isso porque ele ainda não foi sincronizado.
Armazenagem de um objeto
Uma vez que o objeto é adicionado ao armazém (store) ou dele buscado, já podemos conhecer sua relação com aquele armazém. Podemos facilmente verificar a qual armazém um objeto está ligado.
>>> Store.of(joe) is store True >>> Store.of(Person()) is None True
Procurando um objeto
Agora, o que aconteceria se realmente pedíssemos ao armazém que nos desse a pessoa de nome Joe Johnes?
A pessoa está lá! É, tá, você estava esperando isso.
Também podemos buscar o objeto usando sua chave primária.
Cacheando comportamento
Uma coisa interessante é que a pessoa é o Joe na verdade, certo? Nós simplesmente adicionamos esse objeto, então se só há um Joe, por que haveria dois objetos? Não há.
O que está acontecendo por detrás da cortina é que cada armazém possui um cache de objeto. Quando um objeto é ligado ao armazém, ele estará cacheado no armazém enquanto houver referência ao objeto em algum lugar, ou enquanto o objeto não estiver sincronizado (tiver alterações não sincronizadas).
Storm se assegura de que ao menos um certo número de objetos recentemente usados fiquem na memória dentro da transação, de modo que objetos usados com freqüência não sejam buscados no banco de dados muitas vezes.
Sincronizando
Quando tentamos encontrar Joe no banco de dados pela primeira vez, percebemos que a propriedade 'id' foi atribuída magicamente. Isso ocorreu porque o objeto foi sincronizado implicitamente de forma que a operação afetaria da mesma maneira qualquer alteração pendente.
Sincronizações também podem se dar explicitamente.
Alterando objetos com Store
Além de comumente alterar objetos, também podemos nos aproveitar do fato de que objetos estão atados a um banco de dados para alterá-los usando expressões.
Essa operação irá atingir cada objeto correspondente no banco de dados, bem como objetos que estiverem ativos na memória.
Efetivando
Tudo o que fizemos até agora está dentro da transação. A partir de agora, podemos tornar essas mudanças, e qualquer mudança não efetivada, persistente por meio da efetivação delas, ou podemos desfazer tudo por meio da rolagem de volta.
Iremos efetivá-las, com algo tão simples quanto
Isso foi fácil. Tudo está do mesmo modo em que estava, mas agora as mudanças estão lá "de verdade".
''Rolagem de volta''
Abortar mudanças é igualmente fácil.
Vejamos se essas mudanças estão realmente sendo levadas em conta pelo Storm e pelo banco de dados.
Sim, elas estão. Agora, o passo mágico (música de suspense, por favor).
Erm.. Não aconteceu nada?
Na verdade, algo aconteceu.. com Joe. Ele está de volta!
Construtores
Então, estamos trabalhando demais só com pessoas. Vamos introduzir um novo tipo de dado em nosso modelo: empresas. Para as empresas usaremos um construtor, somente para diversão. Será a classe de empresa mais simples que você já viu:
Note que o parâmetro construtor não é opcional. Ele poderia se quiséssemos, mas nossas empresas sempre têm nomes.
Vamos adicionar as tabelas para isso.
Então, crie uma nova empresa.
>>> circus = Company(u"Circus Inc.") >>> print "%r, %r" % (circus.id, circus.name) None, u'Circus Inc.'
O id está ainda indefinido porque não sincronizamos ele. Na verdade, nós nem ainda adicionamos a empresa ao armazém. Faremos isso em breve. Veja só.
Referências e Subclasses
Agora queremos admitir alguns empregados em nossa empresa. Melhor que refazer a definição de pessoa, manteremos ela como está, uma vez que ela é genérica, e criaremos uma nova subclasse dela para empregados, o que inclui um campo extra: o id da empresa.
Preste atenção por um instante na definição. Repare que ela define o que já está na pessoa, e introduz o company_id, e uma propriedade company, que é uma referência para outra classe. Ela também possui um construtor, mas que deixa a empresa sozinha.
Como de costume, precisamos de uma tabela. SQLite não tem idéia do que é uma chave estrangeira, então não iremos nos preocupar em defini-la.
Vamos dar vida a Ben agora.
Podemos ver que eles não estão sincronizados ainda. Mesmo assim, podemos dizer que Bill trabalha no Circo.
Claro, não temos ainda o id da empresa pois ele não foi sincronizado para o banco de dados ainda, e não atribuímos um id explicitamente. Storm ainda assim está mantendo a relação.
Se de qualquer maneira a sincronização está pendente para o banco de dados (implícita ou explicitamente), os objetos obterão seus ids, e quaisquer referências são atualizadas da mesma forma (antes de serem sincronizadas).
Estão ambos sincronizados para o banco de dados. Agora, perceba que a emrpresa Circus não foi adicionada explicitamente em qualquer momento. Storm fará isso automaticamente a objetos a que se fez referência, para ambos os objetos (àquele referido e ao referente).
Vamos criar uma outra empresa para verificar algo. Dessa vez iremos sincronizar o armazém depois de adicioná-lo.
Legal, já obtivemos o id da nova empresa. Agora, o que aconteceria se mudássemos somente o id para a empresa de Ben?
Hah! Aquilo não era esperado, não é?
Vamos efetivar tudo.
Relacionamentos muitos-para-um
Então, enquanto nosso modelo diz que os empregados trabalham para uma única empresa (nós só concebemos pessoas normais aqui), as empresas podem naturalmente ter múltiplos empregados. Representamos isso em Storm usando um conjunto de referências (reference set).
Não definiremos a empresa novamente. Em vez disso iremos adicionar um novo atributo à classe.
Sem maiores complicações, já podemos ver quais empregados estão trabalhando para uma dada empresa.
Vamos criar um outro empregado, e adicioná-lo à Empresa, em vez de determinar a empresa no empregado (isso soa melhor, ao menos).
Isso, é claro, significa que Mike está trabalhando por uma empresa, e isso então deveria ser refletido em tudo mais.
Relacionamentos muitos-para-muitos e chaves compostas
Queremos da mesma forma representar contadores (accountants) em nosso modelo. Empresas têm contadores, mas esses contadores também podem atender a muitas empresas, portanto representaremos isso usando muitos-para-um relacionamento.
Vamos criar uma classe simples para usar com os contadores, e a classe de relacionamento.
1 >>> class Accountant(Person):
2 ... __storm_table__ = "accountant"
3 ... def __init__(self, name):
4 ... self.name = name
5 >>> class CompanyAccountant(object):
6 ... __storm_table__ = "company_accountant"
7 ... __storm_primary__ = "company_id", "accountant_id"
8 ... company_id = Int()
9 ... accountant_id = Int()
Ei, nós só declaramos uma classe com uma chave composta!
Agora, vamos usá-la para declarar o relacionamento muitos-para-muitos na empresa. Mais uma vez, iremos somente colar o novo atributo no objeto existente. Ele pode facilmente ser definido na hora da definição da classe. Depois veremos outra maneira para de fazer a mesma coisa.
Feito! A ordem em que cada atributo foi definido é importante, mas a lógica deve ser bem óbvia.
Estamos deixando de lado algumas tabelas nesse momento.
Vamos dar vida a dois contadores, e registrá-los em ambas empresas.
É isso! De verdade! Repare que nós nem adicionamos eles ao armazém, pois isso acontece implicitamente quando se liga a outro objeto que já esteja no armazém, e desse modo não tivemos que que declarar o objeto de relacionamento, vez que é conhecido para o conjunto de referência.
Podemos agora averiguá-los.
>>> sweets.accountants.count() 2 >>> circus.accountants.count() 1
Mesmo que não tenhamos usado o objeto CompanyAccountant explicitamente, podemos verificar se formos realmente curiosos.
Perceba que passamos um tupla para o método get() devido à chave composta.
Se quiséssemos saber para quais empresas os contadores estão trabalhando, poderíamos facilmente definir a relação inversa.
1 >>> Accountant.companies = ReferenceSet(Accountant.id,
2 ... CompanyAccountant.accountant_id,
3 ... CompanyAccountant.company_id,
4 ... Company.id)
5 >>> [company.name for company in frank.companies]
6 [u'Circus Inc.', u'Sweets Inc.']
7 >>> [company.name for company in karl.companies]
8 [u'Sweets Inc.']
Junções
Já que obtivemos alguns dados legais para trabalhar, vamos tentar fazer algumas consulta interessantes.
Vamos começar verificando quais empresas possuem ao menos um empregado de nome Ben. Temos ao menos duas maneiras de fazê-lo.
Primeiramente, com uma junção implícita.
Dessa forma, podemos também fazer uma junção explícita. Isso é importante para um mapeamento complexo de junções SQL para consultas de Storm.
Se já temos a empresa, e quiséssemos saber qual dos empregados tinha por nome Ben, seria ainda mais fácil.
Sub-seleções
Suponha que queiramos encontrar todos os contadores que não estão associados com a empresa. Podemos usar uma sub-seleção para obter o dado desejado.
Ordenando e limitando resultados
Ordenar e limitar resultados obtidos são certamente dentre outros o mais simples e ainda o mais desejado recurso para esse tido de ferramento, então queremos fazer isso de maneira bem fácil para entender e usar, é claro.
Uma linha de código vale mais que mil palavras, então aqui estão alguns exemplos que demonstram como isso funciona:
>>> garry = store.add(Employee(u"Garry Glare")) >>> result = store.find(Employee) >>> [employee.name for employee in result.order_by(Employee.name)] [u'Ben Bill', u'Garry Glare', u'Mike Mayer'] >>> [employee.name for employee in result.order_by(Desc(Employee.name))] [u'Mike Mayer', u'Garry Glare', u'Ben Bill'] >>> [employee.name for employee in result.order_by(Employee.name)[:2]] [u'Ben Bill', u'Garry Glare']
Múltiplos tipos com uma consulta
Alguma vezes, pode ser interessante buscar mais que um objeto envolvido numa dada consulta. Imagine, por exemplo, que além de saber qual empresa tem um empregado por nome Ben, também queiramos saber quem é o empregado. Isso pode ser conseguido com uma consulta como a seguinte:
A classe base de Storm
Até aqui estivemos definindo nossos conjuntos de referência usando classes ou suas propriedades. Isso tem algumas vantagens, como ficar mais fácil debugar, mas também pode ter algumas desvantagens, como requerer que classes estejam presente no escopo local, o que potencialmente leva a importantes questões circulares.
Para evitar isso tipo de situação, Storm suporta definir essas referências usando a versão stringficada da classe e dos nomes de propriedade. O único inconveniente de fazer isso é que todas as classes envolvidas devem herdar a classe base de Storm.
Vamos definir algumas novas classes para mostrar isso. Para explicar esse ponto, faremos referência à classe antes que seja realmente definida.
1 >>> class Country(Storm):
2 ... __storm_table__ = "country"
3 ... id = Int(primary=True)
4 ... name = Unicode()
5 ... currency_id = Int()
6 ... currency = Reference(currency_id, "Currency.id")
7 >>> class Currency(Storm):
8 ... __storm_table__ = "currency"
9 ... id = Int(primary=True)
10 ... symbol = Unicode()
11 >>> store.execute("CREATE TABLE country "
12 ... "(id INTEGER PRIMARY KEY, name VARCHAR, currency_id INTEGER)",
13 ... noresult=True)
14 >>> store.execute("CREATE TABLE currency "
15 ... "(id INTEGER PRIMARY KEY, symbol VARCHAR)", noresult=True)
Agora, vamos ver se funciona.
Questões!?
Carregando hook
Storm permite às classes definir uns hooks um pouco diferentes chamados para atuar quando certas coisas acontecem. Um dos hooks interessantes disponíveis é o __storm_loaded__.
Vamos trabalhar com ele. Iremos definir uma subclasse temporária da Pessoa para tanto.
1 >>> class PersonWithHook(Person):
2 ... def __init__(self, name):
3 ... print "Creating %s" % name
4 ... self.name = name
5 ...
6 ... def __storm_loaded__(self):
7 ... print "Loaded %s" % self.name
8 >>> earl = store.add(PersonWithHook(u"Earl Easton"))
9 Creating Earl Easton
10 >>> earl = store.find(PersonWithHook, name=u"Earl Easton").one()
11 >>> store.invalidate(earl)
12 >>> del earl
13 >>> import gc
14 >>> collected = gc.collect()
15 >>> earl = store.find(PersonWithHook, name=u"Earl Easton").one()
16 Loaded Earl Easton
Note que na primeira procura, nada foi chamado, já que o objeto ainda estava na memória e cacheado. Portanto, nós invalidamos o objeto de cache interno de Storm e asseguramos, acionando um coletor de lixo, que estava fora-da-memória. Depois disso, o objeto teve que ser buscado do banco de dados novamente, e assim o hook foi chamado (e não o construtor!).
Executando expressões
Storm também oferece um meio de executar expressões de uma maneira agnóstica de banco-de-dados, quando isso for necessário.
Por exemplo:
Esse mecanismo é usado internamente pelo próprio Storm para implementar formas de nível mais alto.
Auto-recarregar valores
Storm oferece alguns valores especiais que podem ser atribuídos sob o controle dele. Um desses valores é o AutoRelad. Qunado usado, fará o objeto recarregar automaticamente o valor do banco de dados quando alcançado. Mesmo as chaves primárias podem se valer desse uso, como demonstrado acima.
>>> from storm.locals import AutoReload >>> ruy = store.add(Person()) >>> ruy.name = u"Ruy" >>> print ruy.id None >>> ruy.id = AutoReload >>> print ruy.id 4
Isso pode ser determinado como o valor padrão para qualquer atributo, fazendo o objeto ser lavado automaticamente se necessário.
Valores de expressão
Ademais de auto-recarregar, é igualmente possível aplicar o que chamamos "expressões preguiçosas" a um atributo. Tais expressões são sincronizadas para o banco de dados quando o atributo é acessado, ou quando o objeto é sincronizado para o bando de dados (hora do INSERT/UPDATE).
Por exemplo:
Perceba que foi somente um exemplo do que pode ser feito. Não há necessidade de escrever sentenças de SQL dessa maneira se você não quiser. Você pode também usar expressões SQL baseada-em-classe proporcionada por Storm, ou ainda não usar nenhuma expressão.
Codinomes (''Aliases'')
Então agora vamos dizer que queremos encontrar cada par de pessoas que trabalha para a mesma empresa. Eu não tenho idéia do porquê alguém iria querer fazer isso, mas é um bom caso para nós para exercitarmos os codinomes.
Primeiro, importaremos ClassAlias para dentro do namespace local ("nota mental: isso deveria da mesma forma estar em storm.locals"), e criar uma referência para ele.
Legal, não é?
Agora podemos facilmente fazer a consulta que queremos, de uma forma direta:
Uou! O Mike e o Ben trabalham para a mesma empresa!
(Questão para o leitor atento: por que "maior que" está sendo usado na consulta acima?)
Muito mais!
Há muito mais sobre Storm a ser mostrado. Este tutorial é somente uma forma de iniciar em alguns dos conceitos. Se suas perguntas não forem respondidas em algum lugar por aí, sinta-se na liberdade de perguntá-las na lista de emails.