SqlAlchemy 2.0 中文文档(五十六)(9)

简介: SqlAlchemy 2.0 中文文档(五十六)

SqlAlchemy 2.0 中文文档(五十六)(8)https://developer.aliyun.com/article/1563161


默认情况下 ORM 行不会唯一化

简介

session.execute(stmt)返回的 ORM 行不再自动“唯一”。这通常是一个受欢迎的变化,除非使用“联接预加载”加载器策略与集合一起使用时:

# In the legacy API, many rows each have the same User primary key, but
# only one User per primary key is returned
users = session.query(User).options(joinedload(User.addresses))
# In the new API, uniquing is available but not implicitly
# enabled
result = session.execute(select(User).options(joinedload(User.addresses)))
# this actually will raise an error to let the user know that
# uniquing should be applied
rows = result.all()

迁移到 2.0 版本

当使用集合的联接加载时,必须调用Result.unique()方法。ORM 实际上将设置一个默认行处理程序,如果未执行此操作,将引发错误,以确保联接预加载集合不返回重复行,同时保持明确性:

# 1.4 / 2.0 code
stmt = select(User).options(joinedload(User.addresses))
# statement will raise if unique() is not used, due to joinedload()
# of a collection.  in all other cases, unique() is not needed.
# By stating unique() explicitly, confusion over discrepancies between
# number of objects/ rows returned vs. "SELECT COUNT(*)" is resolved
rows = session.execute(stmt).unique().all()

讨论

这里的情况有点不同寻常,因为 SQLAlchemy 要求调用一个方法,而实际上完全有能力自动完成。需要调用该方法的原因是确保开发人员“选择”使用Result.unique()方法,这样他们在直接计算行数与实际结果集中记录数不一致时不会感到困惑,这已经是多年来用户困惑和错误报告的长期问题了。默认情况下不会在任何其他情况下进行唯一化,这将提高性能并提高自动唯一化导致混淆结果的情况的清晰度。

在现代 SQLAlchemy 中,当使用联接预加载集合时,必须调用Result.unique()方法时,在现代 SQLAlchemy 中,selectinload()策略提供了一个集合导向的预加载器,在大多数情况下优于joinedload(),应该优先使用。 ### “动态”关系加载器被“仅写入”取代

简介

讨论了lazy="dynamic"关系加载策略,详见动态关系加载器,它使用了在 2.0 中已经过时的Query对象。这种“dynamic”关系不直接兼容 asyncio,除非通过解决方法,此外,它也没有实现其原始目的,即防止大型集合的迭代,因为它有几种行为会隐式发生迭代。

引入了一种名为lazy="write_only"的新加载策略,通过WriteOnlyCollection集合类提供了一个非常严格的“无隐式迭代”API,并且还与 2.0 风格的语句执行集成,支持 asyncio 以及与新的 ORM 启用的批量 DML 功能集成。

与此同时,在 2.0 版本中仍然完全支持lazy="dynamic";应用程序可以延迟迁移这种特定模式,直到完全使用 2.0 系列。

迁移至 2.0

新的“write only”功能仅在 SQLAlchemy 2.0 中可用,不是 1.4 的一部分。与此同时,lazy="dynamic"加载策略在 2.0 版本中仍然得到完全支持,甚至包括新的 pep-484 和带注释的映射支持。

因此,从“dynamic”迁移的最佳策略是等到应用程序完全在 2.0 上运行,然后直接从AppenderQuery迁移到WriteOnlyCollection,后者是“dynamic”策略使用的集合类型。

一些技术可用于在 1.4 版本下以更“2.0”风格使用lazy="dynamic"。有两种方法可以实现 2.0 风格的查询,即针对特定关系的查询:

  • 利用现有lazy="dynamic"关系上的Query.statement属性。我们可以立即使用动态加载器进行方法,如Session.scalars(),如下所示:
class User(Base):
    __tablename__ = "user"
    posts = relationship(Post, lazy="dynamic")
jack = session.get(User, 5)
# filter Jack's blog posts
posts = session.scalars(jack.posts.statement.where(Post.headline == "this is a post"))
  • 使用with_parent()函数直接构造一个select()构造:
from sqlalchemy.orm import with_parent
jack = session.get(User, 5)
posts = session.scalars(
    select(Post)
    .where(with_parent(jack, User.posts))
    .where(Post.headline == "this is a post")
)

