SQLAlchemy 1.1 的新特性?

关于本文档

本文档描述了 SQLAlchemy 版本 1.0 和 SQLAlchemy 版本 1.1 之间的更改。

简介

本指南介绍了 SQLAlchemy 版本 1.1 中的新特性,并记录了影响用户将其应用程序从 SQLAlchemy 1.0 系列迁移到 1.1 的更改。

请仔细阅读关于行为变更的部分,了解潜在的向后不兼容的行为更改。

平台 / 安装程序变更

安装现在需要 Setuptools

多年来,SQLAlchemy 的 setup.py 文件一直支持在安装 Setuptools 和不安装 Setuptools 的情况下运行;支持使用纯 Distutils 的 “fallback” 模式。由于现在已经闻所未闻没有 Setuptools 的 Python 环境,并且为了更全面地支持 Setuptools 的特性集,特别是为了支持 py.test 与它的集成以及诸如 “extras” 之类的东西,setup.py 现在完全依赖于 Setuptools。

另请参阅

安装指南

#3489

启用 / 禁用 C 扩展构建只能通过环境变量

只要有可能,C 扩展在安装期间默认构建。要禁用 C 扩展构建,从 SQLAlchemy 0.8.6 / 0.9.4 开始可以使用 DISABLE_SQLALCHEMY_CEXT 环境变量。使用 --without-cextensions 参数的先前方法已被移除,因为它依赖于 setuptools 的已弃用特性。

另请参阅

构建 Cython 扩展

#3500

新特性和改进 - ORM

新的 Session 生命周期事件

Session 长期以来一直支持允许在一定程度上跟踪对象状态更改的事件,包括 SessionEvents.before_attach()SessionEvents.after_attach()SessionEvents.before_flush()对象状态快速入门 中记录了主要对象状态。但是,一直没有专门跟踪对象在这些转换中传递的系统。此外,“deleted” 对象的状态在历史上一直很模糊,因为对象的状态介于 “persistent” 和 “detached” 状态之间。

为了清理这个区域,并使 session 状态转换领域完全透明,添加了一系列新的事件,旨在涵盖对象可能在状态之间转换的每种可能方式,此外,“deleted” 状态在 session 对象状态领域中被赋予了自己的官方状态名称。

新的状态转换事件

现在可以拦截对象所有状态之间的转换,例如 persistentpending 和其他状态,这些转换以 session 级别的事件表示,旨在涵盖特定的转换。当对象移入 Session、移出 Session 甚至当使用 Session.rollback() 回滚事务时发生的所有转换都显式地存在于 SessionEvents 的接口中。

总共有 十个新事件。这些事件的摘要在最新编写的文档部分 对象生命周期事件 中。

新增对象状态 “deleted”,已删除对象不再是 “persistent”

Session 中,对象的 persistent 状态一直被记录为具有有效数据库身份的对象;然而,对于在刷新中删除的对象,它们一直处于灰色地带,它们实际上并未 “detached” 自 Session,因为它们仍然可以在回滚中恢复,但又不是真正的 “persistent”,因为它们的数据库身份已被删除,并且它们不存在于身份映射中。

为了解决这个灰色地带,考虑到新的事件,引入了一个新的对象状态 deleted。此状态存在于 “persistent” 和 “detached” 状态之间。通过 Session.delete() 标记为删除的对象在刷新继续之前仍保持 “persistent” 状态;此时,它将从身份映射中移除,移动到 “deleted” 状态,并调用 SessionEvents.persistent_to_deleted() hook。如果 Session 对象的事务被回滚,则对象将恢复为 persistent;调用 SessionEvents.deleted_to_persistent() 转换。否则,如果 Session 对象的事务被提交,则调用 SessionEvents.deleted_to_detached() 转换。

此外,对于处于新的 “deleted” 状态的对象,InstanceState.persistent 访问器 不再返回 True;相反,InstanceState.deleted 访问器已得到增强,可以可靠地报告此新状态。当对象 detached 时,InstanceState.deleted 返回 False,而 InstanceState.detached 访问器返回 True。要确定对象是在当前事务还是在之前的事务中被删除,请使用 InstanceState.was_deleted 访问器。

强身份映射已弃用

一系列新的转换事件的灵感之一是能够实现对对象的无泄漏跟踪,因为对象在身份映射中进出,以便可以维护与对象在此映射中进出镜像的 “强引用”。有了这个新功能,不再需要 Session.weak_identity_map 参数和相应的 StrongIdentityMap 对象。多年来,此选项一直保留在 SQLAlchemy 中,因为 “强引用” 行为曾经是唯一可用的行为,并且许多应用程序都是为了假设此行为而编写的。长期以来,一直建议对象的强引用跟踪不应是 Session 的内在工作,而应是应用程序级别的构造,根据应用程序的需要构建;新的事件模型甚至允许复制强身份映射的精确行为。请参阅 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

关于 “unhashable” 类型的更改,影响 ORM 行的去重

Query 对象有一个众所周知的 “去重” 返回行的行为,这些行包含至少一个 ORM 映射实体(例如,完整的映射对象,而不是单独的列值)。这的主要目的是使实体处理与身份映射顺利配合,包括适应连接迫切加载中通常表示的重复实体,以及在连接用于过滤其他列时。

