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

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

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


2.0 迁移 - 核心连接 / 事务

库级别的(但不是驱动级别的)“自动提交”已从核心和 ORM 中删除

概要

在 SQLAlchemy 1.x 中,以下语句将自动提交底层的 DBAPI 事务,但在 SQLAlchemy 2.0 中不会发生:

conn = engine.connect()
# won't autocommit in 2.0
conn.execute(some_table.insert().values(foo="bar"))

这个自动提交也不会发生:

conn = engine.connect()
# won't autocommit in 2.0
conn.execute(text("INSERT INTO table (foo) VALUES ('bar')"))

对于需要提交的自定义 DML 的常见解决方法,“自动提交”执行选项将被移除:

conn = engine.connect()
# won't autocommit in 2.0
conn.execute(text("EXEC my_procedural_thing()").execution_options(autocommit=True))

迁移到 2.0

与 1.x 风格 和 2.0 风格 执行兼容的方法是使用 Connection.begin() 方法,或者使用 Engine.begin() 上下文管理器:

with engine.begin() as conn:
    conn.execute(some_table.insert().values(foo="bar"))
    conn.execute(some_other_table.insert().values(bat="hoho"))
with engine.connect() as conn:
    with conn.begin():
        conn.execute(some_table.insert().values(foo="bar"))
        conn.execute(some_other_table.insert().values(bat="hoho"))
with engine.begin() as conn:
    conn.execute(text("EXEC my_procedural_thing()"))

当使用 create_engine.future 标志和 2.0 风格 时,“逐步提交”风格也可以使用,因为 Connection 特性具有 autobegin 行为,当在没有显式调用 Connection.begin() 的情况下首次调用语句时会发生:

with engine.connect() as conn:
    conn.execute(some_table.insert().values(foo="bar"))
    conn.execute(some_other_table.insert().values(bat="hoho"))
    conn.commit()

当启用 2.0 弃用模式 时,将会发出警告,指示发生了已弃用的“自动提交”功能的地方,表明应该注意明确事务的地方。

讨论

SQLAlchemy 的第一个版本与 Python DBAPI(PEP 249)的精神不合,因为它试图隐藏 PEP 249 强调的“隐式开始”和“显式提交”事务。十五年后,我们现在认识到这实际上是一个错误,因为 SQLAlchemy 的许多尝试“隐藏”事务存在的模式导致 API 更复杂,工作不一致,对那些对关系数据库和 ACID 事务一般都是新手的用户来说极其混乱。SQLAlchemy 2.0 将取消所有隐式提交事务的尝试,使用模式将始终要求用户以某种方式标记事务的“开始”和“结束”,就像在 Python 中读取或写入文件具有“开始”和“结束”一样。

对于纯文本语句的自动提交情况,实际上有一个正则表达式来解析每个语句以检测自动提交!毫不奇怪,这个正则表达式不断地无法适应各种类型的语句和暗示向数据库“写入”的存储过程,导致持续的混淆,因为一些语句在数据库中产生结果,而其他语句则没有。通过阻止用户意识到事务概念,我们在这一点上收到了很多错误报告,因为用户不理解无论某些层是否自动提交事务,数据库都会始终使用事务。

SQLAlchemy 2.0 将要求在每个级别上所有数据库操作都明确指明事务的使用方式。对于绝大多数核心用例,已经推荐的模式是:

with engine.begin() as conn:
    conn.execute(some_table.insert().values(foo="bar"))

对于“随时提交,或者回滚”的用法,这类似于如何通常使用 Session,即 Engine 创建时使用了 create_engine.future 标志后返回的“未来”版本 Connection,包含新的 Connection.commit()Connection.rollback() 方法,这些方法会在首次调用语句时自动开始事务:

# 1.4 / 2.0 code
from sqlalchemy import create_engine
engine = create_engine(..., future=True)
with engine.connect() as conn:
    conn.execute(some_table.insert().values(foo="bar"))
    conn.commit()
    conn.execute(text("some other SQL"))
    conn.rollback()

