Un tigre accroupi se cache dans SQLAlchemy. Les bases


Bonne journée


Aujourd'hui, je veux parler de ORM SQLAlchemy. Parlons de ce que sont ses capacités et sa flexibilité, et considérons également les cas qui ne sont pas toujours clairement décrits.


Cet ORM a un seuil d'entrée supérieur à la moyenne, je vais donc essayer de tout expliquer dans un langage simple et avec des exemples. Cet article sera utile à ceux qui travaillent déjà avec sqlalchemy et souhaitent améliorer leurs compétences ou tout simplement se familiariser avec cette bibliothèque.


Le langage de programmation utilisé est python 3.6.
DB - PostgreSQL.
Lien Github


Alors, qu'est-ce que l'ORM?


ORM (Object-Relational Mapping) est une technologie qui vous permet de mapper des modèles dont les types sont incompatibles. Par exemple: une table de base de données et un objet de langage de programmation.


En d'autres termes, vous pouvez accéder aux objets de classe pour gérer les données dans les tables de base de données. Vous pouvez également créer, modifier, supprimer, filtrer et, surtout, hériter des objets de classe mappés aux tables de base de données, ce qui réduit considérablement le contenu de la base de code.


Pour utiliser SQLAlchemy, vous devez comprendre comment cela fonctionne.


Les développeurs qui utilisent Django-ORM devront reconstruire un peu leur état d'esprit pour créer des requêtes ORM. À mon avis, SQLAlchemy est un monstre fonctionnel dont vous pouvez et devez utiliser les capacités, mais vous devez comprendre que les ORM ne sont pas toujours parfaits. Par conséquent, nous discuterons des moments où l'utilisation de cette technologie est appropriée.


SQLAlchemy a le concept de définitions de modèle déclaratives et non déclaratives.


Les définitions non déclaratives impliquent l'utilisation de mapper (), qui décrit le mappage de chaque colonne de base de données et classe de modèle.


Cet article utilise une définition déclarative des modèles.


Plus ici


Structure DB


Pour une cohérence complète des données, créons les tableaux suivants.


Le modèle de base est utilisé pour déterminer les colonnes de base dans la base de données.


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) 

Employé - un tableau décrivant l'employé qui travaille au bureau


 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'est pas une table. La classe héritée de Employee. Une excellente occasion de séparer la logique et d'utiliser la classe comme s'il s'agissait d'une table distincte.


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

Département - le département dans lequel cet employé travaille. Une personne peut comprendre plusieurs départements.


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

La table de correspondance de l'employé et les unités dont il fait partie.


 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) 

Tableau de correspondance des salariés et de leurs compétences.


 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) 

Nous créons des migrations en utilisant le paquet alembic, qui vous permet de les générer automatiquement. Dans cette leçon, la génération automatique de migrations est parfaitement acceptable.


La dernière migration contient des données de test qui rempliront la base de données.
Comment configurer alambic peut être lu ici
Nous effectuons une précieuse mise à niveau d'alambic pour effectuer la migration.


Demandes et relations


Faisons la première demande et obtenons des informations sur l'employé par son identifiant.
La demande ressemblera à ceci:


leçon 1:


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

.one() à la fin signifie que nous avons l'intention d'obtenir une seule entrée. S'il y a plusieurs entrées, une exception appropriée sera levée.


Si nous voulons obtenir tous les départements disponibles, nous pouvons utiliser la requête suivante en utilisant .all()


leçon 2:


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

Envisagez de travailler avec des fonctions d'agrégation.


Nous pouvons obtenir le nombre de services disponibles en utilisant la fonction intégrée.
.count() ou utilisez func.count() . À l'aide de la deuxième méthode, vous pouvez accéder à toutes les fonctions SQL à l'aide de la select ou du calcul des résultats intermédiaires.


leçon 3:


 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 

De nombreux développeurs utilisent la fonction count() pour vérifier les données dans une demande. Ce n'est pas une bonne pratique, ce qui entraîne l'utilisation de ressources de base de données supplémentaires et augmente le temps d'exécution des requêtes. Une bonne solution serait d'utiliser la fonction exists() qui retourne une valeur scalaire:
leçon 3:


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

En continuant, nous compliquons la tâche et nous familiarisons avec la relation entité ou la relationship . Le fait est que dans SQLAlchemy en plus d'utiliser Foreign_key
au niveau de la base de données, les relations entre les objets sont également utilisées.


Ainsi, nous pouvons obtenir la ligne de base de données en fonction de la clé étrangère dans l'objet.
Ces objets sont une projection sur les tables de la base de données, interconnectées.


Relations dans SQLAlchemy ont une configuration flexible, vous permettant d'obtenir des données de la base de données de différentes manières à différents moments en utilisant l'argument nommé lazy .


