SqlAlchemy-2-0-中文文档-五十一-

SqlAlchemy 2.0 中文文档(五十一)

原文:docs.sqlalchemy.org/en/20/contents.html

SQLAlchemy 0.9 中的新功能是什么?

原文:docs.sqlalchemy.org/en/20/changelog/migration_09.html

关于本文档

本文档描述了 SQLAlchemy 版本 0.8 与版本 0.9 之间的变化,截至 2013 年 5 月,0.8 版本正在进行维护,而 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

C 扩展在 Python 3 上得到支持

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 ### viewonly=True on relationship() prevents history from taking effect

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()的 bug,允许基于列的属性向数据库查询未加载的值,假设 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)

这是一个非常古老的 bug,为此在 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 中的“catch all” 构造函数被移除,这些无意义的参数不再被接受。

使用方言特定参数如 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)

在 0.9 上,当 conditions 不为空时,将产生 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 上,这将生成一个始终在 WHERE 子句中具有 AND true 的 SELECT 语句,这是不被不支持布尔常量的后端(MySQL,MSSQL)接受的。在 0.9 上,true 常量将在 and_() 连接中被删除。

另请参阅

布尔常量、NULL 常量、连接词的渲染已经得到改进

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 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 类可以直接用于提供“绑定”目标,特别是当存在多个父实体时:

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 

该构造足够智能,也可以适应诸如类和 Query 对象等 ORM 对象:

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

试图简化 Core 和 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 子句。目前,“lockmode” 字符串参数仍然被 Session.refresh() 方法接受。### 可配置原生浮点类型的浮点字符串转换精度

每当 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 中创建自定义结果集构造,使用临时列和返回类型,而不涉及映射类的更重量级机制。

另请参见

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

使用 Bundles 对选定属性进行分组

#2824

服务器端版本计数

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

另请参见

服务器端版本计数器

#2793

include_backrefs=False 选项用于 @validates

validates() 函数现在接受一个选项 include_backrefs=True,这将绕过为从 backref 发起的事件触发验证器的情况:

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 的新扩展。 这是一个 实验性 扩展,它扩展了声明性的功能以及 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 通常的解决方法是在像 PostgreSQL 和 MySQL 这样的平台上应用 SELECT 通常会降低性能:

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,如果该后端已知不支持右嵌套 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

多对多连接和急加载将右嵌套“secondary”和“right”表:

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 的解决方法将在检测到 SQLite 版本3.7.16或更高版本时自动禁用自身,因为 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>

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

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 时,将自动使用 RETURNING,当目标后端和 Table 支持“隐式返回”时。

对于某些查询,子查询预加载将在最内层的 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。此行为可以通过为特定的 relationship() 设置标志 distinct_target_key 来无条件地打开或关闭,将值设置为 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中;反向引用处理程序会停止在用p2替换c1.parent而不是p1的操作上。在 0.9 版本中,使用更详细的Event对象以及让反向引用处理程序对这些对象做出更详细的决策,传播可以继续进行,从而将c1p1.children中移除,同时保持检查以防止传播进入无限递归循环。

使用 AttributeEvents.set()AttributeEvents.append()AttributeEvents.remove() 事件的终端用户代码可能需要修改,以防止递归循环,因为在缺少反向引用事件处理程序的情况下,属性系统不再阻止事件链无限传播。此外,依赖于 initiator 值的代码将需要调整到新的 API,并且还必须准备好 initiator 值在一系列反向引用引发的事件中从其原始值更改,因为现在反向引用处理程序可能会为某些操作交换一个新的 initiator 值。

#2789 ### 类型系统现在处理呈现“字面绑定”值的任务

TypeEngineTypeDecorator 分别添加了新的方法 TypeEngine.literal_processor()TypeDecorator.process_literal_param(),它们负责呈现所谓的“内联字面参数” - 由于编译器配置的原因,这些参数通常呈现为“绑定”值,但实际上是被内联呈现到 SQL 语句中。此功能用于生成诸如 CheckConstraint 这样的结构的 DDL,以及当使用诸如 op.inline_literal() 这样的结构时,Alembic 会使用它。以前,一个简单的“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(其中许多是第三方的) - 特别是,更广泛范围的标识符现在能够与所谓的“大写”后端(像 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))

在不具有 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 ### 标签构造现在可以��� ORDER BY 中仅呈现为其名称

在 SELECT 的列子句和 ORDER BY 子句中都使用 Label 的情况下,标签将仅在 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 仅在标签未在 ORDER BY 中进一步嵌入到表达式中时呈现标签,除了简单的 ASCDESC

上述格式在所有经过测试的数据库上都有效,但可能与旧数据库版本(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()方法设置的“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()构造进行就地更改。此外,使用bindparam()在依赖于bindparam()未根据分配给的列进行类型化的事实的InsertUpdate语句的代码将不再以这种方式运行。

#2850 ### 列可以可靠地从通过外键引用的列中获取其类型

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

尽管花费了很长时间,重新组织ForeignKey对象初始化的工作已经完成,使得这个功能最终可以正常工作。这个改变的核心是ForeignKey.column属性不再延迟初始化目标Column的位置;这个系统的问题在于拥有的Column会一直被固定为NullType类型,直到ForeignKey被使用。

在新版本中,ForeignKey通过内部附加事件与最终引用的Column协调,因此一旦引用的ColumnMetaData关联,所有引用它的ForeignKey对象都会收到一条消息,告诉它们需要初始化其父列。这个系统更加复杂,但更加稳固;作为奖励,现在已经为各种Column / ForeignKey配置场景设置了测试,并且错误消息已经改进,对不少于七种不同的错误条件进行了非常具体的描述。

现在正确工作的场景包括:

  1. 当目标Column与相同的MetaData关联时,Column上的类型会立即出现;无论哪一边先配置都可以:

    >>> 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. 它甚至适用于“多跳” - 即,一个指向另一个ColumnColumnForeignKey:

    >>> 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 方言。

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

#2504

Firebird fdbkinterbasdb 默认设置 retaining=False

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

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

另请参阅

sqlalchemy.dialects.firebird.fdb

sqlalchemy.dialects.firebird.kinterbasdb

pythonhosted.org/fdb/usage-guide.html#retaining-transactions - 有关“保留”标志的信息。

#2763

介绍

本指南介绍了 SQLAlchemy 版本 0.9 中的新功能,并记录了影响用户将其应用程序从 SQLAlchemy 0.8 系列迁移到 0.9 的变化。

请仔细查看行为变化 - ORM 和行为变化 - Core,可能存在不兼容的变化。

平台支持

现在针对 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

C 扩展在 Python 3 上受支持

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

#2161

现在目标是 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

C 扩展在 Python 3 上受支持

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 ### viewonly=Truerelationship() 上阻止历史记录生效

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()的修复 bug 允许基于列的属性查询到数据库中未加载的值,假设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 ### 当按属性基础查询时,复合属性现在以其对象形式返回

现在,与复合属性一起使用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)

这是一个非常古老的 bug,为此在 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中的“catch all”构造函数被移除,这些无意义的参数不再被接受。

利用方言特定参数如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_()连接中被删除。

另请参见

改进的布尔常量、NULL 常量、连接词的呈现方式

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

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

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

这是一个非常古老的 bug,已经在 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中的“catch all”构造函数已被移除,这些无意义的参数不再被接受。

使用方言特定参数(如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_()连接中被删除。

另请参阅

布尔常量、NULL 常量、连接的改进渲染

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 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 及其所有子类的监听器。系统跟踪了 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 支持

尝试简化在 Core 和 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 子句。目前,“lockmode”字符串参数仍然被 Session.refresh() 方法接受。 ### 本机浮点字符串转换精度可配置的浮点类型

每当 DBAPI 返回要转换为 Python Decimal() 的 Python 浮点类型时,SQLAlchemy 所做的转换必然涉及将浮点值转换为字符串的中间步骤。此字符串转换的比例以前是硬编码为 10,现在可配置。该设置可用于 Numeric 以及 Float 类型,以及所有 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 中创建自定义结果集构造,使用临时列和返回类型,而不涉及映射类的更重量级机制。

另请参阅

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

使用 Bundles 分组选定属性

#2824

服务器端版本计数

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

另请参阅

服务器端版本计数器

#2793

include_backrefs=False 选项用于 @validates

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

自动映射扩展

新的扩展在0.9.1中添加,称为sqlalchemy.ext.automap。这是一个实验性扩展,它扩展了声明式的功能以及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旨在为快速自动生成临时映射的问题提供一个良好集成的方法。

另请参阅

自动映射 ### 事件移除 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类可以直接用于提供“绑定”目标,特别是当存在多个父实体时:

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

加载类

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

尝试简化在 Core 和 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 子句。目前,“lockmode” 字符串参数仍然被 Session.refresh() 方法接受。

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

每当 DBAPI 返回一个要转换为 Python Decimal() 的 Python 浮点类型时,SQLAlchemy 所做的转换必然涉及一个中间步骤,将浮点值转换为字符串。用于此字符串转换的标度以前是硬编码为 10,现在是可配置的。该设置在 Numeric 以及 Float 类型上都可用,以及所有 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 的版本控制功能(现在还在 配置版本计数器 中有文档记录)现在可以利用服务器端的版本计数方案,例如由触发器或数据库系统列生成的方案,以及版本 _id_counter 函数本身之外的条件编程方案。通过向 version_id_generator 参数提供值 False,ORM 将使用已设置的版本标识符,或者在发出 INSERT 或 UPDATE 时同时从每一行获取版本标识符。当使用服务器生成的版本标识符时,强烈建议仅在具有强大 RETURNING 支持的后端上使用此功能(PostgreSQL、SQL Server;Oracle 也支持 RETURNING,但 cx_oracle 驱动程序仅具有有限的支持),否则额外的 SELECT 语句将增加显著的性能开销。在 服务器端版本计数器 提供的示例中说明了使用 PostgreSQL xmin 系统列将其与 ORM 的版本控制功能集成的用法。

另请参阅

服务器端版本计数器

#2793

include_backrefs=False 选项用于 @validates

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的扩展。这是一个实验性扩展,它扩展了声明式以及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旨在提供一个完全集成的方法来解决迅速自动生成临时映射的问题。

另请参阅

自动映射

行为改进

改进应该不会产生兼容性问题,除非在极为罕见和不寻常的假设情况下,但如果有意外问题,了解这些改进是很好的。

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

多年来,SQLAlchemy ORM 一直无法在现有 JOIN 的右侧嵌套 JOIN(通常是 LEFT OUTER JOIN),因为内部 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 常规的解决方法在诸如 PostgreSQL 和 MySQL 等平台上应用 SELECT 通常会降低性能:

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 子句中存在特殊条件,将会出现另一个问题。考虑下面这样的 eager load 连接:

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以适应目标后端,如果该后端已知不支持右嵌套 JOIN(目前只有 SQLite - 如果其他后端也有此问题,请告诉我们!)。

因此,现在通常会生成一个更简单的表达式:

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))这样的 eager loads 会给每个表起别名,而不是包装在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

多对多连接和 eagerloads 将会将“secondary”和“right”表右嵌套:

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

所有这些连接,当与明确指定use_labels=TrueSelect语句一起渲染时,这对于 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 的解决方法将在检测到 SQLite 版本3.7.16或更高版本时自动禁用,因为 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)
)

不会产生内连接;因为从用户->订单的 LEFT OUTER JOIN,连接的急切加载不能使用从订单->项目的 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。可以针对特定的relationship()使用distinct_target_key标志来无条件地打开或关闭此行为,将值设置为True表示无条件打开,False表示无条件关闭,None表示当目标 SELECT 针对不包含完整主键的列时才生效。在 0.9 版本中,None是默认值。

该选项也被回溯到了 0.8 版本,其中distinct_target_key选项的默认值为False

尽管此功能旨在通过消除重复行来提高性能,但 SQL 中的DISTINCT关键字本身可能会对性能产生负面影响。如果 SELECT 中的列没有索引,DISTINCT可能会对行集执行ORDER BY,这可能是昂贵的。通过将该功能限制在希望在任何情况下都具有索引的外键上,预计新的默认值是合理的。

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

#2836 ### Backref 处理程序现在可以传播超过一层

属性事件沿着它们的“发起者”传递的机制已经发生了变化;不再传递AttributeImpl,而是传递一个新的对象Event;这个对象同时指向AttributeImpl和一个“操作令牌”,表示操作是追加、删除还是替换操作。

属性事件系统不再查看这个“initiator”对象以阻止一系列递归属性事件。相反,防止由于相互依赖的返回处理程序而导致无限递归的系统已经移动到了 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.parentp2而不是p1。在 0.9 版本中,使用更详细的Event对象以及让返回处理程序对这些对象做出更详细的决策,传播可以继续到从p1.children中移除c1,同时保持对传播进入无限递归循环的检查。

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

#2789 ### 类型系统现在处理呈现“文字绑定”值的任务

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

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

#2838 ### 架构标识符现在携带其自身的引号信息

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

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

#2812 ### 改进的布尔常量、NULL 常量、连接的渲染

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

从这样的表格开始:

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

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

在不支持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 ### 标签构造现在可以仅在 ORDER BY 中呈现为它们的名称

对于在 SELECT 的列子句和 ORDER BY 子句中都使用Label的情况,假设底层方言报告支持此功能,则标签将仅在 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 中的表达式中时,ORDER BY 才会呈现标签,除了简单的ASCDESC