在上面,engine.connect()方法将返回一个具有自动开始功能的Connection,这意味着在首次使用执行方法时会发出begin()事件(注意,Python DBAPI 中实际上没有“BEGIN”)。“自动开始”是 SQLAlchemy 1.4 中的一种新模式,它既由Connection特性,也由 ORM Session对象特性;自动开始允许在首次获取对象时显式调用Connection.begin()方法,以便为希望标记事务开始的模式提供方案,但如果不调用该方法,则在首次对对象进行工作时隐式发生。

“隐式”和“无连接”执行,“绑定元数据”已移除的移除与讨论“隐式”和“无连接”执行,移除“绑定元数据”密切相关。所有这些传统模式都是从  Python 在 SQLAlchemy 首次创建时没有上下文管理器或装饰器这一事实上建立起来的,因此没有便捷的惯用模式来标记资源的使用。

驱动程序级别的自动提交仍然可用

真正的“自动提交”行为现在已经在大多数 DBAPI 实现中广泛可用,并且通过Connection.execution_options.isolation_level参数由 SQLAlchemy 支持,如设置事务隔离级别,包括 DBAPI 自动提交中所讨论的。真正的自动提交被视为一种“隔离级别”,因此当使用自动提交时,应用程序代码的结构不会发生变化;Connection.begin()上下文管理器以及诸如Connection.commit()之类的方法仍然可以使用,它们在数据库驱动程序级别只是无操作。

概要

EngineMetaData对象关联起来的能力已被移除,这样就可以使用一系列所谓的“无连接”执行模式:

from sqlalchemy import MetaData
metadata_obj = MetaData(bind=engine)  # no longer supported
metadata_obj.create_all()  # requires Engine or Connection
metadata_obj.reflect()  # requires Engine or Connection
t = Table("t", metadata_obj, autoload=True)  # use autoload_with=engine
result = engine.execute(t.select())  # no longer supported
result = t.select().execute()  # no longer supported

2.0 迁移

对于架构级模式,需要明确使用EngineConnectionEngine仍然可以直接用作MetaData.create_all()操作或 autoload 操作的连接源。对于执行语句,只有Connection对象具有Connection.execute()方法(除了 ORM 级别的Session.execute()方法):

from sqlalchemy import MetaData
metadata_obj = MetaData()
# engine level:
# create tables
metadata_obj.create_all(engine)
# reflect all tables
metadata_obj.reflect(engine)
# reflect individual table
t = Table("t", metadata_obj, autoload_with=engine)
# connection level:
with engine.connect() as connection:
    # create tables, requires explicit begin and/or commit:
    with connection.begin():
        metadata_obj.create_all(connection)
    # reflect all tables
    metadata_obj.reflect(connection)
    # reflect individual table
    t = Table("t", metadata_obj, autoload_with=connection)
    # execute SQL statements
    result = connection.execute(t.select())

讨论

核心文档已经在这里规范了所需的模式,因此大多数现代应用程序可能不需要在任何情况下进行太多更改,但是可能仍然有许多应用程序依赖于engine.execute()调用,这些调用将需要进行调整。

“无连接”执行指的是仍然相当流行的从Engine中调用.execute()的模式:

result = engine.execute(some_statement)

上述操作隐式地获取了一个Connection对象,并在其上运行.execute()方法。虽然这似乎是一个简单的便利功能,但已经证明它引起了几个问题:

  • 程序中出现了大量的engine.execute()调用,这已经变得普遍,过度使用了原本应该很少使用的功能,并导致了低效的非事务性应用程序。新用户对于engine.execute()connection.execute()之间的区别感到困惑,这两种方法之间的微妙差别通常不被理解。
  • 该功能依赖于“应用程序级自动提交”功能才有意义,该功能本身也正在被删除,因为它也是低效和误导性的。
  • 为了处理结果集,Engine.execute 返回一个带有未消耗的游标结果的结果对象。这个游标结果必然仍然链接到保持打开事务的 DBAPI 连接,所有这些在结果集完全消耗了等待在游标中的行之后释放。这意味着 Engine.execute 实际上并没有在调用完成时关闭它声称正在管理的连接资源。SQLAlchemy 的“自动关闭”行为调整得足够好,以至于用户通常不会报告这个系统的任何负面影响,然而,这仍然是 SQLAlchemy 最早版本中遗留下来的一个过于隐式和低效的系统。