Les principaux degrés de "paresse":


  • select est la valeur par défaut. ORM fait une demande uniquement lors de l'accès aux données. Il est effectué dans une demande distincte.
  • dynamic - vous permet d'obtenir un objet de demande, qui peut être modifié à votre guise. Il reçoit des données de la base de données uniquement après avoir appelé all () ou one () ou toute autre méthode disponible.
  • joined - est ajoutée à la requête principale à l'aide de LEFT JOIN. Elle est réalisée immédiatement.
  • subquery - subquery - similaire à select, mais exécutée en tant que sous-requête.

La valeur par défaut est select .


Le filtrage dans les requêtes peut être statique et dynamique. Le filtrage dynamique permet de remplir la demande avec des filtres, qui peuvent varier en fonction de l'avancement de la fonction.


leçon 4:


 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 classe de filtre DFilter définit des filtres basés sur n'importe quelle entrée. Si la classe de filtre est définie, mais plus loin dans les conditions de demande s'appliquent.


La fonction .filter () accepte accepte les conditions binaires SQLAlchemy, donc elle peut être représentée en utilisant *


L'utilisation de filtres dynamiques n'est limitée que par l'imagination. Le résultat de la requête montre quels héros sont actuellement inactifs.


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

Je suggère de travailler avec la relation plusieurs-à-plusieurs.


Nous avons une table des employés dans laquelle il y a une relation avec la table de correspondance des employés. Il contient foreign_key sur la table des employés et foreign_key
à la table des compétences.


leçon 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 

En utilisant la classe EmployeeWithSkills dans la requête ci-dessus, nous l'appelons une table de base de données, mais en réalité une telle table n'existe pas. Cette classe est différente de l'employé ayant une relation, qui a une relation plusieurs-à-plusieurs. Nous pouvons donc différencier la logique des classes en la remplissant d'un ensemble de relations différent. À la suite de la demande, nous verrons les compétences de l'un des employés.


Étant donné que l'employé peut être dans plusieurs départements, créez une relation qui vous permet d'obtenir ces informations.


Créez une classe EmployeeWithDepartments héritée de Employee et ajoutez ce qui suit:


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

La classe créée n'est pas une nouvelle table de base de données. Il s'agit toujours de la même table Employee, uniquement développée à l'aide de relation . De cette façon, vous pouvez accéder à la table Employee ou EmployeeWithDepartments dans les requêtes. La différence ne sera qu'en absence / présence de relation .


Le premier argument indique à quelle table nous allons créer la relation .
primaryjoin est la condition par laquelle la deuxième table sera connectée avant d'être attachée à l'objet.
secondary est le nom de la table contenant les clés étrangères pour la correspondance. Utilisé dans le cas de plusieurs à plusieurs.
secondaryjoin - conditions pour faire correspondre la table intermédiaire avec la dernière.


primaryjoin et le primaryjoin secondaryjoin servent à indiquer explicitement les correspondances dans des situations complexes.


Parfois, des situations surviennent lorsqu'il est nécessaire de créer des filtres dont les champs sont déclarés dans les relations, et les relations, à leur tour, sont des relations de la classe d'origine.


 EmployeeWithCadreMovements -> relation(CadreMovement) -> field 

Si la relation affiche une liste de valeurs, alors .any () doit être utilisé, si une seule valeur est fournie, alors .has () doit être utilisé


Pour une meilleure compréhension, cette construction sera interprétée dans le langage SQL dans la construction exist ().


Nous appelons la fonction get avec le paramètre de paramètre reason, par exemple, simple .


leçon 6


 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


Considérez la possibilité d'obtenir une relation en utilisant la fonction d'agrégation. Par exemple, nous obtenons le dernier mouvement de personnel d'un utilisateur spécifique.
primaryjoin est une condition pour joindre des tables (si lazy = 'join' est utilisé). Rappelons que select est utilisé par défaut.
Dans ce cas, une demande distincte est générée lors de l'accès à l'attribut de classe. C'est pour cette demande que l'on peut spécifier les conditions de filtrage.
Comme vous le savez, vous ne pouvez pas utiliser les fonctions d'agrégation sous une forme «pure» dans une condition WHERE, nous pouvons donc implémenter cette fonctionnalité en spécifiant la relation
avec les paramètres suivants:


 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) ) ) 

Une fois exécutée, la demande se compile comme suit:


 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 ) 

Lien Github


Résumé


SQLAlchemy est un puissant outil de création de requêtes qui réduit le temps de développement en prenant en charge l'héritage.


Mais vous devez garder une fine ligne entre l'utilisation d'ORM et l'écriture de requêtes complexes. Dans certains cas, ORM peut dérouter le développeur ou rendre le code lourd et illisible.
Bonne chance

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


All Articles