SQLAlchemy 0.9 的新特性?

关于本文档

本文档描述了 SQLAlchemy 0.8 版本(截至 2013 年 5 月正在进行维护版本发布)和 SQLAlchemy 0.9 版本(于 2013 年 12 月 30 日首次发布生产版本)之间的更改。

文档最后更新:2015 年 6 月 10 日

介绍

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

请仔细查看 行为变更 - ORM行为变更 - 核心 以了解可能向后不兼容的更改。

平台支持

现在以 Python 2.6 及更高版本为目标,Python 3 无需 2to3

0.9 版本的首要成就是消除了 Python 3 兼容性对 2to3 工具的依赖。为了使这更加直接,现在以 Python 2.6 为最低目标版本,该版本具有与 Python 3 广泛的交叉兼容性。所有 SQLAlchemy 模块和单元测试现在都可以通过从 2.6 开始的任何 Python 解释器(包括 3.1 和 3.2 解释器)进行同等良好的解释。

#2671

Python 3 上支持 C 扩展

C 扩展已被移植以支持 Python 3,现在可以在 Python 2 和 Python 3 环境中构建。

#2161

行为变更 - ORM

当按属性查询时,复合属性现在以其对象形式返回

结合复合属性使用 Query 现在返回该复合属性维护的对象类型,而不是分解为单独的列。使用 复合列类型 中的映射设置

>>> session.query(Vertex.start, Vertex.end).filter(Vertex.start == Point(3, 4)).all()
[(Point(x=3, y=4), Point(x=5, y=6))]

此更改与期望将单个属性扩展为单独列的代码向后不兼容。要获得该行为,请使用 .clauses 访问器

>>> session.query(Vertex.start.clauses, Vertex.end.clauses).filter(
...     Vertex.start == Point(3, 4)
... ).all()
[(3, 4, 5, 6)]

另请参阅

ORM 查询的列束

#2824

Query.select_from() 不再将子句应用于相应的实体

Query.select_from() 方法在最近的版本中已普及,作为控制 Query 对象“从中选择”的第一项内容的一种手段,通常用于控制 JOIN 的渲染方式。

考虑以下针对常用 User 映射的示例

select_stmt = select([User]).where(User.id == 7).alias()

q = (
    session.query(User)
    .join(select_stmt, User.id == select_stmt.c.id)
    .filter(User.name == "ed")
)

上面的语句可预测地渲染如下 SQL

SELECT "user".id AS user_id, "user".name AS user_name
FROM "user" JOIN (SELECT "user".id AS id, "user".name AS name
FROM "user"
WHERE "user".id = :id_1) AS anon_1 ON "user".id = anon_1.id
WHERE "user".name = :name_1

如果我们想反转 JOIN 的左元素和右元素的顺序,文档会引导我们相信我们可以使用 Query.select_from() 来做到这一点

q = (
    session.query(User)
    .select_from(select_stmt)
    .join(User, User.id == select_stmt.c.id)
    .filter(User.name == "ed")
)

但是,在 0.8 及更早版本中,上面 Query.select_from() 的用法会将 select_stmt 应用于替换 User 实体,因为它从与 User 兼容的 user 表中选择

-- SQLAlchemy 0.8 and earlier...
SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name
FROM (SELECT "user".id AS id, "user".name AS name
FROM "user"
WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON anon_1.id = anon_1.id
WHERE anon_1.name = :name_1

上面的语句很混乱,ON 子句引用 anon_1.id = anon_1.id,我们的 WHERE 子句也被 anon_1 替换。

此行为是完全有意的,但与 Query.select_from() 的流行用例不同。上面的行为现在可以通过一种名为 Query.select_entity_from() 的新方法获得。这是一种较少使用的行为,在现代 SQLAlchemy 中,它大致相当于从自定义的 aliased() 构造中选择

select_stmt = select([User]).where(User.id == 7)
user_from_stmt = aliased(User, select_stmt.alias())

q = session.query(user_from_stmt).filter(user_from_stmt.name == "ed")

因此,在 SQLAlchemy 0.9 中,我们从 select_stmt 中选择的查询会生成我们期望的 SQL

-- SQLAlchemy 0.9
SELECT "user".id AS user_id, "user".name AS user_name
FROM (SELECT "user".id AS id, "user".name AS name
FROM "user"
WHERE "user".id = :id_1) AS anon_1 JOIN "user" ON "user".id = id
WHERE "user".name = :name_1

Query.select_entity_from() 方法将在 SQLAlchemy 0.8.2 中可用,因此依赖旧行为的应用程序可以首先过渡到此方法,确保所有测试继续运行,然后升级到 0.9 而不会出现问题。

#2736

relationship() 上的 viewonly=True 阻止历史记录生效

relationship() 上的 viewonly 标志用于阻止对目标属性的更改在刷新过程中产生任何影响。这是通过在刷新期间消除对属性的考虑来实现的。但是,到目前为止,对属性的更改仍会将父对象注册为“脏”并触发潜在的刷新。更改是 viewonly 标志现在也阻止为目标属性设置历史记录。反向引用和用户定义的事件等属性事件仍然继续正常运行。

更改示例如下

from sqlalchemy import Column, Integer, ForeignKey, create_engine
from sqlalchemy.orm import backref, relationship, Session
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import inspect

Base = declarative_base()


class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))
    a = relationship("A", backref=backref("bs", viewonly=True))


e = create_engine("sqlite://")
Base.metadata.create_all(e)

a = A()
b = B()

sess = Session(e)
sess.add_all([a, b])
sess.commit()

b.a = a

assert b in sess.dirty

# before 0.9.0
# assert a in sess.dirty
# assert inspect(a).attrs.bs.history.has_changes()

# after 0.9.0
assert a not in sess.dirty
assert not inspect(a).attrs.bs.history.has_changes()

#2833

关联代理 SQL 表达式的改进和修复

由关联代理实现的 ==!= 运算符,该代理引用标量关系上的标量值,现在生成更完整的 SQL 表达式,旨在在与 None 进行比较时考虑“关联”行是否存在。

考虑以下映射

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)

    b_id = Column(Integer, ForeignKey("b.id"), primary_key=True)
    b = relationship("B")
    b_value = association_proxy("b", "value")


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    value = Column(String)

在 0.8 版本中,如下查询

s.query(A).filter(A.b_value == None).all()

将产生

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NULL)

在 0.9 版本中,现在产生

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE (EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NULL)) OR a.b_id IS NULL

不同之处在于,它不仅检查 b.value,还检查 a 是否根本没有引用 b 行。对于使用这种比较类型(其中一些父行没有关联行)的系统,这将返回与先前版本不同的结果。

更重要的是,为 A.b_value != None 发出正确的表达式。在 0.8 版本中,对于没有 bA 行,这将返回 True

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE NOT (EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NULL))

现在在 0.9 版本中,检查已重新设计,以确保 A.b_id 行存在,此外还要确保 B.value 为非 NULL

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id AND b.value IS NOT NULL)