移除“无连接”执行,然后移除一个更加遗留的模式,“隐式、无连接”执行:

result = some_statement.execute()

上述模式具有“无连接”执行的所有问题,而且它依赖于“绑定元数据”模式,SQLAlchemy 多年来一直试图减少使用这种模式。这是 SQLAlchemy 在 0.1 版本中首次宣传的使用模型,当 Connection 对象被引入并且后来 Python 上下文管理器提供了更好的在固定范围内使用资源的模式时,这种模式几乎立即过时。

随着隐式执行的移除,“绑定元数据”本身也不再在此系统中有用。在现代用法中,“绑定元数据”仍然在 MetaData.create_all() 调用以及 Session 对象中有些方便,然而,让这些函数显式接收一个 Engine 提供了更清晰的应用程序设计。

多种选择变成了一个选择。

总的来说,上述执行模式是在 SQLAlchemy 的第一个 0.1  版本发布之前引入的。经过多年的减少这些模式的使用,“隐式、无连接”执行和“绑定元数据”不再像以前那样广泛使用,所以在 2.0  中,我们试图最终减少从“多种选择”中执行语句到 Core 的选择数量:

# many choices
# bound metadata?
metadata_obj = MetaData(engine)
# or not?
metadata_obj = MetaData()
# execute from engine?
result = engine.execute(stmt)
# or execute the statement itself (but only if you did
# "bound metadata" above, which means you can't get rid of "bound" if any
# part of your program uses this form)
result = stmt.execute()
# execute from connection, but it autocommits?
conn = engine.connect()
conn.execute(stmt)
# execute from connection, but autocommit isn't working, so use the special
# option?
conn.execution_options(autocommit=True).execute(stmt)
# or on the statement ?!
conn.execute(stmt.execution_options(autocommit=True))
# or execute from connection, and we use explicit transaction?
with conn.begin():
    conn.execute(stmt)

对于“一个选择”,我们指的是“与显式事务的显式连接”;根据需要,仍然有一些方法来标记事务块。在写操作的情况下,“一个选择”是获取Connection,然后显式地标记事务:

# one choice - work with explicit connection, explicit transaction
# (there remain a few variants on how to demarcate the transaction)
# "begin once" - one transaction only per checkout
with engine.begin() as conn:
    result = conn.execute(stmt)
# "commit as you go" - zero or more commits per checkout
with engine.connect() as conn:
    result = conn.execute(stmt)
    conn.commit()
# "commit as you go" but with a transaction block instead of autobegin
with engine.connect() as conn:
    with conn.begin():
        result = conn.execute(stmt)

execute() 方法更加严格,执行选项更加突出。

概要

在 SQLAlchemy 2.0 中可能与sqlalchemy.engine.Connection() execute 方法一起使用的参数模式已经大大简化,删除了许多以前可用的参数模式。 1.4 系列中的新 API 在sqlalchemy.engine.Connection()中描述。下面的示例说明了需要修改的模式:

connection = engine.connect()
# direct string SQL not supported; use text() or exec_driver_sql() method
result = connection.execute("select * from table")
# positional parameters no longer supported, only named
# unless using exec_driver_sql()
result = connection.execute(table.insert(), ("x", "y", "z"))
# **kwargs no longer accepted, pass a single dictionary
result = connection.execute(table.insert(), x=10, y=5)
# multiple *args no longer accepted, pass a list
result = connection.execute(
    table.insert(), {"x": 10, "y": 5}, {"x": 15, "y": 12}, {"x": 9, "y": 8}
)

迁移到 2.0

新的Connection.execute()方法现在接受 1.x Connection.execute()方法接受的参数样式的子集,因此以下代码在 1.x 和 2.0 之间是兼容的:

connection = engine.connect()
from sqlalchemy import text
result = connection.execute(text("select * from table"))
# pass a single dictionary for single statement execution
result = connection.execute(table.insert(), {"x": 10, "y": 5})
# pass a list of dictionaries for executemany
result = connection.execute(
    table.insert(), [{"x": 10, "y": 5}, {"x": 15, "y": 12}, {"x": 9, "y": 8}]
)

讨论

删除了使用*args**kwargs的方式,这样做既可以消除对方法传递了哪种类型参数的猜测的复杂性,又可以为其他选项留出空间,即现在可用于根据每个语句提供选项的Connection.execute.execution_options 字典。该方法也经过修改,以使其使用模式与Session.execute()方法相匹配,后者在 2.0 风格中是一个更加突出的 API。

删除直接字符串 SQL 是为了解决Connection.execute()Session.execute()之间的不一致性,前者情况下字符串直接传递给驱动程序原始,而后者情况下首先将其转换为text()构造。通过仅允许text(),这也将接受的参数格式限制为“命名”而不是“位置”。最后,从安全角度来看,字符串 SQL 用例越来越受到审查,text()构造已经成为文本 SQL 领域的明确边界,需要注意不受信任的用户输入。

结果行的行为类似于命名元组

简介

版本 1.4 引入了一个全新的 Result 对象,该对象反过来返回 Row 对象,当使用“future”模式时,它们的行为类似于命名元组:

engine = create_engine(..., future=True)  # using future mode
with engine.connect() as conn:
    result = conn.execute(text("select x, y from table"))
    row = result.first()  # suppose the row is (1, 2)
    "x" in row  # evaluates to False, in 1.x / future=False, this would be True
    1 in row  # evaluates to True, in 1.x / future=False, this would be False

迁移到 2.0

应用程序代码或测试套件,如果正在测试行中是否存在特定键,则需要测试 row.keys() 集合。但是,这是一个不太常见的用例,因为结果行通常由已经知道其中存在哪些列的代码使用。

讨论

已经作为 1.4 的一部分,之前从 Query 对象中选择行时使用的 KeyedTuple 类已被替换为 Row 类,该类是使用 create_engine.future 标志与 Engine 一起使用 Core 语句结果时返回的相同 Row 的基类(当未设置 create_engine.future 标志时,Core 结果集使用 LegacyRow 子类,该子类维护了 __contains__() 方法的向后兼容行为;ORM 专门直接使用 Row 类)。

这个 Row 的行为类似于命名元组,因为它作为序列,但也支持属性名访问,例如 row.some_column。然而,它还通过特殊属性 row._mapping 提供了之前的“映射”行为,该属性产生一个 Python 映射,使得可以使用基于键的访问,如 row["some_column"]

为了一开始就接收到映射的结果,可以在结果上使用 mappings() 修改器:

from sqlalchemy.future.orm import Session
session = Session(some_engine)
result = session.execute(stmt)
for row in result.mappings():
    print("the user is: %s" % row["User"])

ORM 中使用的 Row 类也支持通过实体或属性进行访问:

from sqlalchemy.future import select
stmt = select(User, Address).join(User.addresses)
for row in session.execute(stmt).mappings():
    print("the user is: %s the address is: %s" % (row[User], row[Address]))

另请参阅

RowProxy 不再是“代理”; 现在称为 Row,并且行为类似于增强型命名元组

2.0 迁移 - Core 使用

select() 不再接受不同的构造函数参数,列被按位置传递

简介

select() 构造以及相关方法 FromClause.select() 将不再接受用于构建 WHERE 子句、FROM 列表和 ORDER BY 等元素的关键字参数。列的列表现在可以按位置发送,而不是作为列表。此外,case() 构造现在按位置接受其 WHEN 标准,而不是作为列表:

# select_from / order_by keywords no longer supported
stmt = select([1], select_from=table, order_by=table.c.id)
# whereclause parameter no longer supported
stmt = select([table.c.x], table.c.id == 5)
# whereclause parameter no longer supported
stmt = table.select(table.c.id == 5)
# list emits a deprecation warning
stmt = select([table.c.x, table.c.y])
# list emits a deprecation warning
case_clause = case(
    [(table.c.x == 5, "five"), (table.c.x == 7, "seven")],
    else_="neither five nor seven",
)