讨论

最初的想法是 with_parent() 函数应该足够了,但是继续使用关系本身的特殊属性仍然具有吸引力,并且在这里也可以使用 2.0 风格的构造方法。

新的“write_only”加载器策略提供了一种新的集合类型,不支持隐式迭代或项目访问。而是通过调用其 .select() 方法来读取集合的内容,以帮助构建适当的 SELECT 语句。该集合还包括 .insert().update().delete() 方法,用于为集合中的项目发出批量 DML 语句。与“动态”功能类似,还有 .add().add_all().remove() 方法,它们使用工作单元过程将单个成员排队以添加或删除。对新功能的介绍可以查看 新的“仅写”关系策略取代了“动态”。

另请参阅

新的“仅写”关系策略取代了“动态”

仅写关系 ### Session 中删除了自动提交模式;添加了自动开始支持

概要

Session 将不再支持“自动提交”模式,也就是这种模式:

from sqlalchemy.orm import Session
sess = Session(engine, autocommit=True)
# no transaction begun, but emits SQL, won't be supported
obj = sess.query(Class).first()
# session flushes in a transaction that it begins and
# commits, won't be supported
sess.flush()

2.0 迁移

使用 Session 在“自动提交”模式下的主要原因是为了使 Session.begin() 方法可用,以便框架集成和事件挂钩可以控制此事件的发生。在 1.4 中,Session 现在具有 自动开始行为,解决了这个问题;现在可以调用 Session.begin() 方法:

from sqlalchemy.orm import Session
sess = Session(engine)
sess.begin()  # begin explicitly; if not called, will autobegin
# when database access is needed
sess.add(obj)
sess.commit()

讨论

“自动提交”模式是 SQLAlchemy 最初版本的遗留问题之一。这个标志主要用于允许显式使用 Session.begin(),在 1.4 中已经解决了这个问题,以及允许使用“子事务”,这在 2.0 中也已经移除。### Session “子事务”行为已移除

概要

使用自动提交模式时经常使用的“子事务”模式在 1.4 中也已经弃用。这种模式允许在已经开始事务时使用Session.begin()方法,导致产生一种称为“子事务”的结构,它本质上是一个阻止Session.commit()方法实际提交的块。

迁移到 2.0

为了为使用此模式的应用程序提供向后兼容性,可以使用以下上下文管理器或基于装饰器的类似实现:

import contextlib
@contextlib.contextmanager
def transaction(session):
    if not session.in_transaction():
        with session.begin():
            yield
    else:
        yield

上述上下文管理器可以以与“子事务”标志相同的方式使用,例如以下示例:

# method_a starts a transaction and calls method_b
def method_a(session):
    with transaction(session):
        method_b(session)
# method_b also starts a transaction, but when
# called from method_a participates in the ongoing
# transaction.
def method_b(session):
    with transaction(session):
        session.add(SomeObject("bat", "lala"))
Session = sessionmaker(engine)
# create a Session and call method_a
with Session() as session:
    method_a(session)

为了与首选的惯用模式进行比较,开始块应位于最外层。这样就不需要个别函数或方法关心事务界定的细节:

def method_a(session):
    method_b(session)
def method_b(session):
    session.add(SomeObject("bat", "lala"))
Session = sessionmaker(engine)
# create a Session and call method_a
with Session() as session:
    with session.begin():
        method_a(session)

讨论

这种模式已被证明在实际应用程序中令人困惑,最好是确保应用程序的最顶层数据库操作使用单个 begin/commit 对执行。### ORM 查询与 Core 选择统一

简介

Query对象(以及BakedQueryShardedQuery扩展)成为长期遗留对象,取而代之的是直接使用select()构造与Session.execute()方法结合使用。从Query返回的列表对象或元组形式的结果,或作为标量 ORM 对象从Session.execute()统一作为Result对象返回,其接口与 Core 执行一致。

以下是遗留代码示例:

session = Session(engine)
# becomes legacy use case
user = session.query(User).filter_by(name="some user").one()
# becomes legacy use case
user = session.query(User).filter_by(name="some user").first()
# becomes legacy use case
user = session.query(User).get(5)
# becomes legacy use case
for user in (
    session.query(User).join(User.addresses).filter(Address.email == "some@email.com")
):
    ...