此外,has() 运算符得到了增强,因此您可以仅针对没有条件的标量列值调用它,并且它将生成检查关联行是否存在或不存在的条件

s.query(A).filter(A.b_value.has()).all()

输出

SELECT a.id AS a_id, a.b_id AS a_b_id
FROM a
WHERE EXISTS (SELECT 1
FROM b
WHERE b.id = a.b_id)

这等效于 A.b.has(),但允许直接针对 b_value 进行查询。

#2751

关联代理缺少标量值时返回 None

从标量属性到标量的关联代理现在将在代理对象不存在时返回 None。这与 SQLAlchemy 中缺少多对一关系时返回 None 的事实一致,因此代理值也应该如此。例如:

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.associationproxy import association_proxy

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    b = relationship("B", uselist=False)

    bname = association_proxy("b", "name")


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))
    name = Column(String)


a1 = A()

# this is how m2o's always have worked
assert a1.b is None

# but prior to 0.9, this would raise AttributeError,
# now returns None just like the proxied value.
assert a1.bname is None

#2810

如果值不存在,attributes.get_history() 默认将从数据库查询

关于 get_history() 的错误修复允许基于列的属性查询数据库以获取未加载的值,前提是 passive 标志保留为其默认值 PASSIVE_OFF。以前,此标志不会被遵守。此外,添加了一个新方法 AttributeState.load_history() 以补充 AttributeState.history 属性,这将为未加载的属性发出加载器可调用对象。

这是一个小改动,示例如下

from sqlalchemy import Column, Integer, String, create_engine, inspect
from sqlalchemy.orm import Session, attributes
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    data = Column(String)


e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

sess = Session(e)

a1 = A(data="a1")
sess.add(a1)
sess.commit()  # a1 is now expired

# history doesn't emit loader callables
assert inspect(a1).attrs.data.history == (None, None, None)

# in 0.8, this would fail to load the unloaded state.
assert attributes.get_history(a1, "data") == (
    (),
    [
        "a1",
    ],
    (),
)

# load_history() is now equivalent to get_history() with
# passive=PASSIVE_OFF ^ INIT_OK
assert inspect(a1).attrs.data.load_history() == (
    (),
    [
        "a1",
    ],
    (),
)

#2787

行为变更 - 核心

类型对象不再接受被忽略的关键字参数

在 0.8 系列版本中,大多数类型对象都接受任意关键字参数,这些参数会被静默忽略

from sqlalchemy import Date, Integer

# storage_format argument here has no effect on any backend;
# it needs to be on the SQLite-specific type
d = Date(storage_format="%(day)02d.%(month)02d.%(year)04d")

# display_width argument here has no effect on any backend;
# it needs to be on the MySQL-specific type
i = Integer(display_width=5)

这是一个非常古老的错误,为此在 0.8 系列版本中添加了弃用警告,但由于没有人使用“-W”标志运行 Python,因此基本上从未见过它

$ python -W always::DeprecationWarning ~/dev/sqlalchemy/test.py
/Users/classic/dev/sqlalchemy/test.py:5: SADeprecationWarning: Passing arguments to
type object constructor <class 'sqlalchemy.types.Date'> is deprecated
  d = Date(storage_format="%(day)02d.%(month)02d.%(year)04d")
/Users/classic/dev/sqlalchemy/test.py:9: SADeprecationWarning: Passing arguments to
type object constructor <class 'sqlalchemy.types.Integer'> is deprecated
  i = Integer(display_width=5)

从 0.9 系列版本开始,“捕获所有”构造函数已从 TypeEngine 中删除,并且不再接受这些无意义的参数。

利用方言特定参数(例如 storage_formatdisplay_width)的正确方法是使用适当的方言特定类型

from sqlalchemy.dialects.sqlite import DATE
from sqlalchemy.dialects.mysql import INTEGER

d = DATE(storage_format="%(day)02d.%(month)02d.%(year)04d")

i = INTEGER(display_width=5)

如果我们也想要方言不可知的类型怎么办?我们使用 TypeEngine.with_variant() 方法

from sqlalchemy import Date, Integer
from sqlalchemy.dialects.sqlite import DATE
from sqlalchemy.dialects.mysql import INTEGER

d = Date().with_variant(
    DATE(storage_format="%(day)02d.%(month)02d.%(year)04d"), "sqlite"
)

i = Integer().with_variant(INTEGER(display_width=5), "mysql")

TypeEngine.with_variant() 并不新鲜,它是在 SQLAlchemy 0.7.2 中添加的。因此,在 0.8 系列版本上运行的代码可以被纠正以使用此方法并在升级到 0.9 之前进行测试。

None 不能再用作“部分 AND”构造函数

None 不能再用作“后备”来零散地形成 AND 条件。即使某些 SQLAlchemy 内部结构使用了这种模式,它也不是一种文档化的模式

condition = None

for cond in conditions:
    condition = condition & cond

if condition is not None:
    stmt = stmt.where(condition)

conditions 为非空时,上面的序列在 0.9 版本上将产生 SELECT .. WHERE <condition> AND NULLNone 不再被隐式忽略,而是与在连接之外的其他上下文中解释 None 时保持一致。

0.8 和 0.9 版本的正确代码应如下所示

from sqlalchemy.sql import and_

if conditions:
    stmt = stmt.where(and_(*conditions))

另一种变体在 0.9 版本的所有后端上都有效,但在 0.8 版本上仅在支持布尔常量的后端上有效

from sqlalchemy.sql import true

condition = true()

for cond in conditions:
    condition = cond & condition

stmt = stmt.where(condition)

在 0.8 版本上,这将生成一个 SELECT 语句,该语句始终在 WHERE 子句中包含 AND true,这不被不支持布尔常量的后端(MySQL、MSSQL)接受。在 0.9 版本上,true 常量将在 and_() 连接中被删除。

create_engine() 的“password”部分不再将 + 号视为空格的编码形式

出于某种原因,Python 函数 unquote_plus() 被应用于 URL 的“password”字段,这是对 RFC 1738 中描述的编码规则的不正确应用,因为它将空格转义为加号。URL 的字符串化现在仅编码“:”、“@”或“/”,不再编码其他任何内容,并且现在应用于 usernamepassword 字段(以前仅应用于密码)。在解析时,编码字符被转换,但加号和空格按原样传递

# password: "pass word + other:words"
dbtype://user:pass word + other%3Awords@host/dbname

# password: "apples/oranges"
dbtype://username:apples%2Foranges@hostspec/database

# password: "apples@oranges@@"
dbtype://username:apples%40oranges%40%40@hostspec/database

# password: '', username is "username@"
dbtype://username%40:@hostspec/database

#2873

COLLATE 的优先级规则已更改

以前,如下表达式

print((column("x") == "somevalue").collate("en_EN"))

将生成如下表达式

-- 0.8 behavior
(x = :x_1) COLLATE en_EN

