Un tigre agazapado al acecho en SQLAlchemy. Los fundamentos


Buen dia


Hoy quiero hablar sobre ORM SQLAlchemy. Hablemos de qué se trata sus capacidades y flexibilidad, y también consideremos casos que no siempre se describen claramente.


Este ORM tiene un umbral de entrada superior al promedio, por lo que intentaré explicar todo en un lenguaje simple y con ejemplos. Este artículo será útil para aquellos que ya trabajan con sqlalchemy y desean actualizar sus habilidades o simplemente familiarizarse con esta biblioteca.


El lenguaje de programación utilizado es python 3.6.
DB - PostgreSQL.
Enlace de Github


Entonces, ¿qué es ORM?


ORM (mapeo relacional de objetos) es una tecnología que le permite mapear modelos cuyos tipos son incompatibles. Por ejemplo: una tabla de base de datos y un objeto de lenguaje de programación.


En otras palabras, puede acceder a objetos de clase para administrar datos en tablas de bases de datos. También puede crear, modificar, eliminar, filtrar y, lo más importante, heredar objetos de clase asignados a tablas de base de datos, lo que reduce significativamente el contenido de la base de código.


Para usar SQLAlchemy, debe comprender cómo funciona.


Los desarrolladores que usan Django-ORM tendrán que reconstruir un poco su mentalidad para crear consultas ORM. En mi opinión, SQLAlchemy es un monstruo funcional cuyas capacidades pueden y deben usarse, pero debe comprender que los ORM no siempre son perfectos. Por lo tanto, discutiremos los momentos en que el uso de esta tecnología es aconsejable.


SQLAlchemy tiene el concepto de definiciones de modelo declarativas y no declarativas.


Las definiciones no declarativas implican el uso de mapper (), que describe la asignación de cada columna de base de datos y clase de modelo.


Este artículo utiliza una definición declarativa de modelos.


Más aquí


Estructura DB


Para una coherencia completa de los datos, creemos las siguientes tablas.


El modelo básico se usa para determinar las columnas básicas en la base de datos.


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) 

Empleado: una tabla que describe al empleado que trabaja en la oficina


 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 no es una tabla. La clase heredada de Employee. Una gran oportunidad para separar la lógica y usar la clase como si fuera una tabla separada.


 class EmployeeWithSkills(Employee): skills = relation(Skill, secondary=EmployeesSkills.__tablename__, lazy='joined') 

Departamento: el departamento en el que trabaja este empleado. Una persona puede constar de varios departamentos.


 class Department(BaseModel): __tablename__ = 'departments' name = Column(VARCHAR(255), nullable=False) description = Column(VARCHAR(255), nullable=False) 

La tabla de correspondencia del empleado y las unidades en las que es miembro.


 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) 

Tabla de correspondencia de empleados y sus 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) 

Creamos migraciones utilizando el paquete alambique, que le permite generarlas automáticamente. En esta lección, la generación automática de migraciones es perfectamente aceptable.


La última migración contiene datos de prueba que llenarán la base de datos.
Cómo configurar el alambique se puede leer aquí
Realizamos atesorados cabezales de actualización de alambiques para llevar a cabo la migración.


Solicitudes y relaciones


Hagamos la primera solicitud y obtengamos información sobre el empleado por su identificación.
La solicitud se verá así:


lección1:


 employee = session.query(Employee).filter(Employee.id == eid).one() output: ID: 2, Tony Stark 

.one() al final significa que pretendemos obtener solo una entrada. Si hay varias entradas, se generará una excepción apropiada.


Si queremos obtener todos los departamentos disponibles, entonces podemos usar la siguiente consulta usando .all()


lección2:


 emmployee = session.query(Department).all() output: ID: 2, name: Guards ID: 4, name: Legions 

Considere trabajar con funciones de agregación.


Podemos obtener el número de departamentos disponibles utilizando la función incorporada.
.count() o use func.count() . Usando el segundo método, puede acceder a cualquier función SQL usando select o para calcular resultados intermedios.


lección3:


 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 

Muchos desarrolladores usan la función count() para verificar los datos en una solicitud. Esta no es una buena práctica, ya que provoca el uso de recursos adicionales de la base de datos y aumenta el tiempo de ejecución de la consulta. Una buena solución sería usar la función exists() que devuelve un valor escalar:
lección3:


 def check_department_exists(session: DBSession, department_name: str) -> bool: is_exists = session.query(exists().where(Department.name == department_name)).scalar() return is_exists 

Continuando, complicamos la tarea y nos familiarizamos con la relation entidad o relationship . El hecho es que en SQLAlchemy además de usar foreign_key
a nivel de base de datos, también se usan las relaciones entre objetos.


Por lo tanto, podemos hacer que la fila de la base de datos dependa de la clave externa en el objeto.
Estos objetos son una proyección en las tablas de la base de datos, interconectados.