# becomes legacy use case
users = session.query(User).options(joinedload(User.addresses)).order_by(User.id).all()
# becomes legacy use case
users = session.query(User).from_statement(text("select * from users")).all()
# etc

迁移到 2.0

由于绝大多数 ORM 应用程序预计将使用Query对象,并且Query接口的可用性不会影响新接口,该对象将在 2.0 版本中保留,但不再是文档的一部分,也大多数情况下不再受支持。 select()构造现在适用于 Core 和 ORM 用例,当通过Session.execute()方法调用时,将返回 ORM 导向的结果,即如果请求的是 ORM 对象,则返回 ORM 对象。

Select()构造添加了许多新方法,以与Query兼容,包括Select.filter()Select.filter_by(),重新设计的Select.join()Select.outerjoin()方法,Select.options()等。 Query的其他更多补充方法,如Query.populate_existing()是通过执行选项实现的。

返回结果以Result对象的形式呈现,这是 SQLAlchemy ResultProxy对象的新版本,还添加了许多与Query兼容的新方法,包括Result.one()Result.all()Result.first()Result.one_or_none()等。

然而,Result对象需要一些不同的调用模式,因为当首次返回时,它将始终返回元组,并且不会在内存中去重结果。为了以Query的方式返回单个 ORM 对象,必须首先调用Result.scalars()修饰符。为了返回唯一的对象,就像在使用连接式贪婪加载时所必需的那样,必须首先调用Result.unique()修饰符。

包括执行选项等在内的select()的所有新功能的文档在 ORM 查询指南中。

以下是一些迁移到select()的示例:

session = Session(engine)
user = session.execute(select(User).filter_by(name="some user")).scalar_one()
# for first(), no LIMIT is applied automatically; add limit(1) if LIMIT
# is desired on the query
user = (
    session.execute(select(User).filter_by(name="some user").limit(1)).scalars().first()
)
# get() moves to the Session directly
user = session.get(User, 5)
for user in session.execute(
    select(User).join(User.addresses).filter(Address.email == "some@email.case")
).scalars():
    ...
# when using joinedload() against collections, use unique() on the result
users = (
    session.execute(select(User).options(joinedload(User.addresses)).order_by(User.id))
    .unique()
    .all()
)
# select() has ORM-ish methods like from_statement() that only work
# if the statement is against ORM entities
users = (
    session.execute(select(User).from_statement(text("select * from users")))
    .scalars()
    .all()
)

讨论

SQLAlchemy 既有一个select()构造,也有一个具有极其相似但基本上不兼容的接口的单独的Query对象,这可能是 SQLAlchemy 中最大的不一致性,这是随着时间的小幅增加而产生的两个主要 API 的分歧。

在 SQLAlchemy 的最初版本中,根本不存在Query对象。最初的想法是Mapper构造本身将能够选择行,并且Table对象,而不是类,将用于以 Core 风格的方式创建各种条件。Query在 SQLAlchemy 的历史中的一些月份/年作为一个名为SelectResults的新的“可构建”查询对象的用户提案被接受。像.where()方法这样的概念,在SelectResults中称为.filter(),在 SQLAlchemy 之前不存在,并且select()构造仅使用现在已弃用的“一次性”构造样式,该样式不再接受各种构造函数参数,列是按位置传递的。

随着新方法的推出,该对象演变为Query对象,新增了诸如能够选择单个列、能够一次选择多个实体、能够从Query对象构建子查询而不是从select对象开始的新功能。目标是Query应该具有select的全部功能,可以组合构建完整的 SELECT 语句,而不需要显式使用select()。与此同时,select()也发展出了“生成”方法,如Select.where()Select.order_by()

在现代的 SQLAlchemy 中,这个目标已经实现,这两个对象现在在功能上完全重叠。统一这些对象的主要挑战是select()对象需要保持对 ORM 完全不可知。为了实现这一点,大部分来自Query的逻辑已经移动到 SQL 编译阶段,ORM 特定的编译器插件接收Select构造并根据 ORM 风格的查询解释其内容,然后传递给核心级别的编译器以创建 SQL 字符串。随着新的 SQL 编译缓存系统的出现,大部分这种 ORM 逻辑也被缓存。

另请参阅

ORM 查询在内部与 select、update、delete 统一;2.0 风格执行可用

ORM 查询 - get() 方法移到 Session

概要

