潜伏在SQLAlchemy中的老虎。 基础知识


美好的一天


今天我想谈谈ORM SQLAlchemy。 让我们谈谈它的功能和灵活性是什么,并考虑未总是清楚描述的情况。


这个ORM的入门门槛高于平均水平,因此我将尝试用一种简单的语言和示例来解释所有内容。 对于已经使用sqlalchemy并想要提高其技能或只是熟悉该库的人员,本文将非常有用。


使用的编程语言是python 3.6。
DB-PostgreSQL。
Github链接


那么,ORM是什么?


ORM(对象关系映射)是一项允许您映射类型不兼容的模型的技术。 例如:数据库表和编程语言对象。


换句话说,您可以访问类对象以管理数据库表中的数据。 您还可以创建,修改,删除,过滤,最重要的是,继承映射到数据库表的类对象,这大大减少了代码库的内容。


要使用SQLAlchemy,您需要了解它的工作方式。


使用Django-ORM的开发人员将不得不重新构建思维方式来创建ORM查询。 在我看来,SQLAlchemy是一个功能怪兽,您可以并且应该使用其功能,但是您需要了解ORM并不总是完美的。 因此,我们将讨论建议使用此技术的时刻。


SQLAlchemy具有声明性和非声明性模型定义的概念。


非声明性定义意味着使用了mapper(),它描述了每个数据库列和模型类的映射。


本文使用模型的声明性定义。


这里更多


数据库结构


为了实现完全的数据一致性,让我们创建下表。


基本模型用于确定数据库中的基本列。


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) 

员工-描述在办公室工作的员工的表


 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不是一个表。 该类继承自Employee。 分离逻辑并像使用单独的表一样使用类的绝佳机会。


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

部门-该员工所在的部门。 一个人可以包含多个部门。


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

员工及其所属单位的对应表。


 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) 

员工及其技能的对应表。


 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) 

我们使用Alembic软件包创建迁移,该迁移使您可以自动生成迁移。 在本课程中,自动生成迁移是完全可以接受的。


最新的迁移包含将填充数据库的测试数据。
如何配置Alembic可以在这里阅读
我们进行珍贵的紫苏升级头来进行迁移。


请求和关系


让我们发出第一个请求,并通过其ID获取有关该员工的信息。
该请求将如下所示:


第一课:


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

.one()表示我们仅打算获得一个条目。 如果有多个条目,将引发适当的例外。


如果要获取所有可用部门,则可以使用.all()使用以下查询


第二课:


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

考虑使用聚合功能。


我们可以使用内置功能获得可用的部门数量。
.count()或使用func.count() 。 使用第二种方法,您可以使用select或计算中间结果来访问任何SQL函数。


第三课:


 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 

许多开发人员使用count()函数检查请求中的数据。 这不是一个好习惯,这会导致使用额外的数据库资源并增加查询执行时间。 一个好的解决方案是使用返回一个标量值的exists()函数:
第三课:


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

继续前进,我们使任务复杂化并熟悉实体relationrelationship 。 事实是,在SQLAlchemy除了使用foreign_key
在数据库级别,还使用对象之间的关系。


因此,我们可以获得依赖于对象中外键的数据库行。
这些对象是相互关联的数据库表上的投影。


SQLAlchemy中的Relations具有灵活的配置,允许您使用命名参数lazy在不同的时间以不同的方式从数据库获取数据。


“懒惰”的主要程度:


  • select是默认设置。 ORM仅在访问数据时发出请求。 它是在单独的请求中执行的。
  • dynamic -允许您获取请求对象,可以根据需要对其进行修改。 仅在调用all()或一个()或任何其他可用方法之后,它才从数据库接收数据。
  • join-使用LEFT JOIN添加到主请求中。 立即执行。
  • subquery -与选择类似,但作为子查询执行。

默认值为select


查询中的过滤可以是静态的也可以是动态的。 动态过滤允许使用过滤器填充请求,过滤器可能会根据功能的进度而有所不同。


第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 

DFilter过滤器类基于任何输入定义过滤器。 如果定义了过滤器类,则在请求中进一步适用条件。


.filter()函数接受接受SQLAlchemy二进制条件,因此可以使用*表示


动态过滤器的使用仅受想象力的限制。 查询结果显示哪些英雄当前处于非活动状态。


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

我建议使用多对多关系。


我们有一个Employee表,其中与EmployeesSkills对应表有关系。 它在雇员表上包含foreign_key和foreign_key
到技能表。


第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 

在上面的查询中使用EmployeeWithSkills类,我们将其称为数据库表,但实际上这种表不存在。 该类不同于具有关系的Employee,后者具有多对多关系。 因此,我们可以区分类的逻辑,并用一组不同的关系填充它。 根据要求,我们将看到一名员工的技能。


由于该员工可以在多个部门中,请创建一个关系以使您可以获取此信息。


创建从Employee继承的EmployeeWithDepartments类,并添加以下内容:


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

创建的类不是新的数据库表。 这仍然是相同的Employee表,仅使用relation扩展。 这样,您可以在查询中访问EmployeeEmployeeWithDepartments表。 区别只会在于没有/存在relation


第一个参数指示我们将在哪个表上创建relation
primaryjoin是连接第二张表之前将其连接到对象的条件。
secondary是包含要匹配的foreign_keys的表的名称。 在多对多的情况下使用。
secondaryjoin将中间表与最后一个表匹配的条件。


primaryjoinsecondaryjoin用于明确指示复杂情况下的对应关系。


有时会出现这样的情况:需要创建其字段在关系中声明的过滤器,而关系又是原始类的关系。


 EmployeeWithCadreMovements -> relation(CadreMovement) -> field 

如果关系显示值列表,则必须使用.any(),如果仅提供一个值,则必须使用.has()


为了更好地理解,此构造将以SQL语言解释为exist()构造。


我们使用带有reason参数参数的get函数进行调用,例如simple


第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] 

课7


考虑使用聚合函数获得关系的可能性。 例如,我们得到特定用户的最后人员移动。
primaryjoin是连接表的条件(如果使用lazy ='joined')。 回想一下,默认情况下使用select。
在这种情况下,访问class属性时会生成一个单独的请求。 我们可以为此请求指定过滤条件。
如您所知,您不能在WHERE条件下以“纯”形式使用聚合函数,因此我们可以通过指定关联来实现此功能
具有以下参数:


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

执行后,请求的编译如下:


 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 ) 

Github链接


总结


SQLAlchemy是功能强大的查询构建工具,可通过支持继承来缩短开发时间。


但是您应该在使用ORM和编写复杂查询之间保持良好的界限。 在某些情况下,ORM可能会使开发人员感到困惑或使代码繁琐且难以阅读。
祝你好运

Source: https://habr.com/ru/post/zh-CN470285/


All Articles