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

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

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


迁移至 2.0 第四步 - 在 Engine 上使用 future 标志

Engine 对象在 2.0 版本中具有更新的事务级 API。在 1.4 版本中,通过向 create_engine() 函数传递 future=True 标志即可使用这个新 API。

当使用 create_engine.future 标志时,EngineConnection 对象完全支持 2.0 API,不再支持任何旧特性,包括 Connection.execute() 的新参数格式,移除了“隐式自动提交”,字符串语句需要使用 text() 构造,除非使用 Connection.exec_driver_sql() 方法,以及从 Engine 进行无连接执行的功能被移除。

如果关于使用 EngineConnection 的所有 RemovedIn20Warning 警告都已解决,则可以启用 create_engine.future 标志,并且不应该引发任何错误。

新引擎被描述为 Engine,它提供了一个新的 Connection 对象。除了上述更改外,Connection 对象还具有 Connection.commit()Connection.rollback() 方法,以支持新的“随时提交”操作模式:

from sqlalchemy import create_engine
engine = create_engine("postgresql+psycopg2:///")
with engine.connect() as conn:
    conn.execute(text("insert into table (x) values (:some_x)"), {"some_x": 10})
    conn.commit()  # commit as you go

迁移至 2.0 第五步 - 在 Session 上使用 future 标志

Session 对象在 2.0 版本中还具有更新的事务/连接级 API。在 1.4 版本中,可以在 Sessionsessionmaker 上使用 Session.future 标志来使用此 API。

Session 对象支持“future”模式,并涉及以下更改:

  1. Session 解析用于连接的引擎时,不再支持“bound metadata”。这意味着必须将一个 Engine 对象传递给构造函数(这可以是传统或未来风格对象)。
  2. 不再支持 Session.begin.subtransactions 标志。
  3. Session.commit() 方法总是向数据库发出 COMMIT,而不��尝试协调“子事务”。
  4. Session.rollback() 方法总是一次性回滚整个事务堆栈,而不是尝试保持“子事务”。

在 1.4 版本中,Session 还支持更灵活的创建模式,这些模式现在与 Connection 对象使用的模式紧密匹配。亮点包括 Session 可以用作上下文管理器:

from sqlalchemy.orm import Session
with Session(engine) as session:
    session.add(MyObject())
    session.commit()

此外,sessionmaker 对象支持 sessionmaker.begin() 上下文管理器,将在一个块中创建一个 Session 并开始/提交一个事务:

from sqlalchemy.orm import sessionmaker
Session = sessionmaker(engine)
with Session.begin() as session:
    session.add(MyObject())

参见 Session-level vs. Engine level transaction control 部分,比较了 Session 的创建模式与 Connection 的模式。

一旦应用程序通过所有测试/运行,并且所有 SQLALCHEMY_WARN_20=1 和所有 exc.RemovedIn20Warning 出现的地方都设置为引发错误,应用程序就准备好了!

接下来的部分将详细说明所有主要 API 修改的具体更改。

迁移至 2.0 第六步 - 为显式类型化的 ORM 模型添加 __allow_unmapped__

SQLAlchemy 2.0 新增了对 ORM 模型上 PEP 484 类型注解的运行时解释支持。这些注解的要求是它们必须使用 Mapped 泛型容器。不使用 Mapped 的注解,比如链接到 relationship() 等构造的注解将在 Python 中引发错误,因为它们暗示了错误的配置。

使用 Mypy 插件 的 SQLAlchemy 应用程序,如果在注解中使用了不使用 Mapped 的显式注释,则会出现这些错误,就像下面的示例中所发生的一样:

Base = declarative_base()
class Foo(Base):
    __tablename__ = "foo"
    id: int = Column(Integer, primary_key=True)
    # will raise
    bars: List["Bar"] = relationship("Bar", back_populates="foo")
class Bar(Base):
    __tablename__ = "bar"
    id: int = Column(Integer, primary_key=True)
    foo_id = Column(ForeignKey("foo.id"))
    # will raise
    foo: Foo = relationship(Foo, back_populates="bars", cascade="all")