迁移到 2.0

只支持 select() 的“生成”样式。应按位置传递要从中选择的列 / 表的列表。SQLAlchemy 1.4 中的 select() 构造接受传统样式和新样式,使用自动检测方案,因此下面的代码与 1.4 和 2.0 兼容:

# use generative methods
stmt = select(1).select_from(table).order_by(table.c.id)
# use generative methods
stmt = select(table).where(table.c.id == 5)
# use generative methods
stmt = table.select().where(table.c.id == 5)
# pass columns clause expressions positionally
stmt = select(table.c.x, table.c.y)
# case conditions passed positionally
case_clause = case(
    (table.c.x == 5, "five"), (table.c.x == 7, "seven"), else_="neither five nor seven"
)

讨论

多年来,SQLAlchemy 发展了一个约定,即 SQL 构造接受参数时可以作为列表或按位置参数传递。该约定规定了形成 SQL 语句结构的结构元素应按位置传递。相反,形成 SQL 语句参数化数据的数据元素应作为列表传递。多年来,select() 构造无法顺利参与此约定,因为“WHERE”子句的传递模式非常传统。SQLAlchemy 2.0 最终通过将 select() 构造更改为仅接受多年来一直是 Core 教程中唯一记录的样式的“生成”样式来解决了这个问题。

“结构”与“数据”元素的示例如下:

# table columns for CREATE TABLE - structural
table = Table("table", metadata_obj, Column("x", Integer), Column("y", Integer))
# columns in a SELECT statement - structural
stmt = select(table.c.x, table.c.y)
# literal elements in an IN clause - data
stmt = stmt.where(table.c.y.in_([1, 2, 3]))

另请参阅

select(),case() 现在接受位置表达式

以“传统”模式创建的 select() 构造; 关键字参数等

insert/update/delete DML 不再接受关键字构造函数参数

简介

与之前对 select() 的更改类似,除了表参数之外,insert()update()delete() 的构造函数参数基本上被移除:

# no longer supported
stmt = insert(table, values={"x": 10, "y": 15}, inline=True)
# no longer supported
stmt = insert(table, values={"x": 10, "y": 15}, returning=[table.c.x])
# no longer supported
stmt = table.delete(table.c.x > 15)
# no longer supported
stmt = table.update(table.c.x < 15, preserve_parameter_order=True).values(
    [(table.c.y, 20), (table.c.x, table.c.y + 10)]
)

迁移到 2.0

以下示例说明了上述示例的生成方法使用:

# use generative methods, **kwargs OK for values()
stmt = insert(table).values(x=10, y=15).inline()
# use generative methods, dictionary also still  OK for values()
stmt = insert(table).values({"x": 10, "y": 15}).returning(table.c.x)
# use generative methods
stmt = table.delete().where(table.c.x > 15)
# use generative methods, ordered_values() replaces preserve_parameter_order
stmt = (
    table.update()
    .where(
        table.c.x < 15,
    )
    .ordered_values((table.c.y, 20), (table.c.x, table.c.y + 10))
)

讨论

API 和内部结构正在以类似于 select() 构造的方式进行简化。

2.0 迁移 - ORM 配置

Declarative 成为一流的 API

简介

sqlalchemy.ext.declarative 包大部分已经移动到 sqlalchemy.orm 包中,但有一些例外。declarative_base()declared_attr() 函数没有任何行为上的变化。一个名为 registry 的新超级实现现在作为顶级 ORM 配置构造存在,它还提供了基于装饰器的声明性以及与声明式注册表集成的经典映射的新支持。

迁移到 2.0

更改导入:

from sqlalchemy.ext import declarative_base, declared_attr

至:

from sqlalchemy.orm import declarative_base, declared_attr

讨论