以上内容被 MSSQL 误解,并且通常不是任何数据库建议的语法。该表达式现在将生成大多数数据库文档说明的语法

-- 0.9 behavior
x = :x_1 COLLATE en_EN

如果 ColumnOperators.collate() 运算符应用于右手列,则可能会出现向后不兼容的更改,如下所示

print(column("x") == literal("somevalue").collate("en_EN"))

在 0.8 版本中,这将产生

x = :param_1 COLLATE en_EN

但是在 0.9 版本中,现在将生成更准确但可能不是您想要的格式

x = (:param_1 COLLATE en_EN)

ColumnOperators.collate() 运算符现在在 ORDER BY 表达式中也更适当地工作,因为 ASCDESC 运算符已被赋予特定的优先级,这将再次确保不生成括号

>>> # 0.8
>>> print(column("x").collate("en_EN").desc())
(x COLLATE en_EN) DESC
>>> # 0.9 >>> print(column("x").collate("en_EN").desc())
x COLLATE en_EN DESC

#2879

PostgreSQL CREATE TYPE <x> AS ENUM 现在对值应用引号

ENUM 类型现在将对枚举值中的单引号应用转义

>>> from sqlalchemy.dialects import postgresql
>>> type = postgresql.ENUM("one", "two", "three's", name="myenum")
>>> from sqlalchemy.dialects.postgresql import base
>>> print(base.CreateEnumType(type).compile(dialect=postgresql.dialect()))
CREATE TYPE myenum AS ENUM ('one','two','three''s')

已经转义单引号的现有解决方法需要修改,否则它们现在将双重转义。

#2878

新特性

事件移除 API

使用 listen()listens_for() 建立的事件现在可以使用新的 remove() 函数删除。remove()targetidentifierfn 参数需要与发送用于侦听的参数完全匹配,并且事件将从已建立的所有位置删除

@event.listens_for(MyClass, "before_insert", propagate=True)
def my_before_insert(mapper, connection, target):
    """listen for before_insert"""
    # ...


event.remove(MyClass, "before_insert", my_before_insert)

在上面的示例中,设置了 propagate=True 标志。这意味着 my_before_insert() 被建立为 MyClass 以及 MyClass 的所有子类的侦听器。系统跟踪 my_before_insert() 侦听器函数因该调用而被放置的所有位置,并因调用 remove() 而将其删除。

删除系统使用注册表将传递给 listen() 的参数与事件侦听器集合相关联,在许多情况下,这些集合是原始用户提供的函数的包装版本。此注册表大量使用弱引用,以便允许所有包含的内容(例如侦听器目标)在超出范围时被垃圾回收。

#2268

新的查询选项 API;load_only() 选项

加载器选项系统(例如 joinedload()subqueryload()lazyload()defer() 等)都建立在一个名为 Load 的新系统之上。Load 提供了一种“方法链式”(又名 生成式)的加载器选项方法,因此,不是使用点或多个属性名称将长路径连接在一起,而是为每个路径提供显式的加载器样式。

虽然新方法稍微冗长一些,但它更容易理解,因为要应用于哪些路径的选项没有歧义;它简化了选项的方法签名,并为基于列的选项提供了更大的灵活性。旧系统也将无限期地保持功能,并且可以混合所有样式。

旧方法

要沿多元素路径中的每个链接设置某种加载样式,必须使用 _all() 选项

query(User).options(joinedload_all("orders.items.keywords"))

新方法

加载器选项现在是可链接的,因此相同的 joinedload(x) 方法同样应用于每个链接,而无需在 joinedload()joinedload_all() 之间保持笔直

query(User).options(joinedload("orders").joinedload("items").joinedload("keywords"))

旧方法

在基于子类的路径上设置选项需要将路径中的所有链接都拼写为类绑定的属性,因为需要调用 PropComparator.of_type() 方法

session.query(Company).options(
    subqueryload_all(Company.employees.of_type(Engineer), Engineer.machines)
)

新方法

路径中实际上需要 PropComparator.of_type() 的那些元素才需要设置为类绑定的属性,之后可以恢复基于字符串的名称

session.query(Company).options(
    subqueryload(Company.employees.of_type(Engineer)).subqueryload("machines")
)

旧方法

在长路径中的最后一个链接上设置加载器选项使用的语法看起来很像应该为路径中的所有链接设置选项,从而引起混淆

query(User).options(subqueryload("orders.items.keywords"))

新方法

现在可以使用 defaultload() 为路径中应保持现有加载器样式不变的条目拼写路径。更冗长,但意图更明确

query(User).options(defaultload("orders").defaultload("items").subqueryload("keywords"))

仍然可以利用点状样式,尤其是在跳过多个路径元素的情况下

query(User).options(defaultload("orders.items").subqueryload("keywords"))

旧方法

路径上的 defer() 选项需要为每个列拼写完整路径

query(User).options(defer("orders.description"), defer("orders.isopen"))

新方法

单个到达目标路径的 Load 对象可以多次调用 Load.defer()

query(User).options(defaultload("orders").defer("description").defer("isopen"))

Load 类

Load 类可以直接用于提供“绑定”目标,尤其是在存在多个父实体时

from sqlalchemy.orm import Load

query(User, Address).options(Load(Address).joinedload("entries"))

仅加载

一个新的选项 load_only() 实现了“除了...之外全部延迟加载”的样式,仅加载给定的列,并延迟加载其余列

from sqlalchemy.orm import load_only

query(User).options(load_only("name", "fullname"))

# specify explicit parent entity
query(User, Address).options(Load(User).load_only("name", "fullname"))

# specify path
query(User).options(joinedload(User.addresses).load_only("email_address"))

类特定的通配符

使用 Load,可以使用通配符来设置给定实体上所有关系(或可能是列)的加载方式,而不会影响任何其他实体

# lazyload all User relationships
query(User).options(Load(User).lazyload("*"))

# undefer all User columns
query(User).options(Load(User).undefer("*"))

# lazyload all Address relationships
query(User).options(defaultload(User.addresses).lazyload("*"))

# undefer all Address columns
query(User).options(defaultload(User.addresses).undefer("*"))

#1418

新的 text() 功能

text() 构造获得了新的方法

  • TextClause.bindparams() 允许灵活设置绑定参数类型和值

    # setup values
    stmt = text(
        "SELECT id, name FROM user WHERE name=:name AND timestamp=:timestamp"
    ).bindparams(name="ed", timestamp=datetime(2012, 11, 10, 15, 12, 35))
    
    # setup types and/or values
    stmt = (
        text("SELECT id, name FROM user WHERE name=:name AND timestamp=:timestamp")
        .bindparams(bindparam("name", value="ed"), bindparam("timestamp", type_=DateTime()))
        .bindparam(timestamp=datetime(2012, 11, 10, 15, 12, 35))
    )
  • TextClause.columns() 取代了 text()typemap 选项,返回一个新的构造 TextAsFrom

    # turn a text() into an alias(), with a .c. collection:
    stmt = text("SELECT id, name FROM user").columns(id=Integer, name=String)
    stmt = stmt.alias()
    
    stmt = select([addresses]).select_from(
        addresses.join(stmt), addresses.c.user_id == stmt.c.id
    )
    
    
    # or into a cte():
    stmt = text("SELECT id, name FROM user").columns(id=Integer, name=String)
    stmt = stmt.cte("x")
    
    stmt = select([addresses]).select_from(
        addresses.join(stmt), addresses.c.user_id == stmt.c.id
    )