此去重依赖于行中元素的哈希性。随着 PostgreSQL 特殊类型(如 ARRAYHSTOREJSON)的引入,行中类型不可哈希并在此处遇到问题的体验比以前更加普遍。

实际上,自 0.8 版以来,SQLAlchemy 在标记为 “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 “inspectable” 对象进行特定检查,在这些上下文中,它们将被视为字面值。任何可以合法地作为 SQL 值传递的 SQLAlchemy 内置对象(它还不是 ClauseElement 实例)都包含一个方法 __clause_element__(),该方法为此对象提供有效的 SQL 表达式。对于不提供此方法的 SQLAlchemy 对象,例如映射类、映射器和映射实例,将发出更具信息性的错误消息,而不是允许 DBAPI 接收对象并在稍后失败。下面举例说明,其中基于字符串的属性 User.nameUser() 的完整实例进行比较,而不是与字符串值进行比较

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

新的 Indexable ORM 扩展

Indexable 扩展是混合属性特性的扩展,它允许构建引用 “indexable” 数据类型(例如数组或 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 来跟踪此更改。

另请参阅

Indexable

允许显式持久化 NULL 而不是默认值的新选项

与作为 JSON “null” 在 ORM 操作中按预期插入,不存在时省略 的一部分添加到 PostgreSQL 的新 JSON-NULL 支持相关,基本 TypeEngine 类现在支持方法 TypeEngine.evaluates_none(),该方法允许属性上的 None 值的正集持久化为 NULL,而不是从 INSERT 语句中省略列,这具有使用列级默认值的效果。这允许对现有对象级别技术进行映射器级别配置,即将 null() 分配给属性。

#3250

进一步修复单表继承查询

继续 1.0 的 在使用 from_self()、count() 时更改为单表继承条件,当查询针对子查询表达式(例如 exists)时,Query 不应再不适当地添加 “单继承” 条件

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 时,改进的 Session 状态

MySQL 的常见情况是,当事务中发生死锁时,SAVEPOINT 被取消。Session 已被修改为更优雅地处理此故障模式,以便外部非保存点事务仍然可用

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() 方法负责删除已插入数据库的对象,例如在现在回滚的事务中从 pending 移动到 persistent。进行此状态更改的对象在弱引用集合中跟踪,如果对象从该集合中被垃圾回收,则 Session 不再担心它(否则,它将无法扩展以进行在事务中插入许多新对象的操作)。但是,如果在回滚发生之前,应用程序在该事务中重新加载相同的垃圾回收行,则会出现问题;如果对此对象的强引用保留到下一个事务中,则此对象未插入且应删除的事实将丢失,并且刷新将错误地引发错误

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 功能

连接表继承映射现在可能允许由于 Session.delete() 而继续执行 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

继承映射器上的同名关系不再发出警告

当在继承场景中创建两个映射器时,在两者上放置一个同名关系会发出警告“映射器 <name> 上的关系 ‘<name>’ 取代了继承映射器 ‘<name>’ 上的相同关系;这可能会在刷新期间导致依赖问题”。示例如下

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.propertyQueryableAttribute.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 行,我们可以看到 Addressemail_address 字段将被覆盖三次,在本例中,值分别为 a、b 和最终的 c。

但是,如果主键为 “1” 的 Address 行不存在,则 Session.merge() 将改为创建三个单独的 Address 实例,然后我们将在 INSERT 时遇到主键冲突。新行为是在一个单独的字典中跟踪这些 Address 对象的建议主键,以便我们将三个建议的 Address 对象的状态合并到一个要插入的 Address 对象上。

如果原始情况发出某种警告,表明单个合并树中存在冲突数据,则可能更可取,但是,多年来,对于持久化的情况,值的非确定性合并一直是这种行为;现在它与待处理的情况相匹配。警告冲突值的功能对于这两种情况仍然是可行的,但是会增加相当大的性能开销,因为每个列值都必须在合并期间进行比较。

#3601

修复了涉及用户启动的外键操作的 Many-to-One 对象移动

修复了一个 bug,该 bug 涉及将 Many-to-One 引用替换为另一个对象的机制。在属性操作期间,先前引用的对象的位置现在使用数据库提交的外键值,而不是当前的外键值。此修复的主要效果是,当进行 Many-to-One 更改时,即使外键属性已事先手动移动到新值,指向集合的反向引用事件也将更准确地触发。假设类 ParentSomeClass 的映射,其中 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然后我们实际设置了关系。在 bug 修复之前,反向引用不会触发

# 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() 方法现在从这样的多态可选中提取各个表,并确保所有表都是子查询的“关联”部分。假设来自映射文档的 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) 的调用会无意中尝试与 PersonEngineerManager 的连接作为一个单元进行关联,因此 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 的 Stringify 将咨询 Session 以获取正确的方言

Query 对象上调用 str() 将咨询 Session 以获取要使用的正确“绑定”,以便呈现将传递到数据库的 SQL。特别是,这允许呈现引用方言特定 SQL 构造的 Query,前提是 Query 与适当的 Session 相关联。以前,仅当映射关联到的 MetaData 本身绑定到目标 Engine 时,此行为才会生效。

如果底层 MetaDataSession 都没有与任何绑定的 Engine 相关联,则使用回退到“默认”方言来生成 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_1c_alias_2 都是相同的,这意味着一行中的两组列仅导致向标识映射添加一个新对象。

上面的查询选项仅要求在 c_alias_1 的上下文中加载属性 C.d,而不是在 c_alias_2 的上下文中加载。因此,我们在标识映射中获得的最终 C 对象是否加载了 C.d 属性取决于映射的遍历方式,虽然并非完全随机,但本质上是不确定的。修复方法是,即使对于两个都引用相同标识的单行,c_alias_1 的加载器在 c_alias_2 的加载器之后处理,C.d 元素仍将被加载。以前,加载器不会尝试修改已通过不同路径加载的实体的加载。首先到达实体的加载器始终是不确定的,因此在某些情况下,此修复可能会被检测为行为更改,而在其他情况下则不会。

此修复包括针对“到单个实体的多条路径”情况的两种变体的测试,并且此修复有望涵盖所有其他此类场景。

#3431

新的 MutableList 和 MutableSet 助手已添加到突变跟踪扩展中

新的助手类 MutableListMutableSet 已添加到 突变跟踪 扩展中,以补充现有的 MutableDict 助手。

#3297

新的 “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 在许多领域中无法按预期工作(或者预期内容不明确),例如当查询组合成联合时;不支持这些情况。

#3394

新功能和改进 - Core

引擎现在使连接无效,为 BaseException 运行错误处理程序

1.1 版本新增功能:此更改是 1.1 系列后期添加到 1.1 最终版之前的,并且未在 1.1 beta 版本中出现。

Python BaseException 类低于 Exception 类,但它是系统级异常(如 KeyboardInterruptSystemExit,以及 eventlet 和 gevent 使用的 GreenletExit 异常)的可识别基类。此异常类现在被 Connection 的异常处理例程拦截,并包括 ConnectionEvents.handle_error() 事件的处理。Connection 现在默认情况下在不是 Exception 子类的系统级异常情况下无效,因为它假定操作被中断,并且连接可能处于不可用状态。MySQL 驱动程序是此更改的主要目标,但此更改适用于所有 DBAPI。

请注意,在失效时,Connection 使用的直接 DBAPI 连接将被处置,并且 Connection(如果在异常引发后仍在使用)将在下次使用时为后续操作使用新的 DBAPI 连接;但是,任何正在进行的事务的状态都会丢失,并且如果适用,则必须在继续重用之前调用适当的 .rollback() 方法。

为了识别此更改,很容易证明 pymysql 或 mysqlclient / MySQL-Python 连接在连接执行其工作期间发生这些异常时会进入损坏状态;然后连接将被返回到连接池,后续使用将失败,或者甚至在返回到池之前,也会在异常捕获时调用 .rollback() 的上下文管理器中导致次要故障。此处的行为有望减少 MySQL 错误 “commands out of sync” 以及 ResourceClosedError 的发生率,后者可能在 MySQL 驱动程序未能正确报告 cursor.description 时发生,当在 greenlet 条件下运行时,greenlet 被终止,或者在处理 KeyboardInterrupt 异常而不完全退出程序的情况下发生。

此行为与通常的自动失效功能不同,因为它不假设后端数据库本身已关闭或重新启动;它不会像通常的 DBAPI 断开连接异常那样回收整个连接池。

对于除 任何当前拦截 ``KeyboardInterrupt`` 或 ``GreenletExit`` 并希望在同一事务中继续工作的应用程序 之外的所有用户,此更改都应该是一个净改进。这种操作在理论上对于其他 DBAPI(似乎不受 KeyboardInterrupt 影响,例如 psycopg2)是可能的。对于这些 DBAPI,以下解决方法将禁用连接因特定异常而被回收

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 的 INSERT 现在将在整个语句的顶部呈现 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 元组,并指示特定范围的负值和正值,0 表示 “CURRENT ROW”,None 表示 UNBOUNDED。

另请参阅

使用窗口函数

#3049

支持 SQL LATERAL 关键字

LATERAL 关键字目前已知仅 PostgreSQL 9.3 及更高版本支持,但由于它是 SQL 标准的一部分,因此 Core 中添加了对该关键字的支持。 Select.lateral() 的实现采用了特殊的逻辑,而不仅仅是渲染 LATERAL 关键字,以允许对与 selectable 派生自同一 FROM 子句的表进行关联,例如 lateral 关联

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

#2857

TABLESAMPLE 支持

可以使用 FromClause.tablesample() 方法渲染 SQL 标准 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,此外,dialect 将使用适用于该后端的技术从 Table.insert() 构造的执行中接收这些生成的值。

改变的是,此功能不再为复合主键自动开启; 以前,如下所示的表定义

Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True),
)