大约十年后,sqlalchemy.ext.declarative 包现在已经整合到 sqlalchemy.orm 命名空间中,除了保留为 Declarative 扩展的声明性 “extension” 类。更改在 1.4 迁移指南中进一步详细说明,详见  Declarative is now integrated into the ORM with new features。

另请参阅

ORM 映射类概述 - 用于 Declarative、经典映射、数据类、attrs 等的全新统一文档。

Declarative is now integrated into the ORM with new features

最初的 “mapper()” 函数现在是 Declarative 的核心元素,已更名

简介

sqlalchemy.orm.mapper() 独立函数在幕后移动,由高级 API 调用。这个函数的新版本是从 registry 对象获取的方法 registry.map_imperatively()

迁移到 2.0

使用经典映射的代码应从以下导入和代码更改:

from sqlalchemy.orm import mapper
mapper(SomeClass, some_table, properties={"related": relationship(SomeRelatedClass)})

要从中央registry对象工作:

from sqlalchemy.orm import registry
mapper_reg = registry()
mapper_reg.map_imperatively(
    SomeClass, some_table, properties={"related": relationship(SomeRelatedClass)}
)

上述registry也是声明式映射的源头,经典映射现在可以访问此注册表,包括在relationship()上进行基于字符串的配置:

from sqlalchemy.orm import registry
mapper_reg = registry()
Base = mapper_reg.generate_base()
class SomeRelatedClass(Base):
    __tablename__ = "related"
    # ...
mapper_reg.map_imperatively(
    SomeClass,
    some_table,
    properties={
        "related": relationship(
            "SomeRelatedClass",
            primaryjoin="SomeRelatedClass.related_id == SomeClass.id",
        )
    },
)

讨论

受到广泛需求,“经典映射”仍然存在,但其新形式基于registry对象,并可作为registry.map_imperatively()使用。

此外,“经典映射”的主要理由是保持Table设置与类别不同。声明式一直允许使用所谓的混合声明式来使用这种样式。然而,为了去除基类要求,已经添加了第一类装饰器形式。

又一个独立但相关的增强功能是,支持 Python 数据类已添加到声明装饰器和经典映射形式中。

另请参阅

ORM 映射类概述 - 用于声明式、经典映射、数据类、attrs 等的全新统一文档。


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

相关文章
|
4月前
|
SQL 缓存 API
SqlAlchemy 2.0 中文文档(五十六)(1)
SqlAlchemy 2.0 中文文档(五十六)
42 0
|
4月前
|
SQL 缓存 测试技术
SqlAlchemy 2.0 中文文档(五十六)(8)
SqlAlchemy 2.0 中文文档(五十六)
31 0
|
4月前
|
SQL 缓存 编译器
SqlAlchemy 2.0 中文文档(五十六)(3)
SqlAlchemy 2.0 中文文档(五十六)
26 0
|
4月前
|
SQL 存储 API
SqlAlchemy 2.0 中文文档(五十六)(6)
SqlAlchemy 2.0 中文文档(五十六)
27 0
|
4月前
|
SQL 缓存 API
SqlAlchemy 2.0 中文文档(五十六)(5)
SqlAlchemy 2.0 中文文档(五十六)
49 0
|
4月前
|
SQL 缓存 测试技术
SqlAlchemy 2.0 中文文档(五十六)(4)
SqlAlchemy 2.0 中文文档(五十六)
25 0
|
4月前
|
SQL API Python
SqlAlchemy 2.0 中文文档(五十六)(7)
SqlAlchemy 2.0 中文文档(五十六)
43 0
|
4月前
|
SQL 缓存 测试技术
SqlAlchemy 2.0 中文文档(五十六)(9)
SqlAlchemy 2.0 中文文档(五十六)
32 0
|
4月前
|
SQL API Python
SqlAlchemy 2.0 中文文档(五十七)(6)
SqlAlchemy 2.0 中文文档(五十七)
33 0
|
4月前
|
SQL Oracle 关系型数据库
SqlAlchemy 2.0 中文文档(五十七)(8)
SqlAlchemy 2.0 中文文档(五十七)
52 0