Query.get() 方法仍然保留以供遗留目的,但主要接口现在是Session.get() 方法:

# legacy usage
user_obj = session.query(User).get(5)

迁移到 2.0

在 1.4 / 2.0 中,Session对象添加了一个新的Session.get()方法:

# 1.4 / 2.0 cross-compatible use
user_obj = session.get(User, 5)

讨论

Query对象将在 2.0 中成为遗留对象,因为现在可以使用select()对象进行 ORM 查询。由于Query.get()方法定义了与Session的特殊交互,并且甚至不一定会发出查询,因此将其作为Session的一部分更为适合,其中类似于其他“identity”方法,例如refreshmerge

SQLAlchemy 最初包含“get()”,以类似于 Hibernate Session.load()方法。像往常一样,我们稍微搞错了,因为这个方法实际上更多地与Session有关,而不是写 SQL 查询。

ORM 查询 - 在关系上进行链接/加载使用属性,而不是字符串

概要

这涉及Query.join()等模式,以及查询选项,如joinedload(),它目前接受混合的字符串属性名称或实际类属性。字符串形式将在 2.0 中全部移除:

# string use removed
q = session.query(User).join("addresses")
# string use removed
q = session.query(User).options(joinedload("addresses"))
# string use removed
q = session.query(Address).filter(with_parent(u1, "addresses"))

迁移到 2.0

现代 SQLAlchemy 1.x 版本支持推荐的技术,即使用映射属性:

# compatible with all modern SQLAlchemy versions
q = session.query(User).join(User.addresses)
q = session.query(User).options(joinedload(User.addresses))
q = session.query(Address).filter(with_parent(u1, User.addresses))

相同的技术适用于 2.0 样式的使用:

# SQLAlchemy 1.4 / 2.0 cross compatible use
stmt = select(User).join(User.addresses)
result = session.execute(stmt)
stmt = select(User).options(joinedload(User.addresses))
result = session.execute(stmt)
stmt = select(Address).where(with_parent(u1, User.addresses))
result = session.execute(stmt)

讨论

字符串调用形式不明确,并且需要内部执行额外的工作来确定适当的路径并检索正确的映射属性。通过直接传递 ORM 映射属性,不仅可以提前传递必要的信息,而且该属性还具有类型,并且更具有与 IDE 和 pep-484 集成的潜在兼容性。

ORM 查询 - 使用属性列表进行链接的链式调用,而不是单个调用,已移除

概要

“链接”形式的连接和加载选项接受多个映射属性的列表将被移除:

# chaining removed
q = session.query(User).join("orders", "items", "keywords")

迁移到 2.0

对于 1.x / 2.0 跨兼容使用,使用单独的调用Query.join()

q = session.query(User).join(User.orders).join(Order.items).join(Item.keywords)

对于 2.0 风格的用法,Select具有与Select.join()相同的行为,并且还具有一个新的Select.join_from()方法,允许显式左侧:

# 1.4 / 2.0 cross compatible
stmt = select(User).join(User.orders).join(Order.items).join(Item.keywords)
result = session.execute(stmt)
# join_from can also be helpful
stmt = select(User).join_from(User, Order).join_from(Order, Item, Order.items)
result = session.execute(stmt)

讨论

移除属性的链接符合简化诸如Select.join()等方法的调用接口。

ORM 查询 - join(…, aliased=True),移除 from_joinpoint

概要

Query.join()上的aliased=True选项已被移除,from_joinpoint标志也被移除:

# no longer supported
q = (
    session.query(Node)
    .join("children", aliased=True)
    .filter(Node.name == "some sub child")
    .join("children", from_joinpoint=True, aliased=True)
    .filter(Node.name == "some sub sub child")
)

迁移至 2.0

使用显式别名代替:

n1 = aliased(Node)
n2 = aliased(Node)
q = (
    select(Node)
    .join(Node.children.of_type(n1))
    .where(n1.name == "some sub child")
    .join(n1.children.of_type(n2))
    .where(n2.name == "some sub child")
)

讨论

Query.join()上的aliased=True选项似乎几乎从未被使用,通过广泛的代码搜索来查找实际使用此功能的情况。aliased=True标志所需的内部复杂性是巨大的,并且将在 2.0 中消失。