会将“自增”语义应用于 'x' 列,仅仅因为它在主键列列表中排在第一位。 为了禁用此功能,必须关闭所有列上的 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 期间列的行为产生太大影响。

#3216

支持 IS DISTINCT FROM 和 IS NOT DISTINCT FROM

新的运算符 ColumnOperators.is_distinct_from()ColumnOperators.isnot_distinct_from() 允许 IS DISTINCT FROMIS NOT DISTINCT FROM sql 操作

>>> print(column("x").is_distinct_from(None))
x IS DISTINCT FROM NULL

NULLTrueFalse 提供了处理

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

Core 和 ORM 支持 FULL OUTER JOIN

新的标志 FromClause.outerjoin.full 在 Core 和 ORM 级别都可用,指示编译器渲染 FULL OUTER JOIN,在通常情况下它会渲染 LEFT OUTER JOIN

stmt = select([t1]).select_from(t1.outerjoin(t2, full=True))

该标志也适用于 ORM 级别

q = session.query(MyClass).outerjoin(MyOtherClass, full=True)

#1957

ResultSet 列匹配增强; 文本 SQL 的位置列设置

在 1.0 系列中,作为 #918 的一部分,对 ResultProxy 系统进行了一系列改进,它重新组织了内部结构,以便将光标绑定的结果列与表/ORM 元数据按位置匹配,而不是通过匹配名称,用于包含有关要返回的结果行的完整信息的已编译 SQL 构造。 这大大节省了 Python 开销,并提高了将 ORM 和 Core SQL 表达式链接到结果行的准确性。 在 1.1 中,这种重组在内部得到了进一步发展,并且通过使用最近添加的 TextClause.columns() 方法,也已可用于纯文本 SQL 构造。