#2877

从 SELECT 插入

经过多年的无谓拖延,这个相对次要的语法功能已被添加,并且还向后移植到 0.8.3,因此从技术上讲,在 0.9 中不是“新的”。 一个 select() 构造或其他兼容的构造可以传递给新方法 Insert.from_select(),它将用于渲染 INSERT .. SELECT 构造

>>> from sqlalchemy.sql import table, column
>>> t1 = table("t1", column("a"), column("b"))
>>> t2 = table("t2", column("x"), column("y"))
>>> print(t1.insert().from_select(["a", "b"], t2.select().where(t2.c.y == 5)))
INSERT INTO t1 (a, b) SELECT t2.x, t2.y FROM t2 WHERE t2.y = :y_1

该构造足够智能,可以容纳 ORM 对象,例如类和 Query 对象

s = Session()
q = s.query(User.id, User.name).filter_by(name="ed")
ins = insert(Address).from_select((Address.id, Address.email_address), q)

渲染

INSERT INTO addresses (id, email_address)
SELECT users.id AS users_id, users.name AS users_name
FROM users WHERE users.name = :name_1

#722

select(), Query() 上新的 FOR UPDATE 支持

尝试简化核心和 ORM 中 SELECT 语句上 FOR UPDATE 子句的规范,并为 PostgreSQL 和 Oracle 支持的 FOR UPDATE OF SQL 添加了支持。

使用核心 GenerativeSelect.with_for_update(),可以单独指定诸如 FOR SHARENOWAIT 之类的选项,而不是链接到任意字符串代码

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

在 Posgtresql 上,以上语句可能渲染为

SELECT table.a, table.b FROM table FOR SHARE OF table NOWAIT

Query 对象获得了一个类似的方法 Query.with_for_update(),其行为方式相同。此方法取代了现有的 Query.with_lockmode() 方法,该方法使用不同的系统转换 FOR UPDATE 子句。目前,Session.refresh() 方法仍然接受 “lockmode” 字符串参数。

本机浮点类型的浮点字符串转换精度可配置

每当 DBAPI 返回要转换为 Python Decimal() 的 Python 浮点类型时,SQLAlchemy 所做的转换必然涉及一个中间步骤,该步骤将浮点值转换为字符串。此字符串转换使用的比例以前硬编码为 10,现在可以配置。该设置在 NumericFloat 类型以及所有 SQL 和特定于方言的后代类型上都可用,使用参数 decimal_return_scale。如果该类型支持 .scale 参数,就像 Numeric 和某些浮点类型(如 DOUBLE)一样,如果未另行指定,则 .scale 的值将用作 .decimal_return_scale 的默认值。如果 .scale.decimal_return_scale 都不存在,则默认值为 10。例如:

from sqlalchemy.dialects.mysql import DOUBLE
import decimal

data = Table(
    "data",
    metadata,
    Column("double_value", mysql.DOUBLE(decimal_return_scale=12, asdecimal=True)),
)

conn.execute(
    data.insert(),
    double_value=45.768392065789,
)
result = conn.scalar(select([data.c.double_value]))

# previously, this would typically be Decimal("45.7683920658"),
# e.g. trimmed to 10 decimal places

# now we get 12, as requested, as MySQL can support this
# much precision for DOUBLE
assert result == decimal.Decimal("45.768392065789")

#2867

ORM 查询的列束

Bundle 允许查询列集,然后将这些列集分组到一个名称下,该名称在查询返回的元组中。 Bundle 的初始目的是:1. 允许将“复合”ORM 列作为基于列的结果集中的单个值返回,而不是将它们展开为单独的列;2. 允许在 ORM 中创建自定义结果集构造,使用临时的列和返回类型,而无需涉及更重量级的映射类机制。

#2824

服务器端版本计数

ORM 的版本控制功能(现在也在 配置版本计数器 中记录)现在可以利用服务器端版本计数方案,例如触发器或数据库系统列生成的方案,以及版本计数器函数本身之外的有条件程序化方案。通过为 version_id_generator 参数提供值 False,ORM 将使用已设置的版本标识符,或者在发出 INSERT 或 UPDATE 时同时从每行获取版本标识符。当使用服务器生成的版本标识符时,强烈建议仅在具有强大 RETURNING 支持的后端(PostgreSQL、SQL Server;Oracle 也支持 RETURNING,但 cx_oracle 驱动程序仅提供有限的支持)上使用此功能,否则额外的 SELECT 语句将增加显著的性能开销。 服务器端版本计数器 提供的示例说明了如何使用 PostgreSQL xmin 系统列以将其与 ORM 的版本控制功能集成。

#2793

@validatesinclude_backrefs=False 选项

validates() 函数现在接受一个选项 include_backrefs=True,它将绕过从反向引用启动事件的情况下的验证器触发

from sqlalchemy import Column, Integer, ForeignKey
from sqlalchemy.orm import relationship, validates
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)
    bs = relationship("B", backref="a")

    @validates("bs")
    def validate_bs(self, key, item):
        print("A.bs validator")
        return item


class B(Base):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

    @validates("a", include_backrefs=False)
    def validate_a(self, key, item):
        print("B.a validator")
        return item


a1 = A()
a1.bs.append(B())  # prints only "A.bs validator"

#1535

PostgreSQL JSON 类型

PostgreSQL 方言现在具有 JSON 类型,以补充 HSTORE 类型。

另请参阅

JSON

#2581

Automap 扩展

0.9.1 中添加了一个新的扩展,称为 sqlalchemy.ext.automap。这是一个 实验性 扩展,它扩展了 Declarative 以及 DeferredReflection 类的功能。本质上,该扩展提供了一个基类 AutomapBase,它根据给定的表元数据自动生成映射类和它们之间的关系。

正常使用的 MetaData 可能是通过反射生成的,但是没有要求必须使用反射。最基本用法说明了 sqlalchemy.ext.automap 如何能够基于反射模式交付映射类,包括关系

from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import Session
from sqlalchemy import create_engine

Base = automap_base()

# engine, suppose it has two tables 'user' and 'address' set up
engine = create_engine("sqlite:///mydatabase.db")

# reflect the tables
Base.prepare(engine, reflect=True)

# mapped classes are now created with names matching that of the table
# name.
User = Base.classes.user
Address = Base.classes.address

session = Session(engine)

# rudimentary relationships are produced
session.add(Address(email_address="foo@bar.com", user=User(name="foo")))
session.commit()