上面,Foo.barsBar.foorelationship() 声明将在类构建时引发错误,因为它们不使用 Mapped(相比之下,使用 Column 的注解在 2.0 中被忽略,因为这些能够被识别为传统配置样式)。为了让所有不使用 Mapped 的注解能够无错误通过,可以在类或任何子类上使用 __allow_unmapped__ 属性,这将导致在这些情况下这些注解完全被新的声明式系统忽略。

注意

__allow_unmapped__ 指令适用于 ORM 的运行时行为。它不会影响 Mypy 的行为,上述映射仍然要求安装 Mypy 插件。对于完全符合 2.0 样式的 ORM 模型,可以在不使用插件的情况下正确进行类型化,遵循 迁移现有映射 中的迁移步骤。

下面的示例说明了将 __allow_unmapped__ 应用于声明式 Base 类的情况,它将对所有从 Base 继承的类生效:

# qualify the base with __allow_unmapped__.  Can also be
# applied to classes directly if preferred
class Base:
    __allow_unmapped__ = True
Base = declarative_base(cls=Base)
# existing mapping proceeds, Declarative will ignore any annotations
# which don't include ``Mapped[]``
class Foo(Base):
    __tablename__ = "foo"
    id: int = Column(Integer, primary_key=True)
    bars: List["Bar"] = relationship("Bar", back_populates="foo")
class Bar(Base):
    __tablename__ = "bar"
    id: int = Column(Integer, primary_key=True)
    foo_id = Column(ForeignKey("foo.id"))
    foo: Foo = relationship(Foo, back_populates="bars", cascade="all")

2.0.0beta3 版本中的变更:- 改进了 __allow_unmapped__ 属性支持,允许保持可用的 1.4 样式显式注释关系,而不使用 Mapped

迁移到 2.0 第七步 - 测试针对 SQLAlchemy 2.0 版本

如前所述,SQLAlchemy 2.0 具有额外的 API 和行为变化,旨在向后兼容,但仍可能引入一些不兼容性。因此,在整体移植过程完成后,最后一步是针对最新版本的 SQLAlchemy 2.0 进行测试,以纠正可能存在的任何剩余问题。

在 SQLAlchemy 2.0 有什么新特性? 的指南中提供了一个关于 SQLAlchemy 2.0 的新功能和行为的概述,这些新功能和行为超出了 1.4->2.0 API 变化的基本集。

2.0 迁移 - Core 连接 / 事务

库级别(但不是驱动程序级别)的“自动提交”从 Core 和 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()"))

当使用 2.0 风格 与 create_engine.future 标志时,也可以使用“边提交边进行”风格,因为 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 将要求在每个级别的数据库操作中都明确指定事务的使用方式。对于绝大多数 Core 使用案例,已经推荐了以下模式:

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

对于“边做边提交,或者回滚”的用法,这与如今通常如何使用 Session 很相似,使用 create_engine.future 标志创建的 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 对象,其具有自动开始特性,意味着当首次使用 execute 方法时会触发 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()操作或自动加载操作的连接源。对于执行语句,只有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 的第一个广告使用模型中是 SQLAlchemy 的第一个广告使用模型,在版本 0.1 中立即变得过时,当Connection对象被引入后,后来的 Python 上下文管理器提供了更好的在固定范围内使用资源的模式。

随着隐式执行的移除,“绑定元数据”本身在该系统中也不再有作用。在现代使用中,“绑定元数据”在MetaData.create_all()调用以及Session对象中仍然有一定的方便之处,但是让这些函数明确接收Engine提供了更清晰的应用设计。

多种选择变为一种选择

总的来说,上述执行模式是在 SQLAlchemy 的第一个 0.1 版本中引入的,甚至在Connection对象都不存在的情况下。经过多年的减少这些模式的强调,“隐式,无连接”执行和“绑定元数据”不再被广泛使用,所以在 2.0 版本中我们希望最终减少在核心中执行语句的选择数量:

# 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()执行方法的参数模式大大简化,删除了许多以前可用的参数模式。 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 中已经成为一部分的 KeyedTuple 类被替换为从 Query 对象中选择行时使用的 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 并且行为类似增强的命名元组 ### 库级别(但不是驱动程序级别)“自动提交”从 Core 和 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 具有 自动开始 行为,当第一次调用语句时,在没有显式调用 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 将要求在每个级别的数据库操作都要明确指定事务的使用方式。对于绝大多数 Core 使用情况,这已经是推荐的模式:

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