TextAsFrom.columns() 现在按位置工作

0.9 中添加的 TextClause.columns() 方法接受基于列的位置参数; 在 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” 三次,这通常是不明确的。 使用新功能,我们可以直接应用来自 UserAddress 类的映射列,甚至为了好玩将 Address.user_id 列链接到文本 SQL 中的 users.id 列,并且 Query 对象将接收根据需要正确可定位的行,包括用于预加载的行。

此更改与以与文本语句中存在的顺序不同的顺序将列传递给方法的代码向后不兼容。 希望这种影响会很小,因为即使内部结构没有检查这一点,但此方法一直被记录为说明以与文本 SQL 语句相同的顺序传递列(这似乎是直观的)。 无论如何,该方法本身仅在 0.9 中添加,可能尚未得到广泛使用。 有关如何处理使用它的应用程序的此行为更改的确切说明,请参见 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"]

不太可能收到“不明确的列”错误消息

作为此更改的一部分,错误消息 结果集中不明确的列名 '<name>'! 尝试在选择语句上使用 'use_labels' 选项。 的措辞已调回; 由于在使用 ORM 或 Core 编译的 SQL 构造时,此消息现在应该非常罕见,因此它仅说明 结果集列描述中不明确的列名 '<name>',并且仅当使用实际不明确的字符串名称检索结果列时,例如上面示例中的 row['user_id']。 现在,它还引用了来自渲染的 SQL 语句本身的实际不明确的名称,而不是指示用于获取的构造本地的键或名称。

#3501

支持 Python 的原生 enum 类型和兼容形式

现在可以使用任何符合 PEP-435 标准的枚举类型构造 Enum 类型。 使用此模式时,输入值和返回值是实际的枚举对象,而不是字符串/整数/等值

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

Core 结果行容纳负整数索引

RowProxy 对象现在像常规 Python 序列一样容纳单个负整数索引,无论是在纯 Python 版本还是 C 扩展版本中。 以前,负值仅在切片中有效

>>> from sqlalchemy import create_engine
>>> e = create_engine("sqlite://")
>>> row = e.execute("select 1, 2, 3").first()
>>> row[-1], row[-2], row[1], row[-2:2]
3 2 2 (2,)

Enum 类型现在对值执行 Python 内验证

为了适应 Python 原生枚举对象,以及诸如非原生 ENUM 类型在 ARRAY 中使用且 CHECK 约束不可行的情况,Enum 数据类型现在在使用 Enum.validate_strings 标志时(1.1.0b2)添加了对输入值的 Python 内验证

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

对于没有原生布尔类型的后端(例如 SQLite 和 MySQL),Boolean 数据类型将 Python 布尔值强制转换为整数值。 在这些后端上,通常会设置 CHECK 约束,以确保数据库中的值实际上是这两个值之一。 但是,MySQL 忽略 CHECK 约束,约束是可选的,并且现有数据库可能没有此约束。Boolean 数据类型已修复,以便将传入的 Python 端值(已经是整数值)强制转换为零或一,而不仅仅是按原样传递; 此外,用于结果的 int 到布尔处理器 C 扩展版本现在使用与值的 Python 布尔解释相同的解释,而不是断言精确的一个或零值。 现在,这与纯 Python int 到布尔处理器一致,并且对数据库中已有的现有数据更加宽容。 None/NULL 的值与以前一样保留为 None/NULL

注意

此更改有一个意外的副作用,即非整数值(例如字符串)的解释在行为上也发生了变化,使得字符串值 "0" 将被解释为“true”,但仅在没有原生布尔数据类型的后端上 - 在像 PostgreSQL 这样的“原生布尔”后端上,字符串值 "0" 将直接传递给驱动程序并被解释为“false”。 这是以前的实现中没有发生的不一致之处。 应该注意的是,将 NoneTrueFalse10 之外的字符串或任何其他值传递给 Boolean 数据类型是不支持的,并且 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 支持添加到 Core