# collection-based relationships are by default named "<classname>_collection"
print(u1.address_collection)

除此之外,AutomapBase 类是一个声明性基类,并且支持声明性所做的所有功能。“自动映射”功能可以与现有的显式声明的模式一起使用,以仅生成关系和缺失的类。可以使用可调用函数插入命名方案和关系生成例程。

希望 AutomapBase 系统为非常著名的 SQLSoup 也试图解决的问题提供了一个快速且现代化的解决方案,即从现有的数据库中动态生成一个快速而基本的对象模型。通过严格在映射器配置级别解决问题,并与现有的声明性类技术完全集成,AutomapBase 旨在为快速自动生成临时映射的问题提供一种良好集成的方法。

另请参阅

Automap

行为改进

应该不会产生兼容性问题的改进,除非在极其罕见和不寻常的假设情况下,但最好了解一下,以防出现意外问题。

许多 JOIN 和 LEFT OUTER JOIN 表达式将不再被包装在 (SELECT * FROM ..) AS ANON_1 中

多年来,SQLAlchemy ORM 一直受到限制,无法将 JOIN 嵌套在现有 JOIN 的右侧(通常是 LEFT OUTER JOIN,因为 INNER JOIN 始终可以展平)

SELECT a.*, b.*, c.* FROM a LEFT OUTER JOIN (b JOIN c ON b.id = c.id) ON a.id

这是因为 SQLite 直到 3.7.16 版本都无法解析上述格式的语句

SQLite version 3.7.15.2 2013-01-09 11:53:05
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> create table a(id integer);
sqlite> create table b(id integer);
sqlite> create table c(id integer);
sqlite> select a.id, b.id, c.id from a left outer join (b join c on b.id=c.id) on b.id=a.id;
Error: no such column: b.id

当然,右外连接是解决右侧括号的另一种方法;实现起来会非常复杂且视觉上不愉快,但幸运的是 SQLite 也不支持 RIGHT OUTER JOIN :)

sqlite> select a.id, b.id, c.id from b join c on b.id=c.id
   ...> right outer join a on b.id=a.id;
Error: RIGHT and FULL OUTER JOINs are not currently supported

早在 2005 年,尚不清楚其他数据库是否在这种形式上存在问题,但是今天看来,除了 SQLite 之外,所有经过测试的数据库现在都支持它(Oracle 8 是一个非常古老的数据库,根本不支持 JOIN 关键字,但是 SQLAlchemy 一直为 Oracle 的语法制定了简单的重写方案)。更糟糕的是,SQLAlchemy 常用的应用 SELECT 的解决方法通常会降低 PostgreSQL 和 MySQL 等平台上的性能

SELECT a.*, anon_1.* FROM a LEFT OUTER JOIN (
                SELECT b.id AS b_id, c.id AS c_id
                FROM b JOIN c ON b.id = c.id
            ) AS anon_1 ON a.id=anon_1.b_id

当使用连接表继承结构时,上述形式的 JOIN 很常见;任何时候使用 Query.join() 从某个父类连接到连接表子类时,或者当 joinedload() 以类似方式使用时,SQLAlchemy 的 ORM 始终会确保永远不会渲染嵌套的 JOIN,以免查询无法在 SQLite 上运行。即使 Core 始终支持更紧凑形式的 JOIN,ORM 也必须避免使用它。

当在多对多关系中生成连接,并且 ON 子句中存在特殊条件时,会出现另一个问题。考虑以下预先加载连接

session.query(Order).outerjoin(Order.items)

假设从 OrderItem 的多对多关系实际上引用了像 Subitem 这样的子类,则以上 SQL 看起来像

SELECT order.id, order.name
FROM order LEFT OUTER JOIN order_item ON order.id = order_item.order_id
LEFT OUTER JOIN item ON order_item.item_id = item.id AND item.type = 'subitem'

以上查询有什么问题? 基本上,它将加载许多 order / order_item 行,其中 item.type == 'subitem' 的条件不成立。

从 SQLAlchemy 0.9 开始,采用了一种全新的方法。 ORM 不再担心将 JOIN 嵌套在封闭 JOIN 的右侧,并且现在它将尽可能多地渲染这些 JOIN,同时仍然返回正确的结果。当 SQL 语句传递以进行编译时,方言编译器重写连接 以适应目标后端,如果已知该后端不支持右嵌套 JOIN(目前仅 SQLite - 如果其他后端有此问题,请告知我们!)。

因此,常规的 query(Parent).join(Subclass) 现在通常会产生更简单的表达式

SELECT parent.id AS parent_id
FROM parent JOIN (
        base_table JOIN subclass_table
        ON base_table.id = subclass_table.id) ON parent.id = base_table.parent_id

query(Parent).options(joinedload(Parent.subclasses)) 这样的连接预先加载将别名化各个表,而不是包装在 ANON_1

SELECT parent.*, base_table_1.*, subclass_table_1.* FROM parent
    LEFT OUTER JOIN (
        base_table AS base_table_1 JOIN subclass_table AS subclass_table_1
        ON base_table_1.id = subclass_table_1.id)
        ON parent.id = base_table_1.parent_id

多对多连接和预先加载将右嵌套“二级”表和“右”表

SELECT order.id, order.name
FROM order LEFT OUTER JOIN
(order_item JOIN item ON order_item.item_id = item.id AND item.type = 'subitem')
ON order_item.order_id = order.id

所有这些连接,当使用 Select 语句渲染时,该语句专门指定 use_labels=True,这对于 ORM 发出的所有查询都是正确的,都是“连接重写”的候选者,连接重写是将所有这些右嵌套连接重写为嵌套 SELECT 语句的过程,同时保持 Select 使用的相同标签。因此,SQLite 这个即使在 2013 年也不支持这种非常常见的 SQL 语法的数据库,承担了额外的复杂性,上述查询被重写为

-- sqlite only!
SELECT parent.id AS parent_id
    FROM parent JOIN (
        SELECT base_table.id AS base_table_id,
                base_table.parent_id AS base_table_parent_id,
                subclass_table.id AS subclass_table_id
        FROM base_table JOIN subclass_table ON base_table.id = subclass_table.id
    ) AS anon_1 ON parent.id = anon_1.base_table_parent_id

-- sqlite only!
SELECT parent.id AS parent_id, anon_1.subclass_table_1_id AS subclass_table_1_id,
        anon_1.base_table_1_id AS base_table_1_id,
        anon_1.base_table_1_parent_id AS base_table_1_parent_id
FROM parent LEFT OUTER JOIN (
    SELECT base_table_1.id AS base_table_1_id,
        base_table_1.parent_id AS base_table_1_parent_id,
        subclass_table_1.id AS subclass_table_1_id
    FROM base_table AS base_table_1
    JOIN subclass_table AS subclass_table_1 ON base_table_1.id = subclass_table_1.id
) AS anon_1 ON parent.id = anon_1.base_table_1_parent_id