大多数用户不熟悉这个标志,但它允许沿着连接自动为元素添加别名,然后将自动别名应用于过滤条件。最初的用例是帮助处理长链的自引用连接,就像上面显示的示例一样。然而,过滤条件的自动调整在内部非常复杂,几乎从不在现实世界的应用中使用。这种模式还会导致问题,例如如果需要在链中的每个链接处添加过滤条件;那么模式必须使用from_joinpoint标志,SQLAlchemy 开发人员绝对找不到这个参数在实际应用中的任何使用情况。

aliased=Truefrom_joinpoint参数是在Query对象在关联属性方面还没有良好能力时开发的,像PropComparator.of_type()这样的函数还不存在,而aliased()构造本身在早期也不存在。

使用 DISTINCT 与其他列,但仅选择实体

概要

当使用 DISTINCT 时,Query 将自动添加 ORDER BY 中的列。以下查询将从所有用户列以及“address.email_address” 中选择,但只返回用户对象:

# 1.xx code
result = (
    session.query(User)
    .join(User.addresses)
    .distinct()
    .order_by(Address.email_address)
    .all()
)

在 2.0 版本中,“email_address” 列不会自动添加到列子句中,上述查询将失败,因为关系数据库在使用 DISTINCT 时不允许您按“address.email_address” 排序,如果它也不在列子句中。

迁移到 2.0 版本

在 2.0 版本中,必须显式添加列。要解决仅返回主实体对象而不是额外列的问题,请使用Result.columns() 方法:

# 1.4 / 2.0 code
stmt = (
    select(User, Address.email_address)
    .join(User.addresses)
    .distinct()
    .order_by(Address.email_address)
)
result = session.execute(stmt).columns(User).all()

讨论

这种情况是Query的有限灵活性导致需要添加隐式“神奇”行为的一个例子;“email_address” 列会隐式添加到列子句中,然后额外的内部逻辑会从实际返回的结果中省略该列。

新方法简化了交互并明确了正在发生的事情,同时仍然可以实现原始用例而不会带来不便。

从查询本身作为子查询选择,例如“from_self()”

概要

Query.from_self() 方法将从Query中移除:

# from_self is removed
q = (
    session.query(User, Address.email_address)
    .join(User.addresses)
    .from_self(User)
    .order_by(Address.email_address)
)

迁移到 2.0 版本

aliased() 构造可用于针对任意可选择的实体发出 ORM 查询。它在 1.4 版本中已经得到增强,以便顺利地多次针对相同子查询用于不同实体。这可以在 1.x 风格中与Query一起使用,注意由于最终查询想要查询UserAddress实体,因此创建了两个单独的aliased() 构造:

from sqlalchemy.orm import aliased
subq = session.query(User, Address.email_address).join(User.addresses).subquery()
ua = aliased(User, subq)
aa = aliased(Address, subq)
q = session.query(ua, aa).order_by(aa.email_address)

相同的形式可以在 2.0 风格中使用:

from sqlalchemy.orm import aliased
subq = select(User, Address.email_address).join(User.addresses).subquery()
ua = aliased(User, subq)
aa = aliased(Address, subq)
stmt = select(ua, aa).order_by(aa.email_address)
result = session.execute(stmt)

讨论

Query.from_self()方法是一个非常复杂的方法,很少被使用。这个方法的目的是将Query转换为一个子查询,然后返回一个从该子查询中 SELECT 的新Query。这个方法的复杂之处在于返回的查询应用了 ORM 实体和列的自动翻译,以便以子查询的方式在 SELECT 中陈述,以及允许被 SELECT 的实体和列进行修改。

因为Query.from_self()方法在生成的 SQL 中隐含了大量的转换,虽然它确实允许某种类型的模式被非常简洁地执行,但是这种方法在实际应用中很少见,因为它不容易理解。

新方法利用了aliased()构造,因此 ORM 内部无需猜测应该如何调整哪些实体和列,以及以何种方式进行调整;在上面的示例中,uaaa对象都是AliasedClass实例,为内部提供了一个明确的标记,指示子查询应该在何处引用以及正在考虑查询的给定组件的哪个实体列或关系。

SQLAlchemy 1.4 还提供了一种改进的标签样式,不再需要使用包含表名以消除不同表中具有相同名称的列的歧义的长标签。在上面的示例中,即使我们的UserAddress实体具有重叠的列名称,我们也可以同时从两个实体中选择,而无需指定任何特定的标签:

# 1.4 / 2.0 code
subq = select(User, Address).join(User.addresses).subquery()
ua = aliased(User, subq)
aa = aliased(Address, subq)
stmt = select(ua, aa).order_by(aa.email_address)
result = session.execute(stmt)

上述查询将区分UserAddress.id列,其中Address.id被渲染和跟踪为id_1

SELECT  anon_1.id  AS  anon_1_id,  anon_1.id_1  AS  anon_1_id_1,
  anon_1.user_id  AS  anon_1_user_id,
  anon_1.email_address  AS  anon_1_email_address
FROM  (
  SELECT  "user".id  AS  id,  address.id  AS  id_1,
  address.user_id  AS  user_id,  address.email_address  AS  email_address
  FROM  "user"  JOIN  address  ON  "user".id  =  address.user_id
)  AS  anon_1  ORDER  BY  anon_1.email_address

#5221

从备选可选项选择实体;Query.select_entity_from()

概要

Query.select_entity_from()方法将在 2.0 中被移除:

subquery = session.query(User).filter(User.id == 5).subquery()
user = session.query(User).select_entity_from(subquery).first()

迁移到 2.0

就像在从查询本身选择子查询,例如“from_self()”中描述的情况一样,aliased()对象提供了一个可以实现“从子查询选择实体”等操作的单一位置。使用 1.x 风格:

from sqlalchemy.orm import aliased
subquery = session.query(User).filter(User.name.like("%somename%")).subquery()
ua = aliased(User, subquery)
user = session.query(ua).order_by(ua.id).first()

使用 2.0 风格:

from sqlalchemy.orm import aliased
subquery = select(User).where(User.name.like("%somename%")).subquery()
ua = aliased(User, subquery)
# note that LIMIT 1 is not automatically supplied, if needed
user = session.execute(select(ua).order_by(ua.id).limit(1)).scalars().first()

讨论

这里的要点基本与从查询本身选择子查询的情况相同,例如“from_self()”讨论的内容相同。 Query.select_from_entity() 方法是指示查询从备用可选择的 ORM 映射实体加载行的另一种方法,这涉及到 ORM 在后续查询中无论在何处使用该实体,例如在 WHERE 子句或 ORDER BY 中,都会对该实体进行自动别名。这个非常复杂的特性很少以这种方式使用,就像Query.from_self()的情况一样,使用显式的aliased()对象时更容易理解正在发生的情况,从用户的角度和 SQLAlchemy ORM 的内部处理方式来看都是如此。

默认情况下 ORM 行不唯一

简介

session.execute(stmt) 返回的 ORM 行不再自动“唯一”。这通常是一个受欢迎的变化,但在使用集合的“联接急加载”加载器的情况下可能会出现问题:

# In the legacy API, many rows each have the same User primary key, but
# only one User per primary key is returned
users = session.query(User).options(joinedload(User.addresses))
# In the new API, uniquing is available but not implicitly
# enabled
result = session.execute(select(User).options(joinedload(User.addresses)))
# this actually will raise an error to let the user know that
# uniquing should be applied
rows = result.all()

迁移到 2.0

当使用集合的联接加载时,需要调用Result.unique()方法。ORM 实际上会设置一个默认的行处理程序,如果未执行此操作,它将引发错误,以确保联接急加载集合不会返回重复的行,同时保持显式性:

# 1.4 / 2.0 code
stmt = select(User).options(joinedload(User.addresses))
# statement will raise if unique() is not used, due to joinedload()
# of a collection.  in all other cases, unique() is not needed.
# By stating unique() explicitly, confusion over discrepancies between
# number of objects/ rows returned vs. "SELECT COUNT(*)" is resolved
rows = session.execute(stmt).unique().all()

讨论

这里的情况有点不同寻常,因为 SQLAlchemy 要求调用一个它完全可以自动执行的方法。要求调用该方法的原因是确保开发人员“选择”使用Result.unique() 方法,这样当行数的直接计数与实际结果集中的记录计数不冲突时,他们不会感到困惑,这已经是多年来用户困惑和错误报告的长期问题。默认情况下,不在任何其他情况下唯一化将提高性能,并且在自动唯一化导致混淆的情况下将提高清晰度。

要调用Result.unique()对联接的急加载集合进行处理可能有些不方便,在现代的 SQLAlchemy 中,selectinload() 策略提供了一个针对集合的急加载器,在大多数情况下优于joinedload(),应该优先考虑使用。