由于 MySQL 现在除了 PostgreSQL JSON 数据类型之外还具有 JSON 数据类型,因此 core 现在获得了一个 sqlalchemy.types.JSON 数据类型,它是这两者的基础。 使用此类型可以以跨 PostgreSQL 和 MySQL 不可知的方式访问 “getitem” 运算符以及 “getpath” 运算符。

新的数据类型还在 NULL 值处理和表达式处理方面进行了一系列改进。

#3619

JSON “null” 按预期与 ORM 操作一起插入,如果不存在则省略

JSON 类型及其后代类型 JSONJSON 具有标志 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 常量

为了确保应用程序始终可以在值级别完全控制 JSONJSONJSONJSONB 列应接收 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 运算符

除了在 从 ARRAY、JSON、HSTORE 的索引访问建立正确的 SQL 类型 中描述的对 PostgreSQL ARRAY 类型的增强之外,ARRAY 的基类本身已移至 Core 中的新类 ARRAY

数组是 SQL 标准的一部分,一些面向数组的函数(例如 array_agg()unnest())也是如此。 为了支持这些构造,不仅适用于 PostgreSQL,而且还可能适用于未来其他具有数组功能的后端(例如 DB2),SQL 表达式的大部分数组逻辑现在都在 Core 中。 ARRAY 类型仍然仅适用于 PostgreSQL,但是它可以直接使用,支持特殊的数组用例(例如索引访问),以及支持 ANYALL

mytable = Table("mytable", metadata, Column("data", ARRAY(Integer, dimensions=2)))

expr = mytable.c.data[5][6]

expr = mytable.c.data[5].any(12)

为了支持 ANYALLARRAY 类型保留了与 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_() 运算符在 Core 级别是开放式的,但是后端数据库对它们的解释是有限的。 在 PostgreSQL 后端,这两个运算符仅接受数组值。 而在 MySQL 后端,它们仅接受子查询值。 在 MySQL 上,可以使用如下表达式

from sqlalchemy import any_, all_

subq = select([mytable.c.value])
select([mytable]).where(12 > any_(subq))

#3516

新的函数特性,“WITHIN GROUP”、array_agg 和集合聚合函数

借助新的 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 dialect 本身还提供了一个 array_agg() 包装器,以确保 ARRAY 类型

from sqlalchemy.dialects.postgresql import array_agg

stmt = select([array_agg(table.c.value).contains("foo")])

此外,需要通过 WITHIN GROUP (ORDER BY <expr>) 排序的函数(例如 percentile_cont()percentile_disc()rank()dense_rank() 等)现在可以通过 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_contpercentile_discrankdense_rankmodepercent_rankcume_dist

#3132 #1370

TypeDecorator 现在可以自动与 Enum、Boolean 和 “schema” 类型一起使用

SchemaType 类型包括 EnumBoolean 等类型,除了对应于数据库类型之外,还会生成 CHECK 约束,或者在 PostgreSQL ENUM 的情况下生成新的 CREATE TYPE 语句,现在可以自动与 TypeDecorator 配方一起使用。 以前,TypeDecorator 用于 ENUM 必须像这样编写

# 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 对象的多租户 Schema 转换

为了支持应用程序在多个 schema 中使用同一组 Table 对象(例如每个用户一个 schema)的用例,添加了一个新的执行选项 Connection.execution_options.schema_translate_map。 使用此映射,可以在每个连接的基础上将一组 Table 对象设置为引用任何一组 schema,而不是它们被分配到的 Table.schema。 此转换适用于 DDL 和 SQL 生成,以及 ORM。

例如,如果 User 类被分配了 schema “per_user”

class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)

    __table_args__ = {"schema": "per_user"}

在每个请求中,可以将 Session 设置为每次引用不同的 schema

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)

另请参阅

Schema 名称的转换

#2685

在没有 dialect 的情况下,“友好地” 字符串化 Core SQL 构造

在 Core 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() 函数现在调用一个完全独立的 dialect / 编译器,该编译器专门用于在没有特定 dialect 设置的情况下进行纯字符串打印,因此随着更多 “只需给我显示一个字符串!” 的情况出现,可以将这些情况添加到此 dialect/编译器中,而不会影响真实 dialect 的行为。

#3631

type_coerce 函数现在是一个持久的 SQL 元素

type_coerce() 函数以前会返回类型为 BindParameterLabel 的对象,具体取决于输入。 这会产生一个影响,即在使用表达式转换的情况下(例如将元素从 Column 转换为 BindParameter,这对于 ORM 级别的延迟加载至关重要),类型强制转换信息将不会被使用,因为它已经丢失了。

为了改进此行为,该函数现在返回一个持久的 TypeCoerce 容器,该容器围绕给定的表达式,表达式本身保持不受影响; 此构造由 SQL 编译器显式评估。 这允许强制转换内部表达式,无论语句如何修改,包括如果包含的元素被不同的元素替换(这在 ORM 的延迟加载功能中很常见)。

