What’s New in SQLAlchemy 1.1?
About this Document
本文档描述了 SQLAlchemy 版本 1.0 和版本 1.1 之间的变化。
Introduction
本指南介绍了 SQLAlchemy 1.1 版本的新功能,并记录了影响用户将其应用程序从 SQLAlchemy 1.0 系列迁移到 1.1 系列的变化。
请仔细查看关于行为变化的部分,可能存在与旧版本不兼容的行为变化。
平台/安装程序变更
Setuptools is now required for install
SQLAlchemy 的setup.py
文件多年来一直支持安装 Setuptools 或不安装 Setuptools 两种方式;支持“回退”模式,即使用普通的 Distutils。由于现在几乎不再听说不安装 Setuptools 的 Python 环境了,并且为了更全面地支持 Setuptools 的特性集,特别是支持 py.test 与其集成以及诸如“extras”之类的功能,setup.py
现在完全依赖于 Setuptools。
See also
安装指南
仅通过环境变量启用/禁用 C 扩展构建
默认情况下,在安装时构建 C 扩展,只要可能。要禁用 C 扩展构建,从 SQLAlchemy 0.8.6 / 0.9.4 开始,可使用DISABLE_SQLALCHEMY_CEXT
环境变量。之前使用--without-cextensions
参数的方法已被移除,因为它依赖于已弃用的 setuptools 功能。
See also
构建 Cython 扩展
新功能和改进 - ORM
新的会话生命周期事件
Session
长期以来一直支持事件,允许在某种程度上跟踪对象状态的变化,包括SessionEvents.before_attach()
、SessionEvents.after_attach()
和SessionEvents.before_flush()
。Session 文档还在快速对象状态介绍中记录了主要的对象状态。然而,过去从未有过专门跟踪对象经过这些转换的系统。此外,“已删除”对象的状态一直比较模糊,因为这些对象的行为介于“持久”和“分离”状态之间。
为了清理这个领域并使会话状态转换的范围完全透明化,已添加了一系列新事件,旨在涵盖对象可能在状态之间转换的每种可能方式,而且还给“已删除”状态在会话对象状态领域内赋予了自己的正式状态名称。
新状态转换事件
对象所有状态之间的转换,如 persistent、pending 等,现在都可以通过会话级事件的方式进行拦截,以涵盖特定的转换。当对象进入 Session
、从 Session
移出,甚至当使用 Session.rollback()
回滚事务时发生的所有转换都明确出现在 SessionEvents
接口中。
总共有十个新事件。这些事件的摘要在新编写的文档部分对象生命周期事件中。
添加了新的对象状态“deleted”,已删除的对象不再“persistent”
对象在 Session
中的 persistent 状态一直以来都被记录为具有有效的数据库标识;然而,对于在 flush 中被删除的对象,它们一直处于一个灰色地带,在这个地带中,它们并没有真正“分离”出 Session
,因为它们仍然可以在回滚时恢复,但它们并不真正“persistent”,因为它们的数据库标识已被删除,而且它们不在标识映射中。
为了解决这一新事件中的灰色地带,引入了一个新的对象状态删除。此状态存在于“持久”状态和“分离”状态之间。通过 Session.delete()
标记为删除的对象保持在“持久”状态,直到刷新进行为止;在那时,它将从身份映射中删除,转移到“已删除”状态,并调用 SessionEvents.persistent_to_deleted()
钩子。如果 Session
对象的事务被回滚,则对象将被恢复为持久状态;将调用 SessionEvents.deleted_to_persistent()
转换。否则,如果 Session
对象的事务被提交,则调用 SessionEvents.deleted_to_detached()
转换。
另外,InstanceState.persistent
访问器不再返回 True,表示对象处于新的“已删除”状态;相反,增强了 InstanceState.deleted
访问器以可靠地报告此新状态。当对象分离时,InstanceState.deleted
返回 False,并且 InstanceState.detached
访问器为 True。要确定对象是否在当前事务或上一个事务中被删除,请使用 InstanceState.was_deleted
访问器。
强身份映射已被弃用。
新系列过渡事件的灵感之一是为了实现对象在进出标识映射时的无泄漏跟踪,以便维护“强引用”,反映对象在此映射中进出的情况。有了这种新能力,就不再需要 Session.weak_identity_map
参数和相应的 StrongIdentityMap
对象。这个选项在 SQLAlchemy 中已经存在多年,因为“强引用”行为曾经是唯一可用的行为,许多应用程序都被写成假定这种行为。长期以来,一直建议对象的强引用跟踪不是 Session
的固有工作,而是一个应用级别的构造,根据应用程序的需要构建;新的事件模型甚至允许复制强标识映射的确切行为。查看 会话引用行为 以获取一个新的示例,说明如何替换强标识映射。
#2677 ### 新的 init_scalar() 事件拦截 ORM 级别的默认值
当首次访问未设置的属性时,ORM 会为非持久化对象生成一个值为 None
:
>>> obj = MyObj() >>> obj.some_value None
有一个用例是为了在对象持久化之前,使得 Python 中的值与 Core 生成的默认值对应。为了适应这种用例,添加了一个新的事件 AttributeEvents.init_scalar()
。在 属性仪器化 中的新示例 active_column_defaults.py
说明了一个示例用法,因此效果可以是:
>>> obj = MyObj() >>> obj.some_value "my default"
#1311 ### 关于“不可哈希”类型的更改,影响 ORM 行的去重
Query
对象具有“去重”返回行的良好行为,其中包含至少一个 ORM 映射实体(例如,一个完全映射的对象,而不是单独的列值)。这样做的主要目的是为了使实体的处理与标识映射顺利配合,包括适应通常在连接式急加载中表示的重复实体,以及在使用连接来过滤其他列时。
这种去重依赖于行内元素的可哈希性。随着 PostgreSQL 的特殊类型如ARRAY
、HSTORE
和JSON
的引入,行内类型被标记为不可哈希并在这里遇到问题的经验比以前更普遍。
实际上,SQLAlchemy 自版本 0.8 以来在被标记为“unhashable”的数据类型上包含了一个标志,然而这个标志在内置类型上并没有一致使用。正如 ARRAY 和 JSON 类型现在正确指定“unhashable”中所描述的,这个标志现在对所有 PostgreSQL 的“结构”类型一致设置。
“unhashable”标志也设置在NullType
类型上,因为NullType
用于引用任何未知类型的表达式。
由于NullType
应用于大多数func
的用法,因为func
实际上在大多数情况下并不知道给定的函数名称,使用 func()通常会禁用行去重,除非应用了显式类型。以下示例说明了将func.substr()
应用于字符串表达式,以及将func.date()
应用于日期时间表达式;这两个示例将由于连接的急加载而返回重复行,除非应用了显式类型:
result = ( session.query(func.substr(A.some_thing, 0, 4), A).options(joinedload(A.bs)).all() ) users = ( session.query( func.date(User.date_created, "start of month").label("month"), User, ) .options(joinedload(User.orders)) .all() )
为了保留去重,上述示例应该指定为:
result = ( session.query(func.substr(A.some_thing, 0, 4, type_=String), A) .options(joinedload(A.bs)) .all() ) users = ( session.query( func.date(User.date_created, "start of month", type_=DateTime).label("month"), User, ) .options(joinedload(User.orders)) .all() )
另外,所谓的“unhashable”类型的处理与以前的版本略有不同;在内部,我们使用id()
函数从这些结构中获取“哈希值”,就像我们对待任何普通映射对象一样。这取代了以前将计数器应用于对象的方法。
#3499 ### 为传递映射类、实例作为 SQL 文字添加了特定检查
现在,类型系统对于在本应被处理为字面值的上下文中传递 SQLAlchemy“可检查”对象具有特定检查。任何 SQLAlchemy 内置对象,如果作为 SQL 值传递是合法的(而不是已经是ClauseElement
实例),都包括一个__clause_element__()
方法,该方法为该对象提供有效的 SQL 表达式。对于不提供此方法的 SQLAlchemy 对象,例如映射类、映射器和映射实例,会发出更具信息性的错误消息,而不是允许 DBAPI 接收对象并稍后失败。下面举例说明了一个情况,其中基于字符串的属性User.name
与User()
的完整实例进行比较,而不是与字符串值进行比较:
>>> some_user = User() >>> q = s.query(User).filter(User.name == some_user) sqlalchemy.exc.ArgumentError: Object <__main__.User object at 0x103167e90> is not legal as a SQL literal value
当比较User.name == some_user
时,异常现在会立即发生。以前,像上面这样的比较会产生一个 SQL 表达式,只有在解析为 DBAPI 执行调用时才会失败;映射的User
对象最终会变成一个被 DBAPI 拒绝的绑定参数。
请注意,在上面的示例中,表达式失败是因为User.name
是一个基于字符串的(例如基于列的)属性。这种变化不会影响通常情况下将多对一关系属性与对象进行比较的情况,这种情况会被单独处理:
>>> # Address.user refers to the User mapper, so >>> # this is of course still OK! >>> q = s.query(Address).filter(Address.user == some_user)
#3321 ### 新的可索引 ORM 扩展
可索引 扩展是混合属性功能的扩展,允许构建引用“可索引”数据类型的特定元素的属性,例如数组或 JSON 字段:
class Person(Base): __tablename__ = "person" id = Column(Integer, primary_key=True) data = Column(JSON) name = index_property("data", "name")
上面,name
属性将从 JSON 列data
中读取/写入字段"name"
,在初始化为空字典后:
>>> person = Person(name="foobar") >>> person.name foobar
当修改属性时,该扩展还会触发一个更改事件,因此无需使用MutableDict
来跟踪此更改。
另请参阅
可索引 ### 新选项允许显式持久化 NULL 值而不是默认值
与 PostgreSQL 中新增的 JSON-NULL 支持相关,作为 JSON “null” 在 ORM 操作中被插入时的预期行为,当不存在时被省略 的一部分,基础 TypeEngine
类现在支持一个方法 TypeEngine.evaluates_none()
,允许将属性上的 None
值设置为 NULL,而不是在 INSERT 语句中省略列,这会导致使用列级默认值的效果。这允许在映射器级别配置现有的对象级技术,将 null()
分配给属性。
另请参阅
强制在具有默认值的列上使用 NULL
#3250 ### 进一步修复单表继承查询
继续从 1.0 的 使用 from_self(), count() 时对单表继承条件的更改,Query
在查询针对子查询表达式时,如 exists 时,不应再不适当地添加“单一继承”条件:
class Widget(Base): __tablename__ = "widget" id = Column(Integer, primary_key=True) type = Column(String) data = Column(String) __mapper_args__ = {"polymorphic_on": type} class FooWidget(Widget): __mapper_args__ = {"polymorphic_identity": "foo"} q = session.query(FooWidget).filter(FooWidget.data == "bar").exists() session.query(q).all()
产生:
SELECT EXISTS (SELECT 1 FROM widget WHERE widget.data = :data_1 AND widget.type IN (:type_1)) AS anon_1
里面的 IN 子句是适当的,以限制为 FooWidget 对象,但以前 IN 子句也会在子查询之外生成第二次。
#3582 ### 当数据库取消 SAVEPOINT 时改进了会话状态
MySQL 的一个常见情况是在事务中发生死锁时取消 SAVEPOINT。Session
已经被修改,以更为优雅地处理这种失败模式,使得外部的非 SAVEPOINT 事务仍然可用:
s = Session() s.begin_nested() s.add(SomeObject()) try: # assume the flush fails, flush goes to rollback to the # savepoint and that also fails s.flush() except Exception as err: print("Something broke, and our SAVEPOINT vanished too") # this is the SAVEPOINT transaction, marked as # DEACTIVE so the rollback() call succeeds s.rollback() # this is the outermost transaction, remains ACTIVE # so rollback() or commit() can succeed s.rollback()
这个问题是 #2696 的延续,我们发出警告,以便在 Python 2 上运行时可以看到原始错误,即使 SAVEPOINT 异常优先。在 Python 3 上,异常被链接在一起,因此两个失败都会被单独报告。
#3680 ### 修复了错误的“新实例 X 与持久实例 Y 冲突”刷新错误
Session.rollback()
方法负责删除在数据库中插入的对象,例如在那个现在回滚的事务中从挂起状态移动到持久状态。进行此状态更改的对象在一个弱引用集合中被跟踪,如果一个对象从该集合中被垃圾回收,Session
不再关心它(否则对于在事务中插入许多新对象的操作不会扩展)。然而,如果应用程序在事务中重新加载相同的被垃圾回收的行,在回滚发生之前会出现问题;如果对这个对象的强引用保留到下一个事务中,那么这个对象未被插入并应该被删除的事实将丢失,并且 flush 将不正确地引发错误:
from sqlalchemy import Column, create_engine from sqlalchemy.orm import Session from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) e = create_engine("sqlite://", echo=True) Base.metadata.create_all(e) s = Session(e) # persist an object s.add(A(id=1)) s.flush() # rollback buffer loses reference to A # load it again, rollback buffer knows nothing # about it a1 = s.query(A).first() # roll back the transaction; all state is expired but the # "a1" reference remains s.rollback() # previous "a1" conflicts with the new one because we aren't # checking that it never got committed s.add(A(id=1)) s.commit()
上述程序将引发:
FlushError: New instance <User at 0x7f0287eca4d0> with identity key (<class 'test.orm.test_transaction.User'>, ('u1',)) conflicts with persistent instance <User at 0x7f02889c70d0>
bug 在于当上述异常被触发时,工作单元正在操作原始对象,假设它是一个活动行,而实际上对象已过期,并在测试中发现它已经消失。修复现在测试这种情况,因此在 SQL 日志中我们看到:
BEGIN (implicit) INSERT INTO a (id) VALUES (?) (1,) SELECT a.id AS a_id FROM a LIMIT ? OFFSET ? (1, 0) ROLLBACK BEGIN (implicit) SELECT a.id AS a_id FROM a WHERE a.id = ? (1,) INSERT INTO a (id) VALUES (?) (1,) COMMIT
上面,工作单元现在为我们即将报告为冲突的行执行 SELECT,看到它不存在,并正常进行。只有在我们本来会在任何情况下错误地引发异常时,才会发生这个 SELECT 的开销。
#3677 ### 联接继承映射的 passive_deletes 功能
现在,联接表继承映射可能允许 DELETE 继续进行,作为Session.delete()
的结果,该方法仅为基表发出 DELETE,而不是子类表,从而允许配置的 ON DELETE CASCADE 发生在配置的外键上。这是使用mapper.passive_deletes
选项配置的:
from sqlalchemy import Column, Integer, String, ForeignKey, create_engine from sqlalchemy.orm import Session from sqlalchemy.ext.declarative import declarative_base Base = declarative_base() class A(Base): __tablename__ = "a" id = Column("id", Integer, primary_key=True) type = Column(String) __mapper_args__ = { "polymorphic_on": type, "polymorphic_identity": "a", "passive_deletes": True, } class B(A): __tablename__ = "b" b_table_id = Column("b_table_id", Integer, primary_key=True) bid = Column("bid", Integer, ForeignKey("a.id", ondelete="CASCADE")) data = Column("data", String) __mapper_args__ = {"polymorphic_identity": "b"}
使用上述映射,mapper.passive_deletes
选项在基本映射器上配置;它对于所有具有设置选项的祖先映射器的非基本映射器生效。对于类型为B
的对象的 DELETE 不再需要检索b_table_id
的主键值(如果未加载),也不需要为表本身发出 DELETE 语句:
session.delete(some_b) session.commit()
将生成的 SQL 如下:
DELETE FROM a WHERE a.id = %(id)s -- {'id': 1} COMMIT
一如既往,目标数据库必须具有启用 ON DELETE CASCADE 的外键支持。
#2349 ### 同名反向引用应用于具体继承子类时不会引发错误
以下映射一直是可能的而没有问题:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) b = relationship("B", foreign_keys="B.a_id", backref="a") class A1(A): __tablename__ = "a1" id = Column(Integer, primary_key=True) b = relationship("B", foreign_keys="B.a1_id", backref="a1") __mapper_args__ = {"concrete": True} class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) a1_id = Column(ForeignKey("a1.id"))
上面,即使类A
和类A1
有一个名为b
的关系,也不会出现冲突警告或错误,因为类A1
被标记为“具体”。
然而,如果关系配置反过来,就会出现错误:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) class A1(A): __tablename__ = "a1" id = Column(Integer, primary_key=True) __mapper_args__ = {"concrete": True} class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id")) a1_id = Column(ForeignKey("a1.id")) a = relationship("A", backref="b") a1 = relationship("A1", backref="b")
修复增强了反向引用功能,以便不发出错误,并在映射器逻辑内部进行额外检查,以跳过替换属性的警告。
#3630 ### 继承映射器上的同名关系不再发出警告
在继承场景中创建两个映射器时,在两者上都放置同名关系会发出警告“映射器上的关系‘’取代了继承映射器‘`上的相同关系;这可能在刷新期间引起依赖问题”。示例如下:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) bs = relationship("B") class ASub(A): __tablename__ = "a_sub" id = Column(Integer, ForeignKey("a.id"), primary_key=True) bs = relationship("B") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) a_id = Column(ForeignKey("a.id"))
此警告可以追溯到 2007 年的 0.4 系列,基于一个自那时完全重写的工作单元代码版本。目前,没有关于在基类和子类上放置同名关系的已知问题,因此警告被取消。但是,请注意,由于警告,这种用例在现实世界中可能不常见。虽然为这种用例添加了基本的测试支持,但可能会发现这种模式的一些新问题。
1.1.0b3 版本中新增。
#3749 ### 混合属性和方法现在也传播文档字符串以及.info
现在,混合方法或属性将反映原始文档字符串中存在的__doc__
值:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) name = Column(String) @hybrid_property def some_name(self): """The name field""" return self.name
现在,A.some_name.__doc__
的上述值被尊重:
>>> A.some_name.__doc__ The name field
然而,为了实现这一点,混合属性的机制必然变得更加复杂。以前,混合的类级访问器将是一个简单的传递,也就是说,这个测试将成功:
>>> assert A.name is A.some_name
随着变化,A.some_name
返回的表达式被包装在自己的QueryableAttribute
包装器内:
>>> A.some_name <sqlalchemy.orm.attributes.hybrid_propertyProxy object at 0x7fde03888230>
进行了大量测试,以确保此包装器正常工作,包括对自定义值对象配方等复杂方案的测试,但我们将继续查看用户是否出现其他退化情况。
作为这一变化的一部分,hybrid_property.info
集合现在也从混合描述符本身传播,而不是从底层表达式传播。也就是说,访问A.some_name.info
现在返回与inspect(A).all_orm_descriptors['some_name'].info
相同的字典:
>>> A.some_name.info["foo"] = "bar" >>> from sqlalchemy import inspect >>> inspect(A).all_orm_descriptors["some_name"].info {'foo': 'bar'}
请注意,这个.info
字典独立于混合描述符可能直接代理的映射属性的字典;这是从 1.0 开始的行为变化。包装器仍将代理镜像属性的其他有用属性,如QueryableAttribute.property
和QueryableAttribute.class_
。
#3653 ### Session.merge 解决挂起冲突与持久性相同
Session.merge()
方法现在将跟踪图中给定对象的标识,以维护主键的唯一性,然后再发出 INSERT。当遇到相同标识的重复对象时,非主键属性会被覆盖,因为对象被遇到时是基本上是非确定性的。这种行为与持久对象的行为相匹配,也就是通过主键已经位于数据库中的对象,因此这种行为更具内部一致性。
给定:
u1 = User(id=7, name="x") u1.orders = [ Order(description="o1", address=Address(id=1, email_address="a")), Order(description="o2", address=Address(id=1, email_address="b")), Order(description="o3", address=Address(id=1, email_address="c")), ] sess = Session() sess.merge(u1)
在上面的例子中,我们将一个User
对象与三个新的Order
对象合并,每个对象都指向一个不同的Address
对象,但是它们都具有相同的主键。Session.merge()
的当前行为是在标识映射中查找这个Address
对象,并将其用作目标。如果对象存在,意味着数据库已经有一个带有主键“1”的Address
行,我们可以看到Address
的email_address
字段将在这种情况下被覆盖三次,分别为 a,b 和最后是 c。
然而,如果主键“1”对应的Address
行不存在,Session.merge()
将创建三个单独的Address
实例,然后在 INSERT 时会出现主键冲突。新行为是,这些Address
对象的拟议主键被跟踪在一个单独的字典中,以便我们将三个拟议的Address
对象的状态合并到一个要插入的Address
对象上。
如果原始情况下发出某种警告,表明在单个合并树中存在冲突数据可能更好,但是对于持久情况,多年来非确定性合并值一直是行为方式;现在对于挂起情况也是如此。警告存在冲突值的功能仍然对于两种情况都是可行的,但是会增加相当大的性能开销,因为在合并过程中每个列值都必须进行比较。
#3601 ### 修复涉及用户发起的外键操作的多对一对象移动
已修复了涉及用另一个对象替换多对一引用机制的 bug。在属性操作期间,先前引用的对象位置现在使用数据库提交的外键值,而不是当前外键值。修复的主要效果是,当进行多对一更改时,即使在手动将外键属性移动到新值之前,也会更准确地触发向集合的 backref 事件。假设类 Parent
和 SomeClass
的映射,其中 SomeClass.parent
指向 Parent
,Parent.items
指向 SomeClass
对象的集合:
some_object = SomeClass() session.add(some_object) some_object.parent_id = some_parent.id some_object.parent = some_parent
在上面,我们创建了一个待处理对象 some_object
,将其外键指向 Parent
,然后我们实际设置了关系。在修复错误之前,backref 不会被触发:
# before the fix assert some_object not in some_parent.items
现在的修复是,当我们试图定位 some_object.parent
的先前值时,我们忽略了手动设置的父 id,并寻找数据库提交的值。在这种情况下,它是 None,因为对象是待处理的,所以事件系统将 some_object.parent
记录为净变化:
# after the fix, backref fired off for some_object.parent = some_parent assert some_object in some_parent.items
尽管不鼓励操作由关系管理的外键属性,但对于这种用例有有限的支持。为了允许加载继续进行,经常会使用 Session.enable_relationship_loading()
和 RelationshipProperty.load_on_pending
功能,这些功能会导致基于内存中未持久化的外键值发出延迟加载的关系。无论是否使用这些功能,这种行为改进现在都会显而易见。
#3708 ### 改进了具有多态实体的 Query.correlate 方法
在最近的 SQLAlchemy 版本中,许多形式的“多态”查询生成的 SQL 比以前更“扁平化”,不再无条件地将多个表的 JOIN 捆绑到子查询中。为了适应这一点,Query.correlate()
方法现在从这样的多态可选择中提取各个表,并确保所有表都是子查询的“correlate” 的一部分。假设映射文档中的 Person/Manager/Engineer->Company
设置,使用 with_polymorphic:
sess.query(Person.name).filter( sess.query(Company.name) .filter(Company.company_id == Person.company_id) .correlate(Person) .as_scalar() == "Elbonia, Inc." )
上述查询现在产生:
SELECT people.name AS people_name FROM people LEFT OUTER JOIN engineers ON people.person_id = engineers.person_id LEFT OUTER JOIN managers ON people.person_id = managers.person_id WHERE (SELECT companies.name FROM companies WHERE companies.company_id = people.company_id) = ?
在修复之前,对 correlate(Person)
的调用会错误地尝试将 Person
、Engineer
和 Manager
的连接作为单个单元进行关联,因此 Person
不会被关联:
-- old, incorrect query SELECT people.name AS people_name FROM people LEFT OUTER JOIN engineers ON people.person_id = engineers.person_id LEFT OUTER JOIN managers ON people.person_id = managers.person_id WHERE (SELECT companies.name FROM companies, people WHERE companies.company_id = people.company_id) = ?
对多态映射使用相关子查询仍然存在一些未完善的地方。例如,如果Person
被多态链接到所谓的“具体多态联合”查询,上述子查询可能无法正确引用这个子查询。在所有情况下,完全引用“多态”实体的一种方法是首先从中创建一个aliased()
对象:
# works with all SQLAlchemy versions and all types of polymorphic # aliasing. paliased = aliased(Person) sess.query(paliased.name).filter( sess.query(Company.name) .filter(Company.company_id == paliased.company_id) .correlate(paliased) .as_scalar() == "Elbonia, Inc." )
aliased()
构造保证了“多态可选择性”被包裹在子查询中。通过在相关子查询中明确引用它,多态形式被正确使用。
#3662 ### 查询的字符串化将向会话查询正确的方言
对Query
对象调用str()
将向Session
查询正确的“绑定”,以便渲染将传递给数据库的 SQL。特别是,这允许引用特定于方言的 SQL 构造的Query
可呈现,假设Query
与适当的Session
相关联。以前,只有当映射关联到目标Engine
的MetaData
才会生效。
如果底层的MetaData
或Session
都没有与任何绑定的Engine
相关联,则将使用“默认”方言回退生成 SQL 字符串。
另请参见
“友好”地将核心 SQL 构造字符串化而不使用方言
#3081 ### 在一行中多次出现相同实体的连接急加载
已经修复了一个情况,即使实体已经从不包括属性的不同“路径”上的行加载,也将通过连接的急加载加载属性。这是一个难以复现的深层用例,但一般思路如下:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) b_id = Column(ForeignKey("b.id")) c_id = Column(ForeignKey("c.id")) b = relationship("B") c = relationship("C") class B(Base): __tablename__ = "b" id = Column(Integer, primary_key=True) c_id = Column(ForeignKey("c.id")) c = relationship("C") class C(Base): __tablename__ = "c" id = Column(Integer, primary_key=True) d_id = Column(ForeignKey("d.id")) d = relationship("D") class D(Base): __tablename__ = "d" id = Column(Integer, primary_key=True) c_alias_1 = aliased(C) c_alias_2 = aliased(C) q = s.query(A) q = q.join(A.b).join(c_alias_1, B.c).join(c_alias_1.d) q = q.options( contains_eager(A.b).contains_eager(B.c, alias=c_alias_1).contains_eager(C.d) ) q = q.join(c_alias_2, A.c) q = q.options(contains_eager(A.c, alias=c_alias_2))
上述查询生成的 SQL 如下:
SELECT d.id AS d_id, c_1.id AS c_1_id, c_1.d_id AS c_1_d_id, b.id AS b_id, b.c_id AS b_c_id, c_2.id AS c_2_id, c_2.d_id AS c_2_d_id, a.id AS a_id, a.b_id AS a_b_id, a.c_id AS a_c_id FROM a JOIN b ON b.id = a.b_id JOIN c AS c_1 ON c_1.id = b.c_id JOIN d ON d.id = c_1.d_id JOIN c AS c_2 ON c_2.id = a.c_id
我们可以看到c
表被两次选择;一次在A.b.c -> c_alias_1
的上下文中,另一次在A.c -> c_alias_2
的上下文中。此外,我们可以看到对于单个行,C
标识很可能对于c_alias_1
和c_alias_2
是相同的,这意味着一行中的两组列导致只向标识映射中添加一个新对象。
上述查询选项仅要求在c_alias_1
的上下文中加载属性C.d
,而不是c_alias_2
。因此,我们在标识映射中得到的最终C
对象是否具有加载的C.d
属性取决于映射如何遍历,虽然不是完全随机,但基本上是不确定的。修复方法是,即使对于它们都引用相同标识的单个行,c_alias_1
的加载程序在c_alias_2
的加载程序之后处理,C.d
元素仍将被加载。以前,加载程序不寻求修改已通过不同路径加载的实体的加载。首先到达实体的加载程序一直是不确定的,因此在某些情况下,此修复可能可检测为行为变化,而在其他情况下则不会。
修复包括对“多个路径指向一个实体”的两个变体的测试,并且修复应该希望覆盖此类其他情况。
新的 MutableList 和 MutableSet 辅助程序已添加到变异跟踪扩展中
新的辅助类MutableList
和MutableSet
已添加到 Mutation Tracking 扩展中,以补充现有的MutableDict
辅助程序。
新的“raise” / “raise_on_sql”加载策略
为了帮助防止在加载一系列对象后发生不必要的延迟加载,可以将新的“lazy=‘raise’”和“lazy=‘raise_on_sql’”策略及相应的加载程序选项raiseload()
应用于关系属性,当尝试读取非急切加载的属性时,将导致引发InvalidRequestError
。这两个变体测试任何类型的延迟加载,包括那些只会返回 None 或从标识映射中检索的延迟加载:
>>> from sqlalchemy.orm import raiseload >>> a1 = s.query(A).options(raiseload(A.some_b)).first() >>> a1.some_b Traceback (most recent call last): ... sqlalchemy.exc.InvalidRequestError: 'A.some_b' is not available due to lazy='raise'
或者仅在需要发出 SQL 时进行延迟加载:
>>> from sqlalchemy.orm import raiseload >>> a1 = s.query(A).options(raiseload(A.some_b, sql_only=True)).first() >>> a1.some_b Traceback (most recent call last): ... sqlalchemy.exc.InvalidRequestError: 'A.bs' is not available due to lazy='raise_on_sql'
#3512 ### Mapper.order_by 已弃用
这个来自 SQLAlchemy 最早版本的旧参数是 ORM 的原始设计的一部分,其中包括Mapper
对象作为公共查询结构。这个角色早已被Query
对象取代,我们使用Query.order_by()
来指示结果的排序方式,这种方式对于任何组合的 SELECT 语句、实体和 SQL 表达式都能保持一致。有许多情况下Mapper.order_by
不能按预期工作(或者预期的结果不清楚),比如当查询组合成联合时;这些情况不受支持。
新功能和改进 - 核心
Engines 现在作废连接,运行 BaseException 的错误处理程序
新功能在版本 1.1 中新增:这个变化是在 1.1 系列最终版本之前的一个晚期添加,不包含在 1.1 beta 版本中。
Python BaseException
类位于Exception
之下,但是是系统级异常的可识别基类,例如KeyboardInterrupt
,SystemExit
,以及特别是GreenletExit
异常,这些异常被 eventlet 和 gevent 使用。这个异常类现在被Connection
的异常处理例程拦截,并包括由ConnectionEvents.handle_error()
事件处理。在系统级异常不是Exception
的子类的情况下,默认情况下Connection
现在被作废,因为假定操作被中断,连接可能处于不可用状态。这个变化主要针对 MySQL 驱动程序,但是这个变化适用于所有的 DBAPIs。
请注意,在作废时,Connection
使用的即时 DBAPI 连接被释放,如果在异常抛出后仍在使用Connection
,则在下次使用时将使用新的 DBAPI 连接进行后续操作;然而,正在进行的任何事务的状态将丢失,并且必须在重新使用之前调用适当的.rollback()
方法(如果适用)。
为了识别这种变化,当这些异常发生在连接执行工作过程中时,很容易展示一个 pymysql 或 mysqlclient / MySQL-Python 连接进入损坏状态;连接将被返回到连接池,随后的使用将失败,甚至在返回到池之前会导致调用.rollback()
的上下文管理器中出现次要故障。这里的行为预期将减少 MySQL 错误“commands out of sync”的发生率,以及在 MySQL 驱动程序未能正确报告cursor.description
时可能发生的ResourceClosedError
,在绿色线程条件下运行时,绿色线程被终止,或者处理KeyboardInterrupt
异常而不完全退出程序时。
这种行为与通常的自动失效功能不同,它不假设后端数据库本身已关闭或重新启动;它不像通常的 DBAPI 断开连接异常那样重新生成整个连接池。
这个变化应该对所有用户都是一个净改进,除了任何当前拦截KeyboardInterrupt
或GreenletExit
并希望在同一事务中继续工作的应用程序。对于不受KeyboardInterrupt
影响的其他 DBAPIs,如 psycopg2,这样的操作在理论上是可能的。对于这些 DBAPIs,以下解决方法将禁用特定异常的连接被回收:
engine = create_engine("postgresql+psycopg2://") @event.listens_for(engine, "handle_error") def cancel_disconnect(ctx): if isinstance(ctx.original_exception, KeyboardInterrupt): ctx.is_disconnect = False
#3803 ### CTE 支持 INSERT、UPDATE、DELETE
最广泛请求的功能之一是支持与 INSERT、UPDATE、DELETE 一起工作的通用表达式(CTE),现在已经实现。INSERT/UPDATE/DELETE 可以从位于 SQL 顶部的 WITH 子句中提取,也可以作为更大语句上下文中的 CTE 自身使用。
作为这个变化的一部分,包含 CTE 的 SELECT 插入现在将在整个语句的顶部呈现 CTE,而不像在 1.0 版本中嵌套在 SELECT 语句中。
下面是一个将 UPDATE、INSERT 和 SELECT 全部放在一个语句中的示例:
>>> from sqlalchemy import table, column, select, literal, exists >>> orders = table( ... "orders", ... column("region"), ... column("amount"), ... column("product"), ... column("quantity"), ... ) >>> >>> upsert = ( ... orders.update() ... .where(orders.c.region == "Region1") ... .values(amount=1.0, product="Product1", quantity=1) ... .returning(*(orders.c._all_columns)) ... .cte("upsert") ... ) >>> >>> insert = orders.insert().from_select( ... orders.c.keys(), ... select([literal("Region1"), literal(1.0), literal("Product1"), literal(1)]).where( ... ~exists(upsert.select()) ... ), ... ) >>> >>> print(insert) # Note: formatting added for clarity WITH upsert AS (UPDATE orders SET amount=:amount, product=:product, quantity=:quantity WHERE orders.region = :region_1 RETURNING orders.region, orders.amount, orders.product, orders.quantity ) INSERT INTO orders (region, amount, product, quantity) SELECT :param_1 AS anon_1, :param_2 AS anon_2, :param_3 AS anon_3, :param_4 AS anon_4 WHERE NOT ( EXISTS ( SELECT upsert.region, upsert.amount, upsert.product, upsert.quantity FROM upsert))
#2551 ### 支持窗口函数中的 RANGE 和 ROWS 规范
新的 over.range_
和 over.rows
参数允许为窗口函数使用 RANGE 和 ROWS 表达式:
>>> from sqlalchemy import func >>> print(func.row_number().over(order_by="x", range_=(-5, 10))) row_number() OVER (ORDER BY x RANGE BETWEEN :param_1 PRECEDING AND :param_2 FOLLOWING) >>> print(func.row_number().over(order_by="x", rows=(None, 0))) row_number() OVER (ORDER BY x ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) >>> print(func.row_number().over(order_by="x", range_=(-2, None))) row_number() OVER (ORDER BY x RANGE BETWEEN :param_1 PRECEDING AND UNBOUNDED FOLLOWING)
over.range_
和 over.rows
被指定为 2 元组,表示特定范围的负值和正值,“CURRENT ROW” 为 0,UNBOUNDED 为 None。
另请参阅
使用窗口函数
#3049 ### 支持 SQL LATERAL
关键字
LATERAL
关键字目前仅被 PostgreSQL 9.3 及更高版本支持,然而由于它是 SQL 标准的一部分,对于该关键字的支持已经添加到 Core 中。Select.lateral()
的实现采用了特殊逻辑,不仅仅是渲染 LATERAL
关键字,还允许对从相同 FROM
子句派生的表进行相关性,例如,侧向相关性:
>>> from sqlalchemy import table, column, select, true >>> people = table("people", column("people_id"), column("age"), column("name")) >>> books = table("books", column("book_id"), column("owner_id")) >>> subq = ( ... select([books.c.book_id]) ... .where(books.c.owner_id == people.c.people_id) ... .lateral("book_subq") ... ) >>> print(select([people]).select_from(people.join(subq, true()))) SELECT people.people_id, people.age, people.name FROM people JOIN LATERAL (SELECT books.book_id AS book_id FROM books WHERE books.owner_id = people.people_id) AS book_subq ON true
另请参阅
侧向相关性
Lateral
Select.lateral()
#2857 ### 支持 TABLESAMPLE
SQL 标准的 TABLESAMPLE 可以使用 FromClause.tablesample()
方法呈现,该方法返回一个类似于别名的 TableSample
构造。
from sqlalchemy import func selectable = people.tablesample(func.bernoulli(1), name="alias", seed=func.random()) stmt = select([selectable.c.people_id])
假设 people
有一个列 people_id
,上述语句将渲染为:
SELECT alias.people_id FROM people AS alias TABLESAMPLE bernoulli(:bernoulli_1) REPEATABLE (random())
#3718 ### 对于复合主键列,.autoincrement
指令不再隐式启用
SQLAlchemy 一直具有为单列整数主键启用后端数据库的“自增”功能的便利特性;通过“自增”,我们指的是数据库列将包括数据库提供的任何 DDL 指令,以指示自增整数标识符,例如 PostgreSQL 上的 SERIAL 关键字或 MySQL 上的 AUTO_INCREMENT,并且方言还将通过使用适合该后端的技术从执行 Table.insert()
构造中接收这些生成的值。
发生变化的是,这个特性不再自动为复合主键打开;以前,表定义如下:
Table( "some_table", metadata, Column("x", Integer, primary_key=True), Column("y", Integer, primary_key=True), )
只有因为它是主键列列表中的第一个列,才会对'x'
列应用autoincrement
语义。为了禁用这个,必须关闭所有列上的autoincrement
:
# old way Table( "some_table", metadata, Column("x", Integer, primary_key=True, autoincrement=False), Column("y", Integer, primary_key=True, autoincrement=False), )
有了新的行为,组合主键将不具有自动增量语义,除非列明确标记为autoincrement=True
:
# column 'y' will be SERIAL/AUTO_INCREMENT/ auto-generating Table( "some_table", metadata, Column("x", Integer, primary_key=True), Column("y", Integer, primary_key=True, autoincrement=True), )
为了预见一些潜在的不兼容情况,Table.insert()
构造将对没有设置自动增量的复合主键列上缺失的主键值执行更彻底的检查;给定这样一个表:
Table( "b", metadata, Column("x", Integer, primary_key=True), Column("y", Integer, primary_key=True), )
对于这个表没有提供值的 INSERT 将产生此警告:
SAWarning: Column 'b.x' is marked as a member of the primary key for table 'b', but has no Python-side or server-side default generator indicated, nor does it indicate 'autoincrement=True', and no explicit value is passed. Primary key columns may not store NULL. Note that as of SQLAlchemy 1.1, 'autoincrement=True' must be indicated explicitly for composite (e.g. multicolumn) primary keys if AUTO_INCREMENT/SERIAL/IDENTITY behavior is expected for one of the columns in the primary key. CREATE TABLE statements are impacted by this change as well on most backends.
对于从服务器端默认值或触发器等不太常见的东西接收主键值的列,可以使用FetchedValue
指示存在值生成器:
Table( "b", metadata, Column("x", Integer, primary_key=True, server_default=FetchedValue()), Column("y", Integer, primary_key=True, server_default=FetchedValue()), )
对于极少情况下实际上意图在其列中存储 NULL 的复合主键(仅在 SQLite 和 MySQL 上支持),请使用nullable=True
指定列:
Table( "b", metadata, Column("x", Integer, primary_key=True), Column("y", Integer, primary_key=True, nullable=True), )
在相关更改中,autoincrement
标志可以设置为 True,对于具有客户端或服务器端默认值的列。这通常不会对 INSERT 期间列的行为产生太大影响。
另请参见
不再为具有 AUTO_INCREMENT 的复合主键生成隐式 KEY
#3216 ### 支持 IS DISTINCT FROM 和 IS NOT DISTINCT FROM
新操作符ColumnOperators.is_distinct_from()
和ColumnOperators.isnot_distinct_from()
允许进行 IS DISTINCT FROM 和 IS NOT DISTINCT FROM sql 操作:
>>> print(column("x").is_distinct_from(None)) x IS DISTINCT FROM NULL
处理 NULL、True 和 False:
>>> print(column("x").isnot_distinct_from(False)) x IS NOT DISTINCT FROM false
对于 SQLite,它没有这个运算符,“IS” / “IS NOT”会被渲染,在 SQLite 上可以处理 NULL,不像其他后端:
>>> from sqlalchemy.dialects import sqlite >>> print(column("x").is_distinct_from(None).compile(dialect=sqlite.dialect())) x IS NOT NULL ```### 核心和 ORM 对 FULL OUTER JOIN 的支持 新标志`FromClause.outerjoin.full`,在核心和 ORM 级别可用,指示编译器在通常渲染`LEFT OUTER JOIN`的地方渲染`FULL OUTER JOIN`: ```py stmt = select([t1]).select_from(t1.outerjoin(t2, full=True))
该标志也适用于 ORM 级别:
q = session.query(MyClass).outerjoin(MyOtherClass, full=True)
#1957 ### ResultSet 列匹配增强;文本 SQL 的位置列设置
在 1.0 系列中对ResultProxy
系统进行了一系列改进,作为#918的一部分,重新组织内部以按位置匹配游标绑定的结果列与表/ORM 元数据,而不是通过匹配名称,用于包含有关要返回的结果行的完整信息的编译 SQL 构造。这样可以大大节省 Python 的开销,并且在将 ORM 和 Core SQL 表达式链接到结果行时具有更高的准确性。在 1.1 版本中,这种重新组织在内部进一步进行了,并且还通过最近添加的TextClause.columns()
方法对纯文本 SQL 构造进行了提供。
TextAsFrom.columns() 现在按位置工作
TextClause.columns()
方法在 0.9 版本中添加,接受基于列的参数按位置传递;在 1.1 版本中,当所有列按位置传递时,这些列与最终结果集的关联也按位置执行。这里的关键优势在于,现在可以将文本 SQL 与 ORM 级别的结果集链接起来,而无需处理模糊或重复的列名,也无需将标签方案与 ORM 级别的标签方案匹配。现在只需要在文本 SQL 中的列顺序与传递给TextClause.columns()
的列参数中保持一致即可:
from sqlalchemy import text stmt = text( "SELECT users.id, addresses.id, users.id, " "users.name, addresses.email_address AS email " "FROM users JOIN addresses ON users.id=addresses.user_id " "WHERE users.id = 1" ).columns(User.id, Address.id, Address.user_id, User.name, Address.email_address) query = session.query(User).from_statement(stmt).options(contains_eager(User.addresses)) result = query.all()
在上面的文本 SQL 中,“id”列出现了三次,这通常会产生歧义。使用新功能,我们可以直接应用来自User
和Address
类的映射列,甚至可以将Address.user_id
列链接到文本 SQL 中的users.id
列,以供娱乐,Query
对象将按需接收正确可定位的行,包括用于急加载。
这个改变与将列按照与文本语句中不同顺序传递给方法的代码不兼容。希望由于这个方法一直以来都是按照文本 SQL 语句中列的顺序传递的,即使内部没有检查这一点,因此影响会很小。这个方法只是在 0.9 版本中添加的,可能还没有广泛使用。关于如何处理使用该方法的应用程序的行为变化的详细说明,请参见 TextClause.columns() will match columns positionally, not by name, when passed positionally。
另请参见
使用文本列表达式进行选择
当传递位置参数时,TextClause.columns() 将按位置匹配列,而不是按名称匹配 - 向后兼容性说明
对于 Core/ORM SQL 构造,基于位置的匹配比基于名称的匹配更可靠
这一变化的另一个方面是,对于已编译的 SQL 构造,匹配列的规则也已经修改为更完全地依赖于“位置”匹配。考虑到如下语句:
ua = users.alias("ua") stmt = select([users.c.user_id, ua.c.user_id])
上述语句将被编译为:
SELECT users.user_id, ua.user_id FROM users, users AS ua
在 1.0 版中,执行上述语句时,将会根据位置匹配与其原始编译构造相匹配,但是由于语句中包含了重复的 'user_id'
标签,因此“模糊列”规则仍然会介入并阻止从行中提取列。从 1.1 版开始,“模糊列”规则不会影响从列构造到 SQL 列的精确匹配,这是 ORM 用于获取列的方法:
result = conn.execute(stmt) row = result.first() # these both match positionally, so no error user_id = row[users.c.user_id] ua_id = row[ua.c.user_id] # this still raises, however user_id = row["user_id"]
很少出现“模糊列”错误消息
作为这一变化的一部分,错误消息 Ambiguous column name '' in result set! try 'use_labels' option on select statement.
的措辞已经被减弱;因为当使用 ORM 或 Core 编译的 SQL 构造时,这个消息现在应该非常少见,它仅仅陈述了 Ambiguous column name '' in result set column descriptions
,并且只有在从实际上是模糊的字符串名称检索结果列时才会出现,例如在上面的示例中 row['user_id']
。它现在还引用了来自呈现的 SQL 语句本身的实际模糊名称,而不是指示用于获取的构造本地的键或名称。
#3501 ### 支持 Python 的原生 enum
类型和兼容形式
Enum
类型现在可以使用任何符合 PEP-435 的枚举类型构造。在使用此模式时,输入值和返回值是实际的枚举对象,而不是字符串/整数等值:
import enum from sqlalchemy import Table, MetaData, Column, Enum, create_engine class MyEnum(enum.Enum): one = 1 two = 2 three = 3 t = Table("data", MetaData(), Column("value", Enum(MyEnum))) e = create_engine("sqlite://") t.create(e) e.execute(t.insert(), {"value": MyEnum.two}) assert e.scalar(t.select()) is MyEnum.two
Enum.enums
集合现在是列表,而不是元组
作为对 Enum
的更改的一部分,元素的 Enum.enums
集合现在是列表,而不是元组。这是因为列表适用于长度可变的同类项序列,其中元素的位置不具有语义上的重要性。
#3292 ### 核心结果行支持负整数索引
RowProxy
对象现在支持单个负整数索引,就像普通的 Python 序列一样,在纯 Python 和 C 扩展版本中都可以使用。之前,负值只能在切片中起作用:
【PRE60】### 现在,Enum
类型在 Python 中对值进行验证
为了适应 Python 本地枚举对象,以及诸如在 ARRAY 中使用非本地 ENUM 类型且 CHECK 约束不可行等边缘情况,当使用Enum.validate_strings
标志时,Enum
数据类型现在在 Python 中对输入值进行验证(1.1.0b2):
>>> from sqlalchemy import Table, MetaData, Column, Enum, create_engine >>> t = Table( ... "data", ... MetaData(), ... Column("value", Enum("one", "two", "three", validate_strings=True)), ... ) >>> e = create_engine("sqlite://") >>> t.create(e) >>> e.execute(t.insert(), {"value": "four"}) Traceback (most recent call last): ... sqlalchemy.exc.StatementError: (exceptions.LookupError) "four" is not among the defined enum values [SQL: u'INSERT INTO data (value) VALUES (?)'] [parameters: [{'value': 'four'}]]
默认情况下关闭此验证,因为已经确定了用户不希望进行此类验证的用例(例如字符串比较)。对于非字符串类型,它在所有情况下都必须进行。当从数据库返回值时,结果处理方面也无条件地进行检查。
此验证是在使用非本地枚举类型时创建 CHECK 约束的现有行为之外的。现在可以使用新的Enum.create_constraint
标志来禁用此 CHECK 约束的创建。
#3095 ### 非本地布尔整数值在所有情况下被强制为零/一/None
Boolean
数据类型将 Python 布尔值强制转换为整数值,用于没有本地布尔类型的后端,例如 SQLite 和 MySQL。在这些后端上,通常设置一个 CHECK 约束,以确保数据库中的值实际上是这两个值之一。但是,MySQL 忽略 CHECK 约束,该约束是可选的,并且现有数据库可能没有此约束。已修复Boolean
数据类型,使得已经是整数值的 Python 端值被强制转换为零或一,而不仅仅是传递原样;此外,结果的 int-to-boolean 处理器的 C 扩展版本现在使用与 Python 布尔值解释相同的值,而不是断言确切的一或零值。这现在与纯 Python 的 int-to-boolean 处理器一致,并且对数据库中已有的数据更宽容。None/NULL 的值与以前一样保留为 None/NULL。
注意
这个改变产生了一个意外的副作用,即非整数值(如字符串)的解释也发生了变化,例如字符串值"0"
将被解释为“true”,但仅在没有本地布尔数据类型的后端上 - 在像 PostgreSQL 这样的“本地布尔”后端上,字符串值"0"
将直接传递给驱动程序,并被解释为“false”。这是一个之前实现中没有发生的不一致性。值得注意的是,将字符串或任何其他值传递给Boolean
数据类型之外的None
、True
、False
、1
、0
是不受支持的,版本 1.2 将对此场景引发错误(或可能���是发出警告,待定)。另请参阅#4102。
#3730 ### 在日志和异常显示中现在截断了大参数和行值
SQL 语句中作为绑定参数的大值,以及结果行中存在的大值,现在在日志记录、异常报告以及repr()
中的显示时将被截断:
>>> from sqlalchemy import create_engine >>> import random >>> e = create_engine("sqlite://", echo="debug") >>> some_value = "".join(chr(random.randint(52, 85)) for i in range(5000)) >>> row = e.execute("select ?", [some_value]).first() ... # (lines are wrapped for clarity) ... 2016-02-17 13:23:03,027 INFO sqlalchemy.engine.base.Engine select ? 2016-02-17 13:23:03,027 INFO sqlalchemy.engine.base.Engine ('E6@?>9HPOJB<<BHR:@=TS:5ILU=;JLM<4?B9<S48PTNG9>:=TSTLA;9K;9FPM4M8M@;NM6GU LUAEBT9QGHNHTHR5EP75@OER4?SKC;D:TFUMD:M>;C6U:JLM6R67GEK<A6@S@C@J7>4=4:P GJ7HQ6 ... (4702 characters truncated) ... J6IK546AJMB4N6S9L;;9AKI;=RJP HDSSOTNBUEEC9@Q:RCL:I@5?FO<9K>KJAGAO@E6@A7JI8O:J7B69T6<8;F:S;4BEIJS9HM K:;5OLPM@JR;R:J6<SOTTT=>Q>7T@I::OTDC:CC<=NGP6C>BC8N',) 2016-02-17 13:23:03,027 DEBUG sqlalchemy.engine.base.Engine Col ('?',) 2016-02-17 13:23:03,027 DEBUG sqlalchemy.engine.base.Engine Row (u'E6@?>9HPOJB<<BHR:@=TS:5ILU=;JLM<4?B9<S48PTNG9>:=TSTLA;9K;9FPM4M8M@; NM6GULUAEBT9QGHNHTHR5EP75@OER4?SKC;D:TFUMD:M>;C6U:JLM6R67GEK<A6@S@C@J7 >4=4:PGJ7HQ ... (4703 characters truncated) ... J6IK546AJMB4N6S9L;;9AKI;= RJPHDSSOTNBUEEC9@Q:RCL:I@5?FO<9K>KJAGAO@E6@A7JI8O:J7B69T6<8;F:S;4BEIJS9HM K:;5OLPM@JR;R:J6<SOTTT=>Q>7T@I::OTDC:CC<=NGP6C>BC8N',) >>> print(row) (u'E6@?>9HPOJB<<BHR:@=TS:5ILU=;JLM<4?B9<S48PTNG9>:=TSTLA;9K;9FPM4M8M@;NM6 GULUAEBT9QGHNHTHR5EP75@OER4?SKC;D:TFUMD:M>;C6U:JLM6R67GEK<A6@S@C@J7>4 =4:PGJ7HQ ... (4703 characters truncated) ... J6IK546AJMB4N6S9L;;9AKI; =RJPHDSSOTNBUEEC9@Q:RCL:I@5?FO<9K>KJAGAO@E6@A7JI8O:J7B69T6<8;F:S;4BEIJS9H MK:;5OLPM@JR;R:J6<SOTTT=>Q>7T@I::OTDC:CC<=NGP6C>BC8N',)
#2837 ### 核心中添加了 JSON 支持
由于 MySQL 现在除了 PostgreSQL JSON 数据类型外还有 JSON 数据类型,核心现在获得了一个sqlalchemy.types.JSON
数据类型,这是这两种数据类型的基础。使用这种类型允许以一种跨越 PostgreSQL 和 MySQL 的方式访问“getitem”操作符和“getpath”操作符。
新数据类型还对 NULL 值的处理以及表达式处理进行了一系列改进。
另请参阅
MySQL JSON 支持
JSON
JSON
JSON
JSON “null” 在 ORM 操作中被插入,当不存在时被省略
JSON
类型及其派生类型JSON
和JSON
有一个标志JSON.none_as_null
,当设置为 True 时,表示 Python 值None
应该转换为 SQL NULL 而不是 JSON NULL 值。该标志默认为 False,这意味着 Python 值None
应该导致 JSON NULL 值。
在以下情况下,此逻辑将失败,并已得到纠正:
1. 当列还包含默认值或 server_default 值时,对于期望持久化 JSON “null”的映射属性上的正值 None
仍将触发列级默认值,替换 None
值:
class MyObject(Base): # ... json_value = Column(JSON(none_as_null=False), default="some default") # would insert "some default" instead of "'null'", # now will insert "'null'" obj = MyObject(json_value=None) session.add(obj) session.commit()
2. 当列没有包含默认值或 server_default 值时,对于配置了 none_as_null=False 的 JSON 列的缺失值仍然会呈现为 JSON NULL,而不是回退到不插入任何值,与所有其他数据类型的行为不一致:
class MyObject(Base): # ... some_other_value = Column(String(50)) json_value = Column(JSON(none_as_null=False)) # would result in NULL for some_other_value, # but json "'null'" for json_value. Now results in NULL for both # (the json_value is omitted from the INSERT) obj = MyObject() session.add(obj) session.commit()
这是一个行为变更,对于依赖此功能将缺失值默认为 JSON null 的应用程序来说是不兼容的。这实际上确立了缺失值与存在的 None 值有所区别。详细信息请参见如果未提供值且未设置默认值,则 JSON 列将不插入 JSON NULL。
3. 当使用 Session.bulk_insert_mappings()
方法时,None
在所有情况下都会被忽略:
# would insert SQL NULL and/or trigger defaults, # now inserts "'null'" session.bulk_insert_mappings(MyObject, [{"json_value": None}])
JSON
类型现在实现了 TypeEngine.should_evaluate_none
标志,指示此处不应忽略 None
;它会根据 JSON.none_as_null
的值自动配置。感谢 #3061,我们可以区分用户主动设置的值 None
与根本未设置的情况。
该功能同样适用于新的基础 JSON
类型及其派生类型。
#3514 #### 新增 JSON.NULL 常量
为了确保应用程序始终可以在值级别上完全控制一个 JSON
、JSON
、JSON
或 JSONB
列是否接收 SQL NULL 或 JSON "null"
值,已添加了常量 JSON.NULL
,它与 null()
结合使用可以完全确定 SQL NULL 和 JSON "null"
之间的差别,无论 JSON.none_as_null
设置为什么:
from sqlalchemy import null from sqlalchemy.dialects.postgresql import JSON obj1 = MyObject(json_value=null()) # will *always* insert SQL NULL obj2 = MyObject(json_value=JSON.NULL) # will *always* insert JSON string "null" session.add_all([obj1, obj2]) session.commit()
此功能同样适用于新的基础 JSON
类型及其后代类型。
#3514 ### Core 添加了数组支持;新的 ANY 和 ALL 运算符
随着对 PostgreSQL ARRAY
类型的增强描述在 Correct SQL Types are Established from Indexed Access of ARRAY, JSON, HSTORE 中,ARRAY
类型的基类本身已经移动到 Core 中,成为一个新的类 ARRAY
。
数组是 SQL 标准的一部分,还有一些面向数组的函数,如 array_agg()
和 unnest()
。为了支持这些构造,不仅仅是 PostgreSQL,未来可能还包括其他支持数组的后端,如 DB2,大部分 SQL 表达式的数组逻辑现在都在 Core 中。然而,ARRAY
类型仍只在 PostgreSQL 上工作,但它可以直接使用,支持特殊的数组用例,如索引访问,以及支持 ANY 和 ALL:
mytable = Table("mytable", metadata, Column("data", ARRAY(Integer, dimensions=2))) expr = mytable.c.data[5][6] expr = mytable.c.data[5].any(12)
为了支持 ANY 和 ALL,ARRAY
类型保留了与 PostgreSQL 类型相同的Comparator.any()
和Comparator.all()
方法,但也将这些操作导出为新的独立运算符函数any_()
和all_()
。这两个函数以更传统的 SQL 方式工作,允许右侧表达式形式,如下:
from sqlalchemy import any_, all_ select([mytable]).where(12 == any_(mytable.c.data[5]))
对于 PostgreSQL 特定的运算符“contains”、“contained_by”和“overlaps”,应继续直接使用ARRAY
类型,该类型还提供了ARRAY
类型的所有功能。
any_()
和all_()
运算符在核心级别是开放式的,但是后端数据库对它们的解释是有限的。在 PostgreSQL 后端,这两个运算符只接受数组值。而在 MySQL 后端,它们只接受子查询值。在 MySQL 中,可以使用如下表达式:
from sqlalchemy import any_, all_ subq = select([mytable.c.value]) select([mytable]).where(12 > any_(subq))
#3516 ### 新功能特性,“WITHIN GROUP”,array_agg 和 set 聚合函数
使用新的ARRAY
类型,我们还可以实现一个预定义函数,用于返回数组的array_agg()
SQL 函数,现在可以使用array_agg
:
from sqlalchemy import func stmt = select([func.array_agg(table.c.value)])
通过aggregate_order_by
,为聚合 ORDER BY 添加了一个 PostgreSQL 元素:
from sqlalchemy.dialects.postgresql import aggregate_order_by expr = func.array_agg(aggregate_order_by(table.c.a, table.c.b.desc())) stmt = select([expr])
生成:
SELECT array_agg(table1.a ORDER BY table1.b DESC) AS array_agg_1 FROM table1
PG 方言本身还提供了一个array_agg()
包装器,以确保ARRAY
类型:
from sqlalchemy.dialects.postgresql import array_agg stmt = select([array_agg(table.c.value).contains("foo")])
另外,像percentile_cont()
、percentile_disc()
、rank()
、dense_rank()
等需要通过WITHIN GROUP (ORDER BY )
进行排序的函数现在可以通过FunctionElement.within_group()
修饰符来使用:
from sqlalchemy import func stmt = select( [ department.c.id, func.percentile_cont(0.5).within_group(department.c.salary.desc()), ] )
上述语句将生成类似于的 SQL:
SELECT department.id, percentile_cont(0.5) WITHIN GROUP (ORDER BY department.salary DESC)
现在为这些函数提供了正确返回类型的占位符,包括percentile_cont
、percentile_disc
、rank
、dense_rank
、mode
、percent_rank
和cume_dist
。
#3132 #1370 ### TypeDecorator 现在自动与 Enum、Boolean、“schema” 类型配合工作
SchemaType
类型包括诸如Enum
和Boolean
等类型,除了对应数据库类型外,还会生成一个 CHECK 约束或在 PostgreSQL ENUM 的情况下生成一个新的 CREATE TYPE 语句,现在会自动与TypeDecorator
配方一起工作。以前,对于ENUM
的TypeDecorator
必须像这样:
# old way class MyEnum(TypeDecorator, SchemaType): impl = postgresql.ENUM("one", "two", "three", name="myenum") def _set_table(self, table): self.impl._set_table(table)
TypeDecorator
现在会传播这些额外的事件,因此可以像处理其他类型一样处理:
# new way class MyEnum(TypeDecorator): impl = postgresql.ENUM("one", "two", "three", name="myenum")
#2919 ### 多租户模式下的表对象翻译
为了支持一个应用程序使用相同一组Table
对象在许多模式中的用例,比如每个用户一个模式,添加了一个新的执行选项Connection.execution_options.schema_translate_map
。 使用这个映射,一组Table
对象可以在每个连接基础上指向任何一组模式,而不是它们被分配到的Table.schema
。 翻译适用于 DDL 和 SQL 生成,以及 ORM。
例如,如果User
类被分配了模式“per_user”:
class User(Base): __tablename__ = "user" id = Column(Integer, primary_key=True) __table_args__ = {"schema": "per_user"}
在每个请求上,Session
可以设置为每次引用不同的模式:
session = Session() session.connection( execution_options={"schema_translate_map": {"per_user": "account_one"}} ) # will query from the ``account_one.user`` table session.query(User).get(5)
另请参见
模式名称的翻译
#2685 ### Core SQL 构造的“友好”字符串化,没有方言
对核心 SQL 构造调用str()
现在会在更多情况下生成一个字符串,支持各种通常不在默认 SQL 中出现的 SQL 构造,如 RETURNING、数组索引和非标准数据类型:
>>> from sqlalchemy import table, column t>>> t = table('x', column('a'), column('b')) >>> print(t.insert().returning(t.c.a, t.c.b)) INSERT INTO x (a, b) VALUES (:a, :b) RETURNING x.a, x.b
str()
函数现在调用一个完全独立的方言/编译器,专门用于纯字符串打印,没有设置特定方言,因此当出现更多“只是显示给��一个字符串!”的情况时,可以将这些情况添加到这个方言/编译器中,而不会影响真实方言上的行为。
另请参见
查询的字符串化将查询会话以获取正确的方言
#3631 ### type_coerce 函数现在是一个持久的 SQL 元素
type_coerce()
函数以前会返回一个BindParameter
或 Label
类型的对象,取决于输入。 这样做的一个影响是,在使用表达式转换的情况下,比如将元素从Column
转换为BindParameter
,这对于 ORM 级别的延迟加载至关重要,类型强制转换信息将不会被使用,因为它已经丢失了。
为了改进这种行为,该函数现在返回一个围绕给定表达式的持久TypeCoerce
容器,该表达式本身保持不变;这个构造显式地由 SQL 编译器评估。这允许内部表达式的强制转换保持不变,无论语句如何修改,包括如果包含的元素被替换为不同的元素,这在 ORM 的延迟加载功能中很常见。
用于说明效果的测试用例利用了异构主连接条件与自定义类型和延迟加载。给定一个应用 CAST 作为“绑定表达式”的自定义类型:
class StringAsInt(TypeDecorator): impl = String def column_expression(self, col): return cast(col, Integer) def bind_expression(self, value): return cast(value, String)
然后,一个映射,我们将一个表上的字符串“id”列与另一个表上的整数“id”列进行等价:
class Person(Base): __tablename__ = "person" id = Column(StringAsInt, primary_key=True) pets = relationship( "Pets", primaryjoin=( "foreign(Pets.person_id)==cast(type_coerce(Person.id, Integer), Integer)" ), ) class Pets(Base): __tablename__ = "pets" id = Column("id", Integer, primary_key=True) person_id = Column("person_id", Integer)
在relationship.primaryjoin
表达式中,我们使用type_coerce()
来处理通过延迟加载传递的绑定参数作为整数,因为我们已经知道这些将来自我们的StringAsInt
类型,该类型在 Python 中将值保持为整数。然后我们使用cast()
,以便作为 SQL 表达式,VARCHAR“id”列将被 CAST 为整数,用于常规非转换连接,如Query.join()
或joinedload()
。也就是说,.pets
的 joinedload 看起来像:
SELECT person.id AS person_id, pets_1.id AS pets_1_id, pets_1.person_id AS pets_1_person_id FROM person LEFT OUTER JOIN pets AS pets_1 ON pets_1.person_id = CAST(person.id AS INTEGER)
在连接的 ON 子句中没有 CAST,像 PostgreSQL 这样的强类型数据库将拒绝隐式比较整数并失败。
.pets
的延迟加载情况依赖于在加载时用一个绑定参数替换Person.id
列,该参数接收一个 Python 加载的值。这种替换特别是我们type_coerce()
函数意图会丢失的地方。在更改之前,这种延迟加载如下所示:
SELECT pets.id AS pets_id, pets.person_id AS pets_person_id FROM pets WHERE pets.person_id = CAST(CAST(%(param_1)s AS VARCHAR) AS INTEGER) -- {'param_1': 5}
在上面的例子中,我们看到我们在 Python 中的值5
首先被转换为 VARCHAR,然后再转换回 SQL 中的 INTEGER;这是一个双重转换,虽然有效,但并不是我们要求的。
随着更改,type_coerce()
函数在列被替换为绑定参数后仍保持一个包装器,现在查询看起来像:
SELECT pets.id AS pets_id, pets.person_id AS pets_person_id FROM pets WHERE pets.person_id = CAST(%(param_1)s AS INTEGER) -- {'param_1': 5}
我们的外部 CAST,即我们的主要连接,仍然生效,但是在 StringAsInt
自定义类型的一部分中不必要的 CAST 被 type_coerce()
函数按预期移除了。
键行为变化 - ORM
JSON 列如果没有提供值且没有默认值,则不会插入 JSON NULL
如 JSON “null” is inserted as expected with ORM operations, omitted when not present 中所述,JSON
如果完全缺少值,则不会渲染 JSON “null” 值。为了防止 SQL NULL,应设置默认值。给定以下映射:
class MyObject(Base): # ... json_value = Column(JSON(none_as_null=False), nullable=False)
以下刷新操作将失败并引发完整性错误:
obj = MyObject() # note no json_value session.add(obj) session.commit() # will fail with integrity error
如果列的默认值应为 JSON NULL,则在列上设置它:
class MyObject(Base): # ... json_value = Column(JSON(none_as_null=False), nullable=False, default=JSON.NULL)
或者,确保对象上存在该值:
obj = MyObject(json_value=None) session.add(obj) session.commit() # will insert JSON NULL
请注意,为默认值设置 None
与完全省略它是相同的;JSON.none_as_null
标志不影响传递给 Column.default
或 Column.server_default
的 None
的值:
# default=None is the same as omitting it entirely, does not apply JSON NULL json_value = Column(JSON(none_as_null=False), nullable=False, default=None)
另请参阅
JSON “null” is inserted as expected with ORM operations, omitted when not present ### DISTINCT + ORDER BY 不再冗余添加列
如下查询现在仅增广缺少于 SELECT 列表中的列,而不会出现重复:
q = ( session.query(User.id, User.name.label("name")) .distinct() .order_by(User.id, User.name, User.fullname) )
产生:
SELECT DISTINCT user.id AS a_id, user.name AS name, user.fullname AS a_fullname FROM a ORDER BY user.id, user.name, user.fullname
以前,它会产生:
SELECT DISTINCT user.id AS a_id, user.name AS name, user.name AS a_name, user.fullname AS a_fullname FROM a ORDER BY user.id, user.name, user.fullname
在上述情况下,不必要地添加了 user.name
列。结果不会受影响,因为额外的列无论如何都不包含在结果中,但是这些列是不必要的。
另外,当通过向 Query.distinct()
传递表达式来使用 PostgreSQL DISTINCT ON 格式时,上述“添加列”逻辑将被完全禁用。
当查询被捆绑成子查询以进行连接式快速加载时,“增广列列表”规则必须更加积极,以便仍然可以满足 ORDER BY,因此这种情况保持不变。
#3641 ### 同名的 @validates 装饰器现在会引发异常
validates()
装饰器仅打算对于特定属性名称的类创建一次。现在创建多于一个会引发错误,而以前则会静默选择最后定义的验证器:
class A(Base): __tablename__ = "a" id = Column(Integer, primary_key=True) data = Column(String) @validates("data") def _validate_data_one(self): assert "x" in data @validates("data") def _validate_data_two(self): assert "y" in data configure_mappers()
将引发:
sqlalchemy.exc.InvalidRequestError: A validation function for mapped attribute 'data' on mapper Mapper|A|a already exists.
关键行为变化 - 核心
当通过位置传递时,TextClause.columns()将按位置匹配列,而不是按名称匹配
TextClause.columns()
方法的新行为,该方法本身是最近在 0.9 系列中添加的,是,当列通过位置传递而没有任何额外的关键字参数时,它们与最终结果集列位置相关联,而不再根据名称。希望这种变化的影响很小,因为该方法始终以文档形式说明传递的列与文本 SQL 语句的顺序相同,这似乎是直观的,即使内部部件不检查这一点也是如此。
使用此方法通过位置传递Column
对象的应用程序必须确保这些Column
对象的位置与文本 SQL 中这些列声明的位置相匹配。
例如,像下面这样的代码:
stmt = text("SELECT id, name, description FROM table") # no longer matches by name stmt = stmt.columns(my_table.c.name, my_table.c.description, my_table.c.id)
将不再按预期工作;给定列的顺序现在很重要:
# correct version stmt = stmt.columns(my_table.c.id, my_table.c.name, my_table.c.description)
可能更有可能的是,一个像这样工作的声明:
stmt = text("SELECT * FROM table") stmt = stmt.columns(my_table.c.id, my_table.c.name, my_table.c.description)
现在稍微有点冒险,因为“*”规范通常会按照它们在表中出现的顺序传递列。如果表的结构因模式更改而更改,则此排序可能不再相同。因此,在使用TextClause.columns()
时,建议在文本 SQL 中明确列出所需的列,尽管在文本 SQL 中不再需要担心列名本身。
另请参见
ResultSet 列匹配增强;文本 SQL 的位置列设置 ### 字符串 server_default 现在是文字引用
作为普通 Python 字符串传递给Column.server_default
的服务器默认值现在通过字面引用系统传递:
>>> from sqlalchemy.schema import MetaData, Table, Column, CreateTable >>> from sqlalchemy.types import String >>> t = Table("t", MetaData(), Column("x", String(), server_default="hi ' there")) >>> print(CreateTable(t)) CREATE TABLE t ( x VARCHAR DEFAULT 'hi '' there' )
以前,引用将直接呈现。对于具有此类用例并且正在解决此问题的应用程序,此更改可能是向后不兼容的。
#3809 ### 一个带有 LIMIT/OFFSET/ORDER BY 的 UNION 或类似的 SELECT 现在将嵌入的 SELECT 括起来
一个问题,像其他问题一样,长期以来受 SQLite 功能不足的驱动,现在已经增强,可以在所有支持的后端上工作。我们指的是一个查询,它是 SELECT 语句的 UNION,这些语句本身包含行限制或排序功能,其中包括 LIMIT、OFFSET 和/或 ORDER BY:
(SELECT x FROM table1 ORDER BY y LIMIT 1) UNION (SELECT x FROM table2 ORDER BY y LIMIT 2)
上述查询需要在每个子选择中使用括号以正确分组子结果。在 SQLAlchemy Core 中生成上述语句的方式如下:
stmt1 = select([table1.c.x]).order_by(table1.c.y).limit(1) stmt2 = select([table1.c.x]).order_by(table2.c.y).limit(2) stmt = union(stmt1, stmt2)
以前,上述结构不会为内部的 SELECT 语句生成括号,导致查询在所有后端上都失败。
上述格式在 SQLite 上仍将失败;此外,包含 ORDER BY 但不包含 LIMIT/SELECT 的格式在 Oracle 上仍将失败。这不是一个不兼容的更改,因为没有括号的查询也会失败;通过修复,查询至少在所有其他数据库上能够正常工作。
在所有情况下,为了生成一个在 SQLite 上也能正常工作并且在所有情况下在 Oracle 上也能正常工作的有限 SELECT 语句的 UNION,子查询必须是一个 ALIAS 的 SELECT:
stmt1 = select([table1.c.x]).order_by(table1.c.y).limit(1).alias().select() stmt2 = select([table2.c.x]).order_by(table2.c.y).limit(2).alias().select() stmt = union(stmt1, stmt2)
这个解决方法适用于所有 SQLAlchemy 版本。在 ORM 中,它看起来像:
stmt1 = session.query(Model1).order_by(Model1.y).limit(1).subquery().select() stmt2 = session.query(Model2).order_by(Model2.y).limit(1).subquery().select() stmt = session.query(Model1).from_statement(stmt1.union(stmt2))
这里的行为与 SQLAlchemy 0.9 中引入的“连接重写”行为有许多相似之处,详见许多 JOIN 和 LEFT OUTER JOIN 表达式将不再被包装在(SELECT * FROM …)AS ANON_1 中;然而,在这种情况下,我们选择不添加新的重写行为来适应 SQLite 的情况。现有的重写行为已经非常复杂,而带有括号的 SELECT 语句的 UNION 情况比该功能的“右嵌套连接”用例要少得多。
方言改进和更改 - PostgreSQL
支持 INSERT…ON CONFLICT(DO UPDATE | DO NOTHING)
从 PostgreSQL 9.5 版本开始添加到INSERT
的ON CONFLICT
子句现在可以使用sqlalchemy.dialects.postgresql.dml.insert()
的 PostgreSQL 特定版本的Insert
对象来支持。这个Insert
子类添加了两个新方法Insert.on_conflict_do_update()
和Insert.on_conflict_do_nothing()
,它们实现了 PostgreSQL 9.5 在这个领域支持的完整语法:
from sqlalchemy.dialects.postgresql import insert insert_stmt = insert(my_table).values(id="some_id", data="some data to insert") do_update_stmt = insert_stmt.on_conflict_do_update( index_elements=[my_table.c.id], set_=dict(data="some data to update") ) conn.execute(do_update_stmt)
上述将呈现:
INSERT INTO my_table (id, data) VALUES (:id, :data) ON CONFLICT id DO UPDATE SET data=:data_2
另请参阅
INSERT…ON CONFLICT(Upsert)
#3529 ### ARRAY 和 JSON 类型现在正确指定为“不可哈希”
如关于“不可哈希”类型的更改,影响 ORM 行的去重所述,当查询的选定实体混合了完整的 ORM 实体和列表达式时,ORM 依赖于能够为列值生成哈希函数。现在,对于 PG 的所有“数据结构”类型,包括ARRAY
和JSON
,hashable=False
标志已正确设置。JSONB
和HSTORE
类型已经包含了这个标志。对于ARRAY
,这取决于ARRAY.as_tuple
标志,但是现在不再需要设置此标志以使数组值出现在组合的 ORM 行中。
另请参阅
关于“不可哈希”类型的更改,影响 ORM 行的去重
从 ARRAY、JSON、HSTORE 的索引访问中建立正确的 SQL 类型
#3499 ### 从 ARRAY、JSON、HSTORE 的索引访问中建立正确的 SQL 类型
对于ARRAY
、JSON
和HSTORE
三者,通过索引访问返回的表达式的 SQL 类型,例如col[someindex]
,在所有情况下都应正确。
这包括:
- 对于通过索引访问
ARRAY
的 SQL 类型将考虑配置的维度数量。具有三个维度的ARRAY
将返回一个维度少一的ARRAY
类型的 SQL 表达式。给定类型为ARRAY(Integer, dimensions=3)
的列,我们现在可以执行以下表达式:
int_expr = col[5][6][7] # returns an Integer expression object
- 以前,对于
col[5]
的索引访问会返回一个类型为Integer
的表达式,在这种情况下,除非我们使用cast()
或者type_coerce()
,否则我们无法对剩余的维度进行索引访问。 JSON
和JSONB
类型现在反映了 PostgreSQL 本身对于索引访问的处理方式。这意味着对于JSON
或JSONB
类型的所有索引访问都会返回一个表达式,该表达式本身始终是JSON
或JSONB
本身,除非使用了Comparator.astext
修饰符。这意味着无论 JSON 结构的索引访问最终是引用字符串、列表、数字还是其他 JSON 结构,PostgreSQL 始终将其视为 JSON 本身,除非明确以不同方式进行转换。就像ARRAY
类型一样,现在可以直接生成具有多层索引访问的 JSON 表达式:
json_expr = json_col["key1"]["attr1"][5]
- 通过对
HSTORE
进行索引访问返回的“文本”类型,以及通过与Comparator.astext
修饰符结合使用对JSON
和JSONB
进行索引访问返回的“文本”类型现在是可配置的;在这两种情况下,默认为TextClause
,但可以使用JSON.astext_type
或HSTORE.text_type
参数将其设置为用户定义的类型。
另请参见
JSON cast()操作现在需要显式调用.astext
#3499 #3487 ### JSON cast()操作现在需要显式调用.astext
作为从 ARRAY、JSON、HSTORE 的索引访问中正确建立 SQL 类型的更改的一部分,ColumnElement.cast()
操作符在JSON
和JSONB
上不再隐式调用Comparator.astext
修饰符;PostgreSQL 的 JSON/JSONB 类型支持彼此之间的 CAST 操作而无需“astext”方面。
这意味着在大多数情况下,执行此操作的应用程序:
expr = json_col["somekey"].cast(Integer)
现在需要更改为:
expr = json_col["somekey"].astext.cast(Integer) ```### 带有 ENUM 的 ARRAY 现在将发出 ENUM 的 CREATE TYPE 类似以下的表定义现在将按预期发出 CREATE TYPE: ```py enum = Enum( "manager", "place_admin", "carwash_admin", "parking_admin", "service_admin", "tire_admin", "mechanic", "carwasher", "tire_mechanic", name="work_place_roles", ) class WorkPlacement(Base): __tablename__ = "work_placement" id = Column(Integer, primary_key=True) roles = Column(ARRAY(enum)) e = create_engine("postgresql://scott:tiger@localhost/test", echo=True) Base.metadata.create_all(e)
发出:
CREATE TYPE work_place_roles AS ENUM ( 'manager', 'place_admin', 'carwash_admin', 'parking_admin', 'service_admin', 'tire_admin', 'mechanic', 'carwasher', 'tire_mechanic') CREATE TABLE work_placement ( id SERIAL NOT NULL, roles work_place_roles[], PRIMARY KEY (id) )
检查约束现在反映
PostgreSQL 方言现在支持反射 CHECK 约束,方法包括 Inspector.get_check_constraints()
和 Table
反射中的 Table.constraints
集合。
可以单独检查“普通”和“物化”视图
新参数 PGInspector.get_view_names.include
允许指定应返回哪些视图子类型:
from sqlalchemy import inspect insp = inspect(engine) plain_views = insp.get_view_names(include="plain") all_views = insp.get_view_names(include=("plain", "materialized"))
在 Index 中添加了 tablespace 选项
Index
对象现在接受参数 postgresql_tablespace
,以指定 TABLESPACE,与 Table
对象接受的方式相同。
另请参阅
索引存储参数
对 PyGreSQL 的支持
PyGreSQL DBAPI 现在得到支持。
“postgres” 模块已被移除
长期弃用的 sqlalchemy.dialects.postgres
模块已被移除;多年来一直发出警告,项目应该调用 sqlalchemy.dialects.postgresql
。形式为 postgres://
的 Engine URLs 仍将继续运行。
对 FOR UPDATE SKIP LOCKED / FOR NO KEY UPDATE / FOR KEY SHARE 的支持
新参数 GenerativeSelect.with_for_update.skip_locked
和 GenerativeSelect.with_for_update.key_share
在 Core 和 ORM 中都对 PostgreSQL 后端的 “SELECT…FOR UPDATE” 或 “SELECT…FOR SHARE” 查询应用修改:
- 选择 FOR NO KEY UPDATE:
stmt = select([table]).with_for_update(key_share=True)
- 选择 FOR UPDATE SKIP LOCKED:
stmt = select([table]).with_for_update(skip_locked=True)
- 选择 FOR KEY SHARE:
stmt = select([table]).with_for_update(read=True, key_share=True)
方言改进和变更 - MySQL
MySQL JSON 支持
MySQL 方言新增了一个类型 JSON
,支持 MySQL 5.7 新增的 JSON 类型。该类型既提供 JSON 的持久性,又在内部使用 JSON_EXTRACT
函数进行基本的索引访问。通过使用 MySQL 和 PostgreSQL 共同支持的 JSON
数据类型,可以实现跨 MySQL 和 PostgreSQL 的可索引 JSON 列。
另请参阅
Core 添加了 JSON 支持
#3547 ### 添加了对 AUTOCOMMIT“隔离级别”的支持
MySQL 方言现在接受值“AUTOCOMMIT”用于create_engine.isolation_level
和Connection.execution_options.isolation_level
参数:
connection = engine.connect() connection = connection.execution_options(isolation_level="AUTOCOMMIT")
隔离级别利用了大多数 MySQL DBAPI 提供的各种“自动提交”属性。
#3332 ### 不再为带有 AUTO_INCREMENT 的复合主键生成隐式 KEY
MySQL 方言的行为是,如果 InnoDB 表上的复合主键中有 AUTO_INCREMENT 的列不是第一列,则如下所示:
t = Table( "some_table", metadata, Column("x", Integer, primary_key=True, autoincrement=False), Column("y", Integer, primary_key=True, autoincrement=True), mysql_engine="InnoDB", )
将生成 DDL,例如以下内容:
CREATE TABLE some_table ( x INTEGER NOT NULL, y INTEGER NOT NULL AUTO_INCREMENT, PRIMARY KEY (x, y), KEY idx_autoinc_y (y) )ENGINE=InnoDB
请注意上面带有自动生成名称的“KEY”;这是很多年前为了解决 AUTO_INCREMENT 在 InnoDB 上否则会失败而添加到方言中的一个变更。
这种解决方法已被移除,并替换为更好的系统,即在主键中首先声明 AUTO_INCREMENT 列:
CREATE TABLE some_table ( x INTEGER NOT NULL, y INTEGER NOT NULL AUTO_INCREMENT, PRIMARY KEY (y, x) )ENGINE=InnoDB
要显式控制主键列的顺序,请显式使用PrimaryKeyConstraint
构造(1.1.0b2)(以及根据 MySQL 需要为自动递增列添加 KEY),例如:
t = Table( "some_table", metadata, Column("x", Integer, primary_key=True), Column("y", Integer, primary_key=True, autoincrement=True), PrimaryKeyConstraint("x", "y"), UniqueConstraint("y"), mysql_engine="InnoDB", )
除了变更不再为复合主键列隐式启用.autoincrement 指令外,现在更容易指定具有或不具有自动递增的复合主键;Column.autoincrement
现在默认为值"auto"
,不再需要autoincrement=False
指令:
t = Table( "some_table", metadata, Column("x", Integer, primary_key=True), Column("y", Integer, primary_key=True, autoincrement=True), mysql_engine="InnoDB", )
方言改进和变化 - SQLite
SQLite 版本 3.7.16 取消了右嵌套连接的解决方法
在 0.9 版本中,由 Many JOIN and LEFT OUTER JOIN expressions will no longer be wrapped in (SELECT * FROM …) AS ANON_1 引入的功能经历了大量努力,以支持在 SQLite 上重写连接以始终使用子查询以实现“right-nested-join”效果,因为 SQLite 多年来一直不支持此语法。具有讽刺意味的是,在该迁移说明中指出的 SQLite 版本 3.7.15.2 实际上是最后一个实际存在此限制的 SQLite 版本!下一个发布版本是 3.7.16,支持右嵌套连接被悄悄地添加。在 1.1 中,对特定 SQLite 版本和源提交进行了识别,其中进行了此更改(SQLite 的更改日志将其称为“增强查询优化器以利用传递连接约束”,而没有链接到任何问题编号、更改编号或进一步解释),并且当 DBAPI 报告版本 3.7.16 或更高版本生效时,此更改中存在的解决方法现在已经被取消。
#3634 ### SQLite 版本 3.10.0 解决了带点列名的问题
SQLite 方言长期以来一直存在一个问题的解决方法,即数据库驱动程序在某些 SQL 结果集中未报告正确的列名,特别是在使用 UNION 时。解决方法详见 Dotted Column Names,并要求 SQLAlchemy 假定任何带有点的列名实际上是通过这种错误行为传递的tablename.columnname
组合,可以通过sqlite_raw_colnames
执行选项关闭此选项。
从 SQLite 版本 3.10.0 开始,UNION 和其他查询中的 bug 已经修复;就像 Right-nested join workaround lifted for SQLite version 3.7.16 中描述的更改一样,SQLite 的更改日志只将其神秘地标识为“为 sqlite3_module.xBestIndex 方法添加了 colUsed 字段”,但是 SQLAlchemy 对这些带点列名的翻译在此版本中不再需要,因此当检测到版本 3.10.0 或更高版本时会关闭。
总的来说,截至 1.0 系列,SQLAlchemy 的ResultProxy
在为 Core 和 ORM SQL 构造交付结果时,对结果集中的列名依赖要少得多,因此这个问题的重要性在任何情况下已经降低了。
#3633 ### 改进对远程模式的支持
SQLite 方言现在实现了Inspector.get_schema_names()
方法,并且对从远程模式创建和反射的表和索引提供了改进的支持,在 SQLite 中,远程模式是通过ATTACH
语句分配名称的数据库;以前,CREATE INDEX
DDL 对于绑定模式的表无法正常工作,并且Inspector.get_foreign_keys()
方法现在将在结果中指示给定的模式。不支持跨模式外键。### 主键约束名称的反射
SQLite 后端现在利用 SQLite 的“sqlite_master”视图来从原始 DDL 中提取表的主键约束名称,就像最近的 SQLAlchemy 版本中为外键约束所实现的方式一样。
现在反映检查约束
SQLite 方言现在支持在方法Inspector.get_check_constraints()
以及在Table
反射中的Table.constraints
集合中反映检查约束。
ON DELETE 和 ON UPDATE 外键短语现在反映
Inspector
现在将包括 SQLite 方言上的外键约束的 ON DELETE 和 ON UPDATE 短语,并且作为Table
的一部分反映的ForeignKeyConstraint
对象也将指示这些短语。
SqlAlchemy 2.0 中文文档(七十五)(2)https://developer.aliyun.com/article/1562370