Tutorial básico de SQLAlchemy
Eu trabalhei com o Projeto Jupyter de dezembro/2018 até março/2019 como parte de um estágio no programa Outreachy. Foi uma experiência maravilhosa e super recomendo! Durante o meu estágio, eu lutei com a biblioteca SQLAlchemy que o JupyterHub utiliza internamente.
Como estudei essa biblioteca e tive que fazer algumas buscas no Stack Overflow pra entender várias coisas, decidi criar este post para ajudar a digerir algumas das minhas dúvidas e descobertas.
Todo o código está disponível neste repositório.
Criando e entendendo o Engine (mecanismo)
Para começar a trabalhar com o SQLAlchemy, a primeira coisa que eles ensinam nos tutoriais é que você deve criar um engine. O engine é como o SQLAlchemy se comunica com o banco de dados. Portanto, ao criar o mecanismo, você deve adicionar a URL do banco de dados (chamada pela abreviação em inglês db) e é basicamente isso.
from sqlalchemy import create_engine
engine = create_engine('sqlite:///:memory:', echo=True)
Embora você possa acessar o banco de dados por meio de comandos do engine (veremos como), geralmente esse não é o recomendado. Você pode, mas não deve 🙂 O engine
é para ser apenas a ponte de conexão entre o Python e o banco.
Nesse comando, você está apenas dizendo para o SQLAlchemy onde seu banco de dados está localizado. O atributo echo = True
fará com que SQLAlchemy registre no console todos os comandos SQL que você está executando através dos comandos e os resultados otidos. Esse parâmetro não deve ficar ativado em produção, ok?
Uma vez que seu engine conhece seu banco de dados, é fácil executar comandos usando um método chamado engine.execute(...)
. Veja o exemplo abaixo:
Portanto, você tem uma via de mão dupla: o engine que sabe onde está o seu banco de dados e um método (engine.execute(...)
) para alterar o banco de dados usando o engine:
Engine ou conexão?
Também vi em alguns tutoriais que você tem outra maneira de executar comandos SQL através do engine que é através de uma connection
(conexão). Isso acontece da seguinte forma:
conn = engine.connect()
conn.execute(...)
Isso nos permite criar comandos transientes, o que significa que todos os comandos devem ser executados com êxito no banco de dados ou todos devem ser revertidos em caso de erro [1]:
trans = conn.begin()
conn.execute('INSERT INTO "EX1" (name) '
'VALUES ("Hello")')
trans.commit()
Então, na verdade, a estrutura de comunicação se parece mais com isso:
No entanto, quando eu continuei investigando as diferenças entre engine.execute(...)
e connection.execute(...)
eu descobri que elas não são diferentes:
“Usar engine.execute() e connection.execute() é (quase) a mesma coisa. No primeiro, o objeto connection é criado implicitamente e, no segundo, nós o instanciamos explicitamente."
Então, fique à vontade para usar qualquer uma delas, se quiser :)
Até agora, nos conectamos no banco de dados e puder executar comandos através de instruções SQL. No entanto, o que torna o SQLAlchemy tão atraente é o ORM (Object Relational Mapping), que eu não comentei até agora.
O ORM precisa de uma session
(sessão) para fazer um meio de campo entre os objetos que criamos no Python e o engine
que realmente se comunica com o banco de dados. Vamos criar usar uma função chamada sessionmaker
pra passar o engine
pra nossa sessão atual e criarmos de fato a sessão:
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
session = Session()
Então, daqui pra frente, nós vamos usar o session
para conversar com as tabelas e fazer consultas, mas é o engine
que realmente está implementando coisas no seu banco de dados.
Embora pareça confuso ter três entidades antes mesmo de começar a mexer com tabelas, na maioria das vezes após a configuração inicial você vai usar a session
muito mais do que o engine
e a conexão será feita implicitamente por ele.
Criando tabelas
Agora que entendemos a estrutura básica, a primeira coisa a fazer é começar a criar tabelas em nosso banco de dados e finalmente começar a dar uma olhada no ORM do SQLAlchemy.
Para criar novas tabelas, precisamos criar classes que contêm atributos. Cada classe será uma tabela em nosso banco de dados e cada atributo será uma coluna na tabela. Para mapear qual tabela no banco de dados será relacionada com cada classe em nossos arquivos, usaremos um sistema SQLAlchemy chamado Declarative (Declarativo). Para usar isso, a primeira coisa que devemos fazer é instanciar uma Base
:
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
Agora vamos criar uma classe User
que herda da Base
declarativa que acabamos de criar. Criaremos apenas três atributos para nossa classe: id
(que é uma chave primária), um nome e uma senha. Como estamos usando Declarativos, devemos adicionar pelo menos dois atributos à nossa classe:
-
__tablename__
indica como sua tabela será realmente chamada dentro do banco de dados -
Ao menos um dos atributos deve ser declarado como uma chave primária [2].
Também é bom adicionar um método opcional chamado __repr__
que será um texto (string) que deve ser retornado quando tivermos a instância da classe User
.
from sqlalchemy import Column, Integer, String
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
password = Column(String)
def __repr__(self):
return f'User {self.name}'
Agora temos uma classe que indica como nossa tabela deve ser no nosso banco de dados. No entanto, nada mudou até agora. O banco de dados ainda não conhece essa estrutura. Para realmente criar as tabelas em nosso banco de dados seguindo a estrutura que definimos na classe User
, precisamos usar a Base
declarativa que acabamos de criar e precisamos do engine
:
Base.metadata.create_all(engine)
É só nesse momento que o SQLAlchemy realmente vai implementar as mudanças no banco de dados. Como definimos o parâmetro echo
como verdadeiro (True
), podemos ver exatamente quais são instruções aplicadas via SQL que o engine
está gerando:
Adicionando novos usuários
Agora que a tabela de fato existe no banco de dados, podemos usar a classe para criar um novo registro no banco. Podemos usar a classe User
para criar um novo usuário e session.add(...)
para adicionar a instância ao nosso banco de dados como uma nova linha.
user = User(name='John Snow', password='johnspassword')
session.add(user)
print(user.id) # None
Anteriormente eu comentei que sempre precisamos de uma chave primária, mas no exemplo acima eu não passei uma para o modelo. Se eu tentar imprimir o id
do usuário que acabei de criar, ele vai retornar None
.
Isso ocorre porque o session.add
apenas registra as transações que queremos que sejam feitas, mas na verdade não faz nenhuma mudança no banco [3].
Conforme explicado neste link, temos duas operações que podem ser realizadas aqui:
session.flush() comunica uma série de operação ao banco de dados (inserir, atualizar, apagar). O banco de dados às mantém como operações pendentes em uma trasação. As mudanças não são persistidas no dsco ou visíveis em outras transações até o banco dados receber um COMMIT para a transação atual (que é o que o session.commit() faz).
ou
session.commit() persiste as mudanças no banco de dados. Esse comando sempre chama
session.flush()
como parte dele.
Fazendo buscas
Depois de termos registros no banco de dados, precisamos ter acesso a eles :)
Para isso podemos usar o método query
presente na nossa session
. O método recebe como parâmetro a classe que representa a tabela do banco em que queremos fazer a busca pelo nosso registro. Depois usamos o método filter_by
para buscar uma característica em uma das colunas (ou atributos da classe)
query = session.query(User).filter_by(name='John')
Por fim, passamos um método para indicar o que queremos fazer com esta consulta: contar o número de registros encontrados (.count()
), retornar todos os registros encontrados (.all()
), retornar apenas o primeiro registro (.first()
) e assim por diante:
query.count()
Outra maneira de fazer essa busca é usar o método filter
, em vez do filter_by
, que possui uma sintaxe ligeiramente diferente:
session.query(User).filter(User.name=='John').first()
Com o método filter
, você também pode procurar não por strings exatas mas por partes de strings:
session.query(User).filter(User.name.like('%John%')).first()
No Jupyterhub, foi adicionado a cada modelo um método de classe que simplifica essa sintaxe bastante complicada. Nesse caso criamos um método, que é um classmethod, que apenas precisa receber a session
e consegue fazer a busca de maneira mais simples. O método fica escrito dessa forma:
class User(Base):
...
@classmethod
def find_by_name(cls, session, name):
return session.query(cls).filter_by(name=name).all()
E a busca fica mais simples para encontrar usuários com o nome John:
Product.find_by_name(session, 'John')
Adicionando tabelas depois de iniciar o banco com create_all
Um dos problemas que tive enquanto trabalhava com o Projeto Jupyter, é que eu precisava criar uma nova tabela em um banco de dados e um engine
que já estavam criados, ou seja, depois do Base.metadata.create_all(engine)
.
Então, imagine que agora eu quero uma tabela com Produtos (Product) como a seguinte:
from sqlalchemy import Column, Integer, String
class Product(Base):
__tablename__ = 'products'
id = Column(Integer, primary_key=True)
name = Column(String)
A maneira mais simples que eu encontrei de criar essa nova tabela no banco de dados foi:
Product.__table__.create(engine)
Criando uma relação com chave estrangeira (foreign key)
Imagine que você gostaria de conectar cada produto (product) a um usuário (user) em seu sistema. Portanto, em cada instância da classe Product
, você gostaria de armazenar uma instância da classe User
:
Se você estiver criando todas as tabelas agora, uma classe Column
como atributo da sua classe Product
e indique esse atributo faz referência à chave estrangeira da classe User
e que vai armazenar o atributo id
como chave estrangeira:
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
class Product(Base):
__tablename__ = 'product'
id = Column(Integer, primary_key=True)
name = Column(String)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship('User')
Agora é preciso ir na classe User
e adicionar essa relação com Produto
para que seja possível acessar produtos que estão atrelados a um usuário:
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import relationship
class User(Base):
__tablename__ = 'user' # if you use base it is obligatory
id = Column(Integer, primary_key=True) # obligatory
name = Column(String)
password = Column(String)
products = relationship(Product, backref="users")
É possível criar as tabelas usando o Base.metadata.create_all(engine)
que vimos anteriormente. E agora, você pode criar um usuário e um produto relacionados entre si da seguinte forma:
user = User(name='John')
product = Product(name='wolf', user=user)
session.add_all([user, product])
session.commit()
E é isso 🙂
❤ Abraço! Letícia