-- sqlite only!
SELECT "order".id AS order_id
FROM "order" LEFT OUTER JOIN (
        SELECT order_item_1.order_id AS order_item_1_order_id,
            order_item_1.item_id AS order_item_1_item_id,
            item.id AS item_id, item.type AS item_type
FROM order_item AS order_item_1
    JOIN item ON item.id = order_item_1.item_id AND item.type IN (?)
) AS anon_1 ON "order".id = anon_1.order_item_1_order_id

注意

从 SQLAlchemy 1.1 开始,当检测到 SQLite 版本 3.7.16 或更高版本时,此功能中用于 SQLite 的解决方法将自动禁用,因为 SQLite 已经修复了对右嵌套连接的支持。

Join.alias(), aliased()with_polymorphic() 函数现在支持一个新参数 flat=True,该参数用于构造连接表实体的别名,而无需嵌入到 SELECT 中。 默认情况下,此标志未启用,以帮助向后兼容 - 但是现在可以将“多态”可选对象连接为目标,而无需生成任何子查询

employee_alias = with_polymorphic(Person, [Engineer, Manager], flat=True)

session.query(Company).join(Company.employees.of_type(employee_alias)).filter(
    or_(Engineer.primary_language == "python", Manager.manager_name == "dilbert")
)

生成(除 SQLite 外的所有地方)

SELECT companies.company_id AS companies_company_id, companies.name AS companies_name
FROM companies JOIN (
    people AS people_1
    LEFT OUTER JOIN engineers AS engineers_1 ON people_1.person_id = engineers_1.person_id
    LEFT OUTER JOIN managers AS managers_1 ON people_1.person_id = managers_1.person_id
) ON companies.company_id = people_1.company_id
WHERE engineers.primary_language = %(primary_language_1)s
    OR managers.manager_name = %(manager_name_1)s

#2369 #2587

连接预先加载中可用的右嵌套内连接

从 0.9.4 版本开始,在连接预先加载的情况下,当“外”连接链接到右侧的“内”连接时,可以启用上述右嵌套连接。

通常,像下面这样的连接预先加载链

query(User).options(
    joinedload("orders", innerjoin=False).joinedload("items", innerjoin=True)
)

不会生成内连接;由于从 user->order 的 LEFT OUTER JOIN,连接预先加载无法使用从 order->items 的 INNER JOIN 而不更改返回的用户行,而是会忽略“链式” innerjoin=True 指令。 0.9.0 应该如何交付此功能是,代替

FROM users LEFT OUTER JOIN orders ON <onclause> LEFT OUTER JOIN items ON <onclause>

新的“右嵌套连接没问题”逻辑将启动,我们将获得

FROM users LEFT OUTER JOIN (orders JOIN items ON <onclause>) ON <onclause>

由于我们错过了这一点,为了避免进一步的回归,我们通过将字符串 "nested" 指定给 joinedload.innerjoin 来添加了上述功能

query(User).options(
    joinedload("orders", innerjoin=False).joinedload("items", innerjoin="nested")
)

此功能是 0.9.4 中的新功能。

#2976

ORM 可以使用 RETURNING 有效地获取刚刚生成的 INSERT/UPDATE 默认值

Mapper 长期以来一直支持一个未记录的标志,称为 eager_defaults=True。 此标志的效果是,当 INSERT 或 UPDATE 继续进行,并且已知该行具有服务器生成的默认值时,SELECT 将立即跟随其后,以便“急切地”加载这些新值。 通常,服务器生成的列在对象上标记为“已过期”,因此除非应用程序在刷新后很快访问这些列,否则不会产生任何开销。 因此,eager_defaults 标志用途不大,因为它只会降低性能,并且仅用于支持奇异的事件方案,在这些方案中,用户需要在刷新过程中立即获得默认值。

在 0.9 中,由于版本 ID 增强,eager_defaults 现在可以为这些值发出 RETURNING 子句,因此在具有强大 RETURNING 支持的后端(尤其是 PostgreSQL)上,ORM 可以在 INSERT 或 UPDATE 中内联获取新生成的默认值和 SQL 表达式值。 当启用 eager_defaults 时,当目标后端和 Table 支持“隐式返回”时,将自动使用 RETURNING。

子查询预先加载将对某些查询的最内层 SELECT 应用 DISTINCT

为了减少当涉及多对一关系时子查询预先加载可能生成的重复行数,当连接的目标列不包含主键时,将对最内层 SELECT 应用 DISTINCT 关键字,例如在沿多对一加载时。

也就是说,当在从 A->B 的多对一关系上进行子查询加载时

SELECT b.id AS b_id, b.name AS b_name, anon_1.b_id AS a_b_id
FROM (SELECT DISTINCT a_b_id FROM a) AS anon_1
JOIN b ON b.id = anon_1.a_b_id

由于 a.b_id 是非唯一外键,因此应用 DISTINCT 以消除冗余的 a.b_id。 可以使用标志 distinct_target_key 无条件地打开或关闭特定 relationship() 的行为,将值设置为 True 表示无条件打开,False 表示无条件关闭,None 表示当目标 SELECT 针对不包含完整主键的列时,该功能生效。 在 0.9 中,None 是默认值。

该选项也向后移植到 0.8,其中 distinct_target_key 选项默认为 False

虽然此处的这项功能旨在通过消除重复行来帮助提高性能,但 SQL 中的 DISTINCT 关键字本身可能会对性能产生负面影响。 如果 SELECT 中的列未被索引,则 DISTINCT 可能会在行集上执行 ORDER BY,这可能会很昂贵。 通过将该功能限制为仅用于外键(无论如何,外键有望被索引),预计新的默认值是合理的。

该功能也不能消除每种可能的重复行场景; 如果在连接链的其他地方存在多对一关系,则可能仍然存在重复行。

#2836

反向引用处理程序现在可以传播超过一个级别深度

属性事件传递其“发起者”的机制,即与事件开始关联的对象,已更改; 而不是传递 AttributeImpl,而是传递一个新的对象 Event; 此对象引用 AttributeImpl 以及“操作令牌”,表示该操作是追加、删除还是替换操作。

属性事件系统不再查看此“发起者”对象以停止一系列递归属性事件。 相反,由于相互依赖的反向引用处理程序而阻止无限递归的系统已专门移动到 ORM 反向引用事件处理程序,这些处理程序现在承担确保相互依赖事件链(例如附加到集合 A.bs,响应中设置多对一属性 B.a)不会进入无限递归流的角色。 这里的理由是,反向引用系统可以更详细地控制事件传播,最终允许发生超过一个级别的深度操作; 典型的场景是集合追加导致多对一替换操作,而这反过来应该导致项从先前的集合中删除

class Parent(Base):
    __tablename__ = "parent"

    id = Column(Integer, primary_key=True)
    children = relationship("Child", backref="parent")


class Child(Base):
    __tablename__ = "child"

    id = Column(Integer, primary_key=True)
    parent_id = Column(ForeignKey("parent.id"))


