Conteúdos
- 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.
1 >>> from storm.locals import *
2 >>>
Definição básica
Agora definiremos um tipo com algumas propriedades descrevendo a informação que estamos para mapear.
1 >>> class Person(object):
2 ... __storm_table__ = "person"
3 ... id = Int(primary=True)
4 ... name = Unicode()
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.
1 >>> database = create_database("sqlite:")
2 >>> store = Store(database)
3 >>>
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.
1 >>> store.execute("CREATE TABLE person "
2 ... "(id INTEGER PRIMARY KEY, name VARCHAR)")
3 <storm.databases.sqlite.SQLiteResult object at 0x...>
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.
1 >>> joe = Person()
2 >>> joe.name = u"Joe Johnes"
3 >>> print "%r, %r" % (joe.id, joe.name)
4 None, u'Joe Johnes'
Até agora esse objeto não tem conexão com o banco de dados. Vamos adicioná-lo ao armazém que criamos acima.
1 >>> store.add(joe)
2 <Person object at 0x...>
3 >>> print "%r, %r" % (joe.id, joe.name)
4 None, u'Joe Johnes'
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?
1 >>> person = store.find(Person, Person.name == u"Joe Johnes").one()
2 >>> print "%r, %r" % (person.id, person.name)
3 1, u'Joe Johnes'
4 >>>
A pessoa está lá! É, tá, você estava esperando isso.
Também podemos buscar o objeto usando sua chave primária.
1 >>> store.get(Person, 1).name
2 u'Joe Johnes'
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á.
1 >>> person is joe
2 True
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.
1 >>> mary = Person()
2 >>> mary.name = u"Mary Margaret"
3 >>> store.add(mary)
4 <Person object at 0x...>
5 >>> print "%r, %r" % (mary.id, mary.name)
6 None, u'Mary Margaret'
7 >>> store.flush()
8 >>> print "%r, %r" % (mary.id, mary.name)
9 2, u'Mary Margaret'
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.
1 >>> store.find(Person, Person.name == u"Mary Margaret").set(name=u"Mary Maggie")
2 >>> mary.name
3 u'Mary Maggie'
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
1 >>> store.commit()
2 >>>
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.
1 >>> joe.name = u"Tom Thomas"
2 >>>
Vejamos se essas mudanças estão realmente sendo levadas em conta pelo Storm e pelo banco de dados.
1 >>> person = store.find(Person, Person.name == u"Tom Thomas").one()
2 >>> person is joe
3 True
Sim, elas estão. Agora, o passo mágico (música de suspense, por favor).
1 >>> store.rollback()
2 >>>
Erm.. Não aconteceu nada?
Na verdade, algo aconteceu.. com Joe. Ele está de volta!
1 >>> print "%r, %r" % (joe.id, joe.name)
2 1, u'Joe Johnes'
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:
1 >>> class Company(object):
2 ... __storm_table__ = "company"
3 ... id = Int(primary=True)
4 ... name = Unicode()
5 ...
6 ... def __init__(self, name):
7 ... self.name = name
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.
1 >>> store.execute("CREATE TABLE company "
2 ... "(id INTEGER PRIMARY KEY, name VARCHAR)", noresult=True)
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.
1 >>> class Employee(Person):
2 ... __storm_table__ = "employee"
3 ... company_id = Int()
4 ... company = Reference(company_id, Company.id)
5 ...
6 ... def __init__(self, name):
7 ... self.name = name
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.
1 >>> store.execute("CREATE TABLE employee "
2 ... "(id INTEGER PRIMARY KEY, name VARCHAR, company_id INTEGER)",
3 ... noresult=True)
Vamos dar vida a Ben agora.
1 >>> ben = store.add(Employee(u"Ben Bill"))
2 >>> print "%r, %r, %r" % (ben.id, ben.name, ben.company_id)
3 None, u'Ben Bill', None
Podemos ver que eles não estão sincronizados ainda. Mesmo assim, podemos dizer que Bill trabalha no Circo.
1 >>> ben.company = circus
2 >>> print "%r, %r" % (ben.company_id, ben.company.name)
3 None, u'Circus Inc.'
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).
1 >>> store.flush()
2 >>> print "%r, %r" % (ben.company_id, ben.company.name)
3 1, u'Circus Inc.'
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.
1 >>> sweets = store.add(Company(u"Sweets Inc."))
2 >>> store.flush()
3 >>> sweets.id
4 2
Legal, já obtivemos o id da nova empresa. Agora, o que aconteceria se mudássemos somente o id para a empresa de Ben?
1 >>> ben.company_id = 2
2 >>> ben.company.name
3 u'Sweets Inc.'
4 >>> ben.company is sweets
5 True
Hah! Aquilo não era esperado, não é?
Vamos efetivar tudo.
1 >>> store.commit()
2 >>>
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.
1 >>> Empresa.employees = ReferenceSet(Company.id, Employee.company_id)
2 >>>
Sem maiores complicações, já podemos ver quais empregados estão trabalhando para uma dada empresa.
1 >>> sweets.employees.count()
2 1
3 >>> for employee in sweets.employees:
4 ... print "%r, %r" % (employee.id, employee.name)
5 ... print employee is ben
6 ...
7 1, u'Ben Bill'
8 True
Vamos criar um outro empregado, e adicioná-lo à Empresa, em vez de determinar a empresa no empregado (isso soa melhor, ao menos).
1 >>> mike = store.add(Employee(u"Mike Mayer"))
2 >>> sweets.employees.add(mike)
3 >>>
Isso, é claro, significa que Mike está trabalhando por uma empresa, e isso então deveria ser refletido em tudo mais.
1 >>> mike.company_id
2 2
3 >>> mike.company is sweets
4 True
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.
1 >>> Company.accountants = ReferenceSet(Company.id,
2 ... CompanyAccountant.company_id,
3 ... CompanyAccountant.accountant_id,
4 ... Accountant.id)
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.
1 >>> store.execute("CREATE TABLE accountant "
2 ... "(id INTEGER PRIMARY KEY, name VARCHAR)", noresult=True)
3 ...
4 >>> store.execute("CREATE TABLE company_accountant "
5 ... "(company_id INTEGER, accountant_id INTEGER,"
6 ... " PRIMARY KEY (company_id, accountant_id))", noresult=True)
Vamos dar vida a dois contadores, e registrá-los em ambas empresas.
1 >>> karl = Accountant(u"Karl Kent")
2 >>> frank = Accountant(u"Frank Fourt")
3 >>> sweets.accountants.add(karl)
4 >>> sweets.accountants.add(frank)
5 >>> circus.accountants.add(frank)
6 >>>
É 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.
1 >>> store.get(CompanyAccountant, (sweets.id, frank.id))
2 <CompanyAccountant object at 0x...>
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.
1 >>> result = store.find(Company,
2 ... Company.company_id == Company.id,
3 ... Company.name.like(u"Ben %"))
4 ...
5 >>> [company.name for company in result]
6 [u'Sweets Inc.']
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.
1 >>> origin = [Company, Join(Employee, Employee.company_id == Company.id)]
2 >>> result = store.using(*origin).find(Company, Employee.name.like(u"Ben %"))
3 >>> [company.name for company in result]
4 [u'Sweets Inc.']
Se já temos a empresa, e quiséssemos saber qual dos empregados tinha por nome Ben, seria ainda mais fácil.
1 >>> result = sweets.employees.find(Employee.name.like(u"Ben %"))
2 >>> [employee.name for employee in result]
3 [u'Ben Bill']
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.
1 >>> laura = Accountant(u"Laura Montgomery")
2 >>> store.add(laura)
3 <Accountant ...>
4 >>> subselect = Select(CompanyAccountant.accountant_id, distinct=True)
5 >>> result = store.find(Accountant, Not(Accountant.id.is_in(subselect)))
6 >>> result.one() is laura
7 True
8 >>>
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:
1 >>> result = store.find((Company, Employee),
2 ... Employee.company_id == Company.id,
3 ... Employee.name.like(u"Ben %"))
4 >>> [(company.name, employee.name) for company, employee in result]
5 [(u'Sweets Inc.', u'Ben Bill')]
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.
1 >>> real = store.add(Currency())
2 >>> real.id = 1
3 >>> real.symbol = u"BRL"
4 >>> brazil = store.add(Country())
5 >>> brazil.name = u"Brazil"
6 >>> brazil.currency_id = 1
7 >>> brazil.currency.symbol
8 u'BRL'
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,