上述格式在所有经过测试的数据库上都有效,但可能与旧数据库版本(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()方法在语句中设置的“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 ### Columns can reliably get their type from a column referred to via ForeignKey

有一个长期存在的行为,它规定可以声明一个Column而不需要类型,只要这个Column被一个ForeignKeyConstraint所引用,并且被引用的列的类型会被复制到这个列中。问题在于,这个特性从来没有很好地工作过,并且没有得到维护。核心问题是ForeignKey对象在被询问之前不知道它引用的目标Column是哪一个,通常是第一次使用外键来构造一个Join时。因此,在那之前,父Column将没有类型,或者更具体地说,它将具有一个NullType的默认类型。

虽然花费了很长时间,重新组织ForeignKey对象的初始化工作已经完成,以便使这一功能最终能够令人满意地运行。这一变化的核心是,ForeignKey.column属性不再延迟初始化目标Column的位置;这一系统的问题在于,拥有的Column会一直被固定为NullType类型,直到ForeignKey被使用。

在新版本中,ForeignKey通过内部附加事件与最终将引用的Column协调,因此一旦引用的ColumnMetaData关联,所有引用它的ForeignKey对象都将收到一条消息,告诉它们需要初始化其父列。这一系统更加复杂,但更加稳固;作为奖励,现在已经为各种Column / ForeignKey配置场景设置了测试,并且错误消息已经改进,以便非常具体地指出不少于七种不同的错误条件。

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

  1. 一旦目标Column与相同的MetaData关联,Column上的类型立即存在;无论哪一侧首先配置,这都能正常工作:

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

    >>> 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 ### 许多 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,并且现在将尽可能经常地呈现这些,同时仍然返回正确的结果。当 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

所有这些连接,当与明确指定 use_labels=TrueSelect 语句一起呈现时,这对于 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 的解决方法将在检测到 SQLite 版本 3.7.16 或更高版本时自动禁用自身,因为 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。可以通过在特定的relationship()上设置distinct_target_key标志来无条件地打开或关闭此行为,将值设置为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.parentp2而不是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,以及在使用诸如op.inline_literal()之类的结构时,被 Alembic 使用。以前,一个简单的“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(其中许多是第三方的)的引号标志的变更。特别是,更广泛范围的标识符现在可以与所谓的“大写”后端(如 Oracle、Firebird 和 DB2)正确地工作,这些后端使用全大写存储和报告不区分大小写的名称的表和列名称。

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

#2812

改进的布尔常量、NULL 常量、连接的���现

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

从这样的表开始:

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

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

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

>>> 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 中仅呈现为其名称

对于在 SELECT 的列子句和 ORDER BY 子句中都使用Label的情况,假设底层方言报告支持此功能,则标签将仅在 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 中的表达式中时,ORDER BY 才会呈现标签,除了简单的ASCDESC

上述格式在所有经过测试的数据库上都有效,但可能与旧版本数据库(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()方法在语句中设置的“values”也会发生相同的操作。

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

bp = bindparam("some_col")

如果我们使用这个参数如下:

expr = mytable.c.col == bp

对于bp的类型仍然是NullType,但是如果mytable.c.colString类型,则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()构造进行就地更改。此外,使用bindparam()的代码在InsertUpdate语句中,依赖于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:

    >>> 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。可以通过向create_engine()调用传递标志 retaining=True 来控制值。这是从 0.8.2 开始添加的新标志,因此在 0.8.2 上的应用程序可以根据需要将其设置为 TrueFalse

另见

sqlalchemy.dialects.firebird.fdb

sqlalchemy.dialects.firebird.kinterbasdb

pythonhosted.org/fdb/usage-guide.html#retaining-transactions - 有关“保留”标志的信息。

#2763

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

如果创建引擎时没有指定方言,即 firebird://,则现在使用 fdb 方言。fdb 是一个兼容 kinterbasdb 的 DBAPI,根据 Firebird 项目,现在是他们官方的 Python 驱动程序。

#2504

Firebird fdbkinterbasdb 默认设置 retaining=False

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

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

另请参阅

sqlalchemy.dialects.firebird.fdb

sqlalchemy.dialects.firebird.kinterbasdb

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

#2763

SQLAlchemy 0.8 中的新功能是什么?

原文:docs.sqlalchemy.org/en/20/changelog/migration_08.html

关于本文档

本文档描述了 SQLAlchemy 版本 0.7(截至 2012 年 10 月正在进行维护发布)和 SQLAlchemy 版本 0.8(预计于 2013 年初发布)之间的更改。

文档日期:2012 年 10 月 25 日 更新日期:2013 年 3 月 9 日

介绍

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

SQLAlchemy 发布即将接近 1.0,自 0.5 以来的每个新版本都减少了主要的使用更改。大多数已经适应现代 0.7 模式的应用程序应该可以无需更改地迁移到 0.8。使用 0.6 甚至 0.5 模式的应用程序也应该可以直接迁移到 0.8,尽管较大的应用程序可能希望在每个中间版本中进行测试。

平台支持

现在面向 Python 2.5 及更高版本

SQLAlchemy 0.8 将面向 Python 2.5 及更高版本;对 Python 2.4 的兼容性将被删除。

内部将能够使用 Python 三元表达式(即,x if y else z),这将改善与使用y and x or z相比的情况,后者自然会导致一些错误,以及上下文管理器(即,with:)和在某些情况下可能会有助于代码可读性的try:/except:/else:块。

SQLAlchemy 最终将放弃对 2.5 的支持 - 当达到 2.6 作为基线时,SQLAlchemy 将转而使用 2.6/3.3 的就地兼容性,删除2to3工具的使用,并保持一个同时与 Python 2 和 3 兼容的源代码库。

新的 ORM 功能

重写的relationship()机制

0.8 版本在relationship()如何确定两个实体之间如何连接方面具有更加改进和强大的系统。新系统包括以下功能:

  • 在构建针对具有多个外键路径指向目标的类的relationship()时,不再需要primaryjoin参数。只需要foreign_keys参数来指定应包括的列:

    class Parent(Base):
        __tablename__ = "parent"
        id = Column(Integer, primary_key=True)
        child_id_one = Column(Integer, ForeignKey("child.id"))
        child_id_two = Column(Integer, ForeignKey("child.id"))
    
        child_one = relationship("Child", foreign_keys=child_id_one)
        child_two = relationship("Child", foreign_keys=child_id_two)
    
    class Child(Base):
        __tablename__ = "child"
        id = Column(Integer, primary_key=True)
    
  • 支持自引用、复合外键的关系,其中一列指向自身。典型案例如下:

    class Folder(Base):
        __tablename__ = "folder"
        __table_args__ = (
            ForeignKeyConstraint(
                ["account_id", "parent_id"], ["folder.account_id", "folder.folder_id"]
            ),
        )
    
        account_id = Column(Integer, primary_key=True)
        folder_id = Column(Integer, primary_key=True)
        parent_id = Column(Integer)
        name = Column(String)
    
        parent_folder = relationship(
            "Folder", backref="child_folders", remote_side=[account_id, folder_id]
        )
    

    上面,Folder指的是从account_id到其自身的父Folder的连接,并且parent_idfolder_id的连接。当 SQLAlchemy 构造自动连接时,不能再假设“远程”侧的所有列都被别名化,而“本地”侧的所有列都没有被别名化 - account_id在两侧都存在。因此,内部关系机制完全重写,以支持一个完全不同的系统,其中生成了两个副本的account_id,每个副本包含不同的注释以确定它们在语句中的作用。注意基本急加载中的连接条件:

    SELECT
      folder.account_id  AS  folder_account_id,
      folder.folder_id  AS  folder_folder_id,
      folder.parent_id  AS  folder_parent_id,
      folder.name  AS  folder_name,
      folder_1.account_id  AS  folder_1_account_id,
      folder_1.folder_id  AS  folder_1_folder_id,
      folder_1.parent_id  AS  folder_1_parent_id,
      folder_1.name  AS  folder_1_name
    FROM  folder
      LEFT  OUTER  JOIN  folder  AS  folder_1
      ON
      folder_1.account_id  =  folder.account_id
      AND  folder.folder_id  =  folder_1.parent_id
    
    WHERE  folder.folder_id  =  ?  AND  folder.account_id  =  ?
    
  • 以前的复杂自定义连接条件,比如涉及函数和/或类型转换(CASTing)的条件,现在在大多数情况下都将按预期运行:

    class HostEntry(Base):
        __tablename__ = "host_entry"
    
        id = Column(Integer, primary_key=True)
        ip_address = Column(INET)
        content = Column(String(50))
    
        # relationship() using explicit foreign_keys, remote_side
        parent_host = relationship(
            "HostEntry",
            primaryjoin=ip_address == cast(content, INET),
            foreign_keys=content,
            remote_side=ip_address,
        )
    

    新的relationship()机制利用了 SQLAlchemy 的一个概念,称为注释。这些注释也可以通过foreign()remote()函数明确提供给应用代码,无论是为了提高高级配置的可读性,还是直接注入一个精确的配置,绕过通常的连接检查启发式方法:

    from sqlalchemy.orm import foreign, remote
    
    class HostEntry(Base):
        __tablename__ = "host_entry"
    
        id = Column(Integer, primary_key=True)
        ip_address = Column(INET)
        content = Column(String(50))
    
        # relationship() using explicit foreign() and remote() annotations
        # in lieu of separate arguments
        parent_host = relationship(
            "HostEntry",
            primaryjoin=remote(ip_address) == cast(foreign(content), INET),
        )
    

另请参阅

配置关系连接方式 - 一个重新修订的关于relationship()的部分,详细说明了定制相关属性和集合访问的最新技术。

#1401 #610 ### 新的类/对象检查系统

许多 SQLAlchemy 用户正在编写需要检查映射类属性的系统,包括能够获取主键列、对象关系、普通属性等等,通常是为了构建数据编组系统,比如 JSON/XML 转换方案以及各种表单库。

最初,TableColumn模型是最初的检查点,具有完整的文档系统。虽然 SQLAlchemy ORM 模型也是完全可自省的,但这从来都不是一个完全稳定和受支持的功能,用户往往不清楚如何获取这些信息。

0.8 现在为此提供了一致、稳定且完全文档化的 API,包括一个检查系统,该系统适用于映射类、实例、属性以及其他核心和 ORM 构造。该系统的入口点是核心级的 inspect() 函数。在大多数情况下,被检查的对象已经是 SQLAlchemy 系统的一部分,例如 MapperInstanceStateInspector。在某些情况下,已经添加了新对象,其工作是在某些上下文中提供检查 API,例如 AliasedInspAttributeState

以下是一些关键功能的介绍:

>>> class User(Base):
...     __tablename__ = "user"
...     id = Column(Integer, primary_key=True)
...     name = Column(String)
...     name_syn = synonym(name)
...     addresses = relationship("Address")

>>> # universal entry point is inspect()
>>> b = inspect(User)

>>> # b in this case is the Mapper
>>> b
<Mapper at 0x101521950; User>

>>> # Column namespace
>>> b.columns.id
Column('id', Integer(), table=<user>, primary_key=True, nullable=False)

>>> # mapper's perspective of the primary key
>>> b.primary_key
(Column('id', Integer(), table=<user>, primary_key=True, nullable=False),)

>>> # MapperProperties available from .attrs
>>> b.attrs.keys()
['name_syn', 'addresses', 'id', 'name']

>>> # .column_attrs, .relationships, etc. filter this collection
>>> b.column_attrs.keys()
['id', 'name']

>>> list(b.relationships)
[<sqlalchemy.orm.properties.RelationshipProperty object at 0x1015212d0>]

>>> # they are also namespaces
>>> b.column_attrs.id
<sqlalchemy.orm.properties.ColumnProperty object at 0x101525090>

>>> b.relationships.addresses
<sqlalchemy.orm.properties.RelationshipProperty object at 0x1015212d0>

>>> # point inspect() at a mapped, class level attribute,
>>> # returns the attribute itself
>>> b = inspect(User.addresses)
>>> b
<sqlalchemy.orm.attributes.InstrumentedAttribute object at 0x101521fd0>

>>> # From here we can get the mapper:
>>> b.mapper
<Mapper at 0x101525810; Address>

>>> # the parent inspector, in this case a mapper
>>> b.parent
<Mapper at 0x101521950; User>

>>> # an expression
>>> print(b.expression)
"user".id  =  address.user_id
>>> # inspect works on instances
>>> u1 = User(id=3, name="x")
>>> b = inspect(u1)

>>> # it returns the InstanceState
>>> b
<sqlalchemy.orm.state.InstanceState object at 0x10152bed0>

>>> # similar attrs accessor refers to the
>>> b.attrs.keys()
['id', 'name_syn', 'addresses', 'name']

>>> # attribute interface - from attrs, you get a state object
>>> b.attrs.id
<sqlalchemy.orm.state.AttributeState object at 0x10152bf90>

>>> # this object can give you, current value...
>>> b.attrs.id.value
3

>>> # ... current history
>>> b.attrs.id.history
History(added=[3], unchanged=(), deleted=())

>>> # InstanceState can also provide session state information
>>> # lets assume the object is persistent
>>> s = Session()
>>> s.add(u1)
>>> s.commit()

>>> # now we can get primary key identity, always
>>> # works in query.get()
>>> b.identity
(3,)

>>> # the mapper level key
>>> b.identity_key
(<class '__main__.User'>, (3,))

>>> # state within the session
>>> b.persistent, b.transient, b.deleted, b.detached
(True, False, False, False)

>>> # owning session
>>> b.session
<sqlalchemy.orm.session.Session object at 0x101701150>

另请参见

运行时检查 API

#2208

新的 with_polymorphic() 功能,可以在任何地方使用

Query.with_polymorphic() 方法允许用户指定在针对联接表实体进行查询时应该存在哪些表。不幸的是,该方法很笨拙,只适用于列表中的第一个实体,而且在使用和内部方面都有一些尴尬的行为。现在已经添加了一个新的增强功能到 aliased() 构造中,称为 with_polymorphic(),它允许任何实体被“别名”为其自身的“多态”版本,可以自由地在任何地方使用:

from sqlalchemy.orm import with_polymorphic

palias = with_polymorphic(Person, [Engineer, Manager])
session.query(Company).join(palias, Company.employees).filter(
    or_(Engineer.language == "java", Manager.hair == "pointy")
)

另请参见

使用 with_polymorphic() - 用于多态加载控制的最新更新文档。

#2333

of_type() 与 alias()、with_polymorphic()、any()、has()、joinedload()、subqueryload()、contains_eager() 配合使用

PropComparator.of_type() 方法用于在构建 SQL 表达式时指定要使用的特定子类型,该子类型作为其目标具有 多态 映射的 relationship() 的目标。现在可以使用该方法来针对 任意数量 的目标子类型,通过与新的 with_polymorphic() 函数结合使用:

# use eager loading in conjunction with with_polymorphic targets
Job_P = with_polymorphic(Job, [SubJob, ExtraJob], aliased=True)
q = (
    s.query(DataContainer)
    .join(DataContainer.jobs.of_type(Job_P))
    .options(contains_eager(DataContainer.jobs.of_type(Job_P)))
)

该方法现在在大多数接受常规关系属性的地方同样有效,包括与joinedload()subqueryload()contains_eager()等加载器函数以及与PropComparator.any()PropComparator.has()等比较方法一起:

# use eager loading in conjunction with with_polymorphic targets
Job_P = with_polymorphic(Job, [SubJob, ExtraJob], aliased=True)
q = (
    s.query(DataContainer)
    .join(DataContainer.jobs.of_type(Job_P))
    .options(contains_eager(DataContainer.jobs.of_type(Job_P)))
)

# pass subclasses to eager loads (implicitly applies with_polymorphic)
q = s.query(ParentThing).options(
    joinedload_all(ParentThing.container, DataContainer.jobs.of_type(SubJob))
)

# control self-referential aliasing with any()/has()
Job_A = aliased(Job)
q = (
    s.query(Job)
    .join(DataContainer.jobs)
    .filter(
        DataContainer.jobs.of_type(Job_A).any(
            and_(Job_A.id < Job.id, Job_A.type == "fred")
        )
    )
)

另请参见

加入特定子类型或 with_polymorphic()实体

#2438 #1106

事件可应用于未映射的超类

现在可以将映射器和实例事件与未映射的超类关联,这些事件将随着子类映射而传播。应使用propagate=True标志。此功能允许将事件与声明性基类关联:

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

@event.listens_for("load", Base, propagate=True)
def on_load(target, context):
    print("New instance loaded:", target)

# on_load() will be applied to SomeClass
class SomeClass(Base):
    __tablename__ = "sometable"

    # ...

#2585

Declarative 区分模块/包

Declarative 的一个关键特性是能够使用它们的字符串名称引用其他映射类。类名注册表现在对给定类的拥有模块和包敏感。这些类可以在表达式中通过点名引用:

class Snack(Base):
    # ...

    peanuts = relationship(
        "nuts.Peanut", primaryjoin="nuts.Peanut.snack_id == Snack.id"
    )

该解析允许使用任何完整或部分消歧义的包名称。如果到特定类的路径仍然模糊,将引发错误。

#2338

Declarative 中的新 DeferredReflection 功能

“延迟反射”示例已移至 Declarative 中的受支持功能。此功能允许仅使用占位符Table元数据构建声明性映射类,直到调用prepare()步骤,给定一个Engine,以完全反射所有表并建立实际映射。该系统支持列的覆盖、单一和联接继承,以及每个引擎的不同基础。现在可以在一个步骤中针对现有表创建完整的声明性配置:

class ReflectedOne(DeferredReflection, Base):
    __abstract__ = True

class ReflectedTwo(DeferredReflection, Base):
    __abstract__ = True

class MyClass(ReflectedOne):
    __tablename__ = "mytable"

class MyOtherClass(ReflectedOne):
    __tablename__ = "myothertable"

class YetAnotherClass(ReflectedTwo):
    __tablename__ = "yetanothertable"

ReflectedOne.prepare(engine_one)
ReflectedTwo.prepare(engine_two)

另请参见

DeferredReflection

#2485

ORM 类现在被核心构造所接受

虽然与Query.filter()一起使用的 SQL 表达式,如User.id == 5,一直与核心构造兼容,例如select(),但当传递给select()Select.select_from()Select.correlate()时,映射类本身将不被识别。一个新的 SQL 注册系统允许一个映射类作为核心中的 FROM 子句被接受:

from sqlalchemy import select

stmt = select([User]).where(User.id == 5)

上面,映射的User类将扩展为TableUser被映射到其中的表。

#2245

Query.update()支持 UPDATE..FROM

新的 UPDATE..FROM 机制适用于 query.update()。下面,我们对SomeEntity执行 UPDATE 操作,添加一个 FROM 子句(或等效的,取决于后端)对SomeOtherEntity

query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter(
    SomeOtherEntity.foo == "bar"
).update({"data": "x"})

特别地,支持对连接继承实体的更新,前提是 UPDATE 的目标是本地表上的表,或者如果父表和子表混合,它们在查询中被显式连接。下面,假设EngineerPerson的一个连接子类:

query(Engineer).filter(Person.id == Engineer.id).filter(
    Person.name == "dilbert"
).update({"engineer_data": "java"})

将产生:

UPDATE  engineer  SET  engineer_data='java'  FROM  person
WHERE  person.id=engineer.id  AND  person.name='dilbert'

#2365

rollback()仅会回滚从 begin_nested()开始的“脏”对象

一项行为变更应该提高那些通过Session.begin_nested()使用 SAVEPOINT 的用户的效率 - 在rollback()时,只有自上次刷新以来被标记为脏的对象将被过期,其余的Session保持不变。这是因为对 SAVEPOINT 的 ROLLBACK 不会终止包含事务的隔离,因此除了当前事务中未刷新的更改外,不需要过期。

#2452

缓存示例现在使用 dogpile.cache

缓存示例现在使用dogpile.cache。Dogpile.cache 是 Beaker 缓存部分的重写,具有更简单和更快的操作,以及支持分布式锁定。

请注意,Dogpile 示例以及之前的 Beaker 示例中使用的 SQLAlchemy API 略有变化,特别是需要如 Beaker 示例中所示的这种变化:

--- examples/beaker_caching/caching_query.py
+++ examples/beaker_caching/caching_query.py
@@ -222,7 +222,8 @@

         """
         if query._current_path:
-            mapper, key = query._current_path[-2:]
+            mapper, prop = query._current_path[-2:]
+            key = prop.key

             for cls in mapper.class_.__mro__:
                 if (cls, key) in self._relationship_options:

另请参见

Dogpile 缓存

#2589

新的核心功能

完全可扩展,核心中支持类型级别的操作符

到目前为止,Core 从未有过任何系统来为 Column 和其他表达式构造添加对新 SQL 操作符的支持,除了ColumnOperators.op() 方法,这个方法“刚好”能使事情正常工作。此外,Core 中也从未存在过任何允许覆盖现有操作符行为的系统。直到现在,操作符能够灵活重新定义的唯一方式是在 ORM 层,使用给定 comparator_factory 参数的 column_property()。因此,像 GeoAlchemy 这样的第三方库被迫以 ORM 为中心,并依赖于一系列的黑客技巧来应用新的操作以及正确地传播它们。

Core 中的新操作符系统增加了一直缺失的一个关键点,即将新的和被覆盖的操作符与 类型 关联起来。毕竟,真正驱动操作存在的不是列、CAST 操作符或 SQL 函数,而是表达式的 类型。实现细节很少——只需向核心 ColumnElement 类型添加几个额外的方法,以便它向其 TypeEngine 对象咨询可选的一组操作符。新的或修订过的操作可以与任何类型关联,可以通过对现有类型进行子类化、使用 TypeDecorator,或者通过将新的 Comparator 对象附加到现有类型类来“全面覆盖”地关联。

例如,要为 Numeric 类型添加对数支持:

from sqlalchemy.types import Numeric
from sqlalchemy.sql import func

class CustomNumeric(Numeric):
    class comparator_factory(Numeric.Comparator):
        def log(self, other):
            return func.log(self.expr, other)

新类型可以像任何其他类型一样使用:

data = Table(
    "data",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("x", CustomNumeric(10, 5)),
    Column("y", CustomNumeric(10, 5)),
)

stmt = select([data.c.x.log(data.c.y)]).where(data.c.x.log(2) < value)
print(conn.execute(stmt).fetchall())

从这里产生的新功能包括对 PostgreSQL 的 HSTORE 类型的支持,以及与 PostgreSQL 的 ARRAY 类型相关的新操作。它还为现有类型铺平了道路,使其能够获取更多特定于这些类型的运算符,例如更多的字符串、整数和日期运算符。

另请参阅

重新定义和创建新的操作符

HSTORE

#2547

对插入的多值支持

Insert.values() 方法现在支持字典列表,将呈现多 VALUES 语句,如 VALUES (<row1>), (<row2>), ...。这仅适用于支持此语法的后端,包括 PostgreSQL、SQLite 和 MySQL。这与通常的 executemany() 样式的 INSERT 不同:

users.insert().values(
    [
        {"name": "some name"},
        {"name": "some other name"},
        {"name": "yet another name"},
    ]
)

另请参阅

Insert.values()

#2623

类型表达式

现在可以将 SQL 表达式与类型关联起来。从历史上看,TypeEngine 一直允许 Python 端函数接收绑定参数和结果行值,通过 Python 端转换函数来回传递到/从数据库。新功能允许类似的功能,但在数据库端实现:

from sqlalchemy.types import String
from sqlalchemy import func, Table, Column, MetaData

class LowerString(String):
    def bind_expression(self, bindvalue):
        return func.lower(bindvalue)

    def column_expression(self, col):
        return func.lower(col)

metadata = MetaData()
test_table = Table("test_table", metadata, Column("data", LowerString))

在上面的例子中,LowerString 类型定义了一个 SQL 表达式,每当 test_table.c.data 列在 SELECT 语句的列子句中呈现时,该表达式将被发出:

>>> print(select([test_table]).where(test_table.c.data == "HI"))
SELECT  lower(test_table.data)  AS  data
FROM  test_table
WHERE  test_table.data  =  lower(:data_1) 

这一功能也被新版的 GeoAlchemy 大量使用,可以根据类型规则在 SQL 中内联嵌入 PostGIS 表达式。

另请参阅

应用 SQL 级别的绑定/结果处理

#1534

核心检查系统

New Class/Object Inspection System 中引入的 inspect() 函数也适用于核心。应用于一个 Engine 会产生一个 Inspector 对象:

from sqlalchemy import inspect
from sqlalchemy import create_engine

engine = create_engine("postgresql://scott:tiger@localhost/test")
insp = inspect(engine)
print(insp.get_table_names())

它也可以应用于任何 ClauseElement,它返回 ClauseElement 本身,比如 TableColumnSelect 等。这使得它可以在核心和 ORM 构造之间流畅工作。

新方法 Select.correlate_except()

select() 现在有一个方法 Select.correlate_except(),指定“除了指定的所有 FROM 子句之外的相关性”。它可用于映射场景,其中相关子查询应该正常关联,除了针对特定目标可选择的情况:

class SnortEvent(Base):
    __tablename__ = "event"

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

    signatures = relationship("Signature", lazy=False)

class Signature(Base):
    __tablename__ = "signature"

    id = Column(Integer, primary_key=True)

    sig_count = column_property(
        select([func.count("*")])
        .where(SnortEvent.signature == id)
        .correlate_except(SnortEvent)
    )

另请参阅

Select.correlate_except()

PostgreSQL HSTORE 类型

PostgreSQL 的HSTORE类型的支持现在可用作为HSTORE。此类型充分利用了新的运算符系统,为 HSTORE 类型提供了一整套运算符,包括索引访问、连接和包含方法,如comparator_factory.has_key()comparator_factory.has_any()comparator_factory.matrix()

from sqlalchemy.dialects.postgresql import HSTORE

data = Table(
    "data_table",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("hstore_data", HSTORE),
)

engine.execute(select([data.c.hstore_data["some_key"]])).scalar()

engine.execute(select([data.c.hstore_data.matrix()])).scalar()

另请参阅

HSTORE

hstore

#2606

增强的 PostgreSQL ARRAY 类型

ARRAY 类型将接受一个可选的“维度”参数,将其固定到一个固定数量的维度,并在检索结果时大大提高效率:

# old way, still works since PG supports N-dimensions per row:
Column("my_array", postgresql.ARRAY(Integer))

# new way, will render ARRAY with correct number of [] in DDL,
# will process binds and results more efficiently as we don't need
# to guess how many levels deep to go
Column("my_array", postgresql.ARRAY(Integer, dimensions=2))

该类型还引入了新的运算符,使用新的类型特定运算符框架。新操作包括索引访问:

result = conn.execute(select([mytable.c.arraycol[2]]))

切片访问在 SELECT 中:

result = conn.execute(select([mytable.c.arraycol[2:4]]))

切片更新在 UPDATE 中:

conn.execute(mytable.update().values({mytable.c.arraycol[2:3]: [7, 8]}))

独立的数组文字:

>>> from sqlalchemy.dialects import postgresql
>>> conn.scalar(select([postgresql.array([1, 2]) + postgresql.array([3, 4, 5])]))
[1, 2, 3, 4, 5]

数组连接,在下面,右侧的[4, 5, 6] 被强制转换为数组文字:

select([mytable.c.arraycol + [4, 5, 6]])

另请参阅

ARRAY

array

#2441

新的、可配置的 DATE、TIME 类型用于 SQLite

SQLite 没有内置的 DATE、TIME 或 DATETIME 类型,而是提供了一些支持将日期和时间值存储为字符串或整数的方法。SQLite 的日期和时间类型在 0.8 中得到了增强,可以更加灵活地配置特定格式,包括“微秒”部分是可选的,以及几乎所有其他内容。

Column("sometimestamp", sqlite.DATETIME(truncate_microseconds=True))
Column(
    "sometimestamp",
    sqlite.DATETIME(
        storage_format=(
            "%(year)04d%(month)02d%(day)02d"
            "%(hour)02d%(minute)02d%(second)02d%(microsecond)06d"
        ),
        regexp="(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{6})",
    ),
)
Column(
    "somedate",
    sqlite.DATE(
        storage_format="%(month)02d/%(day)02d/%(year)04d",
        regexp="(?P<month>\d+)/(?P<day>\d+)/(?P<year>\d+)",
    ),
)

非常感谢 Nate Dub 在 Pycon 2012 上的努力。

另请参阅

DATETIME

DATE

TIME

#2363

“COLLATE”在所有方言中都受支持;特别是 MySQL、PostgreSQL、SQLite

“collate”关键字,长期被 MySQL 方言接受,现在已经在所有String类型上建立,并且将在任何后端渲染,包括在使用MetaData.create_all()cast()等功能时:

>>> stmt = select([cast(sometable.c.somechar, String(20, collation="utf8"))])
>>> print(stmt)
SELECT  CAST(sometable.somechar  AS  VARCHAR(20)  COLLATE  "utf8")  AS  anon_1
FROM  sometable 

另请参阅

String

#2276

现在支持“前缀”用于update()delete()

面向 MySQL,一个“前缀”可以在任何这些结构中渲染。例如:

stmt = table.delete().prefix_with("LOW_PRIORITY", dialect="mysql")

stmt = table.update().prefix_with("LOW_PRIORITY", dialect="mysql")

该方法是新增的,除了已经存在于insert()select()Query上的方法之外。

另请参阅

Update.prefix_with()

Delete.prefix_with()

Insert.prefix_with()

Select.prefix_with()

Query.prefix_with()

#2431

行为变更

将“待定”对象视为“孤立”已经更加积极

这是对 0.8 系列的一个晚期补充,但希望新行为在更广泛的情况下更一致和直观。ORM 自至少版本 0.4 以来就包含了这样的行为,即一个“挂起”的对象,意味着它与Session相关联,但尚未插入数据库,当它变成“孤儿”时,即已与引用它的父对象解除关联,并且在配置的relationship()上指定了delete-orphan级联时,将自动从Session中清除。这种行为旨在大致模拟持久对象(即已插入)的行为,ORM 将根据分离事件的拦截发出 DELETE 来删除成为孤儿的对象。

行为变更适用于被多种父对象引用并且每个父对象都指定了delete-orphan的对象;典型示例是在多对多模式中桥接两种其他对象的关联对象。以前,行为是这样的,即挂起对象仅在与所有父对象解除关联时才会被清除。随着行为的变更,只要挂起对象与先前相关联的任何父对象解除关联,它就会被清除。这种行为旨在更接近持久对象的行为,即只要它们与任何父对象解除关联,它们就会被删除。

较旧行为的基本原因可以追溯至至少版本 0.4,基本上是一种防御性决定,试图在对象仍在构建 INSERT 时减轻混淆。但事实是,无论如何,一旦对象附加到任何新父对象,它就会重新与Session关联。

仍然可以刷新一个对象,即使它没有与所有必需的父对象关联,如果该对象一开始就没有与这些父对象关联,或者如果它被清除,但随后通过后续的附加事件重新与Session关联,但仍未完全关联。在这种情况下,预计数据库会发出完整性错误,因为可能存在未填充的 NOT NULL 外键列。ORM 决定让这些 INSERT 尝试发生,基于这样的判断:一个只与其必需的父对象部分关联但已经积极地与其中一些关联的对象,更多的情况下是用户错误,而不是应该被默默跳过的有意遗漏 - 在这里默默跳过 INSERT 会使这种用户错误非常难以调试。

对于可能依赖于旧行为的应用程序,可以通过将标志legacy_is_orphan作为映射器选项指定来重新启用旧行为。

新行为允许以下测试用例正常工作:

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

Base = declarative_base()

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

class UserKeyword(Base):
    __tablename__ = "user_keyword"
    user_id = Column(Integer, ForeignKey("user.id"), primary_key=True)
    keyword_id = Column(Integer, ForeignKey("keyword.id"), primary_key=True)

    user = relationship(
        User, backref=backref("user_keywords", cascade="all, delete-orphan")
    )

    keyword = relationship(
        "Keyword", backref=backref("user_keywords", cascade="all, delete-orphan")
    )

    # uncomment this to enable the old behavior
    # __mapper_args__ = {"legacy_is_orphan": True}

class Keyword(Base):
    __tablename__ = "keyword"
    id = Column(Integer, primary_key=True)
    keyword = Column("keyword", String(64))

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

# note we're using PostgreSQL to ensure that referential integrity
# is enforced, for demonstration purposes.
e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)

Base.metadata.drop_all(e)
Base.metadata.create_all(e)

session = Session(e)

u1 = User(name="u1")
k1 = Keyword(keyword="k1")

session.add_all([u1, k1])

uk1 = UserKeyword(keyword=k1, user=u1)

# previously, if session.flush() were called here,
# this operation would succeed, but if session.flush()
# were not called here, the operation fails with an
# integrity error.
# session.flush()
del u1.user_keywords[0]

session.commit()

#2655

after_attach 事件在项目与会话关联之后触发,而不是之前;before_attach 添加

使用after_attach的事件处理程序现在可以假定给定实例与给定会话关联:

@event.listens_for(Session, "after_attach")
def after_attach(session, instance):
    assert instance in session

有些用例要求以这种方式工作。然而,其他用例要求项目尚未成为会话的一部分,比如当一个查询,旨在加载实例所需的某些状态,首先发出自动刷新,否则会过早刷新目标对象。这些用例应该使用新的“before_attach”事件:

@event.listens_for(Session, "before_attach")
def before_attach(session, instance):
    instance.some_necessary_attribute = (
        session.query(Widget).filter_by(instance.widget_name).first()
    )

#2464

查询现在像select()一样自动关联

以前需要调用Query.correlate()才能使列或 WHERE 子查询与父级关联:

subq = (
    session.query(Entity.value)
    .filter(Entity.id == Parent.entity_id)
    .correlate(Parent)
    .as_scalar()
)
session.query(Parent).filter(subq == "some value")

这与普通的select()构造相反,后者默认情况下会假定自动关联。在 0.8 中,上述语句将自动关联:

subq = session.query(Entity.value).filter(Entity.id == Parent.entity_id).as_scalar()
session.query(Parent).filter(subq == "some value")

就像在select()中一样,可以通过调用query.correlate(None)来禁用关联,或者通过传递一个实体来手动设置关联,query.correlate(someentity)

#2179

关联现在始终是上下文特定的

为了允许更广泛的相关性场景,Select.correlate()Query.correlate() 的行为略有改变,以便 SELECT 语句仅在实际上下文中使用时才从 FROM 子句中省略“相关”的目标。此外,不再可能让作为外部 SELECT 语句中的 FROM 的 SELECT 语句“相关”(即省略)FROM 子句。

这个改变只会在渲染 SQL 方面变得更好,因为不再可能渲染出不合法的 SQL,其中所选内容相对于所选的 FROM 对象不足:

from sqlalchemy.sql import table, column, select

t1 = table("t1", column("x"))
t2 = table("t2", column("y"))
s = select([t1, t2]).correlate(t1)

print(s)

在这个改变之前,上述内容将返回:

SELECT  t1.x,  t2.y  FROM  t2

这是无效的 SQL,因为“t1”在任何 FROM 子句中都没有被引用。

现在,在没有外部 SELECT 的情况下,它将返回:

SELECT  t1.x,  t2.y  FROM  t1,  t2

在 SELECT 中,相关性会如预期地生效:

s2 = select([t1, t2]).where(t1.c.x == t2.c.y).where(t1.c.x == s)
print(s2)
SELECT  t1.x,  t2.y  FROM  t1,  t2
WHERE  t1.x  =  t2.y  AND  t1.x  =
  (SELECT  t1.x,  t2.y  FROM  t2)

这个改变不会影响任何现有应用程序,因为对于正确构建的表达式,相关性行为保持不变。只有依赖于在非相关上下文中使用相关 SELECT 的无效字符串输出的应用程序(很可能是在测试场景中),才会看到任何变化。

#2668 ### create_all() 和 drop_all() 现在将空列表视为如此

方法 MetaData.create_all()MetaData.drop_all() 现在将接受一个空的 Table 对象列表,并且不会发出任何 CREATE 或 DROP 语句。以前,空列表被解释为与传递 None 相同,对所有项目都会无条件发出 CREATE/DROP。

这是一个错误修复,但一些应用程序可能一直依赖于先前的行为。

#2664

修复了 InstrumentationEvents 的事件目标定位

InstrumentationEvents系列事件目标已经记录,事件将仅根据传递的实际类别触发。直到 0.7 版本,这并不是这种情况,应用于InstrumentationEvents的任何事件监听器都将为所有映射的类调用。在 0.8 中,添加了额外的逻辑,使事件仅对发送的那些类调用。这里的propagate标志默认设置为True,因为类仪器事件通常用于拦截尚未创建的类。

#2590

不再将“=”自动转换为 IN,当与 MS-SQL 中的子查询进行比较时

我们在 MSSQL 方言中发现了一个非常古老的行为,当用户尝试执行类似以下操作时,它会试图拯救用户:

scalar_subq = select([someothertable.c.id]).where(someothertable.c.data == "foo")
select([sometable]).where(sometable.c.id == scalar_subq)

SQL Server 不允许将相等比较与标量 SELECT 进行比较,即,“x = (SELECT something)”。 MSSQL 方言会将其转换为 IN。然而,当进行类似“(SELECT something) = x”的比较时,也会发生同样的情况,总体上,这种猜测的水平超出了 SQLAlchemy 通常的范围,因此这种行为被移除。

#2277

修复了Session.is_modified()的行为

Session.is_modified()方法接受一个参数passive,基本上不应该是必要的,所有情况下该参数的值应为True - 当保持默认值False时,它会导致命中数据库,并经常触发自动刷新,这将改变结果。在 0.8 中,passive参数将不起作用,并且未加载的属性永远不会被检查历史记录,因为根据定义,未加载的属性上不会有待处理的状态更改。

另请参阅

Session.is_modified()

#2320

Column.keySelect.c属性中受到Select.apply_labels()的尊重

表达式系统的用户知道Select.apply_labels()会在每个列名前面添加表名,影响从Select.c中可用的名称:

s = select([table1]).apply_labels()
s.c.table1_col1
s.c.table1_col2

在 0.8 版本之前,如果ColumnColumn.key不同,这个键会被忽略,与未使用Select.apply_labels()时不一致:

# before 0.8
table1 = Table("t1", metadata, Column("col1", Integer, key="column_one"))
s = select([table1])
s.c.column_one  # would be accessible like this
s.c.col1  # would raise AttributeError

s = select([table1]).apply_labels()
s.c.table1_column_one  # would raise AttributeError
s.c.table1_col1  # would be accessible like this

在 0.8 版本中,Column.key在两种情况下都受到尊重:

# with 0.8
table1 = Table("t1", metadata, Column("col1", Integer, key="column_one"))
s = select([table1])
s.c.column_one  # works
s.c.col1  # AttributeError

s = select([table1]).apply_labels()
s.c.table1_column_one  # works
s.c.table1_col1  # AttributeError

关于“name”和“key”的所有其他行为都是相同的,包括渲染的 SQL 仍然使用形式<tablename>_<colname> - 这里的重点是防止Column.key内容被渲染到SELECT语句中,以便在Column.key中使用特殊/非 ASCII 字符时不会出现问题。

#2397

single_parent警告现在变成了错误

一个relationship(),它是多对一或多对多关系,并指定“cascade='all, delete-orphan'”,这是一个尴尬但仍然支持的用例(带有限制),如果关系没有指定single_parent=True选项,现在将引发错误。以前只会发出警告,但在任何情况下几乎立即会在属性系统中跟随失败。

#2405

添加inspector参数到column_reflect事件

0.7 版本添加了一个名为column_reflect的新事件,提供了每个列反射时可以增强的机会。我们在这个事件上稍微出了点错,因为事件没有提供获取当前用于反射的InspectorConnection的方法,以防需要来自数据库的额外信息。由于这是一个尚未广泛使用的新事件,我们将直接在其中添加inspector参数:

@event.listens_for(Table, "column_reflect")
def listen_for_col(inspector, table, column_info): ...

#2418

禁用 MySQL 的自动检测排序规则和大小写敏感性

MySQL 方言进行两次调用,其中一次非常昂贵,从数据库加载所有可能的排序规则以及大小写信息,第一次Engine连接时。这两个集合都不会用于任何 SQLAlchemy 函数,因此这些调用将不再自动发出。可能依赖于这些集合存在于engine.dialect上的应用程序将需要直接调用_detect_collations()_detect_casing()

#2404

“未使用的列名”警告变成异常

insert()update()构造中引用不存在的列将引发错误而不是警告:

t1 = table("t1", column("x"))
t1.insert().values(x=5, z=5)  # raises "Unconsumed column names: z"

#2415

Inspector.get_primary_keys()已被弃用,请使用 Inspector.get_pk_constraint

Inspector上的这两种方法是多余的,其中get_primary_keys()将返回与get_pk_constraint()相同的信息,减去约束的名称:

>>> insp.get_primary_keys()
["a", "b"]

>>> insp.get_pk_constraint()
{"name":"pk_constraint", "constrained_columns":["a", "b"]}

#2422

在大多数情况下,不区分大小写的结果行名称将被禁用

一个非常古老的行为,在RowProxy中的列名始终是不区分大小写比较的:

>>> row = result.fetchone()
>>> row["foo"] == row["FOO"] == row["Foo"]
True

这是为了一些早期需要这样做的方言的好处,比如 Oracle 和 Firebird,但在现代用法中,我们有更准确的方法来处理这两个平台的不区分大小写行为。

未来,这种行为将仅可选地通过将标志case_sensitive=False传递给create_engine()来使用,但否则从行中请求的列名必须匹配大小写。

#2423

InstrumentationManager和替代类仪器现在是一个扩展

sqlalchemy.orm.interfaces.InstrumentationManager类已移动到sqlalchemy.ext.instrumentation.InstrumentationManager。 “替代仪器”系统是为了极少数需要使用现有或不寻常的类仪器系统的安装而构建的,并且通常很少使用。这个系统的复杂性已经导出到一个ext.模块中。它保持未使用,直到被导入一次,通常是当第三方库导入InstrumentationManager时,此时它通过用ExtendedInstrumentationRegistry替换默认的InstrumentationFactory注入回sqlalchemy.orm

已移除

SQLSoup

SQLSoup 是一个方便的包,它在 SQLAlchemy ORM 的基础上提供了一个替代接口。SQLSoup 现在已经移动到自己的项目中,并且有单独的文档/发布;请参见bitbucket.org/zzzeek/sqlsoup

SQLSoup 是一个非常简单的工具,也可以受益于对其使用方式感兴趣的贡献者。

#2262

MutableType

SQLAlchemy ORM 中的旧“可变”系统已被移除。这指的是应用于诸如PickleType的类型和有条件地应用于TypeDecoratorMutableType接口,并且自早期的 SQLAlchemy 版本以来一直提供了一种让 ORM 检测所谓的“可变”数据结构(如 JSON 结构和 pickled 对象)变化的方式。然而,实现从未合理,并迫使在单位操作期间发生昂贵的对象扫描的 ORM 使用方式。在 0.7 中,引入了sqlalchemy.ext.mutable扩展,以便用户定义的数据类型可以在发生更改时适当地向单位操作发送事件。

如今,MutableType 的使用预计会很少,因为多年来一直有关于其效率低下的警告。

#2442

sqlalchemy.exceptions(多年来一直是 sqlalchemy.exc)

我们曾留下了一个别名 sqlalchemy.exceptions,以使一些尚未升级以使用 sqlalchemy.exc 的非常老的库稍微容易一些。然而,一些用户仍然感到困惑,因此在 0.8 版本中我们将其完全删除,以消除任何困惑。

#2433

介绍

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

SQLAlchemy 的发布版本即将接近 1.0,自 0.5 版本以来,每个新版本都减少了主要的使用变化。大多数已经适应现代 0.7 模式的应用程序应该可以无需更改地迁移到 0.8 版本。使用 0.6 甚至 0.5 模式的应用程序也应该可以直接迁移到 0.8 版本,尽管较大的应用程序可能需要测试每个中间版本。

平台支持

现在的目标是 Python 2.5 及以上版本

SQLAlchemy 0.8 将以 Python 2.5 为目标版本;不再兼容 Python 2.4。

内部将能够使用 Python 三元表达式(即,x if y else z),这将改善与使用 y and x or z 相比的情况,后者自然地导致了一些错误,以及上下文管理器(即,with:)和在某些情况下 try:/except:/else: 块,这将有助于提高代码的可读性。

SQLAlchemy 最终也会放弃对 2.5 版本的支持 - 当基线达到 2.6 时,SQLAlchemy 将转向使用 2.6/3.3 的就地兼容性,去除 2to3 工具的使用,并保持一个同时适用于 Python 2 和 3 的源代码库。

现在的目标是 Python 2.5 及以上版本

SQLAlchemy 0.8 将以 Python 2.5 为目标版本;不再兼容 Python 2.4。

内部将能够使用 Python 三元表达式(即,x if y else z),这将改善与使用 y and x or z 相比的情况,后者自然地导致了一些错误,以及上下文管理器(即,with:)和在某些情况下 try:/except:/else: 块,这将有助于提高代码的可读性。

SQLAlchemy 最终也会放弃对 2.5 版本的支持 - 当基线达到 2.6 时,SQLAlchemy 将转向使用 2.6/3.3 的就地兼容性,去除 2to3 工具的使用,并保持一个同时适用于 Python 2 和 3 的源代码库。

新的 ORM 特性

重写的 relationship() 机制

0.8 版本中关于 relationship() 如何确定如何在两个实体之间连接的能力得到了大大改进和增强。新系统包括以下功能:

  • 当构建针对具有多个外键路径指向目标的类的 relationship() 时,不再需要 primaryjoin 参数。只需要使用 foreign_keys 参数来指定应包含的列即可:

    class Parent(Base):
        __tablename__ = "parent"
        id = Column(Integer, primary_key=True)
        child_id_one = Column(Integer, ForeignKey("child.id"))
        child_id_two = Column(Integer, ForeignKey("child.id"))
    
        child_one = relationship("Child", foreign_keys=child_id_one)
        child_two = relationship("Child", foreign_keys=child_id_two)
    
    class Child(Base):
        __tablename__ = "child"
        id = Column(Integer, primary_key=True)
    
  • 对于自引用、复合外键的关系,在其中一个列指向自身的情况下,现在已经得到支持。典型案例如下:

    class Folder(Base):
        __tablename__ = "folder"
        __table_args__ = (
            ForeignKeyConstraint(
                ["account_id", "parent_id"], ["folder.account_id", "folder.folder_id"]
            ),
        )
    
        account_id = Column(Integer, primary_key=True)
        folder_id = Column(Integer, primary_key=True)
        parent_id = Column(Integer)
        name = Column(String)
    
        parent_folder = relationship(
            "Folder", backref="child_folders", remote_side=[account_id, folder_id]
        )
    

    上面的示例中,Folder 引用了其父 Folder,从 account_id 到自身的连接,并从 parent_idfolder_id。当 SQLAlchemy 构造自动连接时,不再假设“远程”一侧的所有列都被别名化,并且“本地”一侧的所有列都没有被别名化 - account_id 列在两侧都存在。因此,内部关系机制被完全重写,以支持一种完全不同的系统,其中生成了两个 account_id 的副本,每个副本包含不同的注释以确定它们在语句中的角色。注意基本急加载中的连接条件:

    SELECT
      folder.account_id  AS  folder_account_id,
      folder.folder_id  AS  folder_folder_id,
      folder.parent_id  AS  folder_parent_id,
      folder.name  AS  folder_name,
      folder_1.account_id  AS  folder_1_account_id,
      folder_1.folder_id  AS  folder_1_folder_id,
      folder_1.parent_id  AS  folder_1_parent_id,
      folder_1.name  AS  folder_1_name
    FROM  folder
      LEFT  OUTER  JOIN  folder  AS  folder_1
      ON
      folder_1.account_id  =  folder.account_id
      AND  folder.folder_id  =  folder_1.parent_id
    
    WHERE  folder.folder_id  =  ?  AND  folder.account_id  =  ?
    
  • 以前难以处理的自定义连接条件,比如涉及函数和/或类型转换的条件,现在在大多数情况下将按预期运行:

    class HostEntry(Base):
        __tablename__ = "host_entry"
    
        id = Column(Integer, primary_key=True)
        ip_address = Column(INET)
        content = Column(String(50))
    
        # relationship() using explicit foreign_keys, remote_side
        parent_host = relationship(
            "HostEntry",
            primaryjoin=ip_address == cast(content, INET),
            foreign_keys=content,
            remote_side=ip_address,
        )
    

    新的 relationship() 机制利用了 SQLAlchemy 中称为 annotations 的概念。这些注释也可以通过 foreign()remote() 函数显式地提供给应用程序代码,作为改善高级配置的可读性的手段,或者直接注入精确配置,绕过通常的连接检查启发式方法:

    from sqlalchemy.orm import foreign, remote
    
    class HostEntry(Base):
        __tablename__ = "host_entry"
    
        id = Column(Integer, primary_key=True)
        ip_address = Column(INET)
        content = Column(String(50))
    
        # relationship() using explicit foreign() and remote() annotations
        # in lieu of separate arguments
        parent_host = relationship(
            "HostEntry",
            primaryjoin=remote(ip_address) == cast(foreign(content), INET),
        )
    

另请参阅

配置关系连接方式 - 对于 relationship() 的最新技术进行了全面修订,详细说明了自定义相关属性和集合访问的最新技术。

#1401 #610 ### 新的类/对象检查系统

许多 SQLAlchemy 用户正在编写需要检查映射类的属性的系统,包括能够访问主键列、对象关系、普通属性等,通常是为了构建数据编组系统,如 JSON/XML 转换方案和各种表单库。

最初,TableColumn 模型是最初的检查点,拥有一个完全文档化的系统。虽然 SQLAlchemy ORM 模型也是完全可内省的,但这从未是一个完全稳定和受支持的特性,用户往往不清楚如何获取这些信息。

0.8 现在为此提供了一致、稳定且完全文档化的 API,包括适用于映射类、实例、属性和其他核心和 ORM 结构的检查系统。此系统的入口是核心级别的 inspect() 函数。在大多数情况下,被检查的对象已经是 SQLAlchemy 系统的一部分,比如 MapperInstanceStateInspector 等。在某些情况下,已经添加了新对象,用于在某些情境中提供检查 API,比如 AliasedInspAttributeState

以下是一些关键功能的介绍:

>>> class User(Base):
...     __tablename__ = "user"
...     id = Column(Integer, primary_key=True)
...     name = Column(String)
...     name_syn = synonym(name)
...     addresses = relationship("Address")

>>> # universal entry point is inspect()
>>> b = inspect(User)

>>> # b in this case is the Mapper
>>> b
<Mapper at 0x101521950; User>

>>> # Column namespace
>>> b.columns.id
Column('id', Integer(), table=<user>, primary_key=True, nullable=False)

>>> # mapper's perspective of the primary key
>>> b.primary_key
(Column('id', Integer(), table=<user>, primary_key=True, nullable=False),)

>>> # MapperProperties available from .attrs
>>> b.attrs.keys()
['name_syn', 'addresses', 'id', 'name']

>>> # .column_attrs, .relationships, etc. filter this collection
>>> b.column_attrs.keys()
['id', 'name']

>>> list(b.relationships)
[<sqlalchemy.orm.properties.RelationshipProperty object at 0x1015212d0>]

>>> # they are also namespaces
>>> b.column_attrs.id
<sqlalchemy.orm.properties.ColumnProperty object at 0x101525090>

>>> b.relationships.addresses
<sqlalchemy.orm.properties.RelationshipProperty object at 0x1015212d0>

>>> # point inspect() at a mapped, class level attribute,
>>> # returns the attribute itself
>>> b = inspect(User.addresses)
>>> b
<sqlalchemy.orm.attributes.InstrumentedAttribute object at 0x101521fd0>

>>> # From here we can get the mapper:
>>> b.mapper
<Mapper at 0x101525810; Address>

>>> # the parent inspector, in this case a mapper
>>> b.parent
<Mapper at 0x101521950; User>

>>> # an expression
>>> print(b.expression)
"user".id  =  address.user_id
>>> # inspect works on instances
>>> u1 = User(id=3, name="x")
>>> b = inspect(u1)

>>> # it returns the InstanceState
>>> b
<sqlalchemy.orm.state.InstanceState object at 0x10152bed0>

>>> # similar attrs accessor refers to the
>>> b.attrs.keys()
['id', 'name_syn', 'addresses', 'name']

>>> # attribute interface - from attrs, you get a state object
>>> b.attrs.id
<sqlalchemy.orm.state.AttributeState object at 0x10152bf90>

>>> # this object can give you, current value...
>>> b.attrs.id.value
3

>>> # ... current history
>>> b.attrs.id.history
History(added=[3], unchanged=(), deleted=())

>>> # InstanceState can also provide session state information
>>> # lets assume the object is persistent
>>> s = Session()
>>> s.add(u1)
>>> s.commit()

>>> # now we can get primary key identity, always
>>> # works in query.get()
>>> b.identity
(3,)

>>> # the mapper level key
>>> b.identity_key
(<class '__main__.User'>, (3,))

>>> # state within the session
>>> b.persistent, b.transient, b.deleted, b.detached
(True, False, False, False)

>>> # owning session
>>> b.session
<sqlalchemy.orm.session.Session object at 0x101701150>

另请参阅

运行时检查 API

#2208

新的 with_polymorphic() 功能,可在任何地方使用

Query.with_polymorphic() 方法允许用户指定在针对连接表实体进行查询时应该存在哪些表。不幸的是,该方法很笨拙,仅适用于列表中的第一个实体,并且在使用和内部方面都有令人困扰的行为。已添加了一个名为 with_polymorphic() 的新增强功能,可以将任何实体“别名化”为其自身的“多态”版本,可在任何地方自由使用:

from sqlalchemy.orm import with_polymorphic

palias = with_polymorphic(Person, [Engineer, Manager])
session.query(Company).join(palias, Company.employees).filter(
    or_(Engineer.language == "java", Manager.hair == "pointy")
)

另请参阅

使用 with_polymorphic() - 用于多态加载控制的新更新文档。

#2333

of_type() 与 alias()、with_polymorphic()、any()、has()、joinedload()、subqueryload()、contains_eager() 一起使用

PropComparator.of_type()方法用于在构建 SQL 表达式时指定要使用的特定子类型,该子类型作为relationship()的目标具有多态映射。现在可以通过与新的with_polymorphic()函数结合使用该方法来定位任意数量的目标子类型:

# use eager loading in conjunction with with_polymorphic targets
Job_P = with_polymorphic(Job, [SubJob, ExtraJob], aliased=True)
q = (
    s.query(DataContainer)
    .join(DataContainer.jobs.of_type(Job_P))
    .options(contains_eager(DataContainer.jobs.of_type(Job_P)))
)

该方法现在在大多数接受常规关系属性的地方同样有效,包括与加载器函数一起使用,如joinedload()subqueryload()contains_eager()以及比较方法,如PropComparator.any()PropComparator.has()

# use eager loading in conjunction with with_polymorphic targets
Job_P = with_polymorphic(Job, [SubJob, ExtraJob], aliased=True)
q = (
    s.query(DataContainer)
    .join(DataContainer.jobs.of_type(Job_P))
    .options(contains_eager(DataContainer.jobs.of_type(Job_P)))
)

# pass subclasses to eager loads (implicitly applies with_polymorphic)
q = s.query(ParentThing).options(
    joinedload_all(ParentThing.container, DataContainer.jobs.of_type(SubJob))
)

# control self-referential aliasing with any()/has()
Job_A = aliased(Job)
q = (
    s.query(Job)
    .join(DataContainer.jobs)
    .filter(
        DataContainer.jobs.of_type(Job_A).any(
            and_(Job_A.id < Job.id, Job_A.type == "fred")
        )
    )
)

另请参阅

连接到特定子类型或 with_polymorphic()实体

#2438 #1106

事件可以应用于未映射的超类

Mapper 和实例事件现在可以与未映射的超类关联,这些事件将随着子类被映射而传播。应该使用propagate=True标志。此功能允许将事件与声明基类关联:

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

@event.listens_for("load", Base, propagate=True)
def on_load(target, context):
    print("New instance loaded:", target)

# on_load() will be applied to SomeClass
class SomeClass(Base):
    __tablename__ = "sometable"

    # ...

#2585

Declarative 区分模块/包

Declarative 的一个关键特性是能够通过它们的字符串名称引用其他映射类。类名注册表现在对给定类的所属模块和包敏感。可以通过点名在表达式中引用这些类:

class Snack(Base):
    # ...

    peanuts = relationship(
        "nuts.Peanut", primaryjoin="nuts.Peanut.snack_id == Snack.id"
    )

解析允许使用任何完整或部分消歧义的包名称。如果到特定类的路径仍然模糊不清,则会引发错误。

#2338

Declarative 中的新 DeferredReflection 功能

“延迟反射”示例已移至 Declarative 中的一个支持功能。该功能允许仅使用占位符Table元数据构建声明性映射类,直到调用prepare()步骤,给定一个Engine以完全反映所有表并建立实际映射。该系统支持列的覆盖,单一和联合继承,以及每个引擎的不同基础。现在可以在一个步骤中在引擎创建时针对现有表创建完整的声明性配置:

class ReflectedOne(DeferredReflection, Base):
    __abstract__ = True

class ReflectedTwo(DeferredReflection, Base):
    __abstract__ = True

class MyClass(ReflectedOne):
    __tablename__ = "mytable"

class MyOtherClass(ReflectedOne):
    __tablename__ = "myothertable"

class YetAnotherClass(ReflectedTwo):
    __tablename__ = "yetanothertable"

ReflectedOne.prepare(engine_one)
ReflectedTwo.prepare(engine_two)

另请参见

DeferredReflection

#2485

ORM 类现在被核心构造所接受

虽然与Query.filter()一起使用的 SQL 表达式,例如User.id == 5,一直与核心构造(如select())兼容,但传递给select()Select.select_from()Select.correlate()时,映射类本身将不被识别。一个新的 SQL 注册系统允许一个映射类作为核心中的 FROM 子句被接受:

from sqlalchemy import select

stmt = select([User]).where(User.id == 5)

上面,映射的User类将扩展为User映射到的Table

#2245

Query.update()支持 UPDATE..FROM

新的 UPDATE..FROM 机制在 query.update()中起作用。下面,我们对SomeEntity发出一个 UPDATE,添加一个 FROM 子句(或等效的,取决于后端)对SomeOtherEntity

query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter(
    SomeOtherEntity.foo == "bar"
).update({"data": "x"})

特别是,支持对联合继承实体的更新,前提是 UPDATE 的目标是本地表上的,或者如果父表和子表混合,则它们在查询中明确连接。下面,以Engineer作为Person的联合子类:

query(Engineer).filter(Person.id == Engineer.id).filter(
    Person.name == "dilbert"
).update({"engineer_data": "java"})

会产生:

UPDATE  engineer  SET  engineer_data='java'  FROM  person
WHERE  person.id=engineer.id  AND  person.name='dilbert'

#2365

rollback()仅会回滚从 begin_nested()开始的“脏”对象

通过Session.begin_nested()使用 SAVEPOINT 的用户,应该改变行为以提高效率 - 在rollback()时,只有自上次刷新以来被标记为脏的对象将会过期,其余的Session保持不变。这是因为对 SAVEPOINT 的 ROLLBACK 不会终止包含事务的隔离,因此除了当前事务中未刷新的更改外,不需要过期。

#2452

缓存示例现在使用 dogpile.cache

缓存示例现在使用dogpile.cache。Dogpile.cache 是 Beaker 缓存部分的重写,具有更简单和更快的操作,以及对分布式锁定的支持。

请注意,Dogpile 示例以及之前的 Beaker 示例中使用的 SQLAlchemy API 略有变化,特别是在 Beaker 示例中所示的这种变化是必要的:

--- examples/beaker_caching/caching_query.py
+++ examples/beaker_caching/caching_query.py
@@ -222,7 +222,8 @@

         """
         if query._current_path:
-            mapper, key = query._current_path[-2:]
+            mapper, prop = query._current_path[-2:]
+            key = prop.key

             for cls in mapper.class_.__mro__:
                 if (cls, key) in self._relationship_options:

另请参阅

Dogpile 缓存

#2589

重写的relationship()机制

0.8 版本在relationship()确定如何在两个实体之间连接方面具有更加改进和强大的系统。新系统包括以下功能:

  • 当针对具有多个到目标的外键路径的类构建relationship()时,不再需要primaryjoin参数。只需要使用foreign_keys参数来指定应包含的列:

    class Parent(Base):
        __tablename__ = "parent"
        id = Column(Integer, primary_key=True)
        child_id_one = Column(Integer, ForeignKey("child.id"))
        child_id_two = Column(Integer, ForeignKey("child.id"))
    
        child_one = relationship("Child", foreign_keys=child_id_one)
        child_two = relationship("Child", foreign_keys=child_id_two)
    
    class Child(Base):
        __tablename__ = "child"
        id = Column(Integer, primary_key=True)
    
  • 支持自引用、复合外键的关系,其中一列指向自身。典型案例如下:

    class Folder(Base):
        __tablename__ = "folder"
        __table_args__ = (
            ForeignKeyConstraint(
                ["account_id", "parent_id"], ["folder.account_id", "folder.folder_id"]
            ),
        )
    
        account_id = Column(Integer, primary_key=True)
        folder_id = Column(Integer, primary_key=True)
        parent_id = Column(Integer)
        name = Column(String)
    
        parent_folder = relationship(
            "Folder", backref="child_folders", remote_side=[account_id, folder_id]
        )
    

    在上面的示例中,Folder指向其父Folder,从account_id连接到自身,并且从parent_id连接到folder_id。当 SQLAlchemy 构建自动连接时,不能再假定“远程”一侧的所有列都被别名化,而“本地”一侧的所有列都没有被别名化 - account_id在两侧都存在。因此,内部关系机制被完全重写以支持一个完全不同的系统,其中生成了两个account_id的副本,每个包含不同的注释以确定它们在语句中的角色。请注意基本贪婪加载中的连接条件:

    SELECT
      folder.account_id  AS  folder_account_id,
      folder.folder_id  AS  folder_folder_id,
      folder.parent_id  AS  folder_parent_id,
      folder.name  AS  folder_name,
      folder_1.account_id  AS  folder_1_account_id,
      folder_1.folder_id  AS  folder_1_folder_id,
      folder_1.parent_id  AS  folder_1_parent_id,
      folder_1.name  AS  folder_1_name
    FROM  folder
      LEFT  OUTER  JOIN  folder  AS  folder_1
      ON
      folder_1.account_id  =  folder.account_id
      AND  folder.folder_id  =  folder_1.parent_id
    
    WHERE  folder.folder_id  =  ?  AND  folder.account_id  =  ?
    
  • 以前难以处理的自定义连接条件,例如涉及函数和/或类型转换的情况,现在在大多数情况下将按预期运行:

    class HostEntry(Base):
        __tablename__ = "host_entry"
    
        id = Column(Integer, primary_key=True)
        ip_address = Column(INET)
        content = Column(String(50))
    
        # relationship() using explicit foreign_keys, remote_side
        parent_host = relationship(
            "HostEntry",
            primaryjoin=ip_address == cast(content, INET),
            foreign_keys=content,
            remote_side=ip_address,
        )
    

    新的relationship()机制利用了 SQLAlchemy 中称为 annotations 的概念。这些注释也可以通过foreign()remote()函数显式地提供给应用程序代码,作为改进高级配置的手段或直接注入精确配置的方式,绕过通常的联接检查启发式算法:

    from sqlalchemy.orm import foreign, remote
    
    class HostEntry(Base):
        __tablename__ = "host_entry"
    
        id = Column(Integer, primary_key=True)
        ip_address = Column(INET)
        content = Column(String(50))
    
        # relationship() using explicit foreign() and remote() annotations
        # in lieu of separate arguments
        parent_host = relationship(
            "HostEntry",
            primaryjoin=remote(ip_address) == cast(foreign(content), INET),
        )
    

另请参阅

配置关系连接方式 - 一个新修订的关于relationship()的部分,详细介绍了定制相关属性和集合访问的最新技术。

#1401 #610

新的类/对象检查系统

许多 SQLAlchemy 用户正在编写需要检查映射类属性的系统,包括能够访问主键列、对象关系、普通属性等,通常用于构建数据编组系统,如 JSON/XML 转换方案和各种表单库。

最初,TableColumn模型是最初的检查点,具有良好记录的系统。虽然 SQLAlchemy ORM 模型也是完全可自省的,但这从未是一个完全稳定和受支持的功能,用户往往不清楚如何获取这些信息。

现在,0.8 版本为此提供了一致、稳定且完全文档化的 API,包括一个检查系统,可用于映射类、实例、属性和其他核心和 ORM 构造。 这个系统的入口是核心级别的inspect()函数。 在大多数情况下,被检查的对象已经是 SQLAlchemy 系统的一部分,比如MapperInstanceStateInspector等。 在某些情况下,已添加了新对象,用于在某些上下文中提供检查 API,比如AliasedInspAttributeState

以下是一些关键功能的演示:

>>> class User(Base):
...     __tablename__ = "user"
...     id = Column(Integer, primary_key=True)
...     name = Column(String)
...     name_syn = synonym(name)
...     addresses = relationship("Address")

>>> # universal entry point is inspect()
>>> b = inspect(User)

>>> # b in this case is the Mapper
>>> b
<Mapper at 0x101521950; User>

>>> # Column namespace
>>> b.columns.id
Column('id', Integer(), table=<user>, primary_key=True, nullable=False)

>>> # mapper's perspective of the primary key
>>> b.primary_key
(Column('id', Integer(), table=<user>, primary_key=True, nullable=False),)

>>> # MapperProperties available from .attrs
>>> b.attrs.keys()
['name_syn', 'addresses', 'id', 'name']

>>> # .column_attrs, .relationships, etc. filter this collection
>>> b.column_attrs.keys()
['id', 'name']

>>> list(b.relationships)
[<sqlalchemy.orm.properties.RelationshipProperty object at 0x1015212d0>]

>>> # they are also namespaces
>>> b.column_attrs.id
<sqlalchemy.orm.properties.ColumnProperty object at 0x101525090>

>>> b.relationships.addresses
<sqlalchemy.orm.properties.RelationshipProperty object at 0x1015212d0>

>>> # point inspect() at a mapped, class level attribute,
>>> # returns the attribute itself
>>> b = inspect(User.addresses)
>>> b
<sqlalchemy.orm.attributes.InstrumentedAttribute object at 0x101521fd0>

>>> # From here we can get the mapper:
>>> b.mapper
<Mapper at 0x101525810; Address>

>>> # the parent inspector, in this case a mapper
>>> b.parent
<Mapper at 0x101521950; User>

>>> # an expression
>>> print(b.expression)
"user".id  =  address.user_id
>>> # inspect works on instances
>>> u1 = User(id=3, name="x")
>>> b = inspect(u1)

>>> # it returns the InstanceState
>>> b
<sqlalchemy.orm.state.InstanceState object at 0x10152bed0>

>>> # similar attrs accessor refers to the
>>> b.attrs.keys()
['id', 'name_syn', 'addresses', 'name']

>>> # attribute interface - from attrs, you get a state object
>>> b.attrs.id
<sqlalchemy.orm.state.AttributeState object at 0x10152bf90>

>>> # this object can give you, current value...
>>> b.attrs.id.value
3

>>> # ... current history
>>> b.attrs.id.history
History(added=[3], unchanged=(), deleted=())

>>> # InstanceState can also provide session state information
>>> # lets assume the object is persistent
>>> s = Session()
>>> s.add(u1)
>>> s.commit()

>>> # now we can get primary key identity, always
>>> # works in query.get()
>>> b.identity
(3,)

>>> # the mapper level key
>>> b.identity_key
(<class '__main__.User'>, (3,))

>>> # state within the session
>>> b.persistent, b.transient, b.deleted, b.detached
(True, False, False, False)

>>> # owning session
>>> b.session
<sqlalchemy.orm.session.Session object at 0x101701150>

另请参阅

运行时检查 API

#2208

新的 with_polymorphic()功能,可在任何地方使用

Query.with_polymorphic()方法允许用户指定在针对联接表实体进行查询时应该存在哪些表。 不幸的是,该方法很笨拙,只适用于列表中的第一个实体,否则在使用和内部方面都有一些尴尬的行为。 已添加了一个名为with_polymorphic()的新增强功能,它允许任何实体“别名”为其自身的“多态”版本,可自由在任何地方使用:

from sqlalchemy.orm import with_polymorphic

palias = with_polymorphic(Person, [Engineer, Manager])
session.query(Company).join(palias, Company.employees).filter(
    or_(Engineer.language == "java", Manager.hair == "pointy")
)

另请参阅

使用 with_polymorphic() - 用于多态加载控制的新更新文档。

#2333

of_type() 与 alias()、with_polymorphic()、any()、has()、joinedload()、subqueryload()、contains_eager()一起使用

PropComparator.of_type()方法用于在构建 SQL 表达式时指定要使用的特定子类型,该表达式沿着具有多态映射作为目标的relationship()。 现在,可以通过与新的with_polymorphic()函数结合使用,来指定任意数量的目标子类型:

# use eager loading in conjunction with with_polymorphic targets
Job_P = with_polymorphic(Job, [SubJob, ExtraJob], aliased=True)
q = (
    s.query(DataContainer)
    .join(DataContainer.jobs.of_type(Job_P))
    .options(contains_eager(DataContainer.jobs.of_type(Job_P)))
)

此方法现在在大多数常规关系属性接受的地方同样有效,包括与加载器函数一起使用,如joinedload()subqueryload()contains_eager(),以及比较方法如PropComparator.any()PropComparator.has()

# use eager loading in conjunction with with_polymorphic targets
Job_P = with_polymorphic(Job, [SubJob, ExtraJob], aliased=True)
q = (
    s.query(DataContainer)
    .join(DataContainer.jobs.of_type(Job_P))
    .options(contains_eager(DataContainer.jobs.of_type(Job_P)))
)

# pass subclasses to eager loads (implicitly applies with_polymorphic)
q = s.query(ParentThing).options(
    joinedload_all(ParentThing.container, DataContainer.jobs.of_type(SubJob))
)

# control self-referential aliasing with any()/has()
Job_A = aliased(Job)
q = (
    s.query(Job)
    .join(DataContainer.jobs)
    .filter(
        DataContainer.jobs.of_type(Job_A).any(
            and_(Job_A.id < Job.id, Job_A.type == "fred")
        )
    )
)

另请参阅

连接到特定子类型或 with_polymorphic() 实体

#2438 #1106

事件可以应用于未映射的超类

现在可以将 Mapper 和实例事件与未映射的超类关联,这些事件将传播到子类中,当这些子类被映射时。应该使用propagate=True标志。此功能允许将事件与声明式基类关联起来:

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

@event.listens_for("load", Base, propagate=True)
def on_load(target, context):
    print("New instance loaded:", target)

# on_load() will be applied to SomeClass
class SomeClass(Base):
    __tablename__ = "sometable"

    # ...

#2585

声明式区分模块/包

声明式的一个关键特性是能够使用其字符串名称引用其他映射类。现在,类名的注册表对给定类的拥有模块和包是敏感的。可以通过表达式中的点名引用这些类:

class Snack(Base):
    # ...

    peanuts = relationship(
        "nuts.Peanut", primaryjoin="nuts.Peanut.snack_id == Snack.id"
    )

解析允许使用任何全名或部分消除歧义的包名称。如果对特定类的路径仍然不明确,将会引发错误。

#2338

声明式中的新延迟反射功能

“延迟反射”示例已移至声明式中的支持功能。此功能允许仅使用占位符Table元数据构建声明式映射类,直到调用prepare()步骤,并提供一个Engine以完全反射所有表并建立实际映射为止。该系统支持列的重写、单一和连接继承,以及每个引擎的不同基类。现在可以一次性在引擎创建时针对现有表创建完整的声明式配置:

class ReflectedOne(DeferredReflection, Base):
    __abstract__ = True

class ReflectedTwo(DeferredReflection, Base):
    __abstract__ = True

class MyClass(ReflectedOne):
    __tablename__ = "mytable"

class MyOtherClass(ReflectedOne):
    __tablename__ = "myothertable"

class YetAnotherClass(ReflectedTwo):
    __tablename__ = "yetanothertable"

ReflectedOne.prepare(engine_one)
ReflectedTwo.prepare(engine_two)

另请参阅

DeferredReflection

#2485

ORM 类现在被核心构造所接受

虽然与Query.filter()一起使用的 SQL 表达式,例如User.id == 5,一直与核心构造兼容,例如select(),但当传递给select()Select.select_from()Select.correlate()时,映射类本身将不被识别。一个新的 SQL 注册系统允许映射类作为核心中的 FROM 子句被接受:

from sqlalchemy import select

stmt = select([User]).where(User.id == 5)

上面,映射的User类将扩展为TableUser映射到其中的表。

#2245

Query.update()支持 UPDATE..FROM

新的 UPDATE..FROM 机制适用于 query.update()。下面,我们对SomeEntity发出一个带有 FROM 子句(或等效的,取决于后端)的 UPDATE:

query(SomeEntity).filter(SomeEntity.id == SomeOtherEntity.id).filter(
    SomeOtherEntity.foo == "bar"
).update({"data": "x"})

特别是,支持对连接继承实体的更新,前提是 UPDATE 的目标是过滤表上的本地表,或者如果父表和子表混合,它们在查询中明确连接。下面,给定Engineer作为Person的连接子类:

query(Engineer).filter(Person.id == Engineer.id).filter(
    Person.name == "dilbert"
).update({"engineer_data": "java"})

会产生:

UPDATE  engineer  SET  engineer_data='java'  FROM  person
WHERE  person.id=engineer.id  AND  person.name='dilbert'

#2365

rollback()将仅回滚从 begin_nested()开始的“脏”对象

一项行为变更应该提高那些通过Session.begin_nested()使用 SAVEPOINT 的用户的效率 - 在rollback()时,只有自上次刷新以来被标记为脏的对象将被过期,其余的Session保持不变。这是因为对 SAVEPOINT 的 ROLLBACK 不会终止包含事务的隔离,因此除了当前事务中未刷新的更改外,不需要过期。

#2452

缓存示例现在使用 dogpile.cache

缓存示例现在使用dogpile.cache。Dogpile.cache 是 Beaker 缓存部分的重写,具有更简单和更快的操作,以及对分布式锁定的支持。

注意,Dogpile 示例以及之前的 Beaker 示例中使用的 SQLAlchemy API 略有变化,特别是正如 Beaker 示例中所示,这种变化是必要的:

--- examples/beaker_caching/caching_query.py
+++ examples/beaker_caching/caching_query.py
@@ -222,7 +222,8 @@

         """
         if query._current_path:
-            mapper, key = query._current_path[-2:]
+            mapper, prop = query._current_path[-2:]
+            key = prop.key

             for cls in mapper.class_.__mro__:
                 if (cls, key) in self._relationship_options:

另请参见

Dogpile Caching

#2589

新的核心功能

完全可扩展,类型级别的核心操作符支持

到目前为止,核心从未有过任何系统来为列和其他表达式构造添加对新 SQL 运算符的支持,除了ColumnOperators.op()方法,这个方法“刚好”能让事情正常运行。此外,核心从未有过任何系统允许覆盖现有运算符的行为。直到现在,唯一灵活重新定义运算符的方式是在 ORM 层中,使用column_property()并提供一个comparator_factory参数。因此,像 GeoAlchemy 这样的第三方库被迫以 ORM 为中心,并依赖各种技巧来应用新操作以及使其正确传播。

核心中的新运算符系统添加了一直缺失的关键点,即将新的和覆盖的运算符与类型关联起来。毕竟,真正驱动操作类型的不是列、CAST 运算符或 SQL 函数,而是表达式的类型。实现细节很少 - 只需向核心ColumnElement类型添加几个额外方法,以便它向其TypeEngine对象查询可选的一组运算符。新的或修订的操作可以与任何类型关联,可以通过对现有类型进行子类化,使用TypeDecorator,或者通过将新的Comparator对象附加到现有类型类来“全面推广”。

例如,要为Numeric类型添加对数支持:

from sqlalchemy.types import Numeric
from sqlalchemy.sql import func

class CustomNumeric(Numeric):
    class comparator_factory(Numeric.Comparator):
        def log(self, other):
            return func.log(self.expr, other)

这种新类型可以像任何其他类型一样使用:

data = Table(
    "data",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("x", CustomNumeric(10, 5)),
    Column("y", CustomNumeric(10, 5)),
)

stmt = select([data.c.x.log(data.c.y)]).where(data.c.x.log(2) < value)
print(conn.execute(stmt).fetchall())

由此带来的新功能包括立即支持 PostgreSQL 的 HSTORE 类型,以及与 PostgreSQL 的 ARRAY 类型相关的新操作。它还为现有类型开辟了更多特定于这些类型的运算符的道路,例如更多的字符串、整数和日期运算符。

另请参阅

重新定义和创建新运算符

HSTORE

#2547

插入的多值支持

Insert.values()方法现在支持字典列表,将生成多 VALUES 语句,如VALUES (<row1>), (<row2>), ...。这仅适用于支持此语法的后端,包括 PostgreSQL、SQLite 和 MySQL。这与通常的executemany()风格的 INSERT 不同:

users.insert().values(
    [
        {"name": "some name"},
        {"name": "some other name"},
        {"name": "yet another name"},
    ]
)

另请参阅

Insert.values()

#2623

类型表达式

现在可以将 SQL 表达式与类型关联起来。从历史上看,TypeEngine一直允许 Python 端函数接收绑定参数和结果行值,通过 Python 端转换函数在到达/返回数据库时进行转换。新功能允许类似的功能,但在数据库端进行:

from sqlalchemy.types import String
from sqlalchemy import func, Table, Column, MetaData

class LowerString(String):
    def bind_expression(self, bindvalue):
        return func.lower(bindvalue)

    def column_expression(self, col):
        return func.lower(col)

metadata = MetaData()
test_table = Table("test_table", metadata, Column("data", LowerString))

上面,LowerString类型定义了一个 SQL 表达式,每当test_table.c.data列在 SELECT 语句的列子句中呈现时,该表达式将被发出:

>>> print(select([test_table]).where(test_table.c.data == "HI"))
SELECT  lower(test_table.data)  AS  data
FROM  test_table
WHERE  test_table.data  =  lower(:data_1) 

这个功能也被新版 GeoAlchemy 大量使用,以根据类型规则在 SQL 中内联嵌入 PostGIS 表达式。

另请参阅

应用 SQL 级别的绑定/结果处理

#1534

核心检查系统

inspect()函数引入了新的类/对象检查系统,也适用于核心。应用于Engine会产生一个Inspector对象:

from sqlalchemy import inspect
from sqlalchemy import create_engine

engine = create_engine("postgresql://scott:tiger@localhost/test")
insp = inspect(engine)
print(insp.get_table_names())

它也可以应用于任何ClauseElement,它返回ClauseElement本身,比如TableColumnSelect等。这使得它可以在核心和 ORM 构造之间流畅地工作。

新方法Select.correlate_except()

select() 现在有一个方法Select.correlate_except(),指定“除了指定的所有 FROM 子句之外的所有 FROM 子句”。它可用于映射场景,其中相关子查询应该正常关联,除了针对特定目标可选择的情况:

class SnortEvent(Base):
    __tablename__ = "event"

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

    signatures = relationship("Signature", lazy=False)

class Signature(Base):
    __tablename__ = "signature"

    id = Column(Integer, primary_key=True)

    sig_count = column_property(
        select([func.count("*")])
        .where(SnortEvent.signature == id)
        .correlate_except(SnortEvent)
    )

另请参阅

Select.correlate_except()

PostgreSQL HSTORE 类型

PostgreSQL 的HSTORE类型现在可以作为HSTORE使用。该类型充分利用了新的操作符系统,为 HSTORE 类型提供了一整套操作符,包括索引访问、连接和包含方法,如comparator_factory.has_key()comparator_factory.has_any()comparator_factory.matrix()

from sqlalchemy.dialects.postgresql import HSTORE

data = Table(
    "data_table",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("hstore_data", HSTORE),
)

engine.execute(select([data.c.hstore_data["some_key"]])).scalar()

engine.execute(select([data.c.hstore_data.matrix()])).scalar()

另请参阅

HSTORE

hstore

#2606

增强的 PostgreSQL ARRAY 类型

ARRAY 类型将接受一个可选的“维度”参数,将其固定到一个固定数量的维度,大大提高检索结果的效率:

# old way, still works since PG supports N-dimensions per row:
Column("my_array", postgresql.ARRAY(Integer))

# new way, will render ARRAY with correct number of [] in DDL,
# will process binds and results more efficiently as we don't need
# to guess how many levels deep to go
Column("my_array", postgresql.ARRAY(Integer, dimensions=2))

该类型还引入了新的操作符,使用新的类型特定的操作符框架。新操作包括索引访问:

result = conn.execute(select([mytable.c.arraycol[2]]))

在 SELECT 中的切片访问:

result = conn.execute(select([mytable.c.arraycol[2:4]]))

在 UPDATE 中的切片更新:

conn.execute(mytable.update().values({mytable.c.arraycol[2:3]: [7, 8]}))

独立的数组文字:

>>> from sqlalchemy.dialects import postgresql
>>> conn.scalar(select([postgresql.array([1, 2]) + postgresql.array([3, 4, 5])]))
[1, 2, 3, 4, 5]

数组连接,在下面,右侧的[4, 5, 6]被强制转换为数组文字:

select([mytable.c.arraycol + [4, 5, 6]])

另请参阅

ARRAY

array

#2441

新的可配置的 SQLite 日期、时间类型

SQLite 没有内置的 DATE、TIME 或 DATETIME 类型,而是提供了一些支持将日期和时间值存储为字符串或整数的方法。0.8 版本中增强了 SQLite 的日期和时间类型,使其更加可配置,包括“微秒”部分是可选的,以及几乎所有其他内容。

Column("sometimestamp", sqlite.DATETIME(truncate_microseconds=True))
Column(
    "sometimestamp",
    sqlite.DATETIME(
        storage_format=(
            "%(year)04d%(month)02d%(day)02d"
            "%(hour)02d%(minute)02d%(second)02d%(microsecond)06d"
        ),
        regexp="(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{6})",
    ),
)
Column(
    "somedate",
    sqlite.DATE(
        storage_format="%(month)02d/%(day)02d/%(year)04d",
        regexp="(?P<month>\d+)/(?P<day>\d+)/(?P<year>\d+)",
    ),
)

非常感谢 Nate Dub 在 Pycon 2012 上的努力。

另请参阅

DATETIME

DATE

TIME

#2363

“COLLATE”在所有方言中都受支持;特别是 MySQL、PostgreSQL、SQLite

“collate”关键字,长期以来被 MySQL 方言接受,现在已经在所有 String 类型上建立,并且将在任何后端呈现,包括在使用 MetaData.create_all()cast() 等特性时:

>>> stmt = select([cast(sometable.c.somechar, String(20, collation="utf8"))])
>>> print(stmt)
SELECT  CAST(sometable.somechar  AS  VARCHAR(20)  COLLATE  "utf8")  AS  anon_1
FROM  sometable 

另见

String

#2276

“Prefixes”现在支持于 update(), delete()

面向 MySQL,一个“前缀”可以在这些结构中的任何一个中呈现。例如:

stmt = table.delete().prefix_with("LOW_PRIORITY", dialect="mysql")

stmt = table.update().prefix_with("LOW_PRIORITY", dialect="mysql")

该方法是新增的,除了已经存在于 insert(), select()Query 上的方法。

另见

Update.prefix_with()

Delete.prefix_with()

Insert.prefix_with()

Select.prefix_with()

Query.prefix_with()

#2431

完全可扩展的核心级别操作符支持

迄今为止,核心从未有过为 Column 和其他表达式构造添加新 SQL 运算符的系统,除了 ColumnOperators.op() 方法,它“刚好足够”使事情正常工作。此外,核心中也从未建立过任何系统,允许覆盖现有运算符的行为。直到现在,灵活重新定义运算符的唯一方法是在 ORM 层中,使用 column_property() 给定一个 comparator_factory 参数。因此,像 GeoAlchemy 这样的第三方库被迫是 ORM 中心的,并且依赖于一系列的黑客来应用新的操作以及使其正确传播。

核心中的新运算符系统添加了一直缺失的一个钩子,即将新的和重写的运算符与类型关联起来。毕竟,真正驱动存在哪些操作的不是列、CAST 运算符或 SQL 函数,而是表达式的类型。实现细节很少——只需向核心 ColumnElement 类型添加几个额外的方法,以便它向其 TypeEngine 对象查询一组可选的运算符。新的或修改后的操作可以与任何类型关联,可以通过对现有类型的子类化、使用 TypeDecorator 或通过将新的 Comparator 对象附加到现有类型类来进行“全面的跨越边界”的关联。

例如,要向 Numeric 类型添加对数支持:

from sqlalchemy.types import Numeric
from sqlalchemy.sql import func

class CustomNumeric(Numeric):
    class comparator_factory(Numeric.Comparator):
        def log(self, other):
            return func.log(self.expr, other)

新类型可像其他类型一样使用:

data = Table(
    "data",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("x", CustomNumeric(10, 5)),
    Column("y", CustomNumeric(10, 5)),
)

stmt = select([data.c.x.log(data.c.y)]).where(data.c.x.log(2) < value)
print(conn.execute(stmt).fetchall())

此举带来的新功能包括对 PostgreSQL 的 HSTORE 类型的支持,以及与 PostgreSQL 的 ARRAY 类型相关的新操作。它还为现有类型提供了更多专门针对这些类型的操作符的可能性,如更多字符串、整数和日期操作符。

另请参阅

重新定义和创建新运算符

HSTORE

#2547

插入的多值支持

Insert.values() 方法现在支持字典列表,这将呈现出多值语句,如 VALUES (<row1>), (<row2>), ...。这仅与支持此语法的后端相关,包括 PostgreSQL、SQLite 和 MySQL。这与通常的 executemany() 样式的 INSERT 不同:

users.insert().values(
    [
        {"name": "some name"},
        {"name": "some other name"},
        {"name": "yet another name"},
    ]
)

另请参阅

Insert.values()

#2623

类型表达式

SQL 表达式现在可以与类型关联。在历史上,TypeEngine 一直允许 Python 端函数接收绑定参数和结果行值,并在传递到/从数据库的途中通过 Python 端转换函数进行转换。新功能允许类似的功能,但在数据库端执行:

from sqlalchemy.types import String
from sqlalchemy import func, Table, Column, MetaData

class LowerString(String):
    def bind_expression(self, bindvalue):
        return func.lower(bindvalue)

    def column_expression(self, col):
        return func.lower(col)

metadata = MetaData()
test_table = Table("test_table", metadata, Column("data", LowerString))

上述中,LowerString类型定义了一个 SQL 表达式,每当test_table.c.data列在 SELECT 语句的列子句中被呈现时,该表达式就会被发出:

>>> print(select([test_table]).where(test_table.c.data == "HI"))
SELECT  lower(test_table.data)  AS  data
FROM  test_table
WHERE  test_table.data  =  lower(:data_1) 

这个特性也被新版的 GeoAlchemy 大量使用,以根据类型规则在 SQL 中内联嵌入 PostGIS 表达式。

另请参阅

应用 SQL 级绑定/结果处理

#1534

核心检查系统

引入的inspect()函数新的类/对象检查系统也适用于核心。应用到一个Engine上会产生一个Inspector对象:

from sqlalchemy import inspect
from sqlalchemy import create_engine

engine = create_engine("postgresql://scott:tiger@localhost/test")
insp = inspect(engine)
print(insp.get_table_names())

它也可以应用于任何返回自身的ClauseElement,例如TableColumnSelect等。这使它可以在核心和 ORM 构造之间流畅工作。

新方法Select.correlate_except()

select()现在有一个方法Select.correlate_except(),它指定“在除了指定的 FROM 子句之外的所有 FROM 子句上关联”。它可用于映射方案,其中相关子查询应该正常关联,除了针对特定目标可选择的情况:

class SnortEvent(Base):
    __tablename__ = "event"

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

    signatures = relationship("Signature", lazy=False)

class Signature(Base):
    __tablename__ = "signature"

    id = Column(Integer, primary_key=True)

    sig_count = column_property(
        select([func.count("*")])
        .where(SnortEvent.signature == id)
        .correlate_except(SnortEvent)
    )

另请参阅

Select.correlate_except()

PostgreSQL HSTORE 类型

对 PostgreSQL 的HSTORE类型的支持现在可用作HSTORE。这种类型充分利用了新的运算符系统,为 HSTORE 类型提供了一整套运算符,包括索引访问、连接和包含方法,如comparator_factory.has_key()comparator_factory.has_any()comparator_factory.matrix()

from sqlalchemy.dialects.postgresql import HSTORE

data = Table(
    "data_table",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("hstore_data", HSTORE),
)

engine.execute(select([data.c.hstore_data["some_key"]])).scalar()

engine.execute(select([data.c.hstore_data.matrix()])).scalar()

另请参阅

HSTORE

hstore

#2606

增强的 PostgreSQL ARRAY 类型

ARRAY 类型将接受一个可选的“维度”参数,将其固定到一个固定数量的维度,大大提高检索结果的效率:

# old way, still works since PG supports N-dimensions per row:
Column("my_array", postgresql.ARRAY(Integer))

# new way, will render ARRAY with correct number of [] in DDL,
# will process binds and results more efficiently as we don't need
# to guess how many levels deep to go
Column("my_array", postgresql.ARRAY(Integer, dimensions=2))

该类型还引入了新的操作符,使用新的类型特定的操作符框架。新操作包括索引访问:

result = conn.execute(select([mytable.c.arraycol[2]]))

在 SELECT 中的切片访问:

result = conn.execute(select([mytable.c.arraycol[2:4]]))

在 UPDATE 中的切片更新:

conn.execute(mytable.update().values({mytable.c.arraycol[2:3]: [7, 8]}))

独立的数组文字:

>>> from sqlalchemy.dialects import postgresql
>>> conn.scalar(select([postgresql.array([1, 2]) + postgresql.array([3, 4, 5])]))
[1, 2, 3, 4, 5]

数组连接,下面的右侧[4, 5, 6]被强制转换为数组文字:

select([mytable.c.arraycol + [4, 5, 6]])

另请参见

ARRAY

array

#2441

新的、可配置的 SQLite 日期、时间类型

SQLite 没有内置的 DATE,TIME 或 DATETIME 类型,而是提供了一些支持,用于将日期和时间值存储为字符串或整数。SQLite 中的日期和时间类型在 0.8 中得到了增强,可以更具体地配置特定格式,包括“微秒”部分是可选的,以及几乎所有其他内容。

Column("sometimestamp", sqlite.DATETIME(truncate_microseconds=True))
Column(
    "sometimestamp",
    sqlite.DATETIME(
        storage_format=(
            "%(year)04d%(month)02d%(day)02d"
            "%(hour)02d%(minute)02d%(second)02d%(microsecond)06d"
        ),
        regexp="(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{6})",
    ),
)
Column(
    "somedate",
    sqlite.DATE(
        storage_format="%(month)02d/%(day)02d/%(year)04d",
        regexp="(?P<month>\d+)/(?P<day>\d+)/(?P<year>\d+)",
    ),
)

非常感谢 Nate Dub 在 Pycon 2012 上的努力。

另请参见

DATETIME

DATE

TIME

#2363

“COLLATE”在所有方言中都受支持;特别是 MySQL,PostgreSQL,SQLite

“collate”关键字,长期以来被 MySQL 方言接受,现在已在所有String 类型上建立,并将在任何后端上呈现,包括在使用MetaData.create_all()cast()等功能时:

>>> stmt = select([cast(sometable.c.somechar, String(20, collation="utf8"))])
>>> print(stmt)
SELECT  CAST(sometable.somechar  AS  VARCHAR(20)  COLLATE  "utf8")  AS  anon_1
FROM  sometable 

另请参见

String

#2276

现在支持“前缀”用于update(), delete()

面向 MySQL,可以在任何这些结构中呈现“前缀”。例如:

stmt = table.delete().prefix_with("LOW_PRIORITY", dialect="mysql")

stmt = table.update().prefix_with("LOW_PRIORITY", dialect="mysql")

该方法是新增的,除了已存在于insert()select()Query上的方法之外。

另请参阅

Update.prefix_with()

Delete.prefix_with()

Insert.prefix_with()

Select.prefix_with()

Query.prefix_with()

#2431

行为变化

将“待定”对象视为“孤儿”的考虑变得更加积极

这是 0.8 系列的一个后期添加,但希望新行为在更广泛的情况下更一致和直观。ORM 自至少 0.4 版本以来一直包括这样的行为,即一个“待定”对象,意味着它与Session关联但尚未插入数据库,当它成为“孤儿”时,即已与使用delete-orphan级联的父对象解除关联时,将自动从Session中清除。此行为旨在大致模拟持久对象的行为,其中 ORM 将根据分离事件的拦截发出 DELETE 以删除这些成为孤儿的对象。

行为变化适用于被多种父对象引用并且每个父对象都指定delete-orphan的对象;典型示例是在多对多模式中连接两种其他对象的关联对象。以前,行为是这样的,即当待定对象与所有父对象解除关联时才会被清除。随着行为变化,一旦待定对象与任何先前关联的父对象解除关联,该待定对象就会被清除。此行为旨在更接近持久对象的行为,即一旦与任何父对象解除关联,它们就会被删除。

较旧行为的基本原因可以追溯到至少版本 0.4,基本上是一种防御性决定,试图在对象仍在为 INSERT 构造时减轻混淆。但现实情况是,无论如何,只要对象附加到任何新父级,它就会立即重新与Session关联。

仍然可以刷新一个对象,该对象尚未与其所有必需的父级关联,如果该对象一开始就未与这些父级关联,或者如果它被清除,但随后通过后续附加事件重新与Session关联,但仍未完全关联。在这种情况下,预计数据库会发出完整性错误,因为很可能存在未填充的 NOT NULL 外键列。ORM 做出决定让这些 INSERT 尝试发生,基于这样的判断:一个只与其必需的父级部分关联但已经与其中一些父级积极关联的对象,更多的情况下是用户错误,而不是应该被悄悄跳过的有意遗漏 - 在这里悄悄跳过 INSERT 会使这种性质的用户错误非常难以调试。

对于可能依赖于旧行为的应用程序,可以通过将标志legacy_is_orphan指定为映射器选项来重新启用旧行为。

新行为允许以下测试用例正常工作:

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

Base = declarative_base()

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

class UserKeyword(Base):
    __tablename__ = "user_keyword"
    user_id = Column(Integer, ForeignKey("user.id"), primary_key=True)
    keyword_id = Column(Integer, ForeignKey("keyword.id"), primary_key=True)

    user = relationship(
        User, backref=backref("user_keywords", cascade="all, delete-orphan")
    )

    keyword = relationship(
        "Keyword", backref=backref("user_keywords", cascade="all, delete-orphan")
    )

    # uncomment this to enable the old behavior
    # __mapper_args__ = {"legacy_is_orphan": True}

class Keyword(Base):
    __tablename__ = "keyword"
    id = Column(Integer, primary_key=True)
    keyword = Column("keyword", String(64))

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

# note we're using PostgreSQL to ensure that referential integrity
# is enforced, for demonstration purposes.
e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)

Base.metadata.drop_all(e)
Base.metadata.create_all(e)

session = Session(e)

u1 = User(name="u1")
k1 = Keyword(keyword="k1")

session.add_all([u1, k1])

uk1 = UserKeyword(keyword=k1, user=u1)

# previously, if session.flush() were called here,
# this operation would succeed, but if session.flush()
# were not called here, the operation fails with an
# integrity error.
# session.flush()
del u1.user_keywords[0]

session.commit()

#2655

在项目与会话关联之后而不是之前触发 after_attach 事件;before_attach 添加

使用 after_attach 的事件处理程序现在可以假定给定实例与给定会话关联:

@event.listens_for(Session, "after_attach")
def after_attach(session, instance):
    assert instance in session

有些用例要求它按照这种方式工作。然而,其他用例要求该项尚未成为会话的一部分,例如,当一个查询旨在加载实例所需的某些状态时,首先会触发自动刷新并且否则会过早刷新目标对象。这些用例应该使用新的“before_attach”事件:

@event.listens_for(Session, "before_attach")
def before_attach(session, instance):
    instance.some_necessary_attribute = (
        session.query(Widget).filter_by(instance.widget_name).first()
    )

#2464

查询现在像 select()一样自动相关

以前需要调用Query.correlate()才能使列或 WHERE 子查询与父级相关联:

subq = (
    session.query(Entity.value)
    .filter(Entity.id == Parent.entity_id)
    .correlate(Parent)
    .as_scalar()
)
session.query(Parent).filter(subq == "some value")

这与普通的select()构造相反,后者默认情况下会自动相关。在 0.8 中,上述语句将自动相关:

subq = session.query(Entity.value).filter(Entity.id == Parent.entity_id).as_scalar()
session.query(Parent).filter(subq == "some value")

就像在select()中一样,通过调用query.correlate(None)来禁用相关性,或者通过传递一个实体来手动设置相关性,query.correlate(someentity)

#2179

相关性现在始终是上下文特定的

为了允许更广泛的相关性场景,Select.correlate()Query.correlate() 的行为略有改变,即如果 SELECT 语句实际上在该上下文中使用,那么该语句将仅在 FROM 子句中省略“相关”的目标。此外,不再可能将作为外部 SELECT 语句中的 FROM 的 SELECT 语句“相关”(即省略)FROM 子句。

这个改变只会让 SQL 渲染变得更好,因为不再可能渲染出不合法的 SQL,即在选择的内容相对于 FROM 对象不足的情况下:

from sqlalchemy.sql import table, column, select

t1 = table("t1", column("x"))
t2 = table("t2", column("y"))
s = select([t1, t2]).correlate(t1)

print(s)

在此更改之前,上述将返回:

SELECT  t1.x,  t2.y  FROM  t2

当“t1”在任何 FROM 子句中都没有被引用时,这是无效的 SQL。

现在,在没有外部 SELECT 的情况下,它返回:

SELECT  t1.x,  t2.y  FROM  t1,  t2

在 SELECT 中,相关性会如预期地生效:

s2 = select([t1, t2]).where(t1.c.x == t2.c.y).where(t1.c.x == s)
print(s2)
SELECT  t1.x,  t2.y  FROM  t1,  t2
WHERE  t1.x  =  t2.y  AND  t1.x  =
  (SELECT  t1.x,  t2.y  FROM  t2)

这个改变不会影响任何现有应用程序,因为对于正确构建的表达式,相关性行为保持不变。只有依赖于在非相关上下文中使用相关的 SELECT 的无效字符串输出的应用程序(很可能是在测试场景中),才会看到任何变化。

#2668 ### create_all() 和 drop_all() 现在将空列表视为如此

方法 MetaData.create_all()MetaData.drop_all() 现在将接受一个空列表的 Table 对象,并且不会发出任何 CREATE 或 DROP 语句。以前,将空列表解释为对集合传递 None,并且将无条件地为所有项目发出 CREATE/DROP。

这是一个错误修复,但一些应用程序可能一直依赖于先前的行为。

#2664

修复了 InstrumentationEvents 的事件定位。

InstrumentationEvents系列事件目标已经记录,事件只会根据实际传递的类来触发。在 0.7 版本中,情况并非如此,应用于InstrumentationEvents的任何事件监听器都会对所有映射的类调用。在 0.8 版本中,添加了额外的逻辑,使事件只会为那些发送的类调用。这里的propagate标志默认设置为True,因为类仪器事件通常用于拦截尚未创建的类。

#2590

不再将“=”自动转换为 IN,用于与 MS-SQL 中的子查询进行比较

我们在 MSSQL 方言中发现了一个非常古老的行为,当用户执行类似以下操作时,它会试图拯救用户:

scalar_subq = select([someothertable.c.id]).where(someothertable.c.data == "foo")
select([sometable]).where(sometable.c.id == scalar_subq)

SQL Server 不允许将相等比较与标量 SELECT 进行比较,即“x = (SELECT something)”。MSSQL 方言会将其转换为 IN。然而,当进行类似“(SELECT something) = x”的比较时,也会发生相同的情况,总体而言,这种猜测的水平超出了 SQLAlchemy 通常的范围,因此删除了这种行为。

#2277

修复了Session.is_modified()的行为

Session.is_modified()方法接受一个参数passive,基本上不应该是必要的,所有情况下参数应该是值True - 当保持默认值False时,会影响数据库,并经常触发自动刷新,这将改变结果。在 0.8 版本中,passive参数将不起作用,并且未加载的属性永远不会检查历史记录,因为根据定义,未加载的属性上不会有待处理的状态更改。

另请参见

Session.is_modified()

#2320

Column.keySelect.c属性中受到Select.apply_labels()的尊重

表达式系统的用户知道Select.apply_labels()会在每个列名前添加表名,影响从Select.c中可用的名称:

s = select([table1]).apply_labels()
s.c.table1_col1
s.c.table1_col2

在 0.8 之前,如果Column具有不同的Column.key,则此键将被忽略,与未使用Select.apply_labels()时的不一致性:

# before 0.8
table1 = Table("t1", metadata, Column("col1", Integer, key="column_one"))
s = select([table1])
s.c.column_one  # would be accessible like this
s.c.col1  # would raise AttributeError

s = select([table1]).apply_labels()
s.c.table1_column_one  # would raise AttributeError
s.c.table1_col1  # would be accessible like this

在 0.8 中,Column.key在两种情况下都受到尊重:

# with 0.8
table1 = Table("t1", metadata, Column("col1", Integer, key="column_one"))
s = select([table1])
s.c.column_one  # works
s.c.col1  # AttributeError

s = select([table1]).apply_labels()
s.c.table1_column_one  # works
s.c.table1_col1  # AttributeError

关于“name”和“key”的所有其他行为都是相同的,包括渲染的 SQL 仍然使用形式<tablename>_<colname> - 这里的重点是防止Column.key内容被渲染到SELECT语句中,以便在Column.key中使用特殊/非 ASCII 字符时不会出现问题。

#2397

single_parent警告现在是错误

一个relationship(),它是多对一或多对多关系,并指定了“cascade=’all, delete-orphan’”,这是一个尴尬但仍然支持的用例(带有限制),如果关系没有指定single_parent=True选项,现在将引发错误。以前只会发出警告,但在任何情况下几乎立即会在属性系统中跟随失败。

#2405

inspector参数添加到column_reflect事件

0.7 添加了一个名为column_reflect的新事件,提供了每个列反射时可以增强的事件。我们在这个事件上稍微出了点错,因为事件没有提供获取当前用于反射的InspectorConnection的方法,以防需要来自数据库的其他信息。由于这是一个尚未广泛使用的新事件,我们将直接在其中添加inspector参数:

@event.listens_for(Table, "column_reflect")
def listen_for_col(inspector, table, column_info): ...

#2418

禁用 MySQL 的自动检测排序规则和大小写

MySQL 方言在Engine连接时第一次进行两次调用,其中一次非常昂贵,加载数据库中的所有可能排序规则以及大小写信息。这两个集合都不会用于任何 SQLAlchemy 函数,因此这些调用将被更改为不再自动发出。可能依赖于这些集合存在于engine.dialect上的应用程序将需要直接调用_detect_collations()_detect_casing()

#2404

“未使用的列名”警告变为异常

insert()update()构造中引用不存在的列将引发错误而不是警告:

t1 = table("t1", column("x"))
t1.insert().values(x=5, z=5)  # raises "Unconsumed column names: z"

#2415

Inspector.get_primary_keys()已弃用,请使用 Inspector.get_pk_constraint

这两种Inspector上的方法是多余的,其中get_primary_keys()将返回与get_pk_constraint()相同的信息,只是不包括约束的名称:

>>> insp.get_primary_keys()
["a", "b"]

>>> insp.get_pk_constraint()
{"name":"pk_constraint", "constrained_columns":["a", "b"]}

#2422

大多数情况下将禁用不区分大小写的结果行名称

一个非常古老的行为,RowProxy中的列名始终是不区分大小写比较的:

>>> row = result.fetchone()
>>> row["foo"] == row["FOO"] == row["Foo"]
True

这是为了一些在早期需要这样做的方言,如 Oracle 和 Firebird,但在现代用法中,我们有更准确的方法来处理这两个平台的不区分大小写行为。

未来,此行为将仅可选地可用,通过将标志case_sensitive=False传递给create_engine(),但否则从行中请求的列名必须匹配大小写。

#2423

InstrumentationManager和替代类仪器现在是一个扩展

sqlalchemy.orm.interfaces.InstrumentationManager类已移至sqlalchemy.ext.instrumentation.InstrumentationManager。 “替代仪器”系统是为了极少数需要使用现有或不寻常的类仪器系统的安装而构建的,并且通常很少使用。该系统的复杂性已导出到一个ext.模块中。直到被导入后才会使用,通常是当第三方库导入InstrumentationManager时,此时它将通过用ExtendedInstrumentationRegistry替换默认的InstrumentationFactory将其注入回sqlalchemy.orm中。

将“待定”对象视为“孤儿”的考虑变得更加激进

这是 0.8 系列的一个较晚添加,但希望新行为在更广泛的情况下通常更一致和直观。ORM 自至少 0.4 版本以来已经包含了这样的行为,即一个“待定”对象,意味着它与Session相关联,但尚未插入到数据库中,当它成为“孤儿”时,即已经与引用它的父对象解除关联,并且在配置的relationship()上使用delete-orphan级联时,将自动从Session中删除。此行为旨在大致反映持久对象(即已插入)的行为,ORM 将根据分离事件的拦截为这些成为孤儿的对象发出 DELETE。

行为变更适用于被多种类型父对象引用的对象,每种类型父对象都指定delete-orphan;典型示例是在多对多模式中桥接两种其他对象的关联对象。以前的行为是,挂起的对象仅在与所有父对象解除关联时才会被清除。通过行为变更,只要挂起的对象与先前相关联的任何父对象解除关联,它就会被清除。这种行为旨在更接近持久对象的行为,即只要它们与任何父对象解除关联,它们就会被删除。

较旧行为的基本理由可以追溯到至少版本 0.4,基本上是一种防御性决定,试图在对象仍在为 INSERT 构造时减轻混淆。但事实是,无论如何,只要对象附加到任何新父对象,它就会立即重新与Session相关联。

仍然可以刷新一个与所有必需父对象都不相关联的对象,如果该对象一开始就没有与这些父对象相关联,或者如果它被清除,但后来通过后续的附加事件重新与Session相关联但仍未完全相关联。在这种情况下,预计数据库会发出完整性错误,因为可能存在未填充的 NOT NULL 外键列。ORM 决定让这些 INSERT 尝试发生,基于这样的判断:一个只与其必需父对象部分相关联但已经积极与其中一些相关联的对象,往往更多是用户错误,而不是应该被悄悄跳过的有意遗漏 - 在这里悄悄跳过 INSERT 会使这种用户错误非常难以调试。

对于可能依赖于旧行为的应用程序,可以通过将标志legacy_is_orphan作为映射器选项指定,重新启用任何Mapper的旧行为。

新行为允许以下测试用例工作:

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

Base = declarative_base()

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

class UserKeyword(Base):
    __tablename__ = "user_keyword"
    user_id = Column(Integer, ForeignKey("user.id"), primary_key=True)
    keyword_id = Column(Integer, ForeignKey("keyword.id"), primary_key=True)

    user = relationship(
        User, backref=backref("user_keywords", cascade="all, delete-orphan")
    )

    keyword = relationship(
        "Keyword", backref=backref("user_keywords", cascade="all, delete-orphan")
    )

    # uncomment this to enable the old behavior
    # __mapper_args__ = {"legacy_is_orphan": True}

class Keyword(Base):
    __tablename__ = "keyword"
    id = Column(Integer, primary_key=True)
    keyword = Column("keyword", String(64))

from sqlalchemy import create_engine
from sqlalchemy.orm import Session

# note we're using PostgreSQL to ensure that referential integrity
# is enforced, for demonstration purposes.
e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)

Base.metadata.drop_all(e)
Base.metadata.create_all(e)

session = Session(e)

u1 = User(name="u1")
k1 = Keyword(keyword="k1")

session.add_all([u1, k1])

uk1 = UserKeyword(keyword=k1, user=u1)

# previously, if session.flush() were called here,
# this operation would succeed, but if session.flush()
# were not called here, the operation fails with an
# integrity error.
# session.flush()
del u1.user_keywords[0]

session.commit()

#2655

after_attach 事件在项目与会话关联后而不是之前触发;添加了 before_attach

使用 after_attach 的事件处理程序现在可以假定给定的实例与给定的会话相关联:

@event.listens_for(Session, "after_attach")
def after_attach(session, instance):
    assert instance in session

有些用例要求它按照这种方式工作。然而,其他用例要求该项尚未成为会话的一部分,比如当一个查询,旨在加载实例所需的某些状态时,首先发出自动刷新,否则会过早刷新目标对象。这些用例应该使用新的“before_attach”事件:

@event.listens_for(Session, "before_attach")
def before_attach(session, instance):
    instance.some_necessary_attribute = (
        session.query(Widget).filter_by(instance.widget_name).first()
    )

#2464

查询现在会像select()一样自动关联

以前需要调用Query.correlate()才能使列或 WHERE 子查询与父级关联:

subq = (
    session.query(Entity.value)
    .filter(Entity.id == Parent.entity_id)
    .correlate(Parent)
    .as_scalar()
)
session.query(Parent).filter(subq == "some value")

这是一个普通 select() 构造的相反行为,默认情况下会假定自动关联。在 0.8 中,上述语句将自动关联:

subq = session.query(Entity.value).filter(Entity.id == Parent.entity_id).as_scalar()
session.query(Parent).filter(subq == "some value")

就像在 select() 中一样,可以通过调用 query.correlate(None) 来禁用关联,或者通过传递一个实体来手动设置,query.correlate(someentity)

#2179

关联现在始终是上下文特定的

为了允许更广泛的关联情景,Select.correlate()Query.correlate() 的行为略有变化,这样 SELECT 语句将仅在实际使用它在该上下文中时,从 FROM 子句中省略“相关”的目标。此外,不再可能在一个封闭的 SELECT 语句中作为 FROM 放置一个 SELECT 语句来“关联”(即省略)一个 FROM 子句。

这个变化只会使 SQL 渲染变得更好,因为不再可能渲染出非法的 SQL,其中选择的 FROM 对象不足:

from sqlalchemy.sql import table, column, select

t1 = table("t1", column("x"))
t2 = table("t2", column("y"))
s = select([t1, t2]).correlate(t1)

print(s)

在这个变化之前,上述内容会返回:

SELECT  t1.x,  t2.y  FROM  t2

这是无效的 SQL,因为“t1”在任何 FROM 子句中都没有被引用。

现在,在没有封闭 SELECT 的情况下,它返回:

SELECT  t1.x,  t2.y  FROM  t1,  t2

在 SELECT 中,关联会按预期生效:

s2 = select([t1, t2]).where(t1.c.x == t2.c.y).where(t1.c.x == s)
print(s2)
SELECT  t1.x,  t2.y  FROM  t1,  t2
WHERE  t1.x  =  t2.y  AND  t1.x  =
  (SELECT  t1.x,  t2.y  FROM  t2)

这个变化预计不会影响任何现有的应用程序,因为相关性行为对于正确构建的表达式保持不变。只有一个依赖于在非相关上下文中使用相关 SELECT 的无效字符串输出的应用程序,最有可能是在测试场景中,才会看到任何变化。

#2668

create_all() 和 drop_all() 现在将尊重一个空列表

方法MetaData.create_all()MetaData.drop_all()现在将接受一个空的Table对象列表,并且不会发出任何 CREATE 或 DROP 语句。以前,空列表被解释为传递None给一个集合,对于所有项目都会无条件发出 CREATE/DROP。

这是一个 bug 修复,但某些应用可能一直依赖于以前的行为。

#2664

修复了 InstrumentationEvents 的事件定位

InstrumentationEvents 系列事件目标已经记录,事件将根据实际传递的类来触发。直到 0.7 版本,这并不是这样,任何应用于 InstrumentationEvents 的事件监听器都会对所有映射的类调用。在 0.8 中,添加了额外的逻辑,使事件只会为那些传递的类调用。这里的 propagate 标志默认设置为 True,因为类仪器事件通常用于拦截尚未创建的类。

#2590

不再将“=”在 MS-SQL 中与子查询比较时自动转换为 IN

我们在 MSSQL 方言中发现了一个非常古老的行为,当用户尝试做类似这样的事情时,它会试图拯救用户:

scalar_subq = select([someothertable.c.id]).where(someothertable.c.data == "foo")
select([sometable]).where(sometable.c.id == scalar_subq)

SQL Server 不允许将等号与标量 SELECT 进行比较,即,“x = (SELECT something)”。MSSQL 方言会将其转换为 IN。然而,当进行类似“(SELECT something) = x”的比较时,也会发生同样的情况,总体而言,这种猜测的水平超出了 SQLAlchemy 通常的范围,因此已移除该行为。

#2277

修复了 Session.is_modified() 的行为

Session.is_modified() 方法接受一个参数 passive,基本上不应该是必要的,所有情况下参数应该是值 True - 当保持默认值 False 时,它会影响到数据库,并经常触发自动刷新,这将改变结果。在 0.8 中,passive 参数将不会产生任何影响,并且未加载的属性永远不会被检查历史,因为根据定义,未加载的属性不会有待处理的状态更改。

另请参阅

Session.is_modified()

#2320

Column.keyselect()Select.c 属性中通过 Select.apply_labels() 得到尊重

表达式系统的用户知道Select.apply_labels()会在每个列名前面添加表名,影响从Select.c中可用的名称:

s = select([table1]).apply_labels()
s.c.table1_col1
s.c.table1_col2

在 0.8 版本之前,如果ColumnColumn.key不同,这个键会被忽略,与未使用Select.apply_labels()时不一致:

# before 0.8
table1 = Table("t1", metadata, Column("col1", Integer, key="column_one"))
s = select([table1])
s.c.column_one  # would be accessible like this
s.c.col1  # would raise AttributeError

s = select([table1]).apply_labels()
s.c.table1_column_one  # would raise AttributeError
s.c.table1_col1  # would be accessible like this

在 0.8 版本中,Column.key在两种情况下都受到尊重:

# with 0.8
table1 = Table("t1", metadata, Column("col1", Integer, key="column_one"))
s = select([table1])
s.c.column_one  # works
s.c.col1  # AttributeError

s = select([table1]).apply_labels()
s.c.table1_column_one  # works
s.c.table1_col1  # AttributeError

关于“name”和“key”的所有其他行为都是相同的,包括渲染的 SQL 仍然会使用<tablename>_<colname>的形式 - 这里的重点是防止Column.key的内容被渲染到SELECT语句中,以避免在Column.key中使用特殊/非 ASCII 字符时出现问题。

#2397

single_parent警告现在变成了错误

一个relationship(),它是多对一或多对多关系,并指定“cascade='all, delete-orphan'”,这是一个尴尬但仍然支持的用例(受限制),如果关系没有指定single_parent=True选项,现在将引发错误。以前它只会发出警告,但在任何情况下几乎立即会在属性系统中跟随失败。

#2405

inspector参数添加到column_reflect事件中

0.7 版本添加了一个名为column_reflect的新事件,提供了对每个反射的列进行增强的方式。我们在这个事件中稍微出了点错,因为事件没有提供访问当前用于反射的InspectorConnection的方法,以防需要来自数据库的附加信息。由于这是一个尚未广泛使用的新事件,我们将直接在其中添加inspector参数:

@event.listens_for(Table, "column_reflect")
def listen_for_col(inspector, table, column_info): ...

#2418

禁用 MySQL 的自动检测排序规则和大小写敏感性

MySQL 方言进行两次调用,其中一次非常昂贵,从数据库加载所有可能的排序规则以及大小写敏感性的信息,第一次引擎连接时。这两个集合都不会被任何 SQLAlchemy 函数使用,因此这些调用将被更改为不再自动发出。可能依赖于这些集合存在于engine.dialect上的应用程序将需要直接调用_detect_collations()_detect_casing()

#2404

“未消耗的列名” 警告变为异常

insert()update() 构造中引用不存在的列将引发错误而不是警告:

t1 = table("t1", column("x"))
t1.insert().values(x=5, z=5)  # raises "Unconsumed column names: z"

#2415

Inspector.get_primary_keys() 已弃用,请使用 Inspector.get_pk_constraint

Inspector 上的这两种方法是多余的,get_primary_keys() 将返回与 get_pk_constraint() 相同的信息,但不包括约束的名称:

>>> insp.get_primary_keys()
["a", "b"]

>>> insp.get_pk_constraint()
{"name":"pk_constraint", "constrained_columns":["a", "b"]}

#2422

大多数情况下将禁用不区分大小写的结果行名称

一个非常古老的行为,RowProxy 中的列名总是不区分大小写地进行比较:

>>> row = result.fetchone()
>>> row["foo"] == row["FOO"] == row["Foo"]
True

这是为了一些在早期需要这样做的方言的利益,比如 Oracle 和 Firebird,但在现代用法中,我们有更准确的方法来处理这两个平台的不区分大小写行为。

未来,此行为将仅可选地通过向 create_engine() 传递标志 case_sensitive=False 来使用,但否则从行中请求的列名必须匹配大小写。

#2423

InstrumentationManager 和替代类仪器现在是一个扩展

sqlalchemy.orm.interfaces.InstrumentationManager 类已移动到 sqlalchemy.ext.instrumentation.InstrumentationManager。 “替代仪器”系统是为了极少数需要使用现有或不寻常的类仪器系统的安装而构建的,并且通常很少使用。 这个系统的复杂性已导出到一个 ext. 模块中。 它在导入一次后保持未使用,通常是在第三方库导入 InstrumentationManager 时,此时通过用 ExtendedInstrumentationRegistry 替换默认的 InstrumentationFactory 将其注入回 sqlalchemy.orm

已移除

SQLSoup

SQLSoup 是一个方便的包,它在 SQLAlchemy ORM 之上提供了一个替代接口。 SQLSoup 现在已移至其自己的项目,并单独进行了文档化/发布;请参见 bitbucket.org/zzzeek/sqlsoup

SQLSoup 是一个非常简单的工具,也可以受益于对其使用方式感兴趣的贡献者。

#2262

可变类型

SQLAlchemy ORM 中的旧的“可变”系统已经移除。这指的是应用于诸如PickleType的类型和有条件地应用于TypeDecoratorMutableType接口,自很早的 SQLAlchemy 版本以来一直提供了一种让 ORM 检测所谓的“可变”数据结构(如 JSON 结构和 pickled 对象)变化的方式。然而,该实现从未合理,强制单元操作工作在非常低效的模式下运行,导致在 flush 期间对所有对象进行昂贵的扫描。在 0.7 版本中,引入了sqlalchemy.ext.mutable扩展,以便用户定义的数据类型可以在发生更改时适当地向单元操作发送事件。

今天,MutableType的使用预计会很少,因为多年来已经发出了有关其低效性的警告。

#2442

sqlalchemy.exceptions(多年来一直是 sqlalchemy.exc)

我们保留了别名sqlalchemy.exceptions,以尝试使一些非常旧的库稍微容易些,这些库尚未升级以使用sqlalchemy.exc。然而,一些用户仍然被困惑,因此在 0.8 版本中,我们将其完全删除,以消除任何困惑。

#2433

SQLSoup

SQLSoup 是一个方便的包,它在 SQLAlchemy ORM 之上提供了一个替代接口。SQLSoup 现在已移动到自己的项目中,并且进行了单独的文档编写/发布;请参阅bitbucket.org/zzzeek/sqlsoup

SQLSoup 是一个非常简单的工具,也可以从对其使用风格感兴趣的贡献者中受益。

#2262

MutableType

SQLAlchemy ORM 中的旧的“可变”系统已经移除。这指的是应用于诸如PickleType的类型和有条件地应用于TypeDecoratorMutableType接口,自很早的 SQLAlchemy 版本以来一直提供了一种让 ORM 检测所谓的“可变”数据结构(如 JSON 结构和 pickled 对象)变化的方式。然而,该实现从未合理,强制单元操作工作在非常低效的模式下运行,导致在 flush 期间对所有对象进行昂贵的扫描。在 0.7 版本中,引入了sqlalchemy.ext.mutable扩展,以便用户定义的数据类型可以在发生更改时适当地向单元操作发送事件。

今天,MutableType的使用预计会很少,因为多年来已经发出了有关其低效性的警告。

#2442

sqlalchemy.exceptions(多年来一直是 sqlalchemy.exc)

我们曾经在别名sqlalchemy.exceptions中尝试让一些非常老旧的库更容易使用sqlalchemy.exc。然而,一些用户仍然感到困惑,因此在 0.8 版本中,我们将完全删除它,以消除任何困惑。

#2433

posted @   绝不原创的飞龙  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示