对于“边提交边回滚”使用方式,类似于如何今天通常使用Session,使用了create_engine.future标志创建的Engine返回的“未来”版本的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()方法将返回一个具有autobegin功能的Connection,意味着在首次使用执行方法时会触发begin()事件(但请注意,Python DBAPI 中实际上没有“BEGIN”)。 “autobegin”是 SQLAlchemy 1.4 中的一个新模式,由Connection和 ORM Session 对象共同支持;autobegin 允许在首次获取对象时显式调用Connection.begin()方法,用于希望标记事务开始的方案,但如果未调用该方法,则在首次对对象进行操作时会隐式发生。

“自动提交”功能的移除与讨论中的“隐式”和“无连接”执行的移除密切相关,详见“隐式”和“无连接”执行,“绑定元数据”移除。所有这些传统模式都源自于  Python 在 SQLAlchemy 最初创建时没有上下文管理器或装饰器的事实,因此没有方便的惯用模式来标记资源的使用。

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

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

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

大多数 DBAPI 实现现在广泛支持真正的“自动提交”行为,并且通过 SQLAlchemy 支持Connection.execution_options.isolation_level参数,如设置事务隔离级别,包括 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 版本发布之前引入的,甚至Connection对象都不存在。经过多年的减少这些模式的重要性,“隐式,无连接”执行和“绑定元数据”不再被广泛使用,因此在 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)
多种选择变为一种选择

总的来说,上述执行模式是在 SQLAlchemy 的第一个 0.1 版本发布之前引入的,甚至在Connection对象不存在之前。经过多年的淡化这些模式,“隐式、无连接”执行和“绑定元数据”不再被广泛使用,因此在 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()的执行方法一起使用的参数模式大大简化,删除了许多以前可用的参数模式。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 一起返回的相同 Row 的基类(当未设置 create_engine.future 标志时,Core 结果集使用 LegacyRow 子类,该子类保持了 __contains__() 方法的向后兼容行为;ORM 专门直接使用 Row 类)。

这个 Row 的行为类似于命名元组,它作为一个序列,同时也支持属性名称访问,例如 row.some_column。然而,它还通过特殊属性 row._mapping 提供了以前的“映射”行为,这样就可以使用键访问,例如 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"])

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

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 并且行为类似于增强的命名元组


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

相关文章
|
2月前
|
SQL 缓存 编译器
SqlAlchemy 2.0 中文文档(五十六)(3)
SqlAlchemy 2.0 中文文档(五十六)
15 0
|
2月前
|
SQL API Python
SqlAlchemy 2.0 中文文档(五十六)(7)
SqlAlchemy 2.0 中文文档(五十六)
27 0
|
2月前
|
SQL 缓存 测试技术
SqlAlchemy 2.0 中文文档(五十六)(4)
SqlAlchemy 2.0 中文文档(五十六)
14 0
|
2月前
|
SQL 测试技术 API
SqlAlchemy 2.0 中文文档(五十六)(2)
SqlAlchemy 2.0 中文文档(五十六)
158 0
|
2月前
|
SQL 缓存 API
SqlAlchemy 2.0 中文文档(五十六)(5)
SqlAlchemy 2.0 中文文档(五十六)
23 0
|
2月前
|
SQL 缓存 API
SqlAlchemy 2.0 中文文档(五十六)(1)
SqlAlchemy 2.0 中文文档(五十六)
24 0
|
2月前
|
SQL 缓存 测试技术
SqlAlchemy 2.0 中文文档(五十六)(9)
SqlAlchemy 2.0 中文文档(五十六)
23 0
|
2月前
|
SQL 缓存 测试技术
SqlAlchemy 2.0 中文文档(五十六)(8)
SqlAlchemy 2.0 中文文档(五十六)
20 0
|
2月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(五十九)(3)
SqlAlchemy 2.0 中文文档(五十九)
19 0
|
2月前
|
SQL 关系型数据库 MySQL
SqlAlchemy 2.0 中文文档(五十九)(2)
SqlAlchemy 2.0 中文文档(五十九)
13 0