SQLAlchemy 0.4 的新特性?

关于本文档

本文档描述了 SQLAlchemy 版本 0.3(上次发布于 2007 年 10 月 14 日)和 SQLAlchemy 版本 0.4(上次发布于 2008 年 10 月 12 日)之间的更改。

文档日期: 2008 年 3 月 21 日

首要事项

如果您正在使用任何 ORM 功能,请确保从 sqlalchemy.orm 导入

from sqlalchemy import *
from sqlalchemy.orm import *

其次,在任何您过去使用 engine=, connectable=, bind_to=, something.engine, metadata.connect() 的地方,请使用 bind

myengine = create_engine("sqlite://")

meta = MetaData(myengine)

meta2 = MetaData()
meta2.bind = myengine

session = create_session(bind=myengine)

statement = select([table], bind=myengine)

明白了?很好!您现在 (95%) 与 0.4 兼容。如果您正在使用 0.3.10,您可以立即进行这些更改;它们在那里也能工作。

模块导入

在 0.3 中,“from sqlalchemy import *” 会将 sqlalchemy 的所有子模块导入到您的命名空间中。版本 0.4 不再将子模块导入到命名空间中。这可能意味着您需要在代码中添加额外的导入。

在 0.3 中,此代码有效

from sqlalchemy import *


class UTCDateTime(types.TypeDecorator):
    pass

在 0.4 中,必须这样做

from sqlalchemy import *
from sqlalchemy import types


class UTCDateTime(types.TypeDecorator):
    pass

对象关系映射

查询

新的查询 API

查询在生成器接口上进行了标准化(旧接口仍然存在,只是已弃用)。虽然大多数生成器接口在 0.3 中可用,但 0.4 查询具有与生成器外部匹配的内部结构,并且具有更多技巧。所有结果缩小都通过 filter()filter_by(),限制/偏移量要么通过数组切片,要么通过 limit()/offset(),连接通过 join()outerjoin() (或更手动地,通过 select_from() 以及手动形成的条件)。

为了避免弃用警告,您必须对您的 03 代码进行一些更改

User.query.get_by( **kwargs )

User.query.filter_by(**kwargs).first()

User.query.select_by( **kwargs )

User.query.filter_by(**kwargs).all()

User.query.select()

User.query.filter(xxx).all()

新的基于属性的表达式构造

ORM 中最明显的变化是,您现在可以直接使用基于类的属性来构造查询条件。在使用映射类时,不再需要 “.c.” 前缀

session.query(User).filter(and_(User.name == "fred", User.id > 17))

虽然基于简单列的比较没什么大不了的,但类属性有一些新的“更高级别”的构造可用,包括以前仅在 filter_by() 中可用的构造

# comparison of scalar relations to an instance
filter(Address.user == user)

# return all users who contain a particular address
filter(User.addresses.contains(address))

# return all users who *dont* contain the address
filter(~User.address.contains(address))

# return all users who contain a particular address with
# the email_address like '%foo%'
filter(User.addresses.any(Address.email_address.like("%foo%")))

# same, email address equals 'foo@bar.com'.  can fall back to keyword
# args for simple comparisons
filter(User.addresses.any(email_address="foo@bar.com"))

# return all Addresses whose user attribute has the username 'ed'
filter(Address.user.has(name="ed"))

# return all Addresses whose user attribute has the username 'ed'
# and an id > 5 (mixing clauses with kwargs)
filter(Address.user.has(User.id > 5, name="ed"))

Column 集合在映射类的 .c 属性中仍然可用。请注意,基于属性的表达式仅适用于映射类的映射属性。.c 仍然用于访问常规表和从 SQL 表达式生成的可选择对象中的列。

自动连接别名

我们已经有了 join() 和 outerjoin() 一段时间了

session.query(Order).join("items")

现在您可以为它们设置别名

session.query(Order).join("items", aliased=True).filter(Item.name="item 1").join(
    "items", aliased=True
).filter(Item.name == "item 3")

上面将使用别名从 orders->items 创建两个连接。每个连接之后的 filter() 调用将调整其表条件以匹配别名的条件。要获取 Item 对象,请使用 add_entity() 并使用 id 定位每个连接

session.query(Order).join("items", id="j1", aliased=True).filter(
    Item.name == "item 1"
).join("items", aliased=True, id="j2").filter(Item.name == "item 3").add_entity(
    Item, id="j1"
).add_entity(
    Item, id="j2"
)

