Iniciando com o ORM Pony no Python
Depois de anos só no Django, estou eu sendo iniciado na simplicidade e elegância do Pony ORM
Estou em meio a um choque de cultura no meu novo trampo em relação à utilização de ORMs para Python. Venho de uma experiência focada em Django, e agora estou de volta à beleza de queries SQL puras e da novidade do ORM Pony.
Uma base para chamar de minha
Poderia eu construir uma base para testar aqui, mas encontrei uma base bem interessante nesse tutorial de sqlite:
O arquivo a baixar está em:
Salve o arquivo chinook.zip
em algum diretório de trabalho em sua máquina.
Use o cliente de banco de dados de sua preferência ou cliente oficial do sqlite
, como farei aqui nos exemplos a seguir, que podem ser baixados em:
Descompactando o arquivo baixado do tutorial temos o seguinte:
$ unzip chinook.zip
Archive: chinook.zip
inflating: chinook.db
Para entrar no database:
$ sqlite3 chinook.db
SQLite version 3.27.2 2019-02-25 16:06:06
Enter ".help" for usage hints.
sqlite>
Então se pode listar as tabelas:
sqlite> .tables
albums employees invoices playlists
artists genres media_types tracks
customers invoice_items playlist_track
E ver a estrutura de uma tabela:
sqlite> .schema genres
CREATE TABLE IF NOT EXISTS "genres"
(
[GenreId] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
[Name] NVARCHAR(120)
);
E listar seu conteúdo:
sqlite> SELECT * FROM genres;
1|Rock
2|Jazz
3|Metal
4|Alternative & Punk
5|Rock And Roll
6|Blues
7|Latin
8|Reggae
9|Pop
10|Soundtrack
11|Bossa Nova
12|Easy Listening
13|Heavy Metal
14|R&B/Soul
15|Electronica/Dance
16|World
17|Hip Hop/Rap
18|Science Fiction
19|TV Shows
20|Sci Fi & Fantasy
21|Drama
22|Comedy
23|Alternative
24|Classical
25|Opera
Com essas informações podemos ir para o shell do Python e com o Pony fazer algumas operações.
Preparando o terreno
Sigamos os primeiros passos do manual do Pony:
Admitindo que estamos em um diretório preparado para esse estudo em que foi feito o download e descompactação da base de dados exemplo, sugiro que seja criada uma virtualenv para esse exercício, e em seguida já a "ativamos":
$ python -mvenv .venv
$ . .venv/bin/activate
(.venv) $
Sendo o Pony um módulo não padrão do Python devemos instalá-lo:
$ pip install pony
Collecting pony
Using cached pony-0.7.14.tar.gz (290 kB)
Using legacy 'setup.py install' for pony, since package 'wheel' is not installed.
Installing collected packages: pony
Running setup.py install for pony ... done
Successfully installed pony-0.7.14
WARNING: You are using pip version 20.2.3; however, version 21.1.1 is available.
You should consider upgrading via the '/home/paulohrpinheiro/repo/pony/.venv/bin/python -m pip install --upgrade pip' command.
Se esse warning lhe incomoda como a mim:
$ pip install --upgrade pip
Collecting pip
Downloading pip-21.1.1-py3-none-any.whl (1.5 MB)
|████████████████████████████████| 1.5 MB 2.2 MB/s
Installing collected packages: pip
Attempting uninstall: pip
Found existing installation: pip 20.2.3
Uninstalling pip-20.2.3:
Successfully uninstalled pip-20.2.3
Successfully installed pip-21.1.1
Mas esse passo não é necessário, "em verdade em verdade vos digo" que executo ele de imediato toda vez que crio uma virtualenv, ou fico muito chateado quando esqueço disso fazer :).
Outro módulo que me deixa feliz e confortável é o ipython, então instalemo-lo com o comando pip install ipython
, que gerará uma saída grande, a qual não colarei aqui, mas que em sua linha final será algo parecido com:
Successfully installed backcall-0.2.0 decorator-5.0.7 ipython-7.23.0
ipython-genutils-0.2.0 jedi-0.18.0 matplotlib-inline-0.1.2 parso-0.8.2
pexpect-4.8.0 pickleshare-0.7.5 prompt-toolkit-3.0.18 ptyprocess-0.7.0
pygments-2.8.1 traitlets-5.0.5 wcwidth-0.2.5
Experimentando
Antes de programar qualquer coisa, acredito ser melhor criar uma certa intimidade com a biblioteca, experimentando os comandos. Ao final teremos insumo para um primeiro programa, ainda que simples, mas trazendo o instrumental usado em qualquer outro programa simples ou não.
Vamos primeiro entrar no shell do Python, em sua versão melhorada (ipython) e então importar todos os elementos do módulo Pony que precisamos para trabalhar:
$ ipython
Python 3.9.4 (default, Apr 26 2021, 20:25:48)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.23.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: from pony.orm import *
Agora vamos dar o primeiro passo para conectar ao nosso banco de dados. A classe Database
do Pony é quem gerencia as conexões, portanto vamos instanciá-la:
In [2]: db = Database()
Feito isso, agora a ligamos concretamente a um banco físico através do método bind
:
In [3]: db.bind(provider='sqlite', filename='/home/paulohrpinheiro/repo/pony/chinook.db')
Estamos conectados ao nosso banco. Veja que o procedimento é semelhante a se o banco fosse um MySQL ou Postgres, por exemplo, como veremos em texto a ser publicado (é o próximo, palavra de lobinho). Outro detalhe é que o caminho completo do arquivo deve ser dado quando estamos em modo interativo.
O próximo passo é declarar nossa entidade, ou melhor dizendo, fazer o mapeamento de nossas tabelas, com classes Python equivalentes. Nossa tabela escolhida foi a genres, então vamos a ela:
In [4]: class Genres(db.Entity):
...: GenreId = PrimaryKey(int, auto=True)
...: Name = Required(str)
...:
Uma vez declarada nossa entidade, o mapeamento deve ser feito de maneira concreta, o que é realizado pelo seguinte método:
In [5]: db.generate_mapping()
Com esse setup podemos começar a brincar:
Vendo o que temos na tabela:
In [6]: Genres.select().show()
GenreId|Name
-------+------------------
1 |Rock
2 |Jazz
3 |Metal
4 |Alternative & Punk
5 |Rock And Roll
6 |Blues
7 |Latin
8 |Reggae
9 |Pop
10 |Soundtrack
11 |Bossa Nova
12 |Easy Listening
13 |Heavy Metal
14 |R&B/Soul
15 |Electronica/Dance
16 |World
17 |Hip Hop/Rap
18 |Science Fiction
19 |TV Shows
20 |Sci Fi & Fantasy
21 |Drama
22 |Comedy
23 |Alternative
24 |Classical
25 |Opera
Pesquisando uma Prymary Key:
In [7]: Genres[2]
Out[7]: Genres[2]
Um tanto inútil não? Não! Foi retornado um objeto, que entre outras coisa, tem um útil método to_dict
:
In [8]: Genres[2].to_dict()
Out[8]: {'GenreId': 2, 'Name': 'Jazz'}
Dica: Permita-se explorar o terreno, vendo os métodos que existem através do comando
dir(Genres[2])
.
Ou pode-se, ainda, fazer uma busca por outra coluna, explicitamente:
In [9]: Genres.get(Name='Pop').to_dict()
Out[9]: {'GenreId': 9, 'Name': 'Pop'}
Pegando o objeto, pode-se acessar os atributos:
In [10]: genre = Genres.get(GenreId=22)
In [11]: genre.Name
Out[11]: 'Comedy'
Treino é treino, jogo é jogo
Uma coisa é o shell, outra é um programa. Mas muda pouca coisa. Uma preocupação real é que quando formos fazer uma operação no banco, devemos usar um gerenciador de contexto para ela. No python usamos, em geral, a cláusula with
. O gerenciador disponibilizado pela Pony é o db_session
, por exemplo:
with db_session():
genre = Genres.get(GenreId=10).to_dict()
Juntando tudo que vimos, mais essa observação, temos esse pequeno programa esqueleto:
from pony.orm import Database, db_session, PrimaryKey, Required
db = Database()
db.bind(provider='sqlite', filename='chinook.db')
class Genres(db.Entity):
GenreId = PrimaryKey(int, auto=True)
Name = Required(str)
db.generate_mapping(create_tables=False)
with db_session():
print(Genres.get(GenreId=10).to_dict())
print("="*80)
for genre in Genres.select():
print(genre.to_dict())
Que produz a seguinte saída (salvei ele como le.py
:
$ python le.py
{'GenreId': 10, 'Name': 'Soundtrack'}
================================================================================
{'GenreId': 1, 'Name': 'Rock'}
{'GenreId': 2, 'Name': 'Jazz'}
{'GenreId': 3, 'Name': 'Metal'}
{'GenreId': 4, 'Name': 'Alternative & Punk'}
{'GenreId': 5, 'Name': 'Rock And Roll'}
{'GenreId': 6, 'Name': 'Blues'}
{'GenreId': 7, 'Name': 'Latin'}
{'GenreId': 8, 'Name': 'Reggae'}
{'GenreId': 9, 'Name': 'Pop'}
{'GenreId': 10, 'Name': 'Soundtrack'}
{'GenreId': 11, 'Name': 'Bossa Nova'}
{'GenreId': 12, 'Name': 'Easy Listening'}
{'GenreId': 13, 'Name': 'Heavy Metal'}
{'GenreId': 14, 'Name': 'R&B/Soul'}
{'GenreId': 15, 'Name': 'Electronica/Dance'}
{'GenreId': 16, 'Name': 'World'}
{'GenreId': 17, 'Name': 'Hip Hop/Rap'}
{'GenreId': 18, 'Name': 'Science Fiction'}
{'GenreId': 19, 'Name': 'TV Shows'}
{'GenreId': 20, 'Name': 'Sci Fi & Fantasy'}
{'GenreId': 21, 'Name': 'Drama'}
{'GenreId': 22, 'Name': 'Comedy'}
{'GenreId': 23, 'Name': 'Alternative'}
{'GenreId': 24, 'Name': 'Classical'}
{'GenreId': 25, 'Name': 'Opera'}
Próximas Sprints
Vamos configurar dois bancos Docker, um MySQL e outro Postgres, para testar as conexões e um banco que seja intercambiável entre eles e o sqlite.
Também vamos começar a gerar situações de erro e ver o que os objetos retornam ou que exceções são geradas.
Por fim, consultas mais complexas e demais operações que mostrem o valor do Pony, afinal, espero que não seja só eu a perceber que ele não é apenas mais um ORM.