“动态”关系加载器被“只写”替代

简介

讨论过的 lazy="dynamic" 关系加载策略,利用了 2.0 版本中的遗留 Query 对象。 “动态”关系在没有解决方案的情况下不直接兼容 asyncio,此外,它也没有实现其原始目的,即防止大型集合的迭代,因为它有几种隐式发生迭代的行为。

引入了一种名为 lazy="write_only" 的新加载策略,通过 WriteOnlyCollection 集合类提供了一个非常严格的“无隐式迭代”API,并且与 2.0 风格的语句执行集成,支持 asyncio 以及与新的 ORM-enabled Bulk DML 功能集的直接集成。

与此同时,lazy="dynamic" 在 2.0 版本中仍然得到全面支持;应用程序可以延迟迁移这种特定模式,直到完全升级到 2.0 系列。

迁移到 2.0

新的“write only”功能仅在 SQLAlchemy 2.0 中可用,不是 1.4 的一部分。与此同时,lazy="dynamic" 加载策略在 2.0 版本中仍然得到全面支持,甚至包括新的 pep-484 和注释映射支持。

因此,从“dynamic”迁移的最佳策略是等到应用程序完全运行在 2.0 上,然后直接从 AppenderQuery 迁移到 WriteOnlyCollection,后者是“write_only”策略使用的集合类型。

有一些技术可用于在 1.4 版本下以更“2.0”风格使用 lazy="dynamic"。有两种方法可以实现特定关系的 2.0 风格查询:

  • 利用现有 lazy="dynamic" 关系上的 Query.statement 属性。我们可以立即使用像 Session.scalars() 这样的方法与动态加载器一起使用,如下所示:
class User(Base):
    __tablename__ = "user"
    posts = relationship(Post, lazy="dynamic")
jack = session.get(User, 5)
# filter Jack's blog posts
posts = session.scalars(jack.posts.statement.where(Post.headline == "this is a post"))
  • 使用 with_parent() 函数直接构造一个 select() 构造:
from sqlalchemy.orm import with_parent
jack = session.get(User, 5)
posts = session.scalars(
    select(Post)
    .where(with_parent(jack, User.posts))
    .where(Post.headline == "this is a post")
)

讨论

最初的想法是with_parent()函数应该足够了,但是继续利用关系本身上的特殊属性仍然吸引人,并且没有理由不能在这里使用 2.0 风格的构造。

新的“仅写”加载策略提供了一种不支持隐式迭代或项访问的新类型集合。相反,通过调用其.select()方法来读取集合的内容,以帮助构造适当的 SELECT 语句。该集合还包括.insert().update().delete()等方法,可用于对集合中的项目发出批量 DML 语句。类似于“动态”功能,该特性还包括.add().add_all().remove()方法,使用工作单元过程为单个成员排队进行添加或删除。有关该新功能的介绍,请参阅新的“仅写”关系策略取代“动态”。

请参见

新的“仅写”关系策略取代“动态”

仅写关系

从 Session 中移除了自动提交模式;添加了自动开始支持

简介

Session将不再支持“自动提交”模式,即以下模式:

from sqlalchemy.orm import Session
sess = Session(engine, autocommit=True)
# no transaction begun, but emits SQL, won't be supported
obj = sess.query(Class).first()
# session flushes in a transaction that it begins and
# commits, won't be supported
sess.flush()

迁移到 2.0

使用“自动提交”模式的Session的主要原因是使Session.begin()方法可用,以便框架集成和事件钩子可以控制此事件何时发生。在 1.4 中,Session现在具有自动开始行为,解决了此问题;现在可以调用Session.begin()方法:

from sqlalchemy.orm import Session
sess = Session(engine)
sess.begin()  # begin explicitly; if not called, will autobegin
# when database access is needed
sess.add(obj)
sess.commit()

讨论

“自动提交”模式是 SQLAlchemy 初始版本的另一个遗留功能。这个标志一直存在主要是为了支持允许显式使用Session.begin(),这在 1.4 版本中已经解决,以及允许使用“子事务”,这在 2.0 版本中也被移除了。

移除了会话“子事务”行为

简介

在 1.4 中,经常与自动提交模式一起使用的“子事务”模式已被弃用。这种模式允许在事务已经开始时使用 Session.begin() 方法,导致一个称为“子事务”的构造,其本质上是一个阻止 Session.commit() 方法实际提交的块。

