
Bom dia
Hoje eu quero falar sobre ORM SQLAlchemy. Vamos falar sobre o que é sobre seus recursos e flexibilidade, e também considerar casos que nem sempre são claramente descritos.
Esse ORM tem um limite de entrada acima da média, então tentarei explicar tudo em uma linguagem simples e com exemplos. Este artigo será útil para quem já trabalha com sqlalchemy e deseja atualizar suas habilidades ou apenas se familiarizar com esta biblioteca.
A linguagem de programação usada é python 3.6.
DB - PostgreSQL.
Link do Github
Então, o que é ORM?
ORM (Mapeamento Objeto-Relacional) é uma tecnologia que permite mapear modelos cujos tipos são incompatíveis. Por exemplo: uma tabela de banco de dados e um objeto de linguagem de programação.
Em outras palavras, você pode acessar objetos de classe para gerenciar dados em tabelas de banco de dados. Você também pode criar, modificar, excluir, filtrar e, mais importante, herdar objetos de classe mapeados para tabelas de banco de dados, o que reduz significativamente o conteúdo da base de código.
Para usar o SQLAlchemy, você precisa entender como ele funciona.
Os desenvolvedores que usam o Django-ORM precisarão reconstruir sua mentalidade um pouco para criar consultas ORM. Na minha opinião, SQLAlchemy é um monstro funcional cujas capacidades você pode e deve usar, mas você precisa entender que as ORMs nem sempre são perfeitas. Portanto, discutiremos os momentos em que o uso dessa tecnologia é apropriado.
SQLAlchemy tem o conceito de definições de modelo declarativo e não declarativo.
Definições não declarativas implicam o uso de mapper (), que descreve o mapeamento de cada coluna do banco de dados e classe de modelo.
Este artigo usa uma definição declarativa de modelos.
Mais aqui
Estrutura de banco de dados
Para obter consistência completa dos dados, vamos criar as seguintes tabelas.
O modelo básico é usado para determinar as colunas básicas no banco de dados.
class BaseModel(Base): __abstract__ = True id = Column(Integer, nullable=False, unique=True, primary_key=True, autoincrement=True) created_at = Column(TIMESTAMP, nullable=False) updated_at = Column(TIMESTAMP, nullable=False) def __repr__(self): return "<{0.__class__.__name__}(id={0.id!r})>".format(self)
Funcionário - uma tabela que descreve o funcionário que trabalha no escritório
class Employee(BaseModel): __tablename__ = 'employees' first_name = Column(VARCHAR(255), nullable=False) last_name = Column(VARCHAR(255), nullable=False) phone = Column(VARCHAR(255), unique=True, nullable=True) description = Column(VARCHAR(255), nullable=True)
EmployeeWithSkills não é uma tabela. A classe herdada de Employee. Uma ótima oportunidade para separar a lógica e usar a classe como se fosse uma tabela separada.
class EmployeeWithSkills(Employee): skills = relation(Skill, secondary=EmployeesSkills.__tablename__, lazy='joined')
Departamento - o departamento em que esse funcionário trabalha. Uma pessoa pode consistir em vários departamentos.
class Department(BaseModel): __tablename__ = 'departments' name = Column(VARCHAR(255), nullable=False) description = Column(VARCHAR(255), nullable=False)
A tabela de correspondência do funcionário e as unidades nas quais ele é membro.
class EmployeeDepartments(BaseModel): __tablename__ = 'employee_departments' employee_id = Column(Integer, ForeignKey('employees.id', ondelete='CASCADE'), nullable=False, index=True) department_id = Column(Integer, ForeignKey('departments.id', ondelete='CASCADE'), nullable=False, index=True)
Tabela de correspondência dos funcionários e suas habilidades.
class EmployeesSkills(BaseModel): __tablename__ = 'employees_skills' employee_id = Column(ForeignKey('employee.id', ondelete='CASCADE'), nullable=False, index=True) skill_id = Column(ForeignKey('skills.id', ondelete='CASCADE'), nullable=False, index=True)
Criamos migrações usando o pacote alembic, que permite gerá-las automaticamente. Nesta lição, a geração automática de migrações é perfeitamente aceitável.
A migração mais recente contém dados de teste que preencherão o banco de dados.
Como configurar o alambique pode ser lido aqui
Realizamos a cabeça de atualização de alambique preciosa para realizar a migração.
Pedidos e relações
Vamos fazer a primeira solicitação e obter informações sobre o funcionário por seu ID.
A solicitação terá a seguinte aparência:
lição1:
employee = session.query(Employee).filter(Employee.id == eid).one() output: ID: 2, Tony Stark
.one()
no final significa que pretendemos obter apenas uma entrada. Se houver várias entradas, uma exceção apropriada será gerada.
Se queremos obter todos os departamentos disponíveis, podemos usar a seguinte consulta usando .all()
lição2:
emmployee = session.query(Department).all() output: ID: 2, name: Guards ID: 4, name: Legions
Considere trabalhar com funções de agregação.
Podemos obter o número de departamentos disponíveis usando a função integrada.
.count()
ou use func.count()
. Usando o segundo método, você pode acessar quaisquer funções SQL usando select
ou calcular resultados intermediários.
lição3:
def get_departments_count(session: DBSession) -> int: count = session.query(Department).count() return count def get_departments_func_count(session: DBSession) -> int: count = session.query(func.count(Department.id)).scalar() return count
Muitos desenvolvedores usam a função count()
para verificar os dados em uma solicitação. Essa não é uma boa prática, causando o uso de recursos adicionais do banco de dados e aumentando o tempo de execução da consulta. Uma boa solução seria usar a função exists()
que retorna um valor escalar:
lição3:
def check_department_exists(session: DBSession, department_name: str) -> bool: is_exists = session.query(exists().where(Department.name == department_name)).scalar() return is_exists
Seguindo em frente, complicamos a tarefa e nos familiarizamos com a relation
ou relationship
da entidade. O fato é que, em SQLAlchemy
além de usar o Foreign_key
no nível do banco de dados, também são usados relacionamentos entre objetos.
Assim, podemos obter a linha do banco de dados dependente da chave estrangeira no objeto.
Esses objetos são uma projeção nas tabelas do banco de dados, interconectadas.
Relations
no SQLAlchemy
têm uma configuração flexível, permitindo obter dados do banco de dados de maneiras diferentes em momentos diferentes usando o argumento nomeado lazy
.
Os principais graus de "preguiça":
select
é o padrão. O ORM faz uma solicitação apenas ao acessar dados. É realizado em uma solicitação separada.dynamic
- permite obter um objeto de solicitação, que pode ser modificado conforme desejado. Ele recebe dados do banco de dados somente após chamar todos () ou um () ou qualquer outro método disponível.joined
- é adicionado à solicitação principal usando LEFT JOIN. É realizado imediatamente.subquery
- semelhante à seleção, mas executada como uma subconsulta.
O padrão é select
.
A filtragem de consultas pode ser estática e dinâmica. A filtragem dinâmica permite preencher a solicitação com filtros, que podem variar dependendo do progresso da função.
lição4:
def dynamic_filter(session: DBSession, filter: DFilter = None): query = session.query(Employee) if filter is not None: query = query.filter(*filter.conds) employees = query.all() return employees
A classe de filtro DFilter define filtros com base em qualquer entrada. Se a classe de filtro estiver definida, mas as condições de solicitação serão aplicadas.
A função .filter () aceita aceita as condições binárias SQLAlchemy, para que possa ser representada usando *
O uso de filtros dinâmicos é limitado apenas pela imaginação. O resultado da consulta mostra quais heróis estão inativos no momento.
output: Inactive_heros: Name: Tony Stark Name: Scott Lang Name: Peter Parker
Sugiro trabalhar com o relacionamento muitos-para-muitos.
Temos uma tabela Employee na qual existe uma relação com a tabela de correspondência EmployeesSkills. Ele contém chave estrangeira na tabela de funcionários e chave estrangeira
para a tabela de habilidades.
lição 5:
def get_employee_with_skills(session: DBSession, eid: int): employee = session.query(EmployeeWithSkills).filter(EmployeeWithSkills.id == eid).one() return employee output: Employee Tony Stark has skills: Skill: Fly, Desc: I belive I can Fly. I belive I can touch the sky Skill: Light Shield, Desc: Light protect. Perfect for everything
Usando a classe EmployeeWithSkills na consulta acima, nos referimos a ela como uma tabela de banco de dados, mas, na realidade, essa tabela não existe. Essa classe é diferente de Funcionário que possui um relacionamento, que possui um relacionamento muitos para muitos. Assim, podemos diferenciar a lógica das classes, preenchendo-a com um conjunto diferente de relações. Como resultado da solicitação, veremos as habilidades de um dos funcionários.
Como o funcionário pode estar em vários departamentos, crie uma relação que permita obter essas informações.
Crie uma classe EmployeeWithDepartments herdada de Employee e adicione o seguinte:
class EmployeeWithDepartments(Employee): departments = relation( Department, # primaryjoin=EmployeeDepartments.employee_id == Employee.id, secondary=EmployeeDepartments.__tablename__, # secondaryjoin=EmployeeDepartments.department_id == Department.id, )
A classe criada não é uma nova tabela de banco de dados. Essa ainda é a mesma tabela Employee, somente expandida usando a relation
. Dessa forma, você pode acessar a tabela EmployeeWithDepartments
ou EmployeeWithDepartments
nas consultas. A diferença estará apenas na ausência / presença de relation
.
O primeiro argumento indica para qual tabela criaremos a relation
.
primaryjoin
é a condição pela qual a segunda tabela será conectada antes de ser anexada ao objeto.
secondary
é o nome da tabela que contém chaves estrangeiras para correspondência. Usado no caso de muitos para muitos.
secondaryjoin
- condições para combinar a tabela intermediária com a última.
primaryjoin
e primaryjoin
secondaryjoin
servem para indicar explicitamente correspondências em situações complexas.
Às vezes, surgem situações em que é necessário criar filtros cujos campos são declarados em relações, e relações, por sua vez, são relações da classe original.
EmployeeWithCadreMovements -> relation(CadreMovement) -> field
Se a relação exibir uma lista de valores, .any () deverá ser usado, se apenas um valor for fornecido, .has () deverá ser usado
Para uma melhor compreensão, essa construção será interpretada na linguagem SQL na construção existir ().
Chamamos a função get com o parâmetro reason reason, por exemplo, simple
.
lição6
def has_in_relations(session: DBSession, reason: str): employees = session.query(EmployeeWithCadreMovements).filter(EmployeeWithCadreMovements.cadre_movements.any(CadreMovement.reason == reason)).all() return employees output: [Steve Rogers, Tony Stark]
lession7
Considere a possibilidade de obter uma relação usando a função de agregação. Por exemplo, obtemos o último movimento de pessoal de um usuário específico.
primaryjoin é uma condição para ingressar em tabelas (se lazy = 'join' for usado). Lembre-se de que selecionar é usado por padrão.
Nesse caso, uma solicitação separada é gerada ao acessar o atributo de classe. É para esta solicitação que podemos especificar as condições de filtragem.
Como você sabe, você não pode usar funções de agregação de forma "pura" em uma condição WHERE, para que possamos implementar esse recurso especificando a relação
com os seguintes parâmetros:
last_cadre_movement = relation( CadreMovement, primaryjoin=and_( CadreMovement.employee == Employee.id, uselist=False, CadreMovement.id == select([func.max(CadreMovement.id)]).where(CadreMovement.employee == Employee.id) ) )
Quando executada, a solicitação é compilada assim:
SELECT cadre_movements.id AS cadre_movements_id, cadre_movements.created_at AS cadre_movements_created_at, cadre_movements.updated_at AS cadre_movements_updated_at, cadre_movements.employee AS cadre_movements_employee, cadre_movements.old_department AS cadre_movements_old_department, cadre_movements.new_department AS cadre_movements_new_department, cadre_movements.reason AS cadre_movements_reason FROM cadre_movements WHERE cadre_movements.employee = %(param_1)s AND cadre_movements.id = ( SELECT max(cadre_movements.id) AS max_1 FROM cadre_movements WHERE cadre_movements.employee = %(param_1)s )
Link do Github
Sumário
O SQLAlchemy é uma poderosa ferramenta de criação de consultas que reduz o tempo de desenvolvimento, oferecendo suporte à herança.
Mas você deve manter uma linha tênue entre usar o ORM e escrever consultas complexas. Em alguns casos, o ORM pode confundir o desenvolvedor ou tornar o código complicado e ilegível.
Boa sorte