说明效果的测试用例结合自定义类型和延迟加载使用了异构 primaryjoin 条件。 给定一个将 CAST 应用为 “bind expression” 的自定义类型

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)

如果在 join 的 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 首先被 CAST 为 VARCHAR,然后在 SQL 中又 CAST 回 INTEGER; 双重 CAST 可以工作,但无论如何都不是我们要求的。

通过更改,即使在列被交换为绑定参数之后,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}

在其中,我们在 primaryjoin 中的外部 CAST 仍然生效,但是 StringAsInt 自定义类型的一部分中不必要的 CAST 被删除,这正是 type_coerce() 函数的预期目的。

#3531

关键行为变更 - ORM

如果未提供值且未建立默认值,JSON 列将不会插入 JSON NULL

JSON “null” 会按预期与 ORM 操作一起插入,当不存在时省略 中详述,如果完全缺少值,JSON 将不会呈现 JSON “null” 值。 为了防止 SQL NULL,应设置默认值。 给定以下映射

class MyObject(Base):
    # ...

    json_value = Column(JSON(none_as_null=False), nullable=False)

以下 flush 操作将因完整性错误而失败

obj = MyObject()  # note no json_value
session.add(obj)
session.commit()  # will fail with integrity error

如果列的默认值应为 JSON NULL,请在 Column 上设置此值

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.defaultColumn.server_defaultNone

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

使用 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.

#3776

关键行为变更 - Core

当以位置方式传递时,TextClause.columns() 将按位置而不是名称匹配列

作为 0.9 系列新增功能的 TextClause.columns() 方法的新行为是,当以位置方式传递列而没有任何额外的关键字参数时,它们会按位置链接到最终的结果集列,而不是按名称链接。 考虑到该方法一直以直观的方式记录了以与文本 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)

现在稍有风险,因为 “*” 规范通常会以列在表本身中出现的顺序传递列。 如果表的结构由于 schema 更改而发生更改,则此顺序可能不再相同。 因此,当使用 TextClause.columns() 时,建议在文本 SQL 中显式列出所需的列,尽管不再需要担心文本 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 的 SELECT 的 UNION 或类似操作现在会将嵌入的 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)

上面的查询需要在每个子 select 中使用括号,以便正确地对子结果进行分组。 在 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 上也能工作的有限 SELECT 语句的 UNION,以及在 Oracle 上的所有情况下,子查询都必须是 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 中 引入的 “join rewriting” 行为有很多相似之处; 但是,在这种情况下,我们选择不添加新的重写行为来适应 SQLite 的这种情况。 现有的重写行为已经非常复杂,并且带有括号 SELECT 语句的 UNION 的情况远不如该功能的 “right-nested-join” 用例常见。

#2528

Dialect 改进和更改 - PostgreSQL

支持 INSERT..ON CONFLICT (DO UPDATE | DO NOTHING)