迁移至 2.0

为了向使用这种模式的应用程序提供向后兼容性,可以使用以下上下文管理器或基于装饰器的类似实现:

import contextlib
@contextlib.contextmanager
def transaction(session):
    if not session.in_transaction():
        with session.begin():
            yield
    else:
        yield

上述上下文管理器可以像“子事务”标志的工作方式一样使用,例如以下示例:

# method_a starts a transaction and calls method_b
def method_a(session):
    with transaction(session):
        method_b(session)
# method_b also starts a transaction, but when
# called from method_a participates in the ongoing
# transaction.
def method_b(session):
    with transaction(session):
        session.add(SomeObject("bat", "lala"))
Session = sessionmaker(engine)
# create a Session and call method_a
with Session() as session:
    method_a(session)

为了与首选惯用模式进行比较,begin 块应位于最外层。这样就不需要单个函数或方法关注事务界定的细节:

def method_a(session):
    method_b(session)
def method_b(session):
    session.add(SomeObject("bat", "lala"))
Session = sessionmaker(engine)
# create a Session and call method_a
with Session() as session:
    with session.begin():
        method_a(session)

讨论

已经证明这种模式在实际应用中令人困惑,并且最好是应用确保数据库操作的最高级别由单个 begin/commit 对完成。

2.0 迁移 - ORM 扩展和示例更改

Dogpile 缓存示例和水平分片使用新的 Session API

Query 对象成为遗留对象时,先前依赖于 Query 对象子类化的这两个示例现在使用 SessionEvents.do_orm_execute() 钩子。请参阅 重新执行语句 一节进行示例。

烘焙查询扩展已被内置缓存系统取代

烘焙查询扩展已被内置缓存系统取代,并不再被 ORM 内部使用。

参见 SQL 编译缓存 获取有关新缓存系统的完整背景。

Dogpile 缓存示例和水平分片使用新的 Session API

Query 对象成为遗留对象时,先前依赖于 Query 对象子类化的这两个示例现在使用 SessionEvents.do_orm_execute() 钩子。请参阅 重新执行语句 一节进行示例。

烘焙查询扩展已被内置缓存系统取代

烘焙查询扩展已被内置缓存系统取代,不再被 ORM 内部使用。

有关新缓存系统的完整背景,请参阅 SQL 编译缓存。

异步 IO 支持

SQLAlchemy 1.4 版本为核心和 ORM 都提供了 asyncio 支持。新的 API 专门使用上述的“未来”模式。请参阅核心和 ORM 的异步 IO 支持获取背景信息。

相关文章
|
3月前
|
SQL 关系型数据库 测试技术
SqlAlchemy 2.0 中文文档(五十四)(4)
SqlAlchemy 2.0 中文文档(五十四)
34 1
|
3月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(五十四)(3)
SqlAlchemy 2.0 中文文档(五十四)
26 1
|
3月前
|
SQL 关系型数据库 API
SqlAlchemy 2.0 中文文档(五十四)(5)
SqlAlchemy 2.0 中文文档(五十四)
42 1
|
3月前
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(五十四)(2)
SqlAlchemy 2.0 中文文档(五十四)
86 1
|
3月前
|
SQL 缓存 编译器
SqlAlchemy 2.0 中文文档(五十六)(3)
SqlAlchemy 2.0 中文文档(五十六)
23 0
|
3月前
|
SQL 缓存 API
SqlAlchemy 2.0 中文文档(五十六)(1)
SqlAlchemy 2.0 中文文档(五十六)
35 0
|
3月前
|
SQL 缓存 测试技术
SqlAlchemy 2.0 中文文档(五十六)(4)
SqlAlchemy 2.0 中文文档(五十六)
20 0
|
3月前
|
SQL 存储 API
SqlAlchemy 2.0 中文文档(五十六)(6)
SqlAlchemy 2.0 中文文档(五十六)
22 0
|
3月前
|
SQL 测试技术 API
SqlAlchemy 2.0 中文文档(五十六)(2)
SqlAlchemy 2.0 中文文档(五十六)
171 0
|
3月前
|
SQL API Python
SqlAlchemy 2.0 中文文档(五十六)(7)
SqlAlchemy 2.0 中文文档(五十六)
34 0