Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


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

Logo do projeto Pony

https://ponyorm.org

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:

https://www.sqlitetutorial.net

O arquivo a baixar está em:

https://www.sqlitetutorial.net/sqlite-sample-database/

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:

https://sqlite.org/download.html

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.

FEITO: https://paulohrpinheiro.xyz/texts/python/2021-05-11-iniciando-com-o-orm-pony-no-python-ii-banco-de-dados-com-docker.html

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.

FEITO: https://paulohrpinheiro.xyz/texts/python/2021-05-16-iniciando-com-o-orm-pony-no-python-iii-erros-e-excecoes.html

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.