TutorialStorm

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, 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:

   1 >>> result = store.execute(Select(Person.name, Person.id == 1))
   2 >>> result.get_one()
   3 (u'Joe Johnes',)

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:

   1 from storm.locals import SQL
   2 >>> ruy.name = SQL("(SELECT name || ' Ritcher' FROM person WHERE id=4)")
   3 >>> ruy.name
   4 u'Ruy Ritcher'

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.

   1 >>> from storm.info import ClassAlias
   2 >>> AnotherEmployee = ClassAlias(Employee)

Legal, não é?

Agora podemos facilmente fazer a consulta que queremos, de uma forma direta:

   1 >>> result = store.find((Employee, AnotherEmployee),
   2 ...                     Employee.company_id == AnotherEmployee.company_id,
   3 ...                     Employee.id > AnotherEmployee.id)
   4 >>> for employee1, employee2 in result:
   5 ...     print (employee1.name, employee2.name)
   6 (u'Mike Mayer', u'Ben Bill')

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.

TutorialStorm (editada pela última vez em 2008-09-26 14:07:47 por localhost)