Adicionando relacionamento em tabelas pré-existentes no SQLAlchemy
Neste texto você consegue ter uma ideia pequena de como o SQLAlchemy funciona. No entanto, todo o meu estudo dessa biblioteca aconteceu por causa de um problema que eu demorei muito tempo pra conseguir solucionar.
Como o problema era muito complexo e eu não achei que cabia no outro texto, decidi escrever esse aqui dedicado a ele. Aqui vai :)
Enquanto eu estava criando o Native Authenticator eu percebi que eu ia precisar guardar uma informação sobre o meu usuário que não estava disponível na tabela padrão User
. Informações tipo email e senha, por exemplo. O JupyterHu ja tinha uma classe User
que era mais ou menos assim:
While I was creating the Native Authenticator I realized that I needed to store some information about my user that wasn’t available in the default User
table. Informations such as email or password, for instance. The JupuyterHub User
class was like this:
# jupyterhub/orm.py
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Unicode(255), unique=True)
Eu decidi, então, criar uma classe chamada UserInfo
que iria guardar todas as informações adicionais que eu queria. Minha classe ficou assim:
# nativeauthenticator/orm.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class UserInfo(Base):
__tablename__ = 'users_info'
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String, nullable=False)
password = Column(String, nullable=False)
Uma vez que essa classe estava pronta, tudo o que eu precisava era adicionar essa tabela ao meu banco de dados. Então eu adicionei a criação dela no método __init__
do meu authenticador. Dessa forma:
# nativeauthenticator/nativeauthenticator.py
class NativeAuthenticator(Authenticator):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
inspector = inspect(self.db.bind)
if 'users_info' not in inspector.get_table_names():
UserInfo.__table__.create(self.db.bind)
Obs: self.db.bind é o SQLAlchemy engine.
Primeiro verifiquei se minha tabela já não estava lá (caso contrário, receberia um erro) e, se não estiver, ai sim o comando pra criar a tabela. Tudo estava funcionando até agora :)
O problema começou quando decidi adicionar um relacionamento entre UserInfo
e User
.
Por algum motivo, não consegui fazer funcionar. A tabela UserInfo
nunca encontrava a tabela User
e recebia este erro:
NoReferencedTableError: Foreign key associated with column ‘product.user_id’ could not find table ‘user’ with which to generate a foreign key to target column ‘id’.
Tradução: NoReferencedTableError: A chave estrangeira associada à coluna ‘product.user_id’ não pôde encontrar a tabela ‘user’ com a qual gerar uma chave estrangeira para a coluna de destino ‘id’.
Eu passei um bom tempo no Stack Overflow e depois de muito tempo eu escrevi uma issue pedindo ajuda. Foi quando o André me perguntou se eu estava usando a mesma instância de Base
pra criar os dois modelos. Adivinha? Não estava.
Portanto, a primeira coisa que você deve fazer ao criar duas classes para o mesmo engine
do SQLAlchemy
é usar o mesmoBase
em todos os modelos. Parece trivial, mas não é. Aqui está a solução:
# nativeauthenticator/orm.py
from jupyterhub.orm import Base
from sqlalchemy import Column, Integer, String
class UserInfo(Base):
__tablename__ = 'users_info'
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(String, nullable=False)
password = Column(String, nullable=False)
Depois disso, você pode adicionar os atributos para o relacionamento em sua classe:
class UserInfo(Base):
...
user_id = Column(Integer, ForeignKey('users.id'))
user = relationship(User)
Agora você pode simplesmente adicionar a tabela e ela funcionará!
No entanto, desta forma você não será capaz de ver quais instâncias de UserInfo
estão conectadas ao seu User
, porque apenas UserInfo
conhece User
. Portanto, você deve adicionar um relacionamento à classe User
. Como não estamos adicionando na criação da classe User
(porque eu não queria alterar o arquivo original), nós o adicionamos à classe no momento em que você define o relacionamento:
User.info = relationship(UserInfo, backref='users')
Veja um exemplo completo com as duas classes:
Pronto! Tudo funciona!
Agradecimentos especiais ao André, Yuvi e Pastore por toda a ajuda ❤️!
❤ Abraço! Letícia