PostgreSQL 版本 9.5 中添加的 INSERTON CONFLICT 子句现在通过 Insert 对象的 PostgreSQL 特定版本(通过 sqlalchemy.dialects.postgresql.dml.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

#3529

ARRAY 和 JSON 类型现在正确指定 “unhashable”

关于 “unhashable” 类型的更改,影响 ORM 行的去重 中所述,当查询的选定实体混合了完整的 ORM 实体和列表达式时,ORM 依赖于能够为列值生成哈希函数。 hashable=False 标志现在已正确设置在 PG 的所有 “数据结构” 类型上,包括 ARRAYJSONJSONBHSTORE 类型已经包含此标志。 对于 ARRAY,这是有条件的,基于 ARRAY.as_tuple 标志,但是为了使数组值出现在组合的 ORM 行中,不再需要设置此标志。

#3499

从 ARRAY、JSON、HSTORE 的索引访问中建立正确的 SQL 类型

对于 ARRAYJSONHSTORE 这三种类型,分配给索引访问返回的表达式的 SQL 类型(例如 col[someindex])在所有情况下都应该是正确的。

这包括

  • 分配给 ARRAY 的索引访问的 SQL 类型考虑了配置的维度数量。 具有三个维度的 ARRAY 将返回 SQL 表达式,其类型为维度少一个的 ARRAY。 给定一个类型为 ARRAY(Integer, dimensions=3) 的列,我们现在可以执行此表达式

    int_expr = col[5][6][7]  # returns an Integer expression object

    以前,对 col[5] 的索引访问将返回类型为 Integer 的表达式,我们在其中无法再为剩余维度执行索引访问,除非我们使用 cast()type_coerce()

  • JSONJSONB 类型现在镜像 PostgreSQL 本身对索引访问的操作。 这意味着 JSONJSONB 类型的索引访问返回的表达式本身始终JSONJSONB 本身,除非使用了 Comparator.astext 修饰符。 这意味着,无论 JSON 结构的索引访问最终引用的是字符串、列表、数字还是其他 JSON 结构,PostgreSQL 始终认为它是 JSON 本身,除非显式地进行不同的转换。 像 ARRAY 类型一样,这意味着现在可以轻松生成具有多个索引访问级别的 JSON 表达式

    json_expr = json_col["key1"]["attr1"][5]
  • HSTORE 的索引访问返回的 “textual” 类型以及由 JSONJSONB 的索引访问与 Comparator.astext 修饰符结合返回的 “textual” 类型现在是可配置的; 在这两种情况下,它都默认为 TextClause,但可以使用 JSON.astext_typeHSTORE.text_type 参数设置为用户定义的类型。

#3499 #3487

JSON cast() 操作现在需要显式调用 .astext

作为 从 ARRAY、JSON、HSTORE 的索引访问中建立正确的 SQL 类型 中的更改的一部分,ColumnElement.cast() 运算符在 JSONJSONB 上的工作方式不再隐式调用 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

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

#2729

Check 约束现在可以反射

PostgreSQL dialect 现在支持 CHECK 约束的反射,包括在 Inspector.get_check_constraints() 方法中以及在 Table 反射中的 Table.constraints 集合中。

可以分别检查 “Plain” 和 “Materialized” 视图

新参数 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"))

#3588

为 Index 添加了 tablespace 选项

Index 对象现在接受参数 postgresql_tablespace,以便指定 TABLESPACE,这与 Table 对象接受的方式相同。

另请参阅

索引存储参数

#3720

PyGreSQL 支持

现在支持 PyGreSQL DBAPI。

“postgres” 模块已删除

长期以来已弃用的 sqlalchemy.dialects.postgres 模块已删除; 多年来,它一直在发出警告,项目应调用 sqlalchemy.dialects.postgresql。 形式为 postgres:// 的引擎 URL 仍将继续工作。

支持 FOR UPDATE SKIP LOCKED / FOR NO KEY UPDATE / FOR KEY SHARE

Core 和 ORM 中的新参数 GenerativeSelect.with_for_update.skip_lockedGenerativeSelect.with_for_update.key_share 将修改应用于 PostgreSQL 后端上的 “SELECT…FOR UPDATE” 或 “SELECT…FOR SHARE” 查询

  • SELECT FOR NO KEY UPDATE

    stmt = select([table]).with_for_update(key_share=True)
  • SELECT FOR UPDATE SKIP LOCKED

    stmt = select([table]).with_for_update(skip_locked=True)
  • SELECT FOR KEY SHARE

    stmt = select([table]).with_for_update(read=True, key_share=True)

Dialect 改进和更改 - MySQL

MySQL JSON 支持

一种新的 JSON 类型被添加到 MySQL 方言中,以支持 MySQL 5.7 中新添加的 JSON 类型。此类型提供 JSON 的持久性以及使用 JSON_EXTRACT 函数在内部进行的基本索引访问。通过使用 MySQL 和 PostgreSQL 共有的 JSON 数据类型,可以实现跨 MySQL 和 PostgreSQL 的可索引 JSON 列。

#3547

增加了对 AUTOCOMMIT “隔离级别” 的支持

MySQL 方言现在接受 create_engine.isolation_levelConnection.execution_options.isolation_level 参数的 “AUTOCOMMIT” 值

connection = engine.connect()
connection = connection.execution_options(isolation_level="AUTOCOMMIT")

隔离级别利用了大多数 MySQL DBAPI 提供的各种 “autocommit” 属性。

#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”;这是一个多年前进入方言的更改,是为了响应在没有此额外 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 版本中,由 许多 JOIN 和 LEFT OUTER JOIN 表达式将不再被包裹在 (SELECT * FROM ..) AS ANON_1 中 功能引入的特性,花费了大量精力来支持在 SQLite 上重写连接,始终使用子查询以实现 “右嵌套连接” 效果,因为 SQLite 多年来不支持这种语法。具有讽刺意味的是,迁移说明中提到的 SQLite 版本 3.7.15.2,实际上是最后一个有此限制的 SQLite 版本!下一个版本是 3.7.16,并且悄悄添加了对右嵌套连接的支持。在 1.1 中,完成了识别进行此更改的特定 SQLite 版本和源代码提交的工作(SQLite 的变更日志用隐晦的短语 “增强查询优化器以利用传递连接约束” 来指代它,但没有链接到任何问题编号、更改编号或进一步的解释),并且当 DBAPI 报告版本 3.7.16 或更高版本生效时,此更改中存在的解决方法现在已针对 SQLite 取消。

#3634

针对 SQLite 3.10.0 版本取消了带点列名的解决方法

SQLite 方言长期以来都有一种解决方法,用于解决数据库驱动程序未报告某些 SQL 结果集的正确列名的问题,特别是在使用 UNION 时。解决方法在 带点列名 中详细说明,并要求 SQLAlchemy 假设任何带有点的列名实际上都是通过此错误行为传递的 tablename.columnname 组合,并且可以通过 sqlite_raw_colnames 执行选项将其关闭。

从 SQLite 3.10.0 版本开始,UNION 和其他查询中的错误已修复;与 针对 SQLite 3.7.16 版本取消了右嵌套连接的解决方法 中描述的更改一样,SQLite 的变更日志仅将其隐晦地标识为 “为 sqlite3_module.xBestIndex 方法添加了供 sqlite3_index_info 使用的 colUsed 字段”,但是此版本不再需要 SQLAlchemy 对这些带点列名的转换,因此当检测到 3.10.0 或更高版本时会将其关闭。

总的来说,从 1.0 系列开始,SQLAlchemy ResultProxy 在为 Core 和 ORM SQL 构造传递结果时,在结果集中对列名的依赖大大减少,因此无论如何,这个问题的重要性已经降低。

#3633

改进了对远程 Schema 的支持

SQLite 方言现在实现了 Inspector.get_schema_names(),并且还改进了对从远程 schema 创建和反射的表和索引的支持,在 SQLite 中,远程 schema 是通过 ATTACH 语句分配名称的数据库;以前,``CREATE INDEX`` DDL 对于 schema 绑定的表无法正常工作,并且 Inspector.get_foreign_keys() 方法现在将在结果中指示给定的 schema。不支持跨 schema 的外键。

PRIMARY KEY 约束名称的反射

SQLite 后端现在利用了 SQLite 的 “sqlite_master” 视图,以便从原始 DDL 中提取表的主键约束名称,这与最近的 SQLAlchemy 版本中实现外键约束的方式相同。

#3629

Check 约束现在可以反射

SQLite 方言现在支持 CHECK 约束的反射,包括在 Inspector.get_check_constraints() 方法中以及在 Table 反射的 Table.constraints 集合中。

ON DELETE 和 ON UPDATE 外键短语现在可以反射

Inspector 现在将包含 SQLite 方言上外键约束中的 ON DELETE 和 ON UPDATE 短语,并且作为 Table 一部分被反射的 ForeignKeyConstraint 对象也将指示这些短语。

方言改进和更改 - SQL Server

为 SQL Server 添加了事务隔离级别支持

所有 SQL Server 方言都支持通过 create_engine.isolation_levelConnection.execution_options.isolation_level 参数设置事务隔离级别。支持四个标准级别以及 SNAPSHOT

engine = create_engine(
    "mssql+pyodbc://scott:tiger@ms_2008", isolation_level="REPEATABLE READ"
)

另请参阅

事务隔离级别

#3534

String / varlength 类型在反射时不再显式表示 “max”

当反射诸如 StringTextClause 等包含长度的类型时,SQL Server 下的 “未指定长度” 类型会将 “length” 参数复制为值 "max"

>>> from sqlalchemy import create_engine, inspect
>>> engine = create_engine("mssql+pyodbc://scott:tiger@ms_2008", echo=True)
>>> engine.execute("create table s (x varchar(max), y varbinary(max))")
>>> insp = inspect(engine)
>>> for col in insp.get_columns("s"):
...     print(col["type"].__class__, col["type"].length)
<class 'sqlalchemy.sql.sqltypes.VARCHAR'> max
<class 'sqlalchemy.dialects.mssql.base.VARBINARY'> max

基本类型中的 “length” 参数应仅为整数值或 None;None 表示无界长度,SQL Server 方言将其解释为 “max”。修复方法是使这些长度显示为 None,以便类型对象在非 SQL Server 上下文中工作

>>> for col in insp.get_columns("s"):
...     print(col["type"].__class__, col["type"].length)
<class 'sqlalchemy.sql.sqltypes.VARCHAR'> None
<class 'sqlalchemy.dialects.mssql.base.VARBINARY'> None

可能一直依赖于将 “length” 值与字符串 “max” 进行直接比较的应用程序应考虑将 None 值视为具有相同的含义。

#3504

支持主键上的 “non clustered” 以允许在其他地方使用 clustered

UniqueConstraintPrimaryKeyConstraintIndex 上可用的 mssql_clustered 标志现在默认为 None,并且可以设置为 False,这将为主键特别呈现 NONCLUSTERED 关键字,从而允许将不同的索引用作 “clustered”。

另请参阅

聚集索引支持

legacy_schema_aliasing 标志现在设置为 False

SQLAlchemy 1.0.5 在 MSSQL 方言中引入了 legacy_schema_aliasing 标志,允许关闭所谓的 “legacy mode” 别名。此别名尝试将 schema 限定的表转换为别名;给定如下表

account_table = Table(
    "account",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("info", String(100)),
    schema="customer_schema",
)

legacy 模式的行为将尝试将 schema 限定的表名转换为别名

>>> eng = create_engine("mssql+pymssql://mydsn", legacy_schema_aliasing=True)
>>> print(account_table.select().compile(eng))
SELECT account_1.id, account_1.info FROM customer_schema.account AS account_1

但是,别名已被证明是不必要的,并且在许多情况下会产生不正确的 SQL。

在 SQLAlchemy 1.1 中,legacy_schema_aliasing 标志现在默认为 False,禁用此模式的行为,并允许 MSSQL 方言在 schema 限定的表上正常运行。对于可能依赖于此行为的应用程序,请将该标志设置回 True。

#3434

方言改进和更改 - Oracle

支持 SKIP LOCKED

Core 和 ORM 中的新参数 GenerativeSelect.with_for_update.skip_locked 将为 “SELECT…FOR UPDATE” 或 “SELECT.. FOR SHARE” 查询生成 “SKIP LOCKED” 后缀。