p1 = Parent()
p2 = Parent()
c1 = Child()

p1.children.append(c1)

assert c1.parent is p1  # backref event establishes c1.parent as p1

p2.children.append(c1)

assert c1.parent is p2  # backref event establishes c1.parent as p2
assert c1 not in p1.children  # second backref event removes c1 from p1.children

在上面,在此更改之前,c1 对象仍将存在于 p1.children 中,即使它也同时存在于 p2.children 中; 反向引用处理程序将在将 c1.parent 替换为 p2 而不是 p1 时停止。 在 0.9 中,使用更详细的 Event 对象,并让反向引用处理程序对这些对象做出更详细的决策,传播可以继续到从 p1.children 中删除 c1,同时保持检查以防止传播进入无限递归循环。

最终用户代码 a. 使用 AttributeEvents.set(), AttributeEvents.append(), 或 AttributeEvents.remove() 事件,并且 b. 由于这些事件而启动进一步的属性修改操作,可能需要进行修改以防止递归循环,因为在没有反向引用事件处理程序的情况下,属性系统不再阻止事件链无限期地传播。 此外,依赖于 initiator 值的代码将需要调整为新的 API,并且还必须为 initiator 的值做好准备,以从一系列反向引用启动的事件中的原始值更改,因为反向引用处理程序现在可能会为某些操作换入新的 initiator 值。

#2789

类型系统现在处理渲染“字面绑定”值的任务

TypeEngine 添加了一个新方法 TypeEngine.literal_processor() 以及 TypeDecorator.process_literal_param(),用于 TypeDecorator,它承担渲染所谓的“内联字面参数”的任务 - 通常渲染为“绑定”值的参数,但由于编译器配置而改为内联渲染到 SQL 语句中。 此功能在为诸如 CheckConstraint 之类的构造生成 DDL 时以及 Alembic 在使用诸如 op.inline_literal() 之类的构造时使用。 以前,一个简单的 “isinstance” 检查检查了一些基本类型,并且无条件地使用了“绑定处理器”,从而导致诸如字符串被过早编码为 utf-8 的问题。

使用 TypeDecorator 编写的自定义类型应继续在“内联字面量”场景中工作,因为 TypeDecorator.process_literal_param() 默认回退到 TypeDecorator.process_bind_param(),因为这些方法通常处理数据操作,而不是数据如何呈现给数据库。 可以指定 TypeDecorator.process_literal_param() 以专门生成一个字符串,该字符串表示值应如何渲染到内联 DDL 语句中。

#2838

模式标识符现在携带自己的引用信息

此更改简化了 Core 对所谓“引用”标志的使用,例如传递给 TableColumnquote 标志。该标志现在已在字符串名称本身内部化,该名称现在表示为 quoted_name(字符串子类)的实例。IdentifierPreparer 现在完全依赖于 quoted_name 对象报告的引用偏好,而不是在大多数情况下检查任何显式的 quote 标志。此处解决的问题包括各种区分大小写的方法,例如 Engine.has_table() 以及方言中的类似方法,现在可以使用显式引用的名称,而无需使用引用标志的详细信息来复杂化或引入向后不兼容的更改到这些 API(其中许多是第三方 API) - 特别是,更广泛的标识符现在可以在所谓的“大写”后端(如 Oracle、Firebird 和 DB2)上正确运行(这些后端使用全大写存储和报告表名和列名,以实现不区分大小写的名称)。

quoted_name 对象在内部根据需要使用;但是,如果其他关键字需要固定的引用偏好,则该类是公开可用的。

#2812

改进了布尔常量、NULL 常量、连词的呈现

true()false() 常量添加了新功能,特别是与 and_()or_() 函数以及 WHERE/HAVING 子句与这些类型、总体布尔类型和 null() 常量的行为结合使用时。

从如下表开始

from sqlalchemy import Table, Boolean, Integer, Column, MetaData

t1 = Table("t", MetaData(), Column("x", Boolean()), Column("y", Integer))

现在,select 构造将在不具有 true/false 常量行为的后端上将布尔列呈现为二进制表达式

>>> from sqlalchemy import select, and_, false, true
>>> from sqlalchemy.dialects import mysql, postgresql

>>> print(select([t1]).where(t1.c.x).compile(dialect=mysql.dialect()))
SELECT t.x, t.y FROM t WHERE t.x = 1

and_()or_() 构造现在将表现出准“短路”行为,即当存在 true()false() 常量时,会截断呈现的表达式

>>> print(
...     select([t1]).where(and_(t1.c.y > 5, false())).compile(dialect=postgresql.dialect())
... )
SELECT t.x, t.y FROM t WHERE false

true() 可以用作构建表达式的基础

>>> expr = true()
>>> expr = expr & (t1.c.y > 5)
>>> print(select([t1]).where(expr))
SELECT t.x, t.y FROM t WHERE t.y > :y_1

布尔常量 true()false() 本身对于没有布尔常量的后端呈现为 0 = 11 = 1

>>> print(select([t1]).where(and_(t1.c.y > 5, false())).compile(dialect=mysql.dialect()))
SELECT t.x, t.y FROM t WHERE 0 = 1

None 的解释虽然不是特别有效的 SQL,但至少现在是一致的

>>> print(select([t1.c.x]).where(None))
SELECT t.x FROM t WHERE NULL
>>> print(select([t1.c.x]).where(None).where(None))
SELECT t.x FROM t WHERE NULL AND NULL
>>> print(select([t1.c.x]).where(and_(None, None)))
SELECT t.x FROM t WHERE NULL AND NULL

#2804

Label 构造现在可以在 ORDER BY 中仅呈现其名称

对于 Label 在 SELECT 的 columns 子句和 ORDER BY 子句中都使用的情况,标签将在 ORDER BY 子句中仅呈现为其名称,前提是底层方言报告支持此功能。

例如,像这样的例子

from sqlalchemy.sql import table, column, select, func

t = table("t", column("c1"), column("c2"))
expr = (func.foo(t.c.c1) + t.c.c2).label("expr")

stmt = select([expr]).order_by(expr)

print(stmt)

在 0.9 之前会呈现为

SELECT foo(t.c1) + t.c2 AS expr
FROM t ORDER BY foo(t.c1) + t.c2

现在呈现为

SELECT foo(t.c1) + t.c2 AS expr
FROM t ORDER BY expr

仅当标签未进一步嵌入到 ORDER BY 中的表达式中(除了简单的 ASCDESC 之外),ORDER BY 才会呈现标签。

上述格式适用于所有经过测试的数据库,但可能与较旧的数据库版本(MySQL 4?Oracle 8?等)存在兼容性问题。根据用户报告,我们可以添加规则,根据数据库版本检测禁用该功能。

#1068

RowProxy 现在具有元组排序行为

RowProxy 对象的作用很像元组,但到目前为止,如果使用 sorted() 对它们的列表进行排序,则不会像元组一样排序。__eq__() 方法现在将两侧作为元组进行比较,并且还添加了 __lt__() 方法