Relations en SQLAlchemy tienen una configuración flexible, que le permite obtener datos de la base de datos de diferentes maneras en diferentes momentos utilizando el argumento nombrado lazy .


Los principales grados de "pereza":


  • select es el predeterminado. ORM realiza una solicitud solo cuando accede a datos. Se lleva a cabo en una solicitud por separado.
  • dynamic : le permite obtener un objeto de solicitud, que puede modificarse según lo desee. Recibe datos de la base de datos solo después de llamar a all () o one () o cualquier otro método disponible.
  • joined - se agrega a la solicitud principal usando LEFT JOIN. Se lleva a cabo de inmediato.
  • subquery : similar a select, pero ejecutada como una subconsulta.

El valor predeterminado es select .


El filtrado de consultas puede ser estático y dinámico. El filtrado dinámico permite llenar la solicitud con filtros, que pueden variar según el progreso de la función.


lección4:


 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 

La clase de filtro DFilter define filtros basados ​​en cualquier entrada. Si la clase de filtro está definida, pero más adelante en la solicitud se aplican las condiciones.


La función .filter () acepta acepta las condiciones binarias SQLAlchemy, por lo que se puede representar con *


El uso de filtros dinámicos está limitado solo por la imaginación. El resultado de la consulta muestra qué héroes están actualmente inactivos.


 output: Inactive_heros: Name: Tony Stark Name: Scott Lang Name: Peter Parker 

Sugiero trabajar con la relación de muchos a muchos.


Tenemos una tabla de empleados en la que existe una relación con la tabla de correspondencia de EmployeesSkills. Contiene Foreign_key en la tabla de empleados y Foreign_key
a la tabla de habilidades.


Lección 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 la clase EmployeeWithSkills en la consulta anterior, nos referimos a ella como una tabla de base de datos, pero en realidad dicha tabla no existe. Esta clase es diferente de Empleado que tiene una relación, que tiene una relación de muchos a muchos. Entonces podemos diferenciar la lógica de las clases, llenándola con un conjunto diferente de relaciones. Como resultado de la solicitud, veremos las habilidades de uno de los empleados.


Como el empleado puede estar en varios departamentos, cree una relación que le permita obtener esta información.


Cree una clase EmployeeWithDepartments heredada de Employee y agregue lo siguiente:


 class EmployeeWithDepartments(Employee): departments = relation( Department, # primaryjoin=EmployeeDepartments.employee_id == Employee.id, secondary=EmployeeDepartments.__tablename__, # secondaryjoin=EmployeeDepartments.department_id == Department.id, ) 

La clase creada no es una nueva tabla de base de datos. Esta sigue siendo la misma tabla de empleados, solo expandida usando la relation . De esta manera, puede acceder a la tabla Employee o EmployeeWithDepartments en consultas. La diferencia solo estará en la ausencia / presencia de relation .


El primer argumento indica a qué tabla crearemos la relation .
primaryjoin es la condición por la cual la segunda tabla se conectará antes de que se una al objeto.
secondary es el nombre de la tabla que contiene llaves_extrañas para la coincidencia. Usado en el caso de muchos a muchos.
secondaryjoin : condiciones para hacer coincidir la tabla intermedia con la última.


primaryjoin y secondaryjoin sirven para indicar explícitamente correspondencias en situaciones complejas.


A veces surgen situaciones cuando es necesario crear filtros cuyos campos se declaran en relaciones, y las relaciones, a su vez, son relaciones de la clase original.


 EmployeeWithCadreMovements -> relation(CadreMovement) -> field 

Si la relación muestra una lista de valores, se debe usar .any (), si solo se proporciona un valor, se debe usar .has ()


Para una mejor comprensión, esta construcción se interpretará en el lenguaje SQL en la construcción exist ().


Llamamos a la función get con el parámetro parámetro de razón, por ejemplo, simple .


lección6


 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] 

lesion7


Considere la posibilidad de obtener una relación usando la función de agregación. Por ejemplo, obtenemos el último movimiento de personal de un usuario específico.
primaryjoin es una condición para unir tablas (si se usa lazy = 'join'). Recuerde que select se usa por defecto.
En este caso, se genera una solicitud separada al acceder al atributo de clase. Es para esta solicitud que podemos especificar las condiciones de filtrado.
Como sabe, no puede usar las funciones de agregación en una forma "pura" en una condición WHERE, por lo que podemos implementar esta característica especificando la relación
con los siguientes 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) ) ) 

Cuando se ejecuta, la solicitud se compila así:


 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 ) 

Enlace de Github


Resumen


SQLAlchemy es una poderosa herramienta de creación de consultas que reduce el tiempo de desarrollo al admitir la herencia.


Pero debe mantener una línea muy fina entre usar ORM y escribir consultas complejas. En algunos casos, ORM puede confundir al desarrollador o hacer que el código sea engorroso e ilegible.
Buena suerte

Source: https://habr.com/ru/post/470285/


All Articles