SQLAlchemy 是一个用于 Python 编程语言的数据库工具包和对象关系映射 (ORM) 系统,首次推出于 2005 年。从一开始,它就致力于提供一个端到端系统,用于使用 Python 处理关系数据库,并使用 Python 数据库 API (DBAPI) 进行数据库交互。即使在其最早的版本中,SQLAlchemy 的功能也吸引了大量关注。主要功能包括在处理复杂 SQL 查询和对象映射方面具有极强的灵活性,以及“工作单元”模式的实现,该模式为持久化数据到数据库提供了高度自动化的系统。
从一个粗略实现的概念开始,SQLAlchemy 迅速经历了一系列转型和重构,随着用户群的不断增长,其内部架构和公共 API 也进行了多次迭代。到 2009 年 1 月发布的 0.5 版本时,SQLAlchemy 已经开始形成稳定的形态,并在各种生产部署中得到了证明。在 0.6(2010 年 4 月)和 0.7(2011 年 5 月)版本中,架构和 API 的增强继续推动着构建最有效和稳定的库的过程。在撰写本文时,SQLAlchemy 被众多组织在各个领域使用,并且被许多人认为是使用 Python 处理关系数据库的事实标准。
“数据库抽象”一词通常被认为是指一种数据库通信系统,该系统隐藏了数据存储和查询方式的大部分细节。有时,这个术语会被推向极端,认为这样的系统不仅应该隐藏正在使用的关系数据库的细节,还应该隐藏关系结构本身的细节,甚至隐藏底层存储是否为关系型数据库。
ORM 最常见的批评集中在这样的假设上:这是此类工具的主要目的——“隐藏”关系数据库的使用,接管构建与数据库交互的任务,并将其简化为实现细节。这种隐藏方法的核心在于,设计和查询关系结构的能力被从开发人员手中夺走,而是由一个不透明的库来处理。
那些大量使用关系数据库的人知道,这种方法完全不切实际。关系结构和 SQL 查询功能强大,构成了应用程序设计核心。如何设计、组织和操作这些结构中的查询,不仅取决于所需的数据,还取决于信息的结构。如果隐藏了此实用程序,那么首先就没有必要使用关系数据库。
解决那些试图隐藏底层关系数据库的应用程序与关系数据库需要高度特异性的问题,通常被称为“对象关系阻抗不匹配”问题。SQLAlchemy 对这个问题采取了一种相对新颖的方法。
SQLAlchemy 认为,开发人员必须愿意考虑其数据的关联形式。一个预先确定并隐藏模式和查询设计决策的系统会降低使用关系数据库的实用性,从而导致所有经典的阻抗不匹配问题。
同时,这些决策的实现应尽可能地通过高级模式来执行。将对象模型关联到模式并通过 SQL 查询对其进行持久化是一项高度重复的任务。允许工具自动化这些任务可以开发出更简洁、功能更强大、更高效的应用程序,并且可以在比手动开发这些操作所需的时间少得多的时间内创建。
为此,SQLAlchemy 将自己称为一个工具包,以强调开发人员作为所有关系结构的设计者/构建者的角色,以及这些结构与应用程序之间关联的设计者/构建者,而不是作为库做出的决策的被动使用者。通过公开关系概念,SQLAlchemy 拥抱了“泄漏抽象”的理念,鼓励开发人员在应用程序和关系数据库之间定制一个自定义但完全自动化的交互层。SQLAlchemy 的创新之处在于,它允许在很大程度上实现自动化,而不会牺牲对关系数据库的控制。
SQLAlchemy 提供工具包方法的目标的核心是,它将数据库交互的每一层都作为丰富的 API 公开,并将任务划分为两个主要类别,称为Core 和ORM。Core 包括 Python 数据库 API (DBAPI) 交互、数据库理解的文本 SQL 语句的呈现以及模式管理。所有这些功能都作为公共 API 提供。然后,ORM 或对象关系映射器是在 Core 之上构建的特定库。SQLAlchemy 提供的 ORM 只是可以在 Core 之上构建的众多可能的物体抽象层之一,许多开发人员和组织直接在其 Core 之上构建应用程序。
Core/ORM 分离一直是 SQLAlchemy 最突出的特征,它既有优点也有缺点。SQLAlchemy 中明确的 Core 导致 ORM 将数据库映射的类属性关联到称为Table
的结构,而不是直接关联到数据库中表达的字符串列名;使用名为select
的结构生成 SELECT 查询,而不是将对象属性直接拼接到字符串语句中;并通过名为ResultProxy
的界面接收结果行,该界面将select
透明地映射到每一行结果,而不是将数据直接从数据库游标传输到用户定义的对象。
在非常简单的以 ORM 为中心的应用程序中,Core 元素可能不可见。但是,由于 Core 与 ORM 紧密集成,以允许在 ORM 和 Core 构造之间进行流畅的转换,因此更复杂的以 ORM 为中心的应用程序可以“向下”移动一到两个级别,以便根据需要以更具体和更精细的方式处理数据库。随着 SQLAlchemy 的成熟,随着 ORM 继续提供更复杂和全面的模式,Core API 在常规使用中变得不那么明显了。但是,Core 的可用性也是 SQLAlchemy 早期成功的原因之一,因为它允许早期用户完成比 ORM 仍在开发时可能完成的更多的事情。
ORM/Core 方法的缺点是指令必须经过更多步骤。Python 的传统 C 实现对于单个函数调用有很大的开销损失,而函数调用是运行时速度慢的主要原因。传统的缓解方法包括通过重新排列和内联缩短调用链,以及用 C 代码替换性能关键区域。SQLAlchemy 花了多年时间使用这两种方法来提高性能。但是,PyPy 解释器在 Python 中日益普及,可能会承诺消除剩余的性能问题,而无需用 C 代码替换 SQLAlchemy 的大部分内部代码,因为 PyPy 通过即时内联和编译大大减少了长调用链的影响。
SQLAlchemy 的基础是一个通过 DBAPI 与数据库交互的系统。DBAPI 本身不是一个实际的库,只是一个规范。因此,特定目标数据库(如 MySQL 或 PostgreSQL)或特定非 DBAPI 数据库适配器(如 ODBC 和 JDBC)都提供了 DBAPI 的实现。
DBAPI 带来两个挑战。第一个是围绕 DBAPI 的基本使用模式提供一个易于使用且功能齐全的界面。第二个是处理特定 DBAPI 实现以及底层数据库引擎的极端可变性。
DBAPI 描述的接口非常简单。其核心组件是 DBAPI 模块本身、连接对象和游标对象——数据库术语中的“游标”表示特定语句及其关联结果的上下文。与这些对象进行简单的交互以连接到数据库并从中检索数据如下所示
connection = dbapi.connect(user="user", pw="pw", host="host") cursor = connection.cursor() cursor.execute("select * from user_table where name=?", ("jack",)) print "Columns in result:", [desc[0] for desc in cursor.description] for row in cursor.fetchall(): print "Row:", row cursor.close() connection.close()
SQLAlchemy 在经典 DBAPI 交互周围创建了一个界面。此界面的入口点是create_engine
调用,从中汇集连接和配置信息。生成的实例为Engine
。此对象然后表示到 DBAPI 的网关,DBAPI 本身永远不会直接公开。
对于简单的语句执行,Engine
提供了所谓的隐式执行接口。获取和关闭 DBAPI 连接和游标的工作都在后台处理
engine = create_engine("postgresql://user:pw@host/dbname") result = engine.execute("select * from table") print result.fetchall()
当引入 SQLAlchemy 0.2 时,添加了Connection
对象,从而能够显式维护 DBAPI 连接的范围
conn = engine.connect() result = conn.execute("select * from table") print result.fetchall() conn.close()
Engine
或Connection
的execute
方法返回的结果称为ResultProxy
,它提供了一个类似于 DBAPI 游标但行为更丰富的接口。Engine
、Connection
和ResultProxy
分别对应于 DBAPI 模块、特定 DBAPI 连接的实例和特定 DBAPI 游标的实例。
在幕后,Engine
引用了一个名为Dialect
的对象。Dialect
是一个抽象类,它有许多实现,每个实现都针对特定的 DBAPI/数据库组合。代表Engine
创建的Connection
将参考此Dialect
来做出所有决策,这些决策根据目标 DBAPI 和正在使用的数据库可能有不同的行为。
Connection
在创建时,将从也与Engine
关联的称为Pool
的存储库中获取并维护一个实际的 DBAPI 连接。Pool
负责创建新的 DBAPI 连接,并且通常将它们保存在内存池中以供频繁重复使用。
在语句执行期间,Connection
将创建一个名为ExecutionContext
的附加对象。该对象从执行点持续到ResultProxy
的生命周期。对于某些 DBAPI/数据库组合,它也可能作为特定子类可用。
图 20.2说明了所有这些对象及其彼此之间的关系以及与 DBAPI 组件的关系。
为了管理 DBAPI 行为的可变性,首先我们将考虑问题的范围。DBAPI 规范(当前版本为 2)编写为一系列 API 定义,这些定义允许行为有很大程度的可变性,并且留下了许多未定义的区域。因此,现实生活中的 DBAPI 在几个方面表现出很大程度的可变性,包括何时接受 Python unicode 字符串以及何时不接受;如何在 INSERT 语句后获取“最后一个插入 ID”(即自动生成的 PRIMARY KEY);以及如何指定和解释绑定参数值。它们还具有大量特定于类型的特殊行为,包括处理二进制、高精度数值、日期、布尔值和 unicode 数据。
SQLAlchemy 通过允许在Dialect
和 ExecutionContext
中进行多级子类化来实现这种灵活性。图 20.3 说明了在与 psycopg2 方言一起使用时,Dialect
和 ExecutionContext
之间的关系。PGDialect
类提供了特定于 PostgreSQL 数据库使用的行为,例如 ARRAY 数据类型和模式目录;然后,PGDialect_psycopg2
类提供了特定于 psycopg2 DBAPI 的行为,包括 Unicode 数据处理程序和服务器端游标行为。
在处理支持多个数据库的 DBAPI 时,上述模式会出现一个变体。例如 pyodbc,它通过 ODBC 处理任意数量的数据库后端,以及 zxjdbc,一个仅限 Jython 的驱动程序,它处理 JDBC。上述关系通过使用来自 sqlalchemy.connectors
包的混合类来增强,该类提供了多个后端共有的 DBAPI 行为。图 20.4 说明了sqlalchemy.connectors.pyodbc
的通用功能,这些功能在针对 MySQL 和 Microsoft SQL Server 的 pyodbc 特定方言之间共享。
Dialect
和 ExecutionContext
对象提供了一种定义与数据库和 DBAPI 的每次交互的方式,包括如何格式化连接参数以及如何在语句执行期间处理特殊问题。Dialect
也是 SQL 编译构造的工厂,这些构造可以为目标数据库正确地呈现 SQL,以及类型对象,这些对象定义了如何将 Python 数据编组到目标 DBAPI 和数据库以及从中编组。
在建立数据库连接和交互性之后,接下来的任务是提供后端无关的 SQL 语句的创建和操作。为了实现这一点,我们需要首先定义如何引用数据库中存在的表和列——所谓的“模式”。表和列表示数据是如何组织的,大多数 SQL 语句都包含引用这些结构的表达式和命令。
ORM 或数据访问层需要提供对 SQL 语言的编程访问;基础是一个描述表和列的编程系统。这就是 SQLAlchemy 提供 Core 和 ORM 第一个强有力区分的地方,它提供了Table
和 Column
构造,这些构造独立于用户的模型类定义来描述数据库的结构。将模式定义与对象关系映射区分开来的基本原理是,可以明确地根据关系数据库设计关系模式,必要时包括特定于平台的细节,而不会与对象关系概念混淆——这些仍然是一个单独的问题。独立于 ORM 组件也意味着模式描述系统对于可能构建在 Core 之上的任何其他类型的对象关系系统同样有用。
Table
和 Column
模型属于所谓的元数据的范围,提供了一个名为MetaData
的集合对象来表示Table
对象的集合。该结构主要源自 Martin Fowler 在《企业应用架构模式》中对“元数据映射”的描述。图 20.5 说明了sqlalchemy.schema
包的一些关键元素。
Table
表示目标模式中存在的实际表的名称和其他属性。它的Column
对象集合表示各个表列的命名和类型信息。提供了一系列描述约束、索引和序列的对象来填写更多详细信息,其中一些会影响引擎和 SQL 构造系统的行为。特别是,ForeignKeyConstraint
对于确定如何连接两个表至关重要。
模式包中的Table
和 Column
与包的其余部分不同,它们是双重继承的,既来自sqlalchemy.schema
包也来自sqlalchemy.sql.expression
包,不仅充当模式级构造,而且还充当 SQL 表达式语言中的核心语法单元。这种关系在图 20.6中进行了说明。
在图 20.6中,我们可以看到Table
和 Column
作为 SQL 世界中的特定形式继承自“可以从中选择的事物”,称为FromClause
,以及“可以在 SQL 表达式中使用的事物”,称为ColumnElement
。
在 SQLAlchemy 的创建过程中,SQL 生成的方法并不明确。文本语言可能是候选者;这是一种常见的方法,是像 Hibernate 的 HQL 这样知名对象关系工具的核心。但是,对于 Python 来说,可以使用更有趣的选择:使用 Python 对象和表达式生成性地构建表达式树结构,甚至重新利用 Python 运算符,以便可以为运算符赋予 SQL 语句行为。
虽然它可能不是第一个这样做的工具,但应充分肯定 Ian Bicking 的 SQLObject 中包含的 SQLBuilder 库是 SQLAlchemy 表达式语言使用的 Python 对象和运算符系统的灵感来源。在这种方法中,Python 对象表示 SQL 表达式的词法部分。这些对象上的方法以及重载运算符会生成源自它们的新的词法构造。最常见的对象是“Column”对象——SQLObject 会在 ORM 映射的类上使用通过.q
属性访问的命名空间来表示这些对象;SQLAlchemy 将该属性命名为.c
。.c
属性今天仍然保留在 Core 可选择元素上,例如表示表和选择语句的元素。
SQLAlchemy SQL 表达式构造非常类似于如果您正在解析 SQL 语句将创建的结构——它是一个解析树,除了开发人员直接创建解析树,而不是从字符串派生它。此解析树中的核心节点类型称为ClauseElement
,图 20.7 说明了ClauseElement
与一些关键类之间的关系。
通过使用构造函数、方法和重载的 Python 运算符函数,类似于以下语句的结构:
SELECT id FROM user WHERE name = ?
可以使用 Python 构建如下:
from sqlalchemy.sql import table, column, select user = table('user', column('id'), column('name')) stmt = select([user.c.id]).where(user.c.name=='ed')
上述select
构造的结构显示在图 20.8中。请注意,字面量值'ed'
的表示包含在_BindParam
构造中,因此会导致它在 SQL 字符串中使用问号作为绑定参数标记进行呈现。
从树形图中,可以看出,通过节点进行简单的向下遍历可以快速创建渲染的 SQL 语句,我们将在关于语句编译的部分中更详细地介绍这一点。
在 SQLAlchemy 中,像这样的表达式:
column('a') == 2
既不产生True
也不产生False
,而是产生一个 SQL 表达式构造。关键是使用 Python 特殊运算符函数重载运算符:例如,__eq__
、__ne__
、__le__
、__lt__
、__add__
、__mul__
等方法。面向列的表达式节点通过使用名为ColumnOperators
的混合类提供重载的 Python 运算符行为。使用运算符重载,表达式column('a') == 2
等效于:
from sqlalchemy.sql.expression import _BinaryExpression from sqlalchemy.sql import column, bindparam from sqlalchemy.operators import eq _BinaryExpression( left=column('a'), right=bindparam('a', value=2, unique=True), operator=eq )
eq
构造实际上是源自 Python operator
内置函数的一个函数。将运算符表示为对象(即operator.eq
)而不是字符串(即=
)允许在语句编译时定义字符串表示形式,此时已知数据库方言信息。
负责将 SQL 表达式树呈现为文本 SQL 的核心类是Compiled
类。此类有两个主要子类,SQLCompiler
和 DDLCompiler
。SQLCompiler
处理 SELECT、INSERT、UPDATE 和 DELETE 语句的 SQL 渲染操作,统称为 DQL(数据查询语言)和 DML(数据操作语言),而DDLCompiler
处理各种 CREATE 和 DROP 语句,归类为 DDL(数据定义语言)。还有一个额外的类层次结构专注于类型的字符串表示形式,从TypeCompiler
开始。各个方言然后提供所有三种编译器类型的自己的子类,以定义特定于目标数据库的 SQL 语言方面。图 20.9 提供了关于 PostgreSQL 方言的此类层次结构的概述。
Compiled
子类定义了一系列访问方法,每个方法都由ClauseElement
的特定子类引用。遍历ClauseElement
节点的层次结构,并通过递归连接每个访问函数的字符串输出来构造语句。在此过程中,Compiled
对象维护有关匿名标识符名称、绑定参数名称和子查询嵌套等状态,所有这些都旨在生成字符串 SQL 语句以及最终的绑定参数集合,并带有默认值。图 20.10 说明了访问方法导致文本单元的过程。
已完成的Compiled
结构包含完整的 SQL 字符串和绑定值的集合。这些由ExecutionContext
强制转换为 DBAPI 的execute
方法预期的格式,其中包括诸如 Unicode 语句对象的处理、用于存储绑定值的集合类型以及有关如何将绑定值本身强制转换为适合 DBAPI 和目标数据库的表示形式的详细信息。
现在我们将注意力转向 ORM。第一个目标是使用我们定义的表元数据系统,允许将用户定义的类映射到数据库表中的一组列。第二个目标是允许根据数据库中表之间的关系定义用户定义的类之间的关系。
SQLAlchemy 将此称为“映射”,遵循 Fowler 的《企业架构模式》中描述的著名数据映射模式。总的来说,SQLAlchemy ORM 很大程度上借鉴了 Fowler 详细介绍的做法。它也深受著名的 Java 关系映射器 Hibernate 和 Ian Bicking 的 Python 产品 SQLObject 的影响。
我们使用术语经典映射来指代 SQLAlchemy 将对象关系数据映射应用于现有用户类的系统。此表单将Table
对象和用户定义的类视为两个单独定义的实体,它们通过名为mapper
的函数连接在一起。一旦mapper
已应用于用户定义的类,该类就会获得与表中的列相对应的新属性
class User(object): pass mapper(User, user_table) # now User has an ".id" attribute User.id
mapper
还可以将其他类型的属性附加到类,包括与其他类型对象的引用相对应的属性,以及任意 SQL 表达式。在 Python 世界中,将任意属性附加到类的过程称为“猴子补丁”;但是,由于我们以数据驱动且非任意的方式执行此操作,因此操作的精神可以用术语类检测更好地表达。
SQLAlchemy 的现代用法主要围绕着声明式扩展(Declarative extension),这是一种配置系统,类似于许多其他对象关系工具使用的常见的类ActiveRecord风格声明系统。在这个系统中,最终用户在类定义中明确定义属性,每个属性代表一个要映射的类属性。在大多数情况下,Table
对象不会被显式提及,mapper
函数也不会被显式提及;只有类、Column
对象和其他与 ORM 相关的属性会被命名。
class User(Base): __tablename__ = 'user' id = Column(Integer, primary_key=True)
上面可能看起来像是通过我们在代码中放置 id = Column()
来直接实现类的检测,但事实并非如此。声明式扩展使用 Python 元类,这是一种方便的方法,可以在每次声明新类时运行一系列操作,以根据声明的内容生成一个新的 Table
对象,并将其与类一起传递给 mapper
函数。然后,mapper
函数以完全相同的方式执行其工作,将自己的属性修补到类上,在本例中修补到 id
属性,并替换之前存在的内容。当元类初始化完成后(即,执行流程离开由 User
分隔的代码块时),由 id
标记的 Column
对象已被移动到一个新的 Table
中,并且 User.id
已被替换为映射特有的新属性。
SQLAlchemy 一直都希望有一个简写、声明式的配置形式。但是,声明式扩展的创建被延迟,以支持继续巩固经典映射机制的工作。早期存在一个名为 ActiveMapper 的临时扩展,后来演变成了 Elixir 项目。它在更高级别的声明系统中重新定义了映射结构。声明式的目标是通过建立一个几乎完全保留 SQLAlchemy 经典映射概念的系统来扭转 Elixir 的高度抽象方法的方向,只是重新组织了它们的使用方式,使其比经典映射更简洁,更易于进行类级扩展。
无论使用经典映射还是声明式映射,映射类都会获得新的行为,使其能够用其属性来表达 SQL 结构。SQLAlchemy 最初遵循 SQLObject 的行为,使用一个特殊的属性作为 SQL 列表达式的来源,SQLAlchemy 将其称为 .c
,例如:
result = session.query(User).filter(User.c.username == 'ed').all()
然而,在 0.4 版本中,SQLAlchemy 将此功能移到了映射属性本身。
result = session.query(User).filter(User.username == 'ed').all()
这种属性访问的变化被证明是一个巨大的改进,因为它允许类上存在的类似列的对象获得底层 Table
对象上不存在的额外特定于类的功能。它还允许不同类型的类属性之间的使用集成,例如直接引用表列的属性、引用从这些列派生的 SQL 表达式的属性以及引用相关类的属性。最后,它在映射类及其实例之间提供了对称性,因为同一个属性可以根据父类型的不同而具有不同的行为。类绑定属性返回 SQL 表达式,而实例绑定属性返回实际数据。
附加到 User
类的 id
属性是一种在 Python 中称为描述符的对象,它具有 __get__
、__set__
和 __del__
方法,Python 运行时会将所有涉及此属性的类和实例操作都委托给这些方法。SQLAlchemy 的实现被称为 InstrumentedAttribute
,我们将用另一个例子来说明此外观背后的世界。从 Table
和用户定义的类开始,我们设置一个只包含一个映射列以及一个 relationship
的映射,后者定义了对相关类的引用。
user_table = Table("user", metadata, Column('id', Integer, primary_key=True), ) class User(object): pass mapper(User, user_table, properties={ 'related':relationship(Address) })
映射完成后,与类相关的对象的结构在图 20.11中进行了详细说明。
该图说明了 SQLAlchemy 映射定义为用户定义的类与其映射到的表元数据之间两个独立的交互层。类检测显示在左侧,而 SQL 和数据库功能显示在右侧。正在使用的通用模式是使用对象组合来隔离行为角色,并使用对象继承来区分特定角色内的行为差异。
在类检测领域,ClassManager
与映射类链接,而其 InstrumentedAttribute
对象集合与类上映射的每个属性链接。InstrumentedAttribute
也是前面提到的面向公众的 Python 描述符,在基于类的表达式(例如,User.id==5
)中使用时会生成 SQL 表达式。在处理 User
的实例时,InstrumentedAttribute
会将属性的行为委托给一个 AttributeImpl
对象,该对象有多种变体,针对要表示的数据类型进行定制。
在映射方面,Mapper
表示用户定义的类和可选择单元(最典型的是 Table
)之间的链接。Mapper
维持一个每个属性对象的集合,称为 MapperProperty
,它处理特定属性的 SQL 表示。MapperProperty
最常见的变体是 ColumnProperty
,表示映射列或 SQL 表达式,以及 RelationshipProperty
,表示与另一个映射的链接。
MapperProperty
将属性加载行为(包括属性如何在 SQL 语句中呈现以及如何从结果行填充)委托给一个 LoaderStrategy
对象,该对象有多种变体。不同的 LoaderStrategies
确定属性的加载行为是延迟、急切还是立即。在映射配置时会选择一个默认版本,并可以选择在查询时使用备用策略。RelationshipProperty
还引用一个 DependencyProcessor
,它处理在刷新时如何进行映射间依赖关系和属性同步。DependencyProcessor
的选择基于与关系链接的父和目标可选择项的关系几何形状。
Mapper
/RelationshipProperty
结构形成一个图,其中 Mapper
对象是节点,RelationshipProperty
对象是有向边。一旦应用程序声明了完整的映射器集,就会执行一个称为配置的延迟“初始化”步骤。它主要由每个 RelationshipProperty
使用,以巩固其父和目标映射器之间的细节,包括 AttributeImpl
以及 DependencyProcessor
的选择。此图是在 ORM 操作过程中使用的关键数据结构。它参与诸如定义操作如何沿对象路径传播的所谓“级联”行为的操作、在一次“急切”加载相关对象和集合的查询操作中,以及在对象刷新方面,在发出一系列持久化步骤之前建立所有对象的依赖关系图。
SQLAlchemy 通过一个名为 Query
的对象启动所有对象加载行为。Query
开始时的基本状态包括实体,它是要查询的映射类和/或单个 SQL 表达式的列表。它还引用 Session
,后者表示与一个或多个数据库的连接,以及关于这些连接上的事务已累积的数据缓存。下面是一个基本的用法示例
from sqlalchemy.orm import Session session = Session(engine) query = session.query(User)
我们创建了一个 Query
,它将生成 User
的实例,相对于我们创建的新 Session
。Query
以与前面讨论的 select
结构相同的方式提供生成器构建器模式,其中附加的条件和修饰符一次一个方法调用与语句结构关联。当在 Query
上调用迭代操作时,它会构建一个表示 SELECT 的 SQL 表达式结构,将其发送到数据库,然后将结果集行解释为与最初请求的实体集相对应的面向 ORM 的结果。
Query
在操作的SQL 渲染和数据加载部分之间做出了严格的区分。前者指的是 SELECT 语句的构建,后者指的是将 SQL 结果行解释为 ORM 映射的结构。实际上,数据加载可以在没有 SQL 渲染步骤的情况下进行,因为可以要求 Query
解释用户手动编写的文本查询的结果。
SQL 渲染和数据加载都利用对由一系列主要 Mapper
对象形成的图的递归下降,将每个包含列或 SQL 表达式的 ColumnProperty
视为叶子节点,并将每个通过所谓的“急切加载”包含在查询中的 RelationshipProperty
视为通往另一个 Mapper
节点的边。每个节点的遍历和要采取的操作最终是与每个 MapperProperty
关联的每个 LoaderStrategy
的工作,在 SQL 渲染阶段将列和连接添加到正在构建的 SELECT 语句中,并在数据加载阶段生成处理结果行的 Python 函数。
在数据加载阶段生成的 Python 函数在获取数据库行时分别接收一个数据库行,并因此产生映射属性内存状态的可能变化。它们是根据对结果集中第一行输入的检查以及加载选项有条件地为特定属性生成的。如果属性的加载不应继续,则不会生成可调用函数。
图 20.12 说明了在连接急切加载场景中多个 LoaderStrategy
对象的遍历,说明了它们与 Query
的 _compile_context
方法期间发生的渲染 SQL 语句的连接。它还显示了行填充函数的生成,这些函数接收结果行并填充单个对象属性,此过程发生在 Query
的 instances
方法中。
SQLAlchemy 早期填充结果的方法是使用与每个策略关联的固定对象方法的传统遍历来接收每一行并相应地采取行动。加载器可调用系统(首次引入于 0.5 版本)代表了性能的巨大飞跃,因为许多关于行处理的决策可以在一开始就做出一次,而不是对每一行都做出,并且可以消除大量没有净效果的函数调用。
在 SQLAlchemy 中,Session
对象提供了 ORM 实际用法的公共接口,即加载和持久化数据。它为给定数据库连接的查询和持久化操作提供起点。
Session
除了充当数据库连接的网关外,还维护对相对于该 Session
在内存中存在的所有映射实体集的活动引用。正是通过这种方式,Session
实现标识映射和工作单元模式的外观,这两种模式都由 Fowler 识别。标识映射维护所有特定 Session
对象的数据库标识唯一映射,消除了重复标识带来的问题。工作单元建立在标识映射的基础上,提供了一种自动以最有效的方式将所有状态更改持久化到数据库中的系统。实际的持久化步骤称为“刷新”,在现代 SQLAlchemy 中,此步骤通常是自动的。
Session
最初是一个主要隐藏的系统,负责执行单一任务:发出刷新操作。刷新过程涉及向数据库发出 SQL 语句,这些语句对应于工作单元系统跟踪的对象状态的变化,从而将数据库的当前状态与内存中的状态同步。刷新一直是 SQLAlchemy 执行的最复杂操作之一。
刷新操作的调用在早期版本中是在一个名为 commit
的方法后面进行的,并且该方法存在于一个隐式、线程局部的名为 objectstore
的对象上。当使用 SQLAlchemy 0.1 时,无需调用 Session.add
,也根本没有显式 Session
的概念。唯一面向用户的步骤是创建映射器、创建新对象、修改通过查询加载的现有对象(查询本身是从每个 Mapper
对象直接调用的),然后通过 objectstore.commit
命令持久化所有更改。一组操作的对象池是无条件地模块全局的,并且是无条件地线程局部的。
objectstore.commit
模型一经推出就受到第一批用户的欢迎,但该模型的僵化性很快遇到了瓶颈。刚接触现代 SQLAlchemy 的用户有时会抱怨需要为 Session
对象定义工厂,可能还需要定义注册表,以及需要将他们的对象组织到一次一个 Session
中,但这远比早期整个系统完全隐式的时候要好得多。0.1 使用模式的便利性在现代 SQLAlchemy 中仍然很大程度上存在,后者具有一个会话注册表,通常配置为使用线程局部作用域。
Session
本身直到 SQLAlchemy 0.2 版本才被引入,其模型大致参考了 Hibernate 中的 Session
对象。此版本具有集成的交易控制,其中 Session
可以通过 begin
方法置于交易中,并通过 commit
方法完成。objectstore.commit
方法被重命名为 objectstore.flush
,并且可以随时创建新的 Session
对象。Session
本身是从另一个名为 UnitOfWork
的对象中分离出来的,该对象仍然作为负责执行实际刷新操作的私有对象。
虽然刷新过程最初是由用户显式调用的方法,但 SQLAlchemy 的 0.4 系列引入了自动刷新的概念,这意味着在每个查询之前都会立即发出刷新操作。自动刷新的优点是,查询发出的 SQL 语句在关系方面始终可以访问内存中存在的精确状态,因为所有更改都已发送。早期版本的 SQLAlchemy 无法包含此功能,因为最常见的用法模式是刷新语句也会永久提交更改。但是,当引入自动刷新时,它还伴随着另一个名为事务性 Session
的功能,该功能提供了一个 Session
,该 Session
会自动开始一个事务,该事务一直持续到用户显式调用 commit
为止。随着此功能的引入,flush
方法不再提交其刷新的数据,并且可以安全地以自动方式调用。Session
现在可以通过根据需要刷新来提供内存状态和 SQL 查询状态之间的逐步同步,在显式 commit
步骤之前不会持久化任何内容。事实上,Hibernate for Java 中的行为完全相同。但是,SQLAlchemy 采用这种使用风格是基于 Python 中 Storm ORM 的相同行为,该行为是在 SQLAlchemy 版本 0.3 时引入的。
0.5 版本在引入事务后过期时带来了更多的交易集成;在每次 commit
或 rollback
后,默认情况下,Session
中的所有状态都会过期(擦除),以便在随后的 SQL 语句重新选择数据或在新的上下文中访问剩余过期的对象的属性时再次填充事务。最初,SQLAlchemy 是围绕着尽可能少地发出 SELECT 语句的假设构建的,无条件地。出于这个原因,提交时过期行为出现得很慢;但是,它完全解决了包含过时数据的 Session
的问题,在没有简单方法加载较新数据的情况下,无需重建已加载的完整对象集。早期,似乎这个问题无法合理地解决,因为不清楚 Session
何时应将当前状态视为过时,从而在下次访问时产生一组昂贵的新的 SELECT 语句。但是,一旦 Session
迁移到始终处于事务模型中,事务结束点就变得很明显,成为数据过期的自然点,因为具有高度隔离性的事务的本质在于,它无法看到新数据,直到它被提交或回滚为止。当然,不同的数据库和配置具有不同程度的事务隔离,包括根本没有事务。这些使用模式在 SQLAlchemy 的过期模型中是完全可以接受的;开发人员只需要知道,较低的隔离级别可能会在多个 Session 共享同一行时在 Session 中公开未隔离的更改。这与直接使用两个数据库连接时可能发生的情况没有什么不同。
Session
及其处理的主要结构。上面面向公众的部分是 Session
本身和用户对象的集合,每个对象都是映射类的实例。在这里我们看到,映射对象保留了对名为 InstanceState
的 SQLAlchemy 构造的引用,该构造跟踪单个实例的 ORM 状态,包括挂起的属性更改和属性过期状态。InstanceState
是前面一节“映射的解剖结构”中讨论的属性检测的实例级方面,对应于类级别的 ClassManager
,并代表与类关联的 AttributeImpl
对象维护映射对象字典(即 Python 的 __dict__
属性)的状态。
IdentityMap
是数据库标识到 InstanceState
对象的映射,对于那些具有数据库标识的对象,称为持久性对象。IdentityMap
的默认实现与 InstanceState
一起工作,通过在删除对它们的所有强引用后删除用户映射的实例来自我管理其大小——这样它与 Python 的 WeakValueDictionary
的工作方式相同。Session
通过为具有挂起更改的对象创建强引用,保护所有标记为脏或已删除的对象以及标记为新的挂起对象免于垃圾回收。所有强引用在刷新后都会被丢弃。
InstanceState
还执行维护特定对象属性“哪些已更改”的关键任务,使用一种移动时更改的系统,在将传入值分配给对象的当前字典之前,将特定属性的“先前”值存储在一个名为 committed_state
的字典中。在刷新时,比较 committed_state
的内容和与对象关联的 __dict__
以生成每个对象的净更改集。
对于集合,一个单独的 collections
包与 InstrumentedAttribute
/InstanceState
系统协调,以维护特定映射对象集合的净更改集合。在使用之前,会对 set
、list
和 dict
等常见 Python 类进行子类化,并使用历史跟踪变异器方法进行增强。集合系统在 0.4 中进行了重新设计,使其具有开放性并可用于任何类似集合的对象。
Session
在其默认的使用状态下,为所有操作维护一个打开的事务,该事务在调用 commit
或 rollback
时完成。SessionTransaction
维持一组零个或多个 Connection
对象,每个对象代表特定数据库上的一个打开的事务。SessionTransaction
是一个延迟初始化的对象,开始时没有数据库状态存在。当特定后端需要参与语句执行时,与该数据库对应的 Connection
会添加到 SessionTransaction
的连接列表中。虽然一次一个连接很常见,但支持多个连接的情况,其中用于特定操作的特定连接是根据与操作中涉及的 Table
、Mapper
或 SQL 构造本身关联的配置确定的。对于提供它的那些 DBAPI,多个连接还可以使用两阶段行为协调事务。
Session
提供的 flush
方法将其工作转交给一个名为 unitofwork
的单独模块。如前所述,刷新过程可能是 SQLAlchemy 中最复杂的功能。
工作单元的工作是将特定 Session
中存在的所有挂起状态移到数据库中,清空 Session
维持的 new
、dirty
和 deleted
集合。完成后,Session
的内存状态与当前事务中存在的状态相匹配。主要挑战是确定正确的持久化步骤序列,然后按正确的顺序执行它们。这包括确定 INSERT、UPDATE 和 DELETE 语句的列表,包括由于相关行被删除或以其他方式移动而导致的那些语句;确保 UPDATE 语句仅包含实际修改的列;在新的主键标识符可用时,建立将主键列的状态复制到引用外键列的“同步”操作;确保 INSERT 按将对象添加到 Session
的顺序并尽可能高效地发生;并确保 UPDATE 和 DELETE 语句以确定性顺序发生,以减少死锁的可能性。
工作单元的实现最初是一套错综复杂的结构,以一种临时的方式编写;它的开发可以比作在没有地图的情况下寻找走出森林的路。早期的错误和缺失的行为通过附加的修复程序得到解决,虽然几个重构在 0.5 版本之前改进了情况,但直到 0.6 版本,工作单元——到那时已经稳定、易于理解并经过数百次测试——才能够从头开始完全重写。在考虑一种将由一致的数据结构驱动的新的方法的几个星期后,将其重写为使用此新模型的过程只用了几天时间,因为到那时这个想法已经得到了很好的理解。现有版本对新实现的行为进行仔细交叉检查也极大地帮助了它。这个过程表明,任何事物的第一版,无论多么糟糕,只要它提供了一个工作模型,仍然是有价值的。它进一步表明,对子系统的完全重写通常不仅是合适的,而且是难以开发的系统开发的组成部分。
工作单元背后的关键范式是将所有需要执行的操作组合到一个数据结构中,每个节点表示一个单独的步骤;这在设计模式术语中被称为命令模式。然后,使用拓扑排序将此结构中的“命令”序列组织成特定的顺序。拓扑排序是一种根据偏序对项目进行排序的过程,即只有某些元素必须先于其他元素。 图 20.14 说明了拓扑排序的行为。
工作单元根据必须先于其他持久化命令的持久化命令构建偏序。然后对命令进行拓扑排序并按顺序调用。哪些命令先于哪些命令的确定主要源于relationship
的存在,该relationship
连接两个Mapper
对象——通常,一个Mapper
被认为依赖于另一个,因为relationship
意味着一个Mapper
对另一个具有外键依赖关系。多对多关联表也存在类似的规则,但这里我们重点关注一对多/多对一关系的情况。解决外键依赖关系是为了防止发生约束冲突,而无需依赖于将约束标记为“延迟”。但同样重要的是,排序允许主键标识符(在许多平台上,只有在实际发生INSERT时才会生成)从刚刚执行的INSERT语句的结果填充到即将插入的依赖行的参数列表中。对于删除,使用相反的顺序——依赖行在它们所依赖的行之前删除,因为这些行在没有其外键的参照对象存在的情况下无法存在。
工作单元具有一个系统,其中拓扑排序在两个不同的级别上执行,具体取决于存在的依赖关系的结构。第一级根据映射器之间的依赖关系将持久化步骤组织成桶,即对应于特定类的完整“桶”对象。第二级将这些“桶”中的零个或多个分解成更小的批次,以处理引用循环或自引用表的情况。 图 20.15 说明了为插入一组User
对象,然后是一组Address
对象而生成的“桶”,其中中间步骤将新生成的User
主键值复制到每个Address
对象的user_id
外键列中。
在每个映射器排序的情况下,可以刷新任意数量的User
和Address
对象,而不会影响步骤的复杂性或必须考虑多少个“依赖关系”。
第二级排序根据单个映射器范围内各个对象之间的直接依赖关系组织持久化步骤。发生这种情况的最简单示例是包含对其自身的外键约束的表;表中的特定行需要在表中引用它的另一行之前插入。另一种情况是一系列表具有引用循环:表 A 引用表 B,表 B 引用表 C,然后表 C 引用表 A。一些 A 对象必须先于其他对象插入,以便允许插入 B 和 C 对象。自引用的表是引用循环的特例。
为了确定哪些操作可以保留在其聚合的每个Mapper
桶中,哪些操作将被分解成更大的一组每个对象命令,将循环检测算法应用于映射器之间存在的依赖关系集,使用在Guido Van Rossum 的博客上找到的修改后的循环检测算法版本。然后,参与循环的那些桶被分解成每个对象操作,并通过从每个对象桶到每个映射器桶添加新的依赖规则,混合到每个映射器桶的集合中。 图 20.16 说明了User
对象的桶被分解成单独的每个对象命令,这是由于从User
到自身添加了一个名为contact
的新relationship
导致的。
桶结构背后的基本原理是它允许尽可能多地批处理常用语句,既减少了 Python 中所需的步骤数量,又使与 DBAPI 的交互更加高效,DBAPI 有时可以在单个 Python 方法调用中执行数千个语句。只有当映射器之间存在引用循环时,才会启动更昂贵的每个对象依赖模式,即使在那时,它也只发生在需要它的对象图的那些部分。
从一开始,SQLAlchemy 就目标远大,旨在成为功能最丰富、用途最广泛的数据库产品。它在保持对关系数据库关注的同时做到了这一点,认识到以深入和全面的方式支持关系数据库的实用性是一项重大的工作;即使现在,这项工作的范围仍在继续显示出比以前认为的更大。
基于组件的方法旨在从每个功能领域中提取尽可能多的价值,提供许多不同的单元,应用程序可以单独或组合使用这些单元。这个系统创建、维护和交付起来都很有挑战性。
开发过程旨在缓慢进行,基于这样的理论:对可靠功能进行有条理、广泛的构建最终比快速交付没有基础的功能更有价值。SQLAlchemy 花了很长时间才构建了一个一致且记录良好的用户故事,但在整个过程中,底层架构始终领先一步,在某些情况下会导致“时间机器”效应,即功能几乎可以在用户请求之前添加。
Python 语言一直是一个可靠的主机(如果有点挑剔,尤其是在性能方面)。该语言的一致性和极其开放的运行时模型使 SQLAlchemy 提供了比用其他语言编写的类似产品更好的体验。
SQLAlchemy 项目希望 Python 能在尽可能广泛的领域和行业中获得更深入的认可,并且关系数据库的使用保持活力和进步。SQLAlchemy 的目标是证明关系数据库、Python 和经过深思熟虑的对象模型都是非常有价值的开发工具。