以以下形式返回元组:(Order, Item, Item)

自引用查询

所以 query.join() 现在可以创建别名了。这给我们带来了什么?自引用查询!可以在没有任何 Alias 对象的情况下完成连接

# standard self-referential TreeNode mapper with backref
mapper(
    TreeNode,
    tree_nodes,
    properties={
        "children": relation(
            TreeNode, backref=backref("parent", remote_side=tree_nodes.id)
        )
    },
)

# query for node with child containing "bar" two levels deep
session.query(TreeNode).join(["children", "children"], aliased=True).filter_by(
    name="bar"
)

要为别名连接中沿途的每个表添加条件,您可以使用 from_joinpoint 来继续针对同一行别名进行连接

# search for the treenode along the path "n1/n12/n122"

# first find a Node with name="n122"
q = sess.query(Node).filter_by(name="n122")

# then join to parent with "n12"
q = q.join("parent", aliased=True).filter_by(name="n12")

# join again to the next parent with 'n1'.  use 'from_joinpoint'
# so we join from the previous point, instead of joining off the
# root table
q = q.join("parent", aliased=True, from_joinpoint=True).filter_by(name="n1")

node = q.first()

query.populate_existing()

query.load() (或 session.refresh())的预先加载版本。从查询加载的每个实例,包括所有预先加载的项目,如果已存在于会话中,则立即刷新

session.query(Blah).populate_existing().all()

关系

嵌入在更新/插入中的 SQL 子句

对于 SQL 子句的内联执行,直接嵌入到 UPDATE 或 INSERT 中,在 flush() 期间

myobject.foo = mytable.c.value + 1

user.pwhash = func.md5(password)

order.hash = text("select hash from hashing_table")

列属性在操作后使用延迟加载器设置,以便在您下次访问时发出 SQL 来加载新值。

自引用和循环的预先加载

由于我们的别名技巧得到了改进,relation() 可以沿着同一个表连接 *任意次数*;您告诉它您想要深入的程度。让我们更清楚地展示自引用的 TreeNode

nodes = Table(
    "nodes",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("parent_id", Integer, ForeignKey("nodes.id")),
    Column("name", String(30)),
)


class TreeNode(object):
    pass


mapper(
    TreeNode,
    nodes,
    properties={"children": relation(TreeNode, lazy=False, join_depth=3)},
)

那么当我们说

create_session().query(TreeNode).all()

会发生什么?沿着别名的连接,从父级深入三层

SELECT
nodes_3.id AS nodes_3_id, nodes_3.parent_id AS nodes_3_parent_id, nodes_3.name AS nodes_3_name,
nodes_2.id AS nodes_2_id, nodes_2.parent_id AS nodes_2_parent_id, nodes_2.name AS nodes_2_name,
nodes_1.id AS nodes_1_id, nodes_1.parent_id AS nodes_1_parent_id, nodes_1.name AS nodes_1_name,
nodes.id AS nodes_id, nodes.parent_id AS nodes_parent_id, nodes.name AS nodes_name
FROM nodes LEFT OUTER JOIN nodes AS nodes_1 ON nodes.id = nodes_1.parent_id
LEFT OUTER JOIN nodes AS nodes_2 ON nodes_1.id = nodes_2.parent_id
LEFT OUTER JOIN nodes AS nodes_3 ON nodes_2.id = nodes_3.parent_id
ORDER BY nodes.oid, nodes_1.oid, nodes_2.oid, nodes_3.oid

也请注意漂亮的干净别名名称。连接不关心它是针对同一个立即表还是稍后循环回到起点的某个其他对象。当指定 join_depth 时,任何类型的预先加载链都可以循环回到自身。当不存在时,预先加载会在遇到循环时自动停止。

复合类型

这是来自 Hibernate 阵营的一个功能。复合类型允许您定义一个自定义数据类型,该数据类型由多个列(或一个列,如果您愿意的话)组成。让我们定义一个新类型 Point。存储 x/y 坐标

class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __composite_values__(self):
        return self.x, self.y

    def __eq__(self, other):
        return other.x == self.x and other.y == self.y

    def __ne__(self, other):
        return not self.__eq__(other)

Point 对象的定义方式特定于自定义类型;构造函数接受参数列表,__composite_values__() 方法生成这些参数的序列。顺序将与我们的映射器匹配,我们稍后会看到。

