SQLAlchemy 2.0 文档
变更和迁移
- SQLAlchemy 2.0 - 主要迁移指南
- SQLAlchemy 2.0 的新特性?
- 2.0 更新日志
- 1.4 更新日志
- 1.3 更新日志
- 1.2 更新日志
- 1.1 更新日志
- 1.0 更新日志
- 0.9 更新日志
- 0.8 更新日志
- 0.7 更新日志
- 0.6 更新日志
- 0.5 更新日志
- 0.4 更新日志
- 0.3 更新日志
- 0.2 更新日志
- 0.1 更新日志
- SQLAlchemy 1.4 的新特性?
- SQLAlchemy 1.3 的新特性?
- SQLAlchemy 1.2 的新特性?¶
- 介绍
- 平台支持
- 新特性和改进 - ORM
- “Baked” 加载现在是惰性加载的默认设置
- 新的 “selectin” 预加载,使用 IN 一次性加载所有集合
- “selectin” 多态加载,使用单独的 IN 查询加载子类
- 可以接收 ad-hoc SQL 表达式的 ORM 属性
- ORM 支持多表删除
- 支持混合属性、组合属性的批量更新
- 混合属性支持在子类之间重用,重新定义 @getter
- 新的 bulk_replace 事件
- sqlalchemy.ext.mutable 的新的 “modified” 事件处理程序
- 为 Session.refresh 添加 “for update” 参数
- 原位突变运算符适用于 MutableSet、MutableList
- AssociationProxy 的 any()、has()、contains() 方法适用于链式关联代理
- 身份键增强功能以支持分片
- 新特性和改进 - Core
- 主要行为变更 - ORM
- 主要行为变更 - Core
- 方言改进和变更 - PostgreSQL
- 方言改进和变更 - MySQL
- 方言改进和变更 - Oracle
- 方言改进和变更 - SQL Server
- SQLAlchemy 1.1 的新特性?
- SQLAlchemy 1.0 的新特性?
- SQLAlchemy 0.9 的新特性?
- SQLAlchemy 0.8 的新特性?
- SQLAlchemy 0.7 的新特性?
- SQLAlchemy 0.6 的新特性?
- SQLAlchemy 0.5 的新特性?
- SQLAlchemy 0.4 的新特性?
项目版本
- 上一页: SQLAlchemy 1.3 的新特性?
- 下一页: SQLAlchemy 1.1 的新特性?
- 上级: 首页
- 在本页
- SQLAlchemy 1.2 的新特性?
- 介绍
- 平台支持
- 新特性和改进 - ORM
- “Baked” 加载现在是惰性加载的默认设置
- 新的 “selectin” 预加载,使用 IN 一次性加载所有集合
- “selectin” 多态加载,使用单独的 IN 查询加载子类
- 可以接收 ad-hoc SQL 表达式的 ORM 属性
- ORM 支持多表删除
- 支持混合属性、组合属性的批量更新
- 混合属性支持在子类之间重用,重新定义 @getter
- 新的 bulk_replace 事件
- sqlalchemy.ext.mutable 的新的 “modified” 事件处理程序
- 为 Session.refresh 添加 “for update” 参数
- 原位突变运算符适用于 MutableSet、MutableList
- AssociationProxy 的 any()、has()、contains() 方法适用于链式关联代理
- 身份键增强功能以支持分片
- 新特性和改进 - Core
- 主要行为变更 - ORM
- 主要行为变更 - Core
- 方言改进和变更 - PostgreSQL
- 方言改进和变更 - MySQL
- 方言改进和变更 - Oracle
- 方言改进和变更 - SQL Server
SQLAlchemy 1.2 的新特性?¶
关于本文档
本文档描述了 SQLAlchemy 1.1 版本和 SQLAlchemy 1.2 版本之间的变更。
介绍¶
本指南介绍了 SQLAlchemy 1.2 版本的新特性,并记录了影响用户从 SQLAlchemy 1.1 系列迁移到 1.2 的变更。
请仔细阅读关于行为变更的章节,以了解潜在的向后不兼容的行为变更。
平台支持¶
针对 Python 2.7 及以上版本¶
SQLAlchemy 1.2 现在将最低 Python 版本提升至 2.7,不再支持 2.6。预计新的语言特性将被合并到 1.2 系列中,这些特性在 Python 2.6 中不受支持。对于 Python 3 支持,SQLAlchemy 当前在 3.5 和 3.6 版本上进行测试。
新特性和改进 - ORM¶
“Baked” 加载现在是惰性加载的默认设置¶
在 1.0 系列中首次引入的 sqlalchemy.ext.baked
扩展允许构建所谓的 BakedQuery
对象,该对象生成一个 Query
对象,并结合一个代表查询结构的缓存键;然后将此缓存键链接到生成的字符串 SQL 语句,以便后续使用另一个具有相同结构的 BakedQuery
将绕过构建 Query
对象、构建核心 select()
对象以及将 select()
编译为字符串的所有开销,从而大大减少通常与构造和发出 ORM Query
对象相关的函数调用开销。
ORM 现在默认使用 BakedQuery
,当它为 relationship()
构造的惰性加载生成 “惰性” 查询时,例如默认的 lazy="select"
关系加载器策略。这将大大减少应用程序使用惰性加载查询来加载集合和相关对象范围内的函数调用。以前,此功能在 1.0 和 1.1 中通过使用全局 API 方法或使用 baked_select
策略来实现,现在它是此行为的唯一实现。该功能也得到了改进,使得即使在惰性加载之后,对于具有其他加载器选项的对象,缓存仍然可以进行。
可以使用 relationship.bake_queries
标志在每个关系的基础上禁用缓存行为,这适用于非常不寻常的情况,例如关系使用与缓存不兼容的自定义 Query
实现。
新的 “selectin” 预加载,使用 IN 一次性加载所有集合¶
添加了一个新的预加载器,称为 “selectin” 加载,它在许多方面类似于 “子查询” 加载,但是生成了一个更简单的 SQL 语句,该语句是可缓存的并且更有效。
给定如下查询:
q = (
session.query(User)
.filter(User.name.like("%ed%"))
.options(subqueryload(User.addresses))
)
生成的 SQL 将是对 User
的查询,后跟 User.addresses
的子查询加载(请注意,参数也已列出)
SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)
SELECT addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
addresses.email_address AS addresses_email_address,
anon_1.users_id AS anon_1_users_id
FROM (SELECT users.id AS users_id
FROM users
WHERE users.name LIKE ?) AS anon_1
JOIN addresses ON anon_1.users_id = addresses.user_id
ORDER BY anon_1.users_id
('%ed%',)
使用 “selectin” 加载,我们改为获得一个 SELECT,它引用父查询中加载的实际主键值
q = (
session.query(User)
.filter(User.name.like("%ed%"))
.options(selectinload(User.addresses))
)
生成:
SELECT users.id AS users_id, users.name AS users_name
FROM users
WHERE users.name LIKE ?
('%ed%',)
SELECT users_1.id AS users_1_id,
addresses.id AS addresses_id,
addresses.user_id AS addresses_user_id,
addresses.email_address AS addresses_email_address
FROM users AS users_1
JOIN addresses ON users_1.id = addresses.user_id
WHERE users_1.id IN (?, ?)
ORDER BY users_1.id
(1, 3)
上面的 SELECT 语句包括以下优点:
它不使用子查询,而只是一个 INNER JOIN,这意味着它在像 MySQL 这样不喜欢子查询的数据库上性能会更好
它的结构独立于原始查询;结合新的 扩展 IN 参数系统,我们在大多数情况下可以使用 “baked” 查询来缓存字符串 SQL,从而显着减少每个查询的开销
因为查询仅针对给定的主键标识符列表进行提取,“selectin” 加载可能与
Query.yield_per()
兼容,以便一次处理 SELECT 结果的块,前提是数据库驱动程序允许多个并发游标(SQLite、PostgreSQL;不是 MySQL 驱动程序或 SQL Server ODBC 驱动程序)。连接预加载和子查询预加载均与Query.yield_per()
不兼容。
selectin 预加载的缺点是可能产生大型 SQL 查询,其中包含大量的 IN 参数列表。IN 参数列表本身被分块为 500 个一组,因此超过 500 个引导对象的结果集将有更多额外的 “SELECT IN” 查询。此外,对复合主键的支持取决于数据库使用带有 IN 的元组的能力,例如 (table.column_one, table_column_two) IN ((?, ?), (?, ?) (?, ?))
。目前,已知 PostgreSQL 和 MySQL 与此语法兼容,而 SQLite 不兼容。
另请参阅:
“selectin” 多态加载,使用单独的 IN 查询加载子类¶
与 新的 “selectin” 预加载,使用 IN 一次性加载所有集合 中刚刚描述的 “selectin” 关系加载功能类似的是 “selectin” 多态加载。这是一种多态加载功能,主要针对连接预加载,它允许使用简单的 SELECT 语句加载基本实体,然后使用额外的 SELECT 语句加载其他子类的属性
>>> from sqlalchemy.orm import selectin_polymorphic
>>> query = session.query(Employee).options(
... selectin_polymorphic(Employee, [Manager, Engineer])
... )
>>> query.all()
SELECT
employee.id AS employee_id,
employee.name AS employee_name,
employee.type AS employee_type
FROM employee
()
SELECT
engineer.id AS engineer_id,
employee.id AS employee_id,
employee.type AS employee_type,
engineer.engineer_name AS engineer_engineer_name
FROM employee JOIN engineer ON employee.id = engineer.id
WHERE employee.id IN (?, ?) ORDER BY employee.id
(1, 2)
SELECT
manager.id AS manager_id,
employee.id AS employee_id,
employee.type AS employee_type,
manager.manager_name AS manager_manager_name
FROM employee JOIN manager ON employee.id = manager.id
WHERE employee.id IN (?) ORDER BY employee.id
(3,)
可以接收 ad-hoc SQL 表达式的 ORM 属性¶
添加了一个新的 ORM 属性类型 query_expression()
,它类似于 deferred()
,不同之处在于它的 SQL 表达式是在查询时使用新选项 with_expression()
确定的;如果未指定,则属性默认为 None
from sqlalchemy.orm import query_expression
from sqlalchemy.orm import with_expression
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
x = Column(Integer)
y = Column(Integer)
# will be None normally...
expr = query_expression()
# but let's give it x + y
a1 = session.query(A).options(with_expression(A.expr, A.x + A.y)).first()
print(a1.expr)
另请参阅:
ORM 支持多表删除¶
ORM Query.delete()
方法支持 DELETE 的多表条件,如 多表条件支持 DELETE 中介绍的那样。此功能的工作方式与 UPDATE 的多表条件相同,首次在 0.8 中引入,并在 Query.update() 支持 UPDATE..FROM 中描述。
下面,我们针对 SomeEntity
发出 DELETE,添加一个针对 SomeOtherEntity
的 FROM 子句(或等效子句,取决于后端)
query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter(
SomeOtherEntity.foo == "bar"
).delete()
另请参阅:
支持混合属性、组合属性的批量更新¶
混合属性(例如 sqlalchemy.ext.hybrid
)以及组合属性(组合列类型)现在都支持在使用 Query.update()
时在 UPDATE 语句的 SET 子句中使用。
对于混合属性,可以直接使用简单表达式,或者可以使用新的装饰器 hybrid_property.update_expression()
将一个值分解为多个列/表达式
class Person(Base):
# ...
first_name = Column(String(10))
last_name = Column(String(10))
@hybrid.hybrid_property
def name(self):
return self.first_name + " " + self.last_name
@name.expression
def name(cls):
return func.concat(cls.first_name, " ", cls.last_name)
@name.update_expression
def name(cls, value):
f, l = value.split(" ", 1)
return [(cls.first_name, f), (cls.last_name, l)]
上面,可以使用以下方式呈现 UPDATE:
session.query(Person).filter(Person.id == 5).update({Person.name: "Dr. No"})
组合属性也具有类似的功能,其中组合值将被分解为各自的列以进行批量 UPDATE
session.query(Vertex).update({Edge.start: Point(3, 4)})
另请参阅:
混合属性支持在子类之间重用,重新定义 @getter¶
sqlalchemy.ext.hybrid.hybrid_property
类现在支持在子类之间多次调用 mutator,例如 @setter
、@expression
等,并且现在提供了一个 @getter
mutator,以便可以在子类或其他类之间重新使用特定的混合属性。这现在类似于标准 Python 中 @property
的行为
class FirstNameOnly(Base):
# ...
first_name = Column(String)
@hybrid_property
def name(self):
return self.first_name
@name.setter
def name(self, value):
self.first_name = value
class FirstNameLastName(FirstNameOnly):
# ...
last_name = Column(String)
@FirstNameOnly.name.getter
def name(self):
return self.first_name + " " + self.last_name
@name.setter
def name(self, value):
self.first_name, self.last_name = value.split(" ", maxsplit=1)
@name.expression
def name(cls):
return func.concat(cls.first_name, " ", cls.last_name)
上面,FirstNameOnly.name
混合属性被 FirstNameLastName
子类引用,以便专门将其重新用于新的子类。这是通过在每次调用 @getter
、@setter
以及所有其他 mutator 方法(如 @expression
)时将混合对象复制到一个新的对象来实现的,从而保持先前混合属性的定义不变。以前,诸如 @setter
之类的方法会就地修改现有的混合属性,从而干扰超类上的定义。
注意:
请务必阅读 跨子类重用混合属性 中的文档,以了解有关如何覆盖 hybrid_property.expression()
和 hybrid_property.comparator()
的重要说明,因为在某些情况下,可能需要特殊的限定符 hybrid_property.overrides
以避免与 QueryableAttribute
发生名称冲突。
注意:
@hybrid_property
中的此更改意味着,当向 @hybrid_property
添加 setter 和其他状态时,方法必须保留原始混合属性的名称,否则具有附加状态的新混合属性将以不匹配的名称存在于类上。这与标准 Python 中 @property
构造的行为相同
class FirstNameOnly(Base):
@hybrid_property
def name(self):
return self.first_name
# WRONG - will raise AttributeError: can't set attribute when
# assigning to .name
@name.setter
def _set_name(self, value):
self.first_name = value
class FirstNameOnly(Base):
@hybrid_property
def name(self):
return self.first_name
# CORRECT - note regular Python @property works the same way
@name.setter
def name(self, value):
self.first_name = value
新的 bulk_replace 事件¶
为了适应 @validates 方法在比较之前接收批量集合设置中的所有值 中描述的验证用例,添加了一个新的 AttributeEvents.bulk_replace()
方法,该方法与 AttributeEvents.append()
和 AttributeEvents.remove()
事件一起调用。“bulk_replace” 在 “append” 和 “remove” 之前调用,以便可以在与现有集合进行比较之前修改集合。之后,各个项目被附加到新的目标集合,为集合中的新项目触发 “append” 事件,这与之前的行为相同。下面同时说明了 “bulk_replace” 和 “append”,包括如果使用集合分配,则 “append” 将接收已由 “bulk_replace” 处理的对象。可以使用新的符号 attributes.OP_BULK_REPLACE
来确定此 “append” 事件是否是批量替换的第二部分
from sqlalchemy.orm.attributes import OP_BULK_REPLACE
@event.listens_for(SomeObject.collection, "bulk_replace")
def process_collection(target, values, initiator):
values[:] = [_make_value(value) for value in values]
@event.listens_for(SomeObject.collection, "append", retval=True)
def process_collection(target, value, initiator):
# make sure bulk_replace didn't already do it
if initiator is None or initiator.op is not OP_BULK_REPLACE:
return _make_value(value)
else:
return value
sqlalchemy.ext.mutable 的新的 “modified” 事件处理程序¶
添加了一个新的事件处理程序 AttributeEvents.modified()
,它对应于对 flag_modified()
方法的调用而触发,该方法通常从 sqlalchemy.ext.mutable
扩展中调用
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy import event
Base = declarative_base()
class MyDataClass(Base):
__tablename__ = "my_data"
id = Column(Integer, primary_key=True)
data = Column(MutableDict.as_mutable(JSONEncodedDict))
@event.listens_for(MyDataClass.data, "modified")
def modified_json(instance):
print("json value modified:", instance.data)
上面,当对 .data
字典进行原位更改时,将触发事件处理程序。
为 Session.refresh 添加 “for update” 参数¶
为 Session.refresh()
方法添加了新参数 Session.refresh.with_for_update
。当 Query.with_lockmode()
方法被弃用而改用 Query.with_for_update()
时,Session.refresh()
方法从未更新以反映新选项
session.refresh(some_object, with_for_update=True)
Session.refresh.with_for_update
参数接受一个选项字典,该字典将作为发送到 Query.with_for_update()
的相同参数传递
session.refresh(some_objects, with_for_update={"read": True})
新参数取代了 Session.refresh.lockmode
参数。
原位突变运算符适用于 MutableSet、MutableList¶
为 MutableSet
实现了原位突变运算符 __ior__
、__iand__
、__ixor__
和 __isub__
,为 MutableList
实现了 __iadd__
。虽然这些方法之前可以成功更新集合,但它们不会正确触发更改事件。运算符像以前一样突变集合,但另外发出正确的更改事件,以便更改成为下一个刷新过程的一部分
model = session.query(MyModel).first()
model.json_set &= {1, 3}
AssociationProxy 的 any()、has()、contains() 方法适用于链式关联代理¶
AssociationProxy.any()
、AssociationProxy.has()
和 AssociationProxy.contains()
比较方法现在支持链接到本身也是 AssociationProxy
的属性,以递归方式进行。下面,A.b_values
是一个关联代理,它链接到 AtoB.bvalue
,后者本身也是 B
的关联代理
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
b_values = association_proxy("atob", "b_value")
c_values = association_proxy("atob", "c_value")
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
value = Column(String)
c = relationship("C")
class C(Base):
__tablename__ = "c"
id = Column(Integer, primary_key=True)
b_id = Column(ForeignKey("b.id"))
value = Column(String)
class AtoB(Base):
__tablename__ = "atob"
a_id = Column(ForeignKey("a.id"), primary_key=True)
b_id = Column(ForeignKey("b.id"), primary_key=True)
a = relationship("A", backref="atob")
b = relationship("B", backref="atob")
b_value = association_proxy("b", "value")
c_value = association_proxy("b", "c")
我们可以使用 AssociationProxy.contains()
查询 A.b_values
,以跨两个代理 A.b_values
、AtoB.b_value
进行查询
>>> s.query(A).filter(A.b_values.contains("hi")).all()
SELECT a.id AS a_id
FROM a
WHERE EXISTS (SELECT 1
FROM atob
WHERE a.id = atob.a_id AND (EXISTS (SELECT 1
FROM b
WHERE b.id = atob.b_id AND b.value = :value_1)))
类似地,我们可以使用 AssociationProxy.any()
查询 A.c_values
,以跨两个代理 A.c_values
、AtoB.c_value
进行查询
>>> s.query(A).filter(A.c_values.any(value="x")).all()
SELECT a.id AS a_id
FROM a
WHERE EXISTS (SELECT 1
FROM atob
WHERE a.id = atob.a_id AND (EXISTS (SELECT 1
FROM b
WHERE b.id = atob.b_id AND (EXISTS (SELECT 1
FROM c
WHERE b.id = c.b_id AND c.value = :value_1)))))
身份键增强功能以支持分片¶
ORM 使用的身份键结构现在包含一个额外的成员,以便来自不同上下文的两个相同主键可以在同一身份映射中共存。
水平分片 中的示例已更新以说明此行为。该示例显示了一个分片类 WeatherLocation
,它引用一个依赖的 WeatherReport
对象,其中 WeatherReport
类映射到一个存储简单整数主键的表。来自不同数据库的两个 WeatherReport
对象可能具有相同的主键值。该示例现在说明,新的 identity_token
字段跟踪此差异,以便两个对象可以在同一身份映射中共存
tokyo = WeatherLocation("Asia", "Tokyo")
newyork = WeatherLocation("North America", "New York")
tokyo.reports.append(Report(80.0))
newyork.reports.append(Report(75))
sess = create_session()
sess.add_all([tokyo, newyork, quito])
sess.commit()
# the Report class uses a simple integer primary key. So across two
# databases, a primary key will be repeated. The "identity_token" tracks
# in memory that these two identical primary keys are local to different
# databases.
newyork_report = newyork.reports[0]
tokyo_report = tokyo.reports[0]
assert inspect(newyork_report).identity_key == (Report, (1,), "north_america")
assert inspect(tokyo_report).identity_key == (Report, (1,), "asia")
# the token representing the originating shard is also available directly
assert inspect(newyork_report).identity_token == "north_america"
assert inspect(tokyo_report).identity_token == "asia"
新特性和改进 - Core¶
布尔数据类型现在强制执行严格的 True/False/None 值¶
在 1.1 版本中,在所有情况下,非本地布尔整数值都被强制转换为零/一/None 中描述的更改产生了一个意想不到的副作用,即更改了 Boolean
在遇到非整数值(例如字符串)时的行为方式。特别是,字符串值 "0"
以前会导致生成值 False
,现在会生成 True
。更糟糕的是,行为的改变仅适用于某些后端而不适用于其他后端,这意味着将字符串 "0"
值发送到 Boolean
的代码在不同后端上的中断情况不一致。
解决此问题的最终方案是 Boolean 不支持字符串值,因此在 1.2 中,如果传递非整数/True/False/None 值,则会引发硬性 TypeError
。此外,仅接受整数值 0 和 1。
为了适应希望对布尔值进行更自由解释的应用程序,应使用 TypeDecorator
。下面说明了一种配方,该配方将允许 pre-1.1 Boolean
数据类型的 “自由” 行为
from sqlalchemy import Boolean
from sqlalchemy import TypeDecorator
class LiberalBoolean(TypeDecorator):
impl = Boolean
def process_bind_param(self, value, dialect):
if value is not None:
value = bool(int(value))
return value
悲观断开连接检测已添加到连接池¶
连接池文档长期以来一直提供一个配方,用于使用 ConnectionEvents.engine_connect()
引擎事件在签出的连接上发出一个简单的语句,以测试其活动性。此配方的功能现在已添加到连接池本身中,当与适当的方言结合使用时。使用新参数 create_engine.pool_pre_ping
,每个签出的连接都将在返回之前测试其新鲜度
engine = create_engine("mysql+pymysql://", pool_pre_ping=True)
虽然 “pre-ping” 方法为连接池签出增加了一点延迟,但对于典型的面向事务的应用程序(包括大多数 ORM 应用程序),这种开销是最小的,并且消除了获取陈旧连接的问题,该连接会引发错误,需要应用程序放弃或重试操作。
此功能 不 适用于在正在进行的事务或 SQL 操作中断开的连接。如果应用程序也必须从中恢复,则需要采用自己的操作重试逻辑来预测这些错误。
另请参阅:
IN / NOT IN 运算符的空集合行为现在可配置;默认表达式已简化¶
诸如 column.in_([])
之类的表达式(假定为 false)现在默认生成表达式 1 != 1
,而不是 column != column
。这将 更改结果,即当 SQL 表达式或列与空集比较时,该 SQL 表达式或列的计算结果为 NULL,从而产生布尔值 false 或 true(对于 NOT IN)而不是 NULL。在这种情况下发出的警告也被删除。旧的行为可以使用 create_engine.empty_in_strategy
参数在 create_engine()
中获得。
在 SQL 中,IN 和 NOT IN 运算符不支持与显式为空的值集合进行比较;这意味着,此语法是非法的
mycolumn IN ()
为了解决这个问题,SQLAlchemy 和其他数据库库检测到这种情况并呈现一个替代表达式,该表达式的计算结果为 false,或者在 NOT IN 的情况下为 true,基于 “col IN ()” 始终为 false 的理论,因为 “空集” 中没有任何内容。通常,为了生成一个在数据库之间可移植且在 WHERE 子句上下文中有效的 false/true 常量,使用简单的重言式,例如 1 != 1
计算结果为 false,1 = 1
计算结果为 true(简单的常量 “0” 或 “1” 通常不适用于 WHERE 子句的目标)。
SQLAlchemy 在早期也采用了这种方法,但很快就有人提出,如果“column”为 NULL,SQL 表达式 column IN ()
不会评估为 false;相反,该表达式会产生 NULL,因为 “NULL” 意味着 “未知”,并且 SQL 中与 NULL 的比较通常会产生 NULL。
为了模拟这种结果,SQLAlchemy 从使用 1 != 1
更改为使用表达式 expr != expr
用于空的 “IN” 和 expr = expr
用于空的 “NOT IN”;也就是说,我们没有使用固定值,而是使用了表达式的实际左侧。如果传递的表达式左侧评估为 NULL,那么整个比较也会得到 NULL 结果,而不是 false 或 true。
不幸的是,用户最终抱怨说,这种表达式对某些查询计划器产生了非常严重的性能影响。在那时,当遇到空的 IN 表达式时,添加了一个警告,建议 SQLAlchemy 继续保持 “正确”,并敦促用户避免生成空 IN 谓词的代码,因为通常可以安全地省略它们。然而,这在从输入变量动态构建的查询情况下当然是繁重的,因为传入的值集合可能为空。
最近几个月,人们对这一决定的最初假设提出了质疑。“NULL IN ()” 表达式应该返回 NULL 的概念只是理论上的,并且无法测试,因为数据库不支持这种语法。然而,事实证明,您实际上可以通过如下模拟空集来询问关系数据库对于 “NULL IN ()” 会返回什么值
SELECT NULL IN (SELECT 1 WHERE 1 != 1)
通过上述测试,我们看到数据库本身无法就答案达成一致。PostgreSQL 被大多数人认为是 “最正确” 的数据库,它返回 False;因为即使 “NULL” 代表 “未知”,但 “空集” 意味着没有任何东西存在,包括所有未知值。另一方面,MySQL 和 MariaDB 对上述表达式返回 NULL,默认采用更常见的 “所有与 NULL 的比较都返回 NULL” 的行为。
SQLAlchemy 的 SQL 架构比最初做出此设计决策时更复杂,因此我们现在可以在 SQL 字符串编译时允许调用任一行为。以前,转换为比较表达式是在构造时完成的,也就是说,在调用 ColumnOperators.in_()
或 ColumnOperators.notin_()
运算符的那一刻。通过编译时行为,可以指示方言本身调用任一方法,即 “静态” 1 != 1
比较或 “动态” expr != expr
比较。默认值已**更改**为 “静态” 比较,因为这与 PostgreSQL 在任何情况下的行为一致,并且这也是绝大多数用户喜欢的。这将**更改**将 null 表达式与空集进行比较的查询结果,特别是查询否定 where(~null_expr.in_([]))
的查询,因为现在它评估为 true 而不是 NULL。
现在可以使用标志 create_engine.empty_in_strategy
控制此行为,该标志默认为 "static"
设置,但也可以设置为 "dynamic"
或 "dynamic_warn"
,其中 "dynamic_warn"
设置等同于先前发出 expr != expr
以及性能警告的行为。但是,预计大多数用户会欣赏 “静态” 默认值。
延迟扩展的 IN 参数集允许带有缓存语句的 IN 表达式¶
添加了一种新的 bindparam()
类型,称为 “expanding”。这用于 IN
表达式中,其中元素列表在语句执行时而不是在语句编译时呈现为单独的绑定参数。这允许单个绑定参数名称链接到多个元素的 IN 表达式,并允许将查询缓存与 IN 表达式一起使用。新功能允许 “select in” 加载和 “polymorphic in” 加载的相关功能利用 baked query 扩展来减少调用开销
stmt = select([table]).where(table.c.col.in_(bindparam("foo", expanding=True)))
conn.execute(stmt, {"foo": [1, 2, 3]})
在 1.2 系列中,该功能应被视为**实验性**功能。
展平的比较运算符优先级¶
IN、LIKE、equals、IS、MATCH 和其他比较运算符的运算符优先级已展平为一个级别。这将导致在比较运算符组合在一起时生成更多括号,例如
(column("q") == null()) != (column("y") == null())
现在将生成 (q IS NULL) != (y IS NULL)
而不是 q IS NULL != y IS NULL
。
支持表、列上的 SQL 注释,包括 DDL、反射¶
Core 接收对与表和列关联的字符串注释的支持。这些注释通过 Table.comment
和 Column.comment
参数指定
Table(
"my_table",
metadata,
Column("q", Integer, comment="the Q value"),
comment="my Q table",
)
上面,DDL 将在表创建时适当地呈现,以将上述注释与模式中的表/列关联起来。当使用 Inspector.get_columns()
自动加载或检查上述表时,会包含注释。表注释也可以使用 Inspector.get_table_comment()
方法独立获得。
当前后端支持包括 MySQL、PostgreSQL 和 Oracle。
DELETE 的多表条件支持¶
Delete
构造现在支持多表条件,为那些支持它的后端实现,目前这些后端是 PostgreSQL、MySQL 和 Microsoft SQL Server(也为当前无法工作的 Sybase 方言添加了支持)。此功能的工作方式与 0.7 和 0.8 系列中首次引入的 UPDATE 的多表条件功能相同。
给定如下语句
stmt = (
users.delete()
.where(users.c.id == addresses.c.id)
.where(addresses.c.email_address.startswith("ed%"))
)
conn.execute(stmt)
在 PostgreSQL 后端,上述语句生成的 SQL 将呈现为
DELETE FROM users USING addresses
WHERE users.id = addresses.id
AND (addresses.email_address LIKE %(email_address_1)s || '%%')
另请参阅:
startswith()、endswith() 的新 “autoescape” 选项¶
“autoescape” 参数已添加到 ColumnOperators.startswith()
、ColumnOperators.endswith()
、ColumnOperators.contains()
。当此参数设置为 True
时,它将自动使用转义字符转义所有出现的 %
、_
,转义字符默认为正斜杠 /
;转义字符本身的出现也会被转义。使用正斜杠是为了避免与 PostgreSQL 的 standard_confirming_strings
等设置冲突,其默认值从 PostgreSQL 9.1 开始已更改,以及 MySQL 的 NO_BACKSLASH_ESCAPES
设置。如果需要,现在可以使用现有的 “escape” 参数来更改自动转义字符。
注意:
自 1.2.0 起,此功能已从 1.2.0b2 的初始实现中更改,以便 autoescape 现在作为布尔值传递,而不是作为要用作转义字符的特定字符。
如下表达式
>>> column("x").startswith("total%score", autoescape=True)
呈现为
x LIKE :x_1 || '%' ESCAPE '/'
其中参数 “x_1” 的值为 'total/%score'
。
类似地,具有反斜杠的表达式
>>> column("x").startswith("total/score", autoescape=True)
将以相同的方式呈现,参数 “x_1” 的值为 'total//score'
。
为 “float” 数据类型添加了更强的类型支持¶
一系列更改允许使用 Float
数据类型更紧密地将其自身链接到 Python 浮点值,而不是更通用的 Numeric
。这些更改主要与确保 Python 浮点值不会错误地强制转换为 Decimal()
,并在结果端根据应用程序是否使用普通浮点数,在需要时强制转换为 float
有关。
传递给 SQL 表达式的普通 Python “float” 值现在将被拉入类型为
Float
的文字参数;以前,类型为Numeric
,带有默认的 “asdecimal=True” 标志,这意味着结果类型将强制转换为Decimal()
。特别是,这会在 SQLite 上发出令人困惑的警告float_value = connection.scalar( select([literal(4.56)]) # the "BindParameter" will now be # Float, not Numeric(asdecimal=True) )
Numeric
、Float
和Integer
之间的数学运算现在将在结果表达式的类型中保留Numeric
或Float
类型,包括asdecimal
标志以及类型是否应为Float
# asdecimal flag is maintained expr = column("a", Integer) * column("b", Numeric(asdecimal=False)) assert expr.type.asdecimal == False # Float subclass of Numeric is maintained expr = column("a", Integer) * column("b", Float()) assert isinstance(expr.type, Float)
如果已知 DBAPI 支持本机
Decimal()
模式,则Float
数据类型将无条件地将float()
处理器应用于结果值。某些后端并不总是保证浮点数以普通浮点数而不是精度数字(例如 MySQL)的形式返回。
支持 GROUPING SETS、CUBE、ROLLUP¶
GROUPING SETS、CUBE、ROLLUP 这三者都可以通过 func
命名空间获得。在 CUBE 和 ROLLUP 的情况下,这些函数已经在以前的版本中工作,但是对于 GROUPING SETS,编译器中添加了一个占位符以允许空间。所有三个函数现在都在文档中命名
>>> from sqlalchemy import select, table, column, func, tuple_
>>> t = table("t", column("value"), column("x"), column("y"), column("z"), column("q"))
>>> stmt = select([func.sum(t.c.value)]).group_by(
... func.grouping_sets(
... tuple_(t.c.x, t.c.y),
... tuple_(t.c.z, t.c.q),
... )
... )
>>> print(stmt)
SELECT sum(t.value) AS sum_1
FROM t GROUP BY GROUPING SETS((t.x, t.y), (t.z, t.q))
带有上下文默认生成器的多值 INSERT 的参数助手¶
默认生成函数,例如在 上下文相关的默认函数 中描述的函数,可以通过 DefaultExecutionContext.current_parameters
属性查看与语句相关的当前参数。但是,在通过 Insert.values()
方法指定多个 VALUES 子句的 Insert
构造的情况下,用户定义的函数会被多次调用,每个参数集调用一次,但是没有办法知道 DefaultExecutionContext.current_parameters
中的哪些键子集应用于该列。添加了一个新的函数 DefaultExecutionContext.get_current_parameters()
,其中包括一个关键字参数 DefaultExecutionContext.get_current_parameters.isolate_multiinsert_groups
,默认为 True
,它执行额外的工作,传递 DefaultExecutionContext.current_parameters
的子字典,该子字典的名称已本地化为当前正在处理的 VALUES 子句
def mydefault(context):
return context.get_current_parameters()["counter"] + 12
mytable = Table(
"mytable",
metadata_obj,
Column("counter", Integer),
Column("counter_plus_twelve", Integer, default=mydefault, onupdate=mydefault),
)
stmt = mytable.insert().values([{"counter": 5}, {"counter": 18}, {"counter": 20}])
conn.execute(stmt)
关键行为变更 - ORM¶
after_rollback() Session 事件现在在对象过期之前发出¶
SessionEvents.after_rollback()
事件现在可以在对象状态过期(例如,“快照删除”)之前访问对象的状态属性。这允许事件与 SessionEvents.after_commit()
事件的行为保持一致,后者也在 “快照” 被删除之前发出
sess = Session()
user = sess.query(User).filter_by(name="x").first()
@event.listens_for(sess, "after_rollback")
def after_rollback(session):
# 'user.name' is now present, assuming it was already
# loaded. previously this would raise upon trying
# to emit a lazy load.
print("user name: %s" % user.name)
@event.listens_for(sess, "after_commit")
def after_commit(session):
# 'user.name' is present, assuming it was already
# loaded. this is the existing behavior.
print("user name: %s" % user.name)
if should_rollback:
sess.rollback()
else:
sess.commit()
请注意,Session
仍然禁止在此事件中发出 SQL;这意味着未加载的属性仍然无法在此事件的范围内加载。
修复了涉及带有 select_from()
的单表继承的问题¶
Query.select_from()
方法现在在生成 SQL 时会考虑单表继承列鉴别器;以前,只考虑查询列列表中的表达式。
假设 Manager
是 Employee
的子类。如下查询
sess.query(Manager.id)
将生成如下 SQL
SELECT employee.id FROM employee WHERE employee.type IN ('manager')
但是,如果 Manager
仅通过 Query.select_from()
指定,而不在列列表中指定,则不会添加鉴别器
sess.query(func.count(1)).select_from(Manager)
将生成
SELECT count(1) FROM employee
通过修复,Query.select_from()
现在可以正常工作,我们得到
SELECT count(1) FROM employee WHERE employee.type IN ('manager')
手动提供 WHERE 子句以解决此问题的应用程序可能需要进行调整。
替换后不再修改之前的集合¶
每当映射集合的成员发生更改时,ORM 都会发出事件。在将集合分配给将替换先前集合的属性的情况下,其副作用是将被替换的集合也会被修改,这是具有误导性和不必要的
>>> a1, a2, a3 = Address("a1"), Address("a2"), Address("a3")
>>> user.addresses = [a1, a2]
>>> previous_collection = user.addresses
# replace the collection with a new one
>>> user.addresses = [a2, a3]
>>> previous_collection
[Address('a1'), Address('a2')]
在上面,在更改之前,previous_collection
将删除 “a1” 成员,这对应于不再在新集合中的成员。
@validates 方法在比较之前接收批量集合设置上的所有值¶
使用 @validates
的方法现在将在 “批量设置” 操作期间接收集合的所有成员,然后在与现有集合进行比较之前接收。
给定如下映射
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
bs = relationship("B")
@validates("bs")
def convert_dict_to_b(self, key, value):
return B(data=value["data"])
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id"))
data = Column(String)
上面,我们可以使用验证器如下所示,以在集合追加时将传入的字典转换为 B
的实例
a1 = A()
a1.bs.append({"data": "b1"})
但是,集合分配将失败,因为 ORM 会假定传入的对象已经是 B
的实例,因为它尝试将它们与集合的现有成员进行比较,然后再进行实际调用验证器的集合追加。这将使批量设置操作无法容纳需要预先修改的非 ORM 对象(如字典)
a1 = A()
a1.bs = [{"data": "b1"}]
新逻辑使用新的 AttributeEvents.bulk_replace()
事件来确保所有值都预先发送到 @validates
函数。
作为此更改的一部分,这意味着验证器现在将在批量设置时接收集合的**所有**成员,而不仅仅是新成员。假设一个简单的验证器,例如
class A(Base):
# ...
@validates("bs")
def validate_b(self, key, value):
assert value.data is not None
return value
上面,如果我们从如下集合开始
a1 = A()
b1, b2 = B(data="one"), B(data="two")
a1.bs = [b1, b2]
然后,用一个与第一个集合重叠的集合替换该集合
b3 = B(data="three")
a1.bs = [b2, b3]
以前,第二次赋值只会为 b3
对象触发一次 A.validate_b
方法。b2
对象将被视为已存在于集合中,并且未经过验证。使用新行为,b2
和 b3
都在传递到集合之前传递到 A.validate_b
。因此,验证方法采用幂等行为以适应这种情况非常重要。
另请参阅:
使用 flag_dirty() 将对象标记为 “dirty”,而无需更改任何属性¶
如果使用 flag_modified()
函数将未实际加载的属性标记为已修改,则现在会引发异常
a1 = A(data="adf")
s.add(a1)
s.flush()
# expire, similarly as though we said s.commit()
s.expire(a1, "data")
# will raise InvalidRequestError
attributes.flag_modified(a1, "data")
这是因为如果到 flush 发生时属性仍然不存在,则 flush 过程很可能会失败。要将对象标记为 “已修改”,而无需专门引用任何属性,以便在 flush 过程中考虑用于自定义事件处理程序(例如 SessionEvents.before_flush()
),请使用新的 flag_dirty()
函数
from sqlalchemy.orm import attributes
attributes.flag_dirty(a1)
从 scoped_session 中删除了 “scope” 关键字¶
已删除一个非常旧且未记录的关键字参数 scope
from sqlalchemy.orm import scoped_session
Session = scoped_session(sessionmaker())
session = Session(scope=None)
此关键字的目的是尝试允许可变 “作用域”,其中 None
表示 “无作用域”,因此将返回新的 Session
。该关键字从未被记录,现在如果遇到它将引发 TypeError
。预计此关键字未使用,但是如果用户在 beta 测试期间报告与此相关的问题,则可以恢复并进行弃用。
结合 onupdate 对 post_update 的改进¶
使用 relationship.post_update
功能的关系现在可以更好地与设置了 Column.onupdate
值的列进行交互。如果对象在插入时具有列的显式值,则会在 UPDATE 期间重新声明它,以便 “onupdate” 规则不会覆盖它
class A(Base):
__tablename__ = "a"
id = Column(Integer, primary_key=True)
favorite_b_id = Column(ForeignKey("b.id", name="favorite_b_fk"))
bs = relationship("B", primaryjoin="A.id == B.a_id")
favorite_b = relationship(
"B", primaryjoin="A.favorite_b_id == B.id", post_update=True
)
updated = Column(Integer, onupdate=my_onupdate_function)
class B(Base):
__tablename__ = "b"
id = Column(Integer, primary_key=True)
a_id = Column(ForeignKey("a.id", name="a_fk"))
a1 = A()
b1 = B()
a1.bs.append(b1)
a1.favorite_b = b1
a1.updated = 5
s.add(a1)
s.flush()
上面,之前的行为是在 INSERT 之后发出 UPDATE,从而触发 “onupdate” 并覆盖值 “5”。现在的 SQL 看起来像
INSERT INTO a (favorite_b_id, updated) VALUES (?, ?)
(None, 5)
INSERT INTO b (a_id) VALUES (?)
(1,)
UPDATE a SET favorite_b_id=?, updated=? WHERE a.id = ?
(1, 5, 1)
此外,如果 未 设置 “updated” 的值,那么我们正确地取回 a1.updated
上新生成的值;以前,刷新或使属性过期以允许生成的值存在的逻辑不会为 post-update 触发。InstanceEvents.refresh_flush()
事件也会在这种情况下在 flush 内发生刷新时发出。
post_update 与 ORM 版本控制集成¶
在 指向自身的行 / 相互依赖的行 中记录的 post_update 功能涉及到,除了通常为目标行发出的 INSERT/UPDATE/DELETE 之外,还会在响应于对特定关系绑定的外键的更改而发出 UPDATE 语句。此 UPDATE 语句现在参与版本控制功能,该功能在 配置版本计数器 中记录。
给定一个映射
class Node(Base):
__tablename__ = "node"
id = Column(Integer, primary_key=True)
version_id = Column(Integer, default=0)
parent_id = Column(ForeignKey("node.id"))
favorite_node_id = Column(ForeignKey("node.id"))
nodes = relationship("Node", primaryjoin=remote(parent_id) == id)
favorite_node = relationship(
"Node", primaryjoin=favorite_node_id == remote(id), post_update=True
)
__mapper_args__ = {"version_id_col": version_id}
现在,更新将另一个节点关联为 “favorite” 的节点也将增加版本计数器并匹配当前版本
node = Node()
session.add(node)
session.commit() # node is now version #1
node = session.query(Node).get(node.id)
node.favorite_node = Node()
session.commit() # node is now version #2
请注意,这意味着由于其他属性更改而接收到 UPDATE 的对象,以及由于 post_update 关系更改而接收到第二个 UPDATE 的对象,现在将**为一个 flush 接收两次版本计数器更新**。但是,如果对象在当前 flush 中受到 INSERT 的影响,则版本计数器**不会**再次递增,除非存在服务器端版本控制方案。
现在在 为什么 post_update 除了第一个 UPDATE 之外还发出 UPDATE? 中讨论了 post_update 即使对于 UPDATE 也发出 UPDATE 的原因。
关键行为变更 - Core¶
自定义运算符的类型行为已变得一致¶
可以使用 Operators.op()
函数即时创建用户定义的运算符。以前,针对此类运算符的表达式的类型行为不一致且无法控制。
而在 1.1 中,如下表达式将生成没有返回类型的结果(假设 -%>
是数据库支持的某些特殊运算符)
>>> column("x", types.DateTime).op("-%>")(None).type
NullType()
其他类型将使用左侧类型作为返回类型的默认行为
>>> column("x", types.String(50)).op("-%>")(None).type
String(length=50)
这些行为大多是偶然发生的,因此行为已与第二种形式保持一致,即默认返回类型与左侧表达式相同
>>> column("x", types.DateTime).op("-%>")(None).type
DateTime()
由于大多数用户定义的运算符倾向于成为 “比较” 运算符,通常是 PostgreSQL 定义的许多特殊运算符之一,因此 Operators.op.is_comparison
标志已修复,以遵循其文档化的行为,即在所有情况下(包括对于 ARRAY
和 JSON
)允许返回类型为 Boolean
>>> column("x", types.String(50)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.ARRAY(types.Integer)).op("-%>", is_comparison=True)(None).type
Boolean()
>>> column("x", types.JSON()).op("-%>", is_comparison=True)(None).type
Boolean()
为了帮助进行布尔比较运算符,添加了一种新的简写方法 Operators.bool_op()
。对于即时布尔运算符,应首选此方法
>>> print(column("x", types.Integer).bool_op("-%>")(5))
x -%> :x_1
literal_column() 中的百分号现在有条件地转义¶
literal_column
构造现在根据正在使用的 DBAPI 是否使用百分号敏感的 paramstyle(例如 ‘format’ 或 ‘pyformat’),有条件地转义百分号字符。
以前,不可能生成声明单个百分号的 literal_column
构造
>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
some%%symbol
对于未设置为使用 ‘format’ 或 ‘pyformat’ paramstyle 的方言,百分号现在不受影响;大多数 MySQL 方言(声明了这些 paramstyle 之一)将继续按适当的方式转义
>>> from sqlalchemy import literal_column
>>> print(literal_column("some%symbol"))
some%symbol
>>> from sqlalchemy.dialects import mysql
>>> print(literal_column("some%symbol").compile(dialect=mysql.dialect()))
some%%symbol
作为此更改的一部分,当使用诸如 ColumnOperators.contains()
、ColumnOperators.startswith()
和 ColumnOperators.endswith()
之类的运算符时存在的加倍也进行了改进,使其仅在适当的情况下发生。
列级 COLLATE 关键字现在引用排序规则名称¶
修复了 collate()
和 ColumnOperators.collate()
函数中的一个错误,该函数用于在语句级别提供临时列排序规则,其中区分大小写的名称不会被引用
stmt = select([mytable.c.x, mytable.c.y]).order_by(
mytable.c.somecolumn.collate("fr_FR")
)
现在呈现为
SELECT mytable.x, mytable.y,
FROM mytable ORDER BY mytable.somecolumn COLLATE "fr_FR"
以前,区分大小写的名称 “fr_FR” 不会被引用。目前,**不**检测到手动引用 “fr_FR” 名称,因此手动引用标识符的应用程序应进行调整。请注意,此更改不会影响在类型级别使用排序规则(例如,在表级别像 String
上指定的数据类型),在这种情况下,已经应用了引用。
方言改进和更改 - PostgreSQL¶
支持批量模式 / 快速执行助手¶
据确定,psycopg2 的 cursor.executemany()
方法性能不佳,尤其是在使用 INSERT 语句时。为了缓解这个问题,psycopg2 添加了快速执行助手,它通过批量发送多个 DML 语句来减少服务器往返次数。SQLAlchemy 1.2 现在包含了对这些助手的支持,以便在 Engine
使用 cursor.executemany()
调用针对多个参数集的语句时,可以透明地使用这些助手。此功能默认关闭,可以使用 create_engine()
上的 use_batch_mode
参数启用。
engine = create_engine(
"postgresql+psycopg2://scott:tiger@host/dbname", use_batch_mode=True
)
目前,此功能被认为是实验性的,但在未来的版本中可能会默认启用。
另请参阅:
INTERVAL 中字段规范的支持,包括完整反射¶
PostgreSQL 的 INTERVAL 数据类型中的 “fields” 说明符允许指定要存储的间隔字段,包括诸如 “YEAR”、“MONTH”、“YEAR TO MONTH” 等值。INTERVAL
数据类型现在允许指定这些值
from sqlalchemy.dialects.postgresql import INTERVAL
Table("my_table", metadata, Column("some_interval", INTERVAL(fields="DAY TO SECOND")))
此外,所有 INTERVAL 数据类型现在都可以独立于 “fields” 说明符进行反射;数据类型本身中的 “fields” 参数也将存在
>>> inspect(engine).get_columns("my_table")
[{'comment': None,
'name': u'some_interval', 'nullable': True,
'default': None, 'autoincrement': False,
'type': INTERVAL(fields=u'day to second')}]
方言改进和更改 - MySQL¶
支持 INSERT..ON DUPLICATE KEY UPDATE¶
MySQL 支持的 INSERT
的 ON DUPLICATE KEY UPDATE
子句现在通过 Insert
对象的 MySQL 特定版本(通过 sqlalchemy.dialects.mysql.dml.insert()
)得到支持。这个 Insert
子类添加了一个新的方法 Insert.on_duplicate_key_update()
,用于实现 MySQL 的语法
from sqlalchemy.dialects.mysql import insert
insert_stmt = insert(my_table).values(id="some_id", data="some data to insert")
on_conflict_stmt = insert_stmt.on_duplicate_key_update(
data=insert_stmt.inserted.data, status="U"
)
conn.execute(on_conflict_stmt)
以上将渲染为
INSERT INTO my_table (id, data)
VALUES (:id, :data)
ON DUPLICATE KEY UPDATE data=VALUES(data), status=:status_1
方言改进和更改 - Oracle¶
对 cx_Oracle 方言、类型系统进行重大重构¶
随着 cx_Oracle DBAPI 6.x 系列的引入,SQLAlchemy 的 cx_Oracle 方言已经过重新设计和简化,以利用 cx_Oracle 最近的改进,并放弃对 cx_Oracle 5.x 系列之前更相关的模式的支持。
现在支持的最低 cx_Oracle 版本为 5.1.3;建议使用 5.3 或最新的 6.x 系列。
数据类型的处理已重构。根据 cx_Oracle 开发人员的建议,
cursor.setinputsizes()
方法不再用于任何数据类型,LOB 类型除外。因此,参数auto_setinputsizes
和exclude_setinputsizes
已被弃用,并且不再具有任何效果。当设置为 False 以指示不应将具有精度和小数位数的数字类型强制转换为
Decimal
时,coerce_to_decimal
标志仅影响未类型化的语句(例如,没有TypeEngine
对象的纯字符串)。包含Numeric
类型或子类型的 Core 表达式现在将遵循该类型的十进制强制转换规则。方言中的 “两阶段” 事务支持已在 cx_Oracle 6.x 系列中删除,现在已完全移除,因为此功能从未正确工作,并且不太可能在生产中使用。因此,
allow_twophase
方言标志已被弃用,并且也无效。修复了 RETURNING 返回的列键相关的错误。给定如下语句
result = conn.execute(table.insert().values(x=5).returning(table.c.a, table.c.b))
以前,结果中每行的键将是
ret_0
和ret_1
,它们是 cx_Oracle RETURNING 实现的内部标识符。现在,键将是a
和b
,这与其他方言的预期一致。cx_Oracle 的 LOB 数据类型将返回值表示为
cx_Oracle.LOB
对象,这是一个与游标关联的代理,它通过.read()
方法返回最终数据值。从历史上看,如果在这些 LOB 对象被消耗之前读取了更多行(特别是,超过 cursor.arraysize 值的行,这会导致读取一批新行),这些 LOB 对象将引发错误 “LOB variable no longer valid after subsequent fetch”。SQLAlchemy 通过在其类型系统中自动调用这些 LOB 中的.read()
,以及使用特殊的BufferedColumnResultSet
来解决这个问题,后者将确保在使用cursor.fetchmany()
或cursor.fetchall()
等调用时缓冲此数据。该方言现在使用 cx_Oracle outputtypehandler 来处理这些
.read()
调用,以便始终预先调用它们,而不管正在获取多少行,从而不再发生此错误。因此,BufferedColumnResultSet
的使用,以及 CoreResultSet
的一些其他特定于此用例的内部组件已被移除。类型对象也得到了简化,因为它们不再需要处理二进制列结果。此外,cx_Oracle 6.x 已经消除了在任何情况下都会发生此错误的条件,因此不再可能发生该错误。如果极少(即使有)使用的
auto_convert_lobs=False
选项与之前的 5.x 系列 cx_Oracle 结合使用,并且在 LOB 对象可以被消耗之前读取了更多行,则 SQLAlchemy 上可能会发生该错误。升级到 cx_Oracle 6.x 将解决该问题。
Oracle Unique、Check 约束现在可以反射¶
UNIQUE 和 CHECK 约束现在可以通过 Inspector.get_unique_constraints()
和 Inspector.get_check_constraints()
进行反射。反射的 Table
对象现在将包含 CheckConstraint
对象。有关此处的行为怪癖的说明,请参阅 约束反射,包括大多数 Table
对象仍然不包含任何 UniqueConstraint
对象,因为这些对象通常通过 Index
表示。
另请参阅:
Oracle 外键约束名称现在“名称规范化”¶
在表反射期间以及在 Inspector.get_foreign_keys()
方法中,传递给 ForeignKeyConstraint
对象的外键约束名称现在将“名称规范化”,也就是说,对于不区分大小写的名称,表示为小写,而不是 Oracle 使用的原始 UPPERCASE 格式
>>> insp.get_indexes("addresses")
[{'unique': False, 'column_names': [u'user_id'],
'name': u'address_idx', 'dialect_options': {}}]
>>> insp.get_pk_constraint("addresses")
{'name': u'pk_cons', 'constrained_columns': [u'id']}
>>> insp.get_foreign_keys("addresses")
[{'referred_table': u'users', 'referred_columns': [u'id'],
'referred_schema': None, 'name': u'user_id_fk',
'constrained_columns': [u'user_id']}]
以前,外键结果将如下所示
[
{
"referred_table": "users",
"referred_columns": ["id"],
"referred_schema": None,
"name": "USER_ID_FK",
"constrained_columns": ["user_id"],
}
]
上述情况可能会导致问题,尤其是在 Alembic autogenerate 中。
方言改进和更改 - SQL Server¶
支持带有点号的 SQL Server 架构名称¶
SQL Server 方言具有这样的行为:带有点号的架构名称被假定为 “database”.”owner” 标识符对,在表和组件反射操作期间,以及在渲染架构名称的引号时,必须将其拆分为这些单独的组件,以便单独引用这两个符号。现在可以使用方括号传递 schema 参数,以手动指定此拆分发生的位置,从而允许数据库和/或所有者名称本身包含一个或多个点
Table("some_table", metadata, Column("q", String(50)), schema="[MyDataBase.dbo]")
上面的表会将 “owner” 视为 MyDataBase.dbo
,它也将在渲染时被引用,并将 “database” 视为 None。要单独引用数据库名称和所有者,请使用两对方括号
Table(
"some_table",
metadata,
Column("q", String(50)),
schema="[MyDataBase.SomeDB].[MyDB.owner]",
)
此外,当传递给 SQL Server 方言的 “schema” 时,quoted_name
构造现在被遵守;如果 quote 标志为 True,则给定的符号将不会在点号上拆分,并将被解释为 “owner”。
另请参阅:
AUTOCOMMIT 隔离级别支持¶
PyODBC 和 pymssql 方言现在都支持通过 Connection.execution_options()
设置的 “AUTOCOMMIT” 隔离级别,这将会在 DBAPI 连接对象上建立正确的标志。
flambé! 龙和 炼金术士 图像设计由 Rotem Yaari 创建并慷慨捐赠。
使用 Sphinx 7.2.6 创建。文档最后生成时间:2025 年 3 月 11 日星期二下午 02:40:17 EDT