users.insert().execute(
    dict(user_id=1, user_name="foo"),
    dict(user_id=2, user_name="bar"),
    dict(user_id=3, user_name="def"),
)

rows = users.select().order_by(users.c.user_name).execute().fetchall()

eq_(rows, [(2, "bar"), (3, "def"), (1, "foo")])

eq_(sorted(rows), [(1, "foo"), (2, "bar"), (3, "def")])

#2848

当类型可用时,没有类型的 bindparam() 构造会通过复制进行升级

当封闭表达式的类型可用时,“升级” bindparam() 构造的逻辑在两个方面得到了改进。首先,bindparam() 对象在分配新类型之前会被复制,以便给定的 bindparam() 不会被就地修改。其次,当编译 InsertUpdate 构造时,也会发生相同的操作,关于通过 ValuesBase.values() 方法在语句中设置的“值”。

如果给定一个未类型的 bindparam()

bp = bindparam("some_col")

如果我们按如下方式使用此参数

expr = mytable.c.col == bp

bp 的类型仍然是 NullType,但是如果 mytable.c.col 的类型是 String,那么 expr.right,即二进制表达式的右侧,将采用 String 类型。以前,bp 本身会被就地更改为具有 String 作为其类型。

类似地,此操作发生在 InsertUpdate

stmt = mytable.update().values(col=bp)

在上面,bp 保持不变,但是在执行语句时将使用 String 类型,我们可以通过检查 binds 字典看到这一点

>>> compiled = stmt.compile()
>>> compiled.binds["some_col"].type
String

该功能允许自定义类型在 INSERT/UPDATE 语句中发挥其预期效果,而无需在每个 bindparam() 表达式中显式指定这些类型。

潜在的向后兼容性更改涉及两种不太可能发生的情况。由于绑定参数是克隆的,因此用户不应依赖于在创建 bindparam() 构造后对其进行就地更改。此外,在 InsertUpdate 语句中使用 bindparam() 的代码,该代码依赖于 bindparam() 未根据分配给它的列进行类型化这一事实,将不再以这种方式工作。

#2850

列可以可靠地从通过 ForeignKey 引用的列获取其类型

存在一个长期存在的行为,即声明 Column 时可以不带类型,只要该 ColumnForeignKeyConstraint 引用,并且引用列的类型将被复制到此列中。问题是此功能一直运行不佳且未得到维护。核心问题是 ForeignKey 对象不知道它引用的目标 Column,直到被询问,通常是第一次使用外键构造 Join 时。因此,在那之前,父 Column 将没有类型,或者更具体地说,它将具有 NullType 的默认类型。

虽然花费了很长时间,但重新组织 ForeignKey 对象初始化的工作已经完成,因此此功能最终可以令人接受地工作。此更改的核心是 ForeignKey.column 属性不再延迟初始化目标 Column 的位置;此系统的原因是,拥有 Column 的类型将一直卡在 NullType,直到 ForeignKey 恰好被使用。

在新版本中,ForeignKey 与它将引用的最终 Column 协调,使用内部附加事件,以便在引用 ColumnMetaData 关联的那一刻,所有引用它的 ForeignKey 对象都将收到一条消息,表明它们需要初始化其父列。此系统更复杂,但工作更可靠;作为奖励,现在有针对各种 Column / ForeignKey 配置场景的测试,并且错误消息已得到改进,对于不少于七种不同的错误情况非常具体。

现在可以正确工作的场景包括

  1. Column 上的类型在目标 Column 与同一 MetaData 关联后立即出现;无论哪一方先配置,这都有效

    >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKey
    >>> metadata = MetaData()
    >>> t2 = Table("t2", metadata, Column("t1id", ForeignKey("t1.id")))
    >>> t2.c.t1id.type
    NullType()
    >>> t1 = Table("t1", metadata, Column("id", Integer, primary_key=True))
    >>> t2.c.t1id.type
    Integer()
  2. 该系统现在也适用于 ForeignKeyConstraint

    >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKeyConstraint
    >>> metadata = MetaData()
    >>> t2 = Table(
    ...     "t2",
    ...     metadata,
    ...     Column("t1a"),
    ...     Column("t1b"),
    ...     ForeignKeyConstraint(["t1a", "t1b"], ["t1.a", "t1.b"]),
    ... )
    >>> t2.c.t1a.type
    NullType()
    >>> t2.c.t1b.type
    NullType()
    >>> t1 = Table(
    ...     "t1",
    ...     metadata,
    ...     Column("a", Integer, primary_key=True),
    ...     Column("b", Integer, primary_key=True),
    ... )
    >>> t2.c.t1a.type
    Integer()
    >>> t2.c.t1b.type
    Integer()
  3. 它甚至适用于“多跳” - 也就是说,ForeignKey 引用 Column,而该 Column 又引用另一个 Column

    >>> from sqlalchemy import Table, MetaData, Column, Integer, ForeignKey
    >>> metadata = MetaData()
    >>> t2 = Table("t2", metadata, Column("t1id", ForeignKey("t1.id")))
    >>> t3 = Table("t3", metadata, Column("t2t1id", ForeignKey("t2.t1id")))
    >>> t2.c.t1id.type
    NullType()
    >>> t3.c.t2t1id.type
    NullType()
    >>> t1 = Table("t1", metadata, Column("id", Integer, primary_key=True))
    >>> t2.c.t1id.type
    Integer()
    >>> t3.c.t2t1id.type
    Integer()

#1765

方言更改

Firebird fdb 现在是默认的 Firebird 方言。

如果引擎在没有方言说明符的情况下创建,即 firebird://,则现在使用 fdb 方言。fdb 是一个与 kinterbasdb 兼容的 DBAPI,根据 Firebird 项目的说法,它现在是他们的官方 Python 驱动程序。

#2504

Firebird fdbkinterbasdb 默认设置 retaining=False

fdbkinterbasdb DBAPI 都支持一个标志 retaining=True,可以将其传递给其连接的 commit()rollback() 方法。此标志的文档化理由是,DBAPI 可以为后续事务重用内部事务状态,以提高性能。但是,较新的文档提到了对 Firebird“垃圾回收”的分析,该分析表明此标志可能会对数据库处理清理任务的能力产生负面影响,并且据报告会降低性能。

鉴于此信息,尚不清楚此标志实际上如何可用,并且由于它似乎只是一个性能增强功能,因此现在默认为 False。可以通过将标志 retaining=True 传递给 create_engine() 调用来控制该值。这是一个自 0.8.2 版起添加的新标志,因此 0.8.2 版上的应用程序可以开始根据需要将其设置为 TrueFalse

另请参阅

sqlalchemy.dialects.firebird.fdb

sqlalchemy.dialects.firebird.kinterbasdb

https://pythonhosted.org/fdb/usage-guide.html#retaining-transactions - 关于“retaining”标志的信息。

#2763