让我们创建一个顶点表,每行存储两个点

vertices = Table(
    "vertices",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("x1", Integer),
    Column("y1", Integer),
    Column("x2", Integer),
    Column("y2", Integer),
)

然后,映射它!我们将创建一个 Vertex 对象,该对象存储两个 Point 对象

class Vertex(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end


mapper(
    Vertex,
    vertices,
    properties={
        "start": composite(Point, vertices.c.x1, vertices.c.y1),
        "end": composite(Point, vertices.c.x2, vertices.c.y2),
    },
)

一旦您设置了复合类型,它就可以像任何其他类型一样使用

v = Vertex(Point(3, 4), Point(26, 15))
session.save(v)
session.flush()

# works in queries too
q = session.query(Vertex).filter(Vertex.start == Point(3, 4))

如果您想定义映射属性在表达式中使用时生成 SQL 子句的方式,请创建您自己的 sqlalchemy.orm.PropComparator 子类,定义任何常用运算符(例如 __eq__(), __le__() 等),并将其发送到 composite()。复合类型也可用作主键,并且可在 query.get() 中使用

# a Document class which uses a composite Version
# object as primary key
document = query.get(Version(1, "a"))

dynamic_loader() 关系

一个 relation(),它为所有读取操作返回一个实时 Query 对象。写入操作仅限于 append()remove(),对集合的更改在会话刷新之前不可见。此功能在使用“自动刷新”会话时特别方便,该会话将在每次查询之前刷新。

mapper(
    Foo,
    foo_table,
    properties={
        "bars": dynamic_loader(
            Bar,
            backref="foo",
            # <other relation() opts>
        )
    },
)

session = create_session(autoflush=True)
foo = session.query(Foo).first()

foo.bars.append(Bar(name="lala"))

for bar in foo.bars.filter(Bar.name == "lala"):
    print(bar)

session.commit()

新选项: undefer_group(), eagerload_all()

一些方便的查询选项。undefer_group() 将整组“延迟”列标记为未延迟

mapper(
    Class,
    table,
    properties={
        "foo": deferred(table.c.foo, group="group1"),
        "bar": deferred(table.c.bar, group="group1"),
        "bat": deferred(table.c.bat, group="group1"),
    },
)

session.query(Class).options(undefer_group("group1")).filter(...).all()

并且 eagerload_all() 设置一个属性链在一个过程中进行预先加载

mapper(Foo, foo_table, properties={"bar": relation(Bar)})
mapper(Bar, bar_table, properties={"bat": relation(Bat)})
mapper(Bat, bat_table)

# eager load bar and bat
session.query(Foo).options(eagerload_all("bar.bat")).filter(...).all()

新的集合 API

集合不再由 {{{InstrumentedList}}} 代理代理,并且对成员、方法和属性的访问是直接的。装饰器现在拦截进入和离开集合的对象,现在可以轻松编写自定义集合类来管理其自己的成员资格。灵活的装饰器也取代了 0.3 中自定义集合的命名方法接口,允许任何类轻松适应以用作集合容器。

基于字典的集合现在更容易使用,并且完全像 dict 一样。对于 dict``s,不再需要更改 __iter__,新的内置 dict 类型涵盖了许多需求

# use a dictionary relation keyed by a column
relation(Item, collection_class=column_mapped_collection(items.c.keyword))
# or named attribute
relation(Item, collection_class=attribute_mapped_collection("keyword"))
# or any function you like
relation(Item, collection_class=mapped_collection(lambda entity: entity.a + entity.b))

现有的 0.3 类 dict-like 和自由形式对象派生的集合类需要为新的 API 进行更新。在大多数情况下,这只是在类定义中添加几个装饰器的问题。

来自外部表/子查询的映射关系

此功能在 0.3 中悄然出现,但在 0.4 中得到了改进,这要归功于将针对表的子查询转换为针对该表的别名的子查询的更好能力;这对于预先加载、查询中的别名连接等至关重要。它减少了在只需要添加一些额外的列或子查询时创建针对 select 语句的映射器的需求

mapper(
    User,
    users,
    properties={
        "fullname": column_property(
            (users.c.firstname + users.c.lastname).label("fullname")
        ),
        "numposts": column_property(
            select([func.count(1)], users.c.id == posts.c.user_id)
            .correlate(users)
            .label("posts")
        ),
    },
)

一个典型的查询如下所示

SELECT (SELECT count(1) FROM posts WHERE users.id = posts.user_id) AS count,
users.firstname || users.lastname AS fullname,
users.id AS users_id, users.firstname AS users_firstname, users.lastname AS users_lastname
FROM users ORDER BY users.oid

水平扩展 (分片) API

[browser:/sqlalchemy/trunk/examples/sharding/attribute_shard .py]

会话

新的会话创建范例;SessionContext,assignmapper 已弃用

没错,整个过程都被两个配置函数取代了。两者都使用将产生自 0.1 以来我们拥有的最 0.1 风格的感觉(即,最少的输入量)。

在您定义 engine 的位置(或任何位置)配置您自己的 Session

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine("myengine://")
Session = sessionmaker(bind=engine, autoflush=True, transactional=True)

# use the new Session() freely
sess = Session()
sess.save(someobject)
sess.flush()

如果您需要后配置您的会话,例如使用引擎,请稍后使用 configure() 添加它

Session.configure(bind=create_engine(...))

SessionContext 的所有行为以及 assignmapperquery__init__ 方法都已移至新的 scoped_session() 函数中,该函数与 sessionmaker 以及 create_session() 都兼容

from sqlalchemy.orm import scoped_session, sessionmaker

Session = scoped_session(sessionmaker(autoflush=True, transactional=True))
Session.configure(bind=engine)

u = User(name="wendy")

sess = Session()
sess.save(u)
sess.commit()

# Session constructor is thread-locally scoped.  Everyone gets the same
# Session in the thread when scope="thread".
sess2 = Session()
assert sess is sess2

当使用线程本地 Session 时,返回的类具有作为类方法实现的所有 Session's 接口,并且 “assignmapper“ 的功能可以使用 mapper 类方法获得。就像旧的 objectstore 时代一样……。

# "assignmapper"-like functionality available via ScopedSession.mapper
Session.mapper(User, users_table)

u = User(name="wendy")

Session.commit()

会话再次默认为弱引用

weak_identity_map 标志现在默认在 Session 上设置为 True。外部取消引用并超出范围的实例会自动从会话中删除。但是,具有“脏”更改的项目将保持强引用,直到这些更改被刷新,在这种情况下,对象将恢复为弱引用(这也适用于 ‘mutable’ 类型,例如可 pickle 的属性)。将 weak_identity_map 设置为 False 会为那些将会话用作缓存的用户恢复旧的强引用行为。

自动事务会话

正如您可能在上面注意到的,我们正在 Session 上调用 commit()。标志 transactional=True 意味着 Session 始终处于事务中,commit() 持久保存。

自动刷新会话

此外,autoflush=True 意味着 Session 将在每次 query 之前以及在您调用 flush()commit()flush()。所以现在这将工作

Session = sessionmaker(bind=engine, autoflush=True, transactional=True)

u = User(name="wendy")

sess = Session()
sess.save(u)

# wendy is flushed, comes right back from a query
wendy = sess.query(User).filter_by(name="wendy").one()

事务方法已移至会话

commit()rollback() 以及 begin() 现在直接在 Session 上。不再需要为任何事情使用 SessionTransaction(它仍然在后台运行)。

Session = sessionmaker(autoflush=True, transactional=False)

sess = Session()
sess.begin()

# use the session

sess.commit()  # commit transaction

与封闭的引擎级(即非 ORM)事务共享 Session 很简单

Session = sessionmaker(autoflush=True, transactional=False)

conn = engine.connect()
trans = conn.begin()
sess = Session(bind=conn)

# ... session is transactional

# commit the outermost transaction
trans.commit()

带有 SAVEPOINT 的嵌套会话事务

在引擎和 ORM 级别可用。到目前为止的 ORM 文档

https://sqlalchemy.org.cn/docs/04/session.html#unitofwork_managing

两阶段提交会话

在引擎和 ORM 级别可用。到目前为止的 ORM 文档

https://sqlalchemy.org.cn/docs/04/session.html#unitofwork_managing

继承

没有连接或联合的多态继承

继承的新文档: https://sqlalchemy.org.cn/docs/04 /mappers.html#advdatamapping_mapper_inheritance_joined

使用 get() 更好的多态行为

连接表继承层次结构中的所有类都使用基类获取 _instance_key,即 (BaseClass, (1, ), None)。这样,当您调用 get() 时,针对基类的 Query 可以找到当前身份映射中的子类实例,而无需查询数据库。

类型

sqlalchemy.types.TypeDecorator 的自定义子类

有一个 新 API 用于子类化 TypeDecorator。使用 0.3 API 在某些情况下会导致编译错误。

SQL 表达式

全新的确定性标签/别名生成

所有“匿名”标签和别名现在都使用简单的 <name>_<number> 格式。SQL 更易于阅读,并且与计划优化器缓存兼容。只需查看教程中的一些示例:https://sqlalchemy.org.cn/docs/04/ormtutorial.html https://sqlalchemy.org.cn/docs/04/sqlexpression.html

生成式 select() 构造

这绝对是使用 select() 的方法。请参阅 htt p://sqlalchemy.org.cn/docs/04/sqlexpression.html#sql_transf orm 。

新的运算符系统

SQL 运算符和或多或少每个 SQL 关键字现在都抽象到编译器层中。它们现在智能地运行,并且类型/后端感知,请参阅:https://sqlalchemy.org.cn/docs/04/sqlexpression.html#sql_operators

所有 type 关键字参数重命名为 type_

正如它所说

b = bindparam("foo", type_=String)

in_ 函数更改为接受序列或可选择对象

in_ 函数现在将值序列或可选择对象作为其唯一参数。以前将值作为位置参数传递的 API 仍然有效,但现在已弃用。这意味着

my_table.select(my_table.c.id.in_(1, 2, 3))
my_table.select(my_table.c.id.in_(*listOfIds))

应该更改为

my_table.select(my_table.c.id.in_([1, 2, 3]))
my_table.select(my_table.c.id.in_(listOfIds))

模式和反射

MetaData, BoundMetaData, DynamicMetaData

在 0.3.x 系列中,BoundMetaDataDynamicMetaData 已弃用,取而代之的是 MetaDataThreadLocalMetaData。旧名称已在 0.4 中删除。更新很简单

+-------------------------------------+-------------------------+
|If You Had                           | Now Use                 |
+=====================================+=========================+
| ``MetaData``                        | ``MetaData``            |
+-------------------------------------+-------------------------+
| ``BoundMetaData``                   | ``MetaData``            |
+-------------------------------------+-------------------------+
| ``DynamicMetaData`` (with one       | ``MetaData``            |
| engine or threadlocal=False)        |                         |
+-------------------------------------+-------------------------+
| ``DynamicMetaData``                 | ``ThreadLocalMetaData`` |
| (with different engines per thread) |                         |
+-------------------------------------+-------------------------+

MetaData 类型的很少使用的 name 参数已被删除。ThreadLocalMetaData 构造函数现在不接受任何参数。这两种类型现在都可以绑定到 Engine 或单个 Connection

一步到位多表反射

您现在可以加载表定义,并从整个数据库或模式一步到位自动创建 Table 对象

>>> metadata = MetaData(myengine, reflect=True)
>>> metadata.tables.keys()
['table_a', 'table_b', 'table_c', '...']

MetaData 还获得了一个 .reflect() 方法,可以更精细地控制加载过程,包括指定要加载的可用表的子集。

SQL 执行

engine, connectable, 和 bind_to 现在都为 bind

Transactions, NestedTransactionsTwoPhaseTransactions

连接池事件

连接池现在在新 DB-API 连接创建、检出和检回到池中时触发事件。您可以使用这些在新的连接上执行会话范围的 SQL 设置语句,例如。

Oracle 引擎已修复

在 0.3.11 中,Oracle 引擎在处理主键方面存在错误。这些错误可能导致在其他引擎(例如 sqlite)上运行良好的程序在使用 Oracle 引擎时失败。在 0.4 中,Oracle 引擎已得到改进,修复了这些主键问题。

Oracle 的输出参数

result = engine.execute(
    text(
        "begin foo(:x, :y, :z); end;",
        bindparams=[
            bindparam("x", Numeric),
            outparam("y", Numeric),
            outparam("z", Numeric),
        ],
    ),
    x=5,
)
assert result.out_parameters == {"y": 10, "z": 75}

连接绑定的 MetaData, Sessions

MetaDataSession 可以显式绑定到连接

conn = engine.connect()
sess = create_session(bind=conn)

更快、更可靠的 ResultProxy 对象