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

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

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


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选项,这个特性的内部复杂性是巨大的,并且将在 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 对象。 “dynamic” 关系在没有解决方法的情况下无法直接兼容 asyncio,此外,它也不能实现其原始目的,即防止大型集合的迭代,因为它有几种隐式迭代的行为。

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

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

迁移到 2.0

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

因此,从 “dynamic” 迁移到 2.0 的最佳策略是等到应用程序完全运行在 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 样式的构造在这里起作用。

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

另请参阅

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

仅写关系 ### 从会话中移除自动提交模式;添加了自动开始支持

简介

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 编译缓存以获取新缓存系统的完整背景。


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

相关文章
|
2月前
|
SQL 缓存 API
SqlAlchemy 2.0 中文文档(五十六)(5)
SqlAlchemy 2.0 中文文档(五十六)
22 0
|
2月前
|
SQL 缓存 测试技术
SqlAlchemy 2.0 中文文档(五十六)(9)
SqlAlchemy 2.0 中文文档(五十六)
23 0
|
2月前
|
SQL API Python
SqlAlchemy 2.0 中文文档(五十六)(7)
SqlAlchemy 2.0 中文文档(五十六)
25 0
|
2月前
|
SQL 缓存 测试技术
SqlAlchemy 2.0 中文文档(五十六)(8)
SqlAlchemy 2.0 中文文档(五十六)
18 0
|
2月前
|
SQL 缓存 编译器
SqlAlchemy 2.0 中文文档(五十六)(3)
SqlAlchemy 2.0 中文文档(五十六)
15 0
|
2月前
|
SQL 测试技术 API
SqlAlchemy 2.0 中文文档(五十六)(2)
SqlAlchemy 2.0 中文文档(五十六)
150 0
|
2月前
|
SQL 缓存 API
SqlAlchemy 2.0 中文文档(五十六)(1)
SqlAlchemy 2.0 中文文档(五十六)
20 0
|
2月前
|
SQL 存储 API
SqlAlchemy 2.0 中文文档(五十六)(6)
SqlAlchemy 2.0 中文文档(五十六)
15 0
|
2月前
|
SQL 关系型数据库 PostgreSQL
SqlAlchemy 2.0 中文文档(五十九)(7)
SqlAlchemy 2.0 中文文档(五十九)
12 0
|
2月前
|
SQL 缓存 Oracle
SqlAlchemy 2.0 中文文档(五十九)(1)
SqlAlchemy 2.0 中文文档(五十九)
126 0