SqlAlchemy 2.0 中文文档(二十三)

2024-06-26 14:52:41 浏览数 (2)

原文:docs.sqlalchemy.org/en/20/contents.html

级联

原文:docs.sqlalchemy.org/en/20/orm/cascades.html

映射器支持在relationship()构造上配置可配置级联行为的概念。这涉及到相对于特定Session上执行的操作应如何传播到由该关系引用的项目(例如“子”对象),并且受到relationship.cascade选项的影响。

级联的默认行为仅限于所谓的 save-update 和 merge 设置的级联。级联的典型“替代”设置是添加 delete 和 delete-orphan 选项;这些设置适用于只有在附加到其父对象时才存在的相关对象,并且在其他情况下将被删除。

使用relationship()上的relationship.cascade选项配置级联行为:

代码语言:javascript复制
class Order(Base):
    __tablename__ = "order"

    items = relationship("Item", cascade="all, delete-orphan")
    customer = relationship("User", cascade="save-update")

要在反向引用上设置级联,可以使用相同的标志与backref()函数一起使用,该函数最终将其参数反馈到relationship()中:

代码语言:javascript复制
class Item(Base):
    __tablename__ = "item"

    order = relationship(
        "Order", backref=backref("items", cascade="all, delete-orphan")
    )

relationship.cascade的默认值为save-update, merge。此参数的典型替代设置为all或更常见的是all, delete-orphanall符号是save-update, merge, refresh-expire, expunge, delete的同义词,与delete-orphan结合使用表示子对象应在所有情况下跟随其父对象,并且一旦不再与该父对象关联就应该被删除。

警告

all级联选项意味着 refresh-expire 级联设置,当使用异步 I/O(asyncio)扩展时可能不可取,因为它将比在显式 I/O 上下文中通常适当地更积极地使相关对象过期。有关更多背景信息,请参阅在使用 AsyncSession 时防止隐式 I/O 中的注释。

可以为relationship.cascade参数指定的可用值列表在以下各小节中进行描述。

save-update

save-update级联指示当通过Session.add()将对象放入Session中时,通过这个relationship()与之关联的所有对象也应该添加到同一个Session中。假设我们有一个对象user1,其中包含两个相关对象address1address2

代码语言:javascript复制
>>> user1 = User()
>>> address1, address2 = Address(), Address()
>>> user1.addresses = [address1, address2]

如果我们将user1添加到Session中,它也会隐式添加address1address2

代码语言:javascript复制
>>> sess = Session()
>>> sess.add(user1)
>>> address1 in sess
True

save-update级联也会影响已经存在于Session中的对象的属性操作。如果我们将第三个对象address3添加到user1.addresses集合中,它将成为该Session的状态的一部分:

代码语言:javascript复制
>>> address3 = Address()
>>> user1.addresses.append(address3)
>>> address3 in sess
True

当从集合中移除一个项目或将对象从标量属性中解除关联时,save-update级联可能会表现出令人惊讶的行为。在某些情况下,被孤立的对象仍然可能被拉入原父级的Session中;这是为了使刷新过程可以适当地处理相关对象。这种情况通常只会在一个对象从一个Session中移除并添加到另一个对象时出现:

代码语言:javascript复制
>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1 = user1.addresses[0]
>>> sess1.close()  # user1, address1 no longer associated with sess1
>>> user1.addresses.remove(address1)  # address1 no longer associated with user1
>>> sess2 = Session()
>>> sess2.add(user1)  # ... but it still gets added to the new session,
>>> address1 in sess2  # because it's still "pending" for flush
True

save-update级联默认启用,并且通常被视为理所当然;它通过允许单个调用Session.add()一次性在该Session中注册整个对象结构来简化代码。虽然它可以被禁用,但通常没有必要这样做。

双向关系中 save-update 级联的行为

在双向关系的上下文中,即使用relationship.back_populatesrelationship.backref参数创建相互引用的两个独立的relationship()对象时,save-update级联是单向的

当一个未关联Session的对象被赋给与关联Session相关的父对象的属性或集合时,它将自动添加到同一Session中。然而,反向操作不会产生此效果;当一个未关联Session的对象被赋给与关联Session相关的子对象时,不会自动将该父对象添加到Session中。这种行为的总体主题称为“级联反向引用”,并代表了从 SQLAlchemy 2.0 开始标准化的行为变更。

以示例说明,假设给定了一个Order对象的映射,它与一系列Item对象通过关系Order.itemsItem.order双向关联:

代码语言:javascript复制
mapper_registry.map_imperatively(
    Order,
    order_table,
    properties={"items": relationship(Item, back_populates="order")},
)

mapper_registry.map_imperatively(
    Item,
    item_table,
    properties={"order": relationship(Order, back_populates="items")},
)

如果一个Order已经与一个Session相关联,并且然后创建一个Item对象并将其附加到该OrderOrder.items集合中,Item将自动级联到相同的Session中:

代码语言:javascript复制
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True

>>> i1 = Item()
>>> o1.items.append(i1)
>>> o1 is i1.order
True
>>> i1 in session
True

在上述情况下,Order.itemsItem.order的双向性意味着附加到Order.items也会赋值给Item.order。同时,save-update级联允许将Item对象添加到与父Order已关联的相同Session中。

然而,如果上述操作以反向方向执行,即赋值Item.order而不是直接附加到Order.item,则级联操作不会自动进行,即使对象赋值Order.itemsItem.order与上一个示例中的状态相同:

代码语言:javascript复制
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True

>>> i1 = Item()
>>> i1.order = o1
>>> i1 in order.items
True
>>> i1 in session
False

在上述情况下,Item对象创建并设置完所有期望的状态后,应明确将其添加到Session中:

代码语言:javascript复制
>>> session.add(i1)

在较旧版本的 SQLAlchemy 中,保存-更新级联在所有情况下都会双向发生。然后,使用一个称为cascade_backrefs的选项使其成为可选项。最后,在 SQLAlchemy 1.4 中,旧行为被弃用,并且在 SQLAlchemy 2.0 中删除了cascade_backrefs选项。其理由是用户通常不会觉得将对象的属性分配给对象上的属性是直观的,如上面所示的i1.order = o1的分配,会改变对象i1的持久状态,使其现在在Session中处于挂起状态,并且在那些给定对象仍在构建并且尚未准备好被刷新的情况下,自动刷新会过早地刷新对象并导致错误。选择在单向和双向行为之间选择的选项也被删除,因为此选项创建了两种略有不同的工作方式,增加了 ORM 的整体学习曲线以及文档和用户支持负担。

另请参阅

在 2.0 中弃用以删除的 cascade_backrefs 行为 - 关于“级联反向引用”行为变更的背景 ## 删除

delete级联表示当“父”对象标记为删除时,其相关的“子”对象也应标记为删除。例如,如果我们有一个关系User.addresses配置了delete级联:

代码语言:javascript复制
class User(Base):
    # ...

    addresses = relationship("Address", cascade="all, delete")

如果使用上述映射,我们有一个User对象和两个相关的Address对象:

代码语言:javascript复制
>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1, address2 = user1.addresses

如果我们标记user1进行删除,在刷新操作进行后,address1address2也将被删除:

代码语言:javascript复制
>>> sess.delete(user1)
>>> sess.commit()
DELETE  FROM  address  WHERE  address.id  =  ?
((1,),  (2,))
DELETE  FROM  user  WHERE  user.id  =  ?
(1,)
COMMIT 

或者,如果我们的User.addresses关系没有delete级联,SQLAlchemy 的默认行为是通过将它们的外键引用设置为NULL来解除user1address1address2的关联。使用以下映射:

代码语言:javascript复制
class User(Base):
    # ...

    addresses = relationship("Address")

在删除父User对象时,address中的行不会被删除,而是被解除关联:

代码语言:javascript复制
>>> sess.delete(user1)
>>> sess.commit()
UPDATE  address  SET  user_id=?  WHERE  address.id  =  ?
(None,  1)
UPDATE  address  SET  user_id=?  WHERE  address.id  =  ?
(None,  2)
DELETE  FROM  user  WHERE  user.id  =  ?
(1,)
COMMIT 

在一对多关系中,delete级联通常与delete-orphan级联结合使用,如果“子”对象与父对象解除关联,则会为相关行发出 DELETE。deletedelete-orphan级联的组合涵盖了 SQLAlchemy 必须在将外键列设置为 NULL 与完全删除行之间做出决定的两种情况。

该功能默认完全独立于数据库配置的FOREIGN KEY约束,这些约束本身可能配置CASCADE行为。为了更有效地与此配置集成,应使用描述在使用 ORM 关系中的外键 ON DELETE 级联的附加指令。

警告

请注意,ORM 的“delete”和“delete-orphan”行为适用于使用Session.delete()方法在 unit of work 过程中标记单个 ORM 实例以进行删除。它适用于“批量”删除,这将使用delete()构造发出,如在 ORM UPDATE and DELETE with Custom WHERE Criteria 中所示。有关更多背景信息,请参阅 ORM 启用的更新和删除的重要说明和警告。

另请参阅

使用 ORM 关系的外键 ON DELETE 级联

使用删除级联处理多对多关系

delete-orphan

使用删除级联处理多对多关系

cascade="all, delete"选项在多对多关系中同样有效,该关系使用relationship.secondary指示一个关联表。当删除父对象,因此与其相关对象解除关联时,工作单元过程通常会从关联表中删除行,但保留相关对象。与cascade="all, delete"结合使用时,将为子行本身执行额外的DELETE语句。

以下示例将 Many To Many 的示例调整为说明关联的一侧设置为cascade="all, delete"

代码语言:javascript复制
association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", Integer, ForeignKey("left.id")),
    Column("right_id", Integer, ForeignKey("right.id")),
)

class Parent(Base):
    __tablename__ = "left"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=association_table,
        back_populates="parents",
        cascade="all, delete",
    )

class Child(Base):
    __tablename__ = "right"
    id = mapped_column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children",
    )

在上述情况下,当使用Session.delete()标记Parent对象进行删除时,刷新过程通常会从association表中删除相关行,但根据级联规则,它还将删除所有相关的Child行。

警告

如果上述cascade="all, delete"设置在两个关系上都配置了,则级联操作将继续通过所有ParentChild对象进行级联,加载遇到的每个childrenparents集合并删除所有连接的内容。通常不希望“delete”级联双向配置。

另请参阅

从多对多表中删除行

使用外键 ON DELETE 处理多对多关系 ### 使用 ORM 关系的外键 ON DELETE 级联处理

SQLAlchemy 的“delete”级联行为与数据库 FOREIGN KEY 约束的 ON DELETE 特性重叠。SQLAlchemy 允许使用 ForeignKeyForeignKeyConstraint 构造配置这些模式级 DDL 行为;如何在 Table 元数据与这些对象的使用一起配置,在 ON UPDATE and ON DELETE 中有描述。

为了将 ON DELETE 外键级联与 relationship() 结合使用,首先必须注意 relationship.cascade 设置仍然必须配置为匹配所需的delete或“set null”行为(使用 delete 级联或将其省略),以便无论是 ORM 还是数据库级约束将处理实际修改数据库中的数据的任务,ORM 仍将能够适当跟踪可能受到影响的本地存在对象的状态。

relationship() 上有一个额外的选项,指示 ORM 应该尝试自行运行与相关行的 DELETE/UPDATE 操作的程度,而不是依赖于期望数据库端的 FOREIGN KEY 约束级联处理任务;这是 relationship.passive_deletes 参数,它接受 False(默认值)、True"all" 选项。

最典型的例子是当删除父行时要删除子行,并且在相关的 FOREIGN KEY 约束上配置了 ON DELETE CASCADE

代码语言:javascript复制
class Parent(Base):
    __tablename__ = "parent"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        back_populates="parent",
        cascade="all, delete",
        passive_deletes=True,
    )

class Child(Base):
    __tablename__ = "child"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("parent.id", ondelete="CASCADE"))
    parent = relationship("Parent", back_populates="children")

当删除父行时,上述配置的行为如下:

  1. 应用程序调用 session.delete(my_parent),其中 my_parentParent 的一个实例。
  2. Session 下次将更改刷新到数据库时,my_parent.children 集合中的所有 当前加载的 项目都将由 ORM 删除,这意味着为每条记录发出一个 DELETE 语句。
  3. 如果my_parent.children集合是未加载的,则不会发出DELETE语句。如果在此relationship()上未设置relationship.passive_deletes标志,则将为未加载的Child对象发出SELECT语句。
  4. 然后会为my_parent行本身发出DELETE语句。
  5. 数据库级别的ON DELETE CASCADE设置确保将删除所有引用受影响的parent行的child中的行。
  6. my_parent引用的Parent实例,以及所有与该对象相关联且已加载(即执行了步骤 2)的Child实例,将从Session中解除关联。

注意

要使用“ON DELETE CASCADE”,底层数据库引擎必须支持FOREIGN KEY约束,并且它们必须被强制执行:

  • 在使用 MySQL 时,必须选择适当的存储引擎。有关详细信息,请参见 CREATE TABLE arguments including Storage Engines。
  • 在使用 SQLite 时,必须显式启用外键支持。有关详细信息,请参见 Foreign Key Support。### 使用外键 ON DELETE 处理多对多关系

正如在使用级联删除处理多对多关系中描述的那样,“删除”级联也适用于多对多关系。要利用ON DELETE CASCADE外键与多对多结合使用,需要在关联表上配置FOREIGN KEY指令。这些指令可以处理自动从关联表中删除,但不能自动删除相关对象本身。

在这种情况下,relationship.passive_deletes指令可以在删除操作期间节省一些额外的SELECT语句,但仍然有一些集合,ORM 将继续加载它们,以定位受影响的子对象并正确处理它们。

注意

对此的假设优化可以包括一次针对关联表的所有父关联行的单个DELETE语句,然后使用RETURNING来定位受影响的相关子行,但是这目前不是 ORM 工作单元实现的一部分。

在这个配置中,我们在关联表的两个外键约束上都配置了ON DELETE CASCADE。我们在父->子关系的一侧配置了cascade="all, delete",然后我们可以在双向关系的另一侧上配置passive_deletes=True,如下所示:

代码语言:javascript复制
association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", Integer, ForeignKey("left.id", ondelete="CASCADE")),
    Column("right_id", Integer, ForeignKey("right.id", ondelete="CASCADE")),
)

class Parent(Base):
    __tablename__ = "left"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=association_table,
        back_populates="parents",
        cascade="all, delete",
    )

class Child(Base):
    __tablename__ = "right"
    id = mapped_column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children",
        passive_deletes=True,
    )

使用上述配置,删除Parent对象的操作如下:

  1. 使用Session.delete()标记要删除的Parent对象。
  2. 当发生刷新时,如果未加载Parent.children集合,则 ORM 将首先发出 SELECT 语句,以加载与Parent.children对应的Child对象。
  3. 然后,将发出针对与该父行对应的association中的行的DELETE语句。
  4. 对于每个受此立即删除影响的Child对象,由于配置了passive_deletes=True,工作单元将不需要尝试为每个Child.parents集合发出 SELECT 语句,因为假定将删除association中的相应行。
  5. 对于从Parent.children加载的每个Child对象,都会发出DELETE语句。 ## delete-orphan

delete-orphan级联会为delete级联添加行为,这样当子对象与父对象取消关联时,子对象将被标记为删除,而不仅仅是在父对象被标记为删除时。当处理一个与父对象“拥有”关系的相关对象时,这是一种常见的特性,该关系具有 NOT NULL 外键,因此从父集合中删除项目会导致其被删除。

delete-orphan级联意味着每个子对象一次只能有一个父对象,并且在绝大多数情况下,它只配置在一对多关系上。对于设置在多对一或多对多关系上的非常罕见的情况,可以通过配置relationship.single_parent参数来强制“多”端一次只允许一个对象,该参数建立了 Python 端验证,确保对象一次只与一个父对象关联,但这大大限制了“多”关系的功能,通常不是所需的。

另请参阅

对于关系,delete-orphan 级联通常仅配置在一对多关系的“一”端,而不是多对一或多对多关系的“多”端。 - 关于涉及 delete-orphan 级联的常见错误场景的背景。 ## merge

merge级联表示应该从Session.merge()操作从调用Session.merge()的主体父对象向下传播到引用的对象。这个级联也是默认开启的。 ## refresh-expire

refresh-expire 是一个不常见的选项,表示Session.refresh()操作应该从父对象传播到引用的对象。当使用Session.refresh()时,引用的对象仅被过期,而不会实际刷新。## expunge

expunge 级联表示当使用Session.expunge()Session中移除父对象时,操作应该向下传播到引用的对象。## 删除说明 - 删除从集合和标量关系引用的对象

通常情况下,ORM 在刷新过程中不会修改集合或标量关系的内容。这意味着,如果你的类有一个指向对象集合的relationship(),或者一个指向单个对象的引用,比如一对多关系,那么当刷新过程发生时,这个属性的内容不会被修改。相反,预期的是Session最终会过期,要么通过Session.commit()的提交时过期行为,要么通过显式使用Session.expire()。在那时,与该Session关联的任何引用对象或集合将被清除,并且在下次访问时将重新加载自己。

关于此行为常见的混淆涉及 Session.delete() 方法的使用。当调用 Session.delete() 删除一个对象并且刷新了 Session 时,该行将从数据库中删除。通过外键引用目标行的行,假设它们使用两个映射对象类型之间的 relationship() 跟踪,还将看到它们的外键属性被更新为 null,或者如果设置了级联删除,则相关行也将被删除。然而,即使与被删除对象相关的行可能也被修改,在刷新本身的范围内,涉及操作的关系绑定集合或对象引用上不会发生任何更改。这意味着如果对象是相关集合的成员,则在 Python 端它仍然存在,直到该集合过期。同样,如果对象通过多对一或一对一从另一个对象引用,那个引用也将保留在该对象上,直到该对象也过期。

在下面的例子中,我们可以看到,即使将一个 Address 对象标记为删除,在刷新后,它仍然存在于与父 User 关联的集合中:

代码语言:javascript复制
>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True

当上述会话提交时,所有属性都将过期。下一次访问 user.addresses 将重新加载集合,显示所需的状态:

代码语言:javascript复制
>>> session.commit()
>>> address in user.addresses
False

拦截 Session.delete() 并自动调用其过期的方法有一种方法;请参阅 ExpireRelationshipOnFKChange 查看详情。然而,通常的做法是在集合内删除项目时直接放弃使用 Session.delete(),而是使用级联行为自动调用删除操作,因为将对象从父集合中删除的结果。delete-orphan 级联可以实现这一点,如下例所示:

代码语言:javascript复制
class User(Base):
    __tablename__ = "user"

    # ...

    addresses = relationship("Address", cascade="all, delete-orphan")

# ...

del user.addresses[1]
session.flush()

在上面的情况中,从 User.addresses 集合中移除 Address 对象后,delete-orphan 级联的效果与将其传递给 Session.delete() 相同,标记了 Address 对象以删除。

delete-orphan 级联也可以应用于多对一或一对一关系,这样当一个对象从其父对象中取消关联时,它也会自动标记为删除。在多对一或一对一关系上使用delete-orphan级联需要额外的标志relationship.single_parent,该标志会触发一个断言,即此相关对象不应同时与任何其他父对象共享:

代码语言:javascript复制
class User(Base):
    # ...

    preference = relationship(
        "Preference", cascade="all, delete-orphan", single_parent=True
    )

如果上面的情况下,一个假设的Preference对象从一个User中移除,它将在 flush 时被删除:

代码语言:javascript复制
some_user.preference = None
session.flush()  # will delete the Preference object

另请参阅

有关级联的详细信息,请参阅 Cascades。## save-update

save-update 级联表示当通过Session.add()将对象放入Session时,通过此relationship()与之关联的所有对象也应该被添加到同一个Session中。假设我们有一个对象user1,它有两个相关对象address1address2

代码语言:javascript复制
>>> user1 = User()
>>> address1, address2 = Address(), Address()
>>> user1.addresses = [address1, address2]

如果我们将user1添加到Session中,它也会隐式添加address1address2

代码语言:javascript复制
>>> sess = Session()
>>> sess.add(user1)
>>> address1 in sess
True

save-update 级联也会影响已经存在于Session中的对象的属性操作。如果我们向user1.addresses集合添加第三个对象address3,它将成为该Session的状态的一部分:

代码语言:javascript复制
>>> address3 = Address()
>>> user1.addresses.append(address3)
>>> address3 in sess
True

当从集合中删除项目或将对象与标量属性取消关联时,save-update 级联可能会表现出令人惊讶的行为。在某些情况下,被孤立的对象仍然可能被拉入原父对象的Session;这是为了 flush 进程能够适当处理该相关对象。这种情况通常只会在对象从一个Session中移除并添加到另一个Session时出现:

代码语言:javascript复制
>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1 = user1.addresses[0]
>>> sess1.close()  # user1, address1 no longer associated with sess1
>>> user1.addresses.remove(address1)  # address1 no longer associated with user1
>>> sess2 = Session()
>>> sess2.add(user1)  # ... but it still gets added to the new session,
>>> address1 in sess2  # because it's still "pending" for flush
True

save-update 级联默认启用,并且通常被视为理所当然;它通过允许对那个Session一次注册整个对象结构的单个调用来简化代码。虽然它可以被禁用,但通常没有必要这样做。

具有双向关系的 save-update 级联的行为

save-update级联在双向关系的情况下单向发生,即当使用relationship.back_populatesrelationship.backref参数创建两个相互引用的relationship()对象时。

当将一个未与Session关联的对象分配给与Session关联的父对象的属性或集合时,该对象将自动添加到同一个Session中。然而,反向操作不会产生这种效果;当分配一个未与Session关联的对象时,分配给一个与Session关联的子对象,不会自动将父对象添加到Session中。这种行为的总体主题被称为“级联反向引用”,代表了作为 SQLAlchemy 2.0 的标准化行为的变化。

为了说明,假设有一系列通过关系Order.itemsItem.orderItem对象双向关联的Order对象的映射:

代码语言:javascript复制
mapper_registry.map_imperatively(
    Order,
    order_table,
    properties={"items": relationship(Item, back_populates="order")},
)

mapper_registry.map_imperatively(
    Item,
    item_table,
    properties={"order": relationship(Order, back_populates="items")},
)

如果Order已经与一个Session关联,并且然后创建一个Item对象并附加到该OrderOrder.items集合中,Item将自动级联到相同的Session中:

代码语言:javascript复制
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True

>>> i1 = Item()
>>> o1.items.append(i1)
>>> o1 is i1.order
True
>>> i1 in session
True

在上面的例子中,Order.itemsItem.order的双向性意味着附加到Order.items也会赋值给Item.order。同时,save-update级联允许将Item对象添加到与父Order已关联的同一个Session中。

然而,如果上述操作在反向方向进行,即将Item.order赋值而不是直接附加到Order.item,则级联操作不会自动进行到Session中,即使对象赋值Order.itemsItem.order的状态与前面的示例相同:

代码语言:javascript复制
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True

>>> i1 = Item()
>>> i1.order = o1
>>> i1 in order.items
True
>>> i1 in session
False

在上述情况下,在创建Item对象并设置所有所需状态之后,应明确将其添加到Session中:

代码语言:javascript复制
>>> session.add(i1)

在旧版本的 SQLAlchemy 中,保存-更新级 learning method 会在所有情况下双向发生。然后,通过一个名为cascade_backrefs的选项将其变为可选。最后,在 SQLAlchemy 1.4 中,旧行为被弃用,并且在 SQLAlchemy 2.0 中删除了cascade_backrefs选项。其理由是,用户通常不会觉得在对象的属性上赋值(如上面所示的i1.order = o1的赋值)会改变该对象i1的持久化状态,使其现在处于Session中处于挂起状态,并且经常会出现自动刷新会过早刷新对象并导致错误的情况,在这些情况下,给定对象仍在构建中且尚未处于准备好刷新的状态状态。选择单向和双向行为之间的选项也被删除,因为此选项创建了两种略有不同的工作方式,增加了 ORM 的整体学习曲线以及文档和用户支持负担。

另请参阅

在 2.0 中弃用了 cascade_backrefs 行为 - 关于“级联 backrefs”行为变更的背景 ### 双向关系中保存-更新级联的行为

save-update级联在双向关系的上下文中单向发生,即在使用relationship.back_populatesrelationship.backref参数创建相互引用的两个单独的relationship()对象时。

一个未与Session相关联的对象,当分配给与Session相关联的父对象的属性或集合时,将自动添加到相同的Session中。但是,相反的操作不会产生这种效果;一个未与Session相关联的对象,其中一个与Session相关联的子对象被分配,将不会自动将该父对象添加到Session中。此行为的整体主题称为“级联反向引用”,并代表了作为 SQLAlchemy 2.0 的标准化行为的变化。

为了说明,假设有一系列通过关系Order.itemsItem.orderItem对象双向关联的Order对象的映射:

代码语言:javascript复制
mapper_registry.map_imperatively(
    Order,
    order_table,
    properties={"items": relationship(Item, back_populates="order")},
)

mapper_registry.map_imperatively(
    Item,
    item_table,
    properties={"order": relationship(Order, back_populates="items")},
)

如果Order已与Session相关联,并且然后创建Item对象并将其附加到该OrderOrder.items集合中,那么Item将自动级联到相同的Session中:

代码语言:javascript复制
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True

>>> i1 = Item()
>>> o1.items.append(i1)
>>> o1 is i1.order
True
>>> i1 in session
True

上述案例中,Order.itemsItem.order的双向性意味着附加到Order.items也会赋值给Item.order。同时,save-update级联允许将Item对象添加到与父Order已关联的相同Session中。

但是,如果上述操作是以相反方向执行的,即将Item.order赋值而不是直接附加到Order.item,则即使对象分配Order.itemsItem.order的状态与前面的示例相同,也不会自动进入到Session的级联操作中:

代码语言:javascript复制
>>> o1 = Order()
>>> session.add(o1)
>>> o1 in session
True

>>> i1 = Item()
>>> i1.order = o1
>>> i1 in order.items
True
>>> i1 in session
False

在上述情况下,在创建Item对象并设置所有所需状态之后,应明确将其添加到Session中:

代码语言:javascript复制
>>> session.add(i1)

在较旧版本的 SQLAlchemy 中,保存-更新级联在所有情况下都会双向发生。然后,使用称为cascade_backrefs的选项将其变为可选。最后,在 SQLAlchemy 1.4 中,旧行为被弃用,并且在 SQLAlchemy 2.0 中删除了cascade_backrefs选项。其理由是用户通常不会觉得将对象的属性分配给对象上的属性是直观的,如上面所示的i1.order = o1的赋值会改变对象i1的持久化状态,使其现在处于Session中处于挂起状态,并且在那些给定对象仍在构建并且尚未准备好被刷新的情况下,会经常出现自动刷新会过早刷新对象并导致错误的情况。选择单向和双向行为之间的选项也被删除,因为此选项创建了两种略有不同的工作方式,增加了 ORM 的整体学习曲线以及文档和用户支持负担。

另请参阅

2.0 中将删除的 cascade_backrefs 行为已弃用 - 关于“级联反向引用”行为变更的背景信息

删除

删除级联表示当“父”对象标记为删除时,其相关的“子”对象也应标记为删除。例如,如果我们有一个配置了删除级联的关系User.addresses

代码语言:javascript复制
class User(Base):
    # ...

    addresses = relationship("Address", cascade="all, delete")

如果使用上述映射,我们有一个User对象和两个相关的Address对象:

代码语言:javascript复制
>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1, address2 = user1.addresses

如果我们标记user1进行删除,在刷新操作进行后,address1address2也将被删除:

代码语言:javascript复制
>>> sess.delete(user1)
>>> sess.commit()
DELETE  FROM  address  WHERE  address.id  =  ?
((1,),  (2,))
DELETE  FROM  user  WHERE  user.id  =  ?
(1,)
COMMIT 

或者,如果我们的User.addresses关系没有删除级联,SQLAlchemy 的默认行为是通过将它们的外键引用设置为NULL来解除user1address1address2的关联。使用以下映射:

代码语言:javascript复制
class User(Base):
    # ...

    addresses = relationship("Address")

在删除父User对象时,address中的行不会被删除,而是被解除关联:

代码语言:javascript复制
>>> sess.delete(user1)
>>> sess.commit()
UPDATE  address  SET  user_id=?  WHERE  address.id  =  ?
(None,  1)
UPDATE  address  SET  user_id=?  WHERE  address.id  =  ?
(None,  2)
DELETE  FROM  user  WHERE  user.id  =  ?
(1,)
COMMIT 

删除 在一对多关系上的级联通常与删除孤儿级联结合使用,如果“子”对象与父对象解除关联,则会发出与相关行相关的 DELETE 操作。删除删除孤儿级联的组合涵盖了 SQLAlchemy 需要在将外键列设置为 NULL 与完全删除行之间做出决定的情况。

默认情况下,该功能完全独立于数据库配置的可能配置CASCADE行为的FOREIGN KEY约束。为了更有效地与此配置集成,应使用在使用 ORM 关系的外键 ON DELETE 级联中描述的附加指令。

警告

请注意,ORM 的“删除”和“删除孤立对象”行为仅适用于使用Session.delete()方法在工作单元过程中标记个别 ORM 实例进行删除。它不适用于“批量”删除,这将使用delete()构造来发出,如 ORM UPDATE and DELETE with Custom WHERE Criteria 中所示。有关更多背景信息,请参见 ORM-启用的更新和删除的重要说明和注意事项。

另见

使用 ORM 关系的外键 ON DELETE 级联

使用多对多关系的级联删除

delete-orphan

使用多对多关系的级联删除

cascade="all, delete"选项与多对多关系同样适用,这种关系使用relationship.secondary来指示一个关联表。当删除父对象时,因此取消与其相关的对象的关联时,工作单元过程通常会从关联表中删除行,但会保留相关的对象。当与cascade="all, delete"组合时,额外的DELETE语句将对子行本身进行操作。

下面的示例调整了多对多的例子,以说明关联的端上的cascade="all, delete"设置:

代码语言:javascript复制
association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", Integer, ForeignKey("left.id")),
    Column("right_id", Integer, ForeignKey("right.id")),
)

class Parent(Base):
    __tablename__ = "left"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=association_table,
        back_populates="parents",
        cascade="all, delete",
    )

class Child(Base):
    __tablename__ = "right"
    id = mapped_column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children",
    )

当使用Session.delete()标记要删除的Parent对象时,上述情况下,刷新过程通常会从association表中删除关联行,但根据级联规则,它还将删除所有相关的Child行。

警告

如果上述cascade="all, delete"设置在两个关系上都配置了,则级联操作将继续通过所有ParentChild对象,加载遇到的每个childrenparents集合,并删除所有连接的内容。通常不希望将“删除”级联配置为双向。

另见

从多对多表中删除行

使用外键 ON DELETE 与多对多关系 ### 使用 ORM 关系的外键 ON DELETE 级联

SQLAlchemy 的“delete”级联行为与数据库FOREIGN KEY约束的ON DELETE特性重叠。SQLAlchemy 允许使用ForeignKeyForeignKeyConstraint构造配置这些模式级 DDL 行为;与Table元数据一起使用这些对象的用法在 ON UPDATE and ON DELETE 中有描述。

为了在与relationship()一起使用ON DELETE外键级联时,首先需要注意的是relationship.cascade设置仍然必须配置为与所需的delete或“set null”行为匹配(使用delete级联或将其省略),以便 ORM 或数据库级约束将处理实际修改数据库中数据的任务时,ORM 仍然能够适当跟踪可能受影响的本地存在的对象的状态。

然后,在relationship()上有一个附加选项,指示 ORM 应该尝试自己运行 DELETE/UPDATE 操作相关行的程度,还是应该依赖于期望数据库端 FOREIGN KEY 约束级联处理任务;这是relationship.passive_deletes参数,它接受False(默认值),True"all"选项。

最典型的示例是,在删除父行时删除子行,并且在相关的FOREIGN KEY约束上配置了ON DELETE CASCADE

代码语言:javascript复制
class Parent(Base):
    __tablename__ = "parent"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        back_populates="parent",
        cascade="all, delete",
        passive_deletes=True,
    )

class Child(Base):
    __tablename__ = "child"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("parent.id", ondelete="CASCADE"))
    parent = relationship("Parent", back_populates="children")

当删除父行时,上述配置的行为如下:

  1. 应用程序调用session.delete(my_parent),其中my_parentParent类的一个实例。
  2. Session下次将更改刷新到数据库时,my_parent.children集合中的当前加载的所有项目都将被 ORM 删除,这意味着为每个记录发出一个DELETE语句。
  3. 如果 my_parent.children 集合未加载,则不会发出任何 DELETE 语句。如果在这个 relationship()设置 relationship.passive_deletes 标志,则会发出一个用于未加载的 Child 对象的 SELECT 语句。
  4. 然后为 my_parent 行本身发出一个 DELETE 语句。
  5. 数据库级别的 ON DELETE CASCADE 设置确保了所有引用受影响的 parent 行的 child 行也被删除。
  6. my_parent 引用的 Parent 实例,以及与此对象相关且已经加载(即发生了步骤 2)的所有 Child 实例,都会从 Session 中解除关联。

注意

要使用“ON DELETE CASCADE”,底层数据库引擎必须支持FOREIGN KEY约束,并且它们必须是强制执行的:

  • 当使用 MySQL 时,必须选择适当的存储引擎。详情请参阅包括存储引擎的 CREATE TABLE 参数。
  • 当使用 SQLite 时,必须显式启用外键支持。详情请参阅外键支持。### 使用外键 ON DELETE 处理多对多关系

如 使用级联删除处理多对多关系 中所述,“delete”级联也适用于多对多关系。要使用 ON DELETE CASCADE 外键与多对多一起使用,必须在关联表上配置 FOREIGN KEY 指令。这些指令可以处理自动从关联表中删除,但不能自动删除相关对象本身。

在这种情况下,relationship.passive_deletes 指令可以在删除操作期间为我们节省一些额外的 SELECT 语句,但仍然有一些集合 ORM 将继续加载,以便定位受影响的子对象并正确处理它们。

注意

对此的假设优化可能包括一条针对关联表的所有父关联行的单个 DELETE 语句,然后使用 RETURNING 定位受影响的相关子行,但是这目前不是 ORM 工作单元实现的一部分。

在此配置中,我们在关联表的两个外键约束上都配置了 ON DELETE CASCADE。我们在关系的父->子方向上配置了 cascade="all, delete",然后我们可以在双向关系的另一侧上配置 passive_deletes=True,如下所示:

代码语言:javascript复制
association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", Integer, ForeignKey("left.id", ondelete="CASCADE")),
    Column("right_id", Integer, ForeignKey("right.id", ondelete="CASCADE")),
)

class Parent(Base):
    __tablename__ = "left"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=association_table,
        back_populates="parents",
        cascade="all, delete",
    )

class Child(Base):
    __tablename__ = "right"
    id = mapped_column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children",
        passive_deletes=True,
    )

使用上述配置,删除 Parent 对象的过程如下:

  1. 使用 Session.delete() 标记要删除的 Parent 对象。
  2. 当刷新发生时,如果未加载 Parent.children 集合,则 ORM 将首先发出 SELECT 语句以加载与 Parent.children 对应的 Child 对象。
  3. 然后会为对应于该父行的 association 中的行发出 DELETE 语句。
  4. 对于由此立即删除受影响的每个 Child 对象,因为配置了 passive_deletes=True,工作单元不需要尝试为每个 Child.parents 集合发出 SELECT 语句,因为假设将删除 association 中对应的行。
  5. 然后对从 Parent.children 加载的每个 Child 对象发出 DELETE 语句。 ### 使用删除级联处理多对多关系

cascade="all, delete" 选项与多对多关系同样有效,即使用 relationship.secondary 指示关联表的关系。当删除父对象并因此取消关联其相关对象时,工作单元进程通常会删除关联表中的行,但保留相关对象。当与 cascade="all, delete" 结合使用时,将为子行本身执行额外的 DELETE 语句。

以下示例将多对多的示例调整为示例,以说明在关联的一侧上设置 cascade="all, delete"

代码语言:javascript复制
association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", Integer, ForeignKey("left.id")),
    Column("right_id", Integer, ForeignKey("right.id")),
)

class Parent(Base):
    __tablename__ = "left"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=association_table,
        back_populates="parents",
        cascade="all, delete",
    )

class Child(Base):
    __tablename__ = "right"
    id = mapped_column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children",
    )

上面,当使用 Session.delete() 标记要删除的 Parent 对象时,刷新过程将按照惯例从 association 表中删除相关行,但根据级联规则,它还将删除所有相关的 Child 行。

警告

如果上述 cascade="all, delete" 设置被配置在两个关系上,那么级联操作将继续通过所有 ParentChild 对象进行级联,加载遇到的每个 childrenparents 集合,并删除所有连接的内容。通常不希望双向配置“delete”级联。

另请参阅

从多对多表中删除行

使用外键 ON DELETE 处理多对多关系

使用 ORM 关系中的外键 ON DELETE 级联

SQLAlchemy 的“delete”级联的行为与数据库FOREIGN KEY约束的ON DELETE特性重叠。SQLAlchemy 允许使用 ForeignKeyForeignKeyConstraint 构造配置这些模式级别的 DDL 行为;在与 Table 元数据结合使用这些对象的用法在 ON UPDATE and ON DELETE 中有描述。

要在 relationship() 中使用ON DELETE外键级联,首先要注意的是 relationship.cascade 设置必须仍然配置为匹配所需的“删除”或“设置为 null”行为(使用delete级联或将其省略),以便 ORM 或数据库级别的约束将处理实际修改数据库中的数据的任务时,ORM 仍将能够适当地跟踪可能受到影响的本地存在的对象的状态。

然后在relationship() 上有一个额外的选项,指示 ORM 应该尝试在相关行上自行运行 DELETE/UPDATE 操作的程度,而不是依靠期望数据库端 FOREIGN KEY 约束级联处理该任务;这是 relationship.passive_deletes 参数,它接受选项 False(默认值)、True"all"

最典型的例子是,在删除父行时要删除子行,并且在相关的FOREIGN KEY约束上配置了ON DELETE CASCADE

代码语言:javascript复制
class Parent(Base):
    __tablename__ = "parent"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        back_populates="parent",
        cascade="all, delete",
        passive_deletes=True,
    )

class Child(Base):
    __tablename__ = "child"
    id = mapped_column(Integer, primary_key=True)
    parent_id = mapped_column(Integer, ForeignKey("parent.id", ondelete="CASCADE"))
    parent = relationship("Parent", back_populates="children")

当删除父行时,上述配置的行为如下:

  1. 应用程序调用session.delete(my_parent),其中my_parentParent的实例。
  2. Session 下次将更改刷新到数据库时,my_parent.children 集合中的所有当前加载的项目都将被 ORM 删除,这意味着为每个记录发出了一个DELETE语句。
  3. 如果my_parent.children集合未加载,则不会发出DELETE语句。 如果在此relationship()上未设置relationship.passive_deletes标志,那么将会发出一个针对未加载的Child对象的SELECT语句。
  4. 针对my_parent行本身发出了一个DELETE语句。
  5. 数据库级别的ON DELETE CASCADE设置确保了所有引用受影响的parent行的child中的行也被删除。
  6. my_parent引用的Parent实例以及所有与此对象相关联且已加载Child实例(即发生了步骤 2)都将从Session中解除关联。

注意

要使用“ON DELETE CASCADE”,底层数据库引擎必须支持FOREIGN KEY约束,并且它们必须是强制性的:

  • 使用 MySQL 时,必须选择适当的存储引擎。 有关详细信息,请参阅 CREATE TABLE arguments including Storage Engines。
  • 使用 SQLite 时,必须显式启用外键支持。 有关详细信息,请参阅 Foreign Key Support。
在多对多关系中使用外键 ON DELETE

如使用 delete cascade 与多对多关系所述,“delete”级联也适用于多对多关系。 要利用ON DELETE CASCADE外键与多对多关系,必须在关联表上配置FOREIGN KEY指令。 这些指令可以处理自动从关联表中删除,但无法适应相关对象本身的自动删除。

在这种情况下,relationship.passive_deletes指令可以在删除操作期间为我们节省一些额外的SELECT语句,但仍然有一些集合是 ORM 将继续加载的,以定位受影响的子对象并正确处理它们。

注意

对此的假设优化可以包括一次针对关联表的所有父关联行的单个DELETE语句,然后使用RETURNING来定位受影响的相关子行,但这目前不是 ORM 工作单元实现的一部分。

在此配置中,我们在关联表的两个外键约束上都配置了ON DELETE CASCADE。我们在关系的父->子方向上配置了cascade="all, delete",然后我们可以在双向关系的另一侧上配置passive_deletes=True,如下所示:

代码语言:javascript复制
association_table = Table(
    "association",
    Base.metadata,
    Column("left_id", Integer, ForeignKey("left.id", ondelete="CASCADE")),
    Column("right_id", Integer, ForeignKey("right.id", ondelete="CASCADE")),
)

class Parent(Base):
    __tablename__ = "left"
    id = mapped_column(Integer, primary_key=True)
    children = relationship(
        "Child",
        secondary=association_table,
        back_populates="parents",
        cascade="all, delete",
    )

class Child(Base):
    __tablename__ = "right"
    id = mapped_column(Integer, primary_key=True)
    parents = relationship(
        "Parent",
        secondary=association_table,
        back_populates="children",
        passive_deletes=True,
    )

使用上述配置,删除 Parent 对象的过程如下:

  1. 使用 Session.delete() 标记要删除的 Parent 对象。
  2. 当发生刷新时,如果未加载 Parent.children 集合,则 ORM 首先会发出 SELECT 语句,以加载与 Parent.children 对应的 Child 对象。
  3. 然后会为与该父行对应的 association 中的行发出 DELETE 语句。
  4. 对于受此即时删除影响的每个 Child 对象,因为配置了 passive_deletes=True,工作单元不需要尝试为每个 Child.parents 集合发出 SELECT 语句,因为假设将删除 association 中的相应行。
  5. 然后会为从 Parent.children 中加载的每个 Child 对象发出 DELETE 语句。

删除孤立

delete-orphan 级联为 delete 级联增加了行为,使得当子对象与父对象取消关联时,子对象将被标记为删除,而不仅仅是当父对象被标记为删除时。当处理由其父对象“拥有”的相关对象时,这是一个常见功能,具有非空的外键,以便从父集合中移除项目会导致其删除。

delete-orphan 级联意味着每个子对象一次只能有一个父对象,并且在绝大多数情况下仅配置在一对多关系上。在很少见的情况下,在多对一或多对多关系上设置它,“多”方可以通过配置 relationship.single_parent 参数,强制允许一次只有一个对象与父对象关联,从而在 Python 端建立验证,确保对象一次只与一个父对象关联,但这严重限制了“多”关系的功能,通常不是所期望的。

另请参阅

对于关系,delete-orphan 级联通常仅配置在一对多关系的“一”方,并且不配置在多对一或多对多关系的“多”方。 - 关于涉及 delete-orphan 级联的常见错误场景的背景信息。

合并

merge 级联表示 Session.merge() 操作应从 Session.merge() 调用的主体父对象传播到引用对象。此级联默认也是打开的。

刷新-过期

refresh-expire是一个不常见的选项,表示Session.expire()操作应该从父对象传播到引用的对象。当使用Session.refresh()时,引用的对象只是过期了,而不是实际刷新了。

清除

清除级联指的是当父对象从Session中使用Session.expunge()移除时,该操作应传播到引用的对象。

删除注意事项 - 删除集合和标量关系中引用的对象

通常,ORM 在刷新过程中永远不会修改集合或标量关系的内容。这意味着,如果你的类有一个指向对象集合的relationship(),或者一个指向单个对象的引用,比如多对一,当刷新过程发生时,这个属性的内容不会被修改。相反,预计Session最终会过期,通过Session.commit()的提交时过期行为或通过Session.expire()的显式使用。在那时,与该Session相关联的任何引用对象或集合都将被清除,并在下次访问时重新加载自身。

关于这种行为产生的常见困惑涉及到Session.delete()方法的使用。当在一个对象上调用Session.delete()并且Session被刷新时,该行将从数据库中删除。通过外键引用目标行的行,假设它们是使用两个映射对象类型之间的relationship()进行跟踪的,也会看到它们的外键属性被更新为 null,或者如果设置了删除级联,相关行也将被删除。然而,即使与已删除对象相关的行可能也被修改,在刷新范围内操作的对象上的关系绑定集合或对象引用不会发生任何更改。这意味着如果对象是相关集合的成员,它将仍然存在于 Python 端,直到该集合过期为止。同样,如果对象通过另一个对象的多对一或一对一引用,则该引用也将保留在该对象上,直到该对象也过期为止。

下面,我们说明了在将Address对象标记为删除后,即使在刷新后,它仍然存在于与父User关联的集合中:

代码语言:javascript复制
>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True

当以上会话提交时,所有属性都会过期。对user.addresses的下一次访问将重新加载集合,显示所需的状态:

代码语言:javascript复制
>>> session.commit()
>>> address in user.addresses
False

有一个拦截Session.delete()并自动调用此过期的配方;参见ExpireRelationshipOnFKChange。然而,删除集合中的项目的通常做法是直接放弃使用Session.delete(),而是使用级联行为自动调用删除作为从父集合中删除对象的结果。delete-orphan级联实现了这一点,如下例所示:

代码语言:javascript复制
class User(Base):
    __tablename__ = "user"

    # ...

    addresses = relationship("Address", cascade="all, delete-orphan")

# ...

del user.addresses[1]
session.flush()

在上面的例子中,当从User.addresses集合中移除Address对象时,delete-orphan级联的效果与将其传递给Session.delete()相同,都将Address对象标记为删除。

delete-orphan级联也可以应用于多对一或一对一关系,这样当一个对象与其父对象解除关联时,它也会被自动标记为删除。在多对一或一对一关系上使用delete-orphan级联需要一个额外的标志relationship.single_parent,它调用一个断言,指出这个相关对象不会同时与任何其他父对象共享:

代码语言:javascript复制
class User(Base):
    # ...

    preference = relationship(
        "Preference", cascade="all, delete-orphan", single_parent=True
    )

上面,如果一个假设的Preference对象从一个User中移除,它将在刷新时被删除:

代码语言:javascript复制
some_user.preference = None
session.flush()  # will delete the Preference object

另请参阅

级联以获取级联的详细信息。

事务和连接管理

原文:docs.sqlalchemy.org/en/20/orm/session_transaction.html

事务管理

在 1.4 版本中更改:会话事务管理已经修订为更清晰、更易于使用。特别是,它现在具有“自动开始”操作,这意味着可以控制事务开始的时间点,而不使用传统的“自动提交”模式。

Session一次跟踪单个“虚拟”事务的状态,使用一个称为SessionTransaction的对象。然后,此对象利用绑定到Session对象的基础Engine或引擎,根据需要使用Connection对象开始真实的连接级事务。

此“虚拟”事务在需要时会自动创建,或者可以使用Session.begin()方法启动。尽可能地支持 Python 上下文管理器的使用,既在创建Session对象的层面,也在维护SessionTransaction的范围方面。

假设我们从Session开始:

代码语言:javascript复制
from sqlalchemy.orm import Session

session = Session(engine)

我们现在可以使用上下文管理器在一个确定的事务中运行操作:

代码语言:javascript复制
with session.begin():
    session.add(some_object())
    session.add(some_other_object())
# commits transaction at the end, or rolls back if there
# was an exception raised

在上下文结束时,假设没有引发任何异常,则任何待处理的对象都将被刷新到数据库,并且数据库事务将被提交。如果在上述块内引发了异常,则事务将被回滚。在这两种情况下,上述Session在退出块后都已准备好用于后续的事务。

Session.begin()方法是可选的,Session也可以使用按需自动开始事务的“随时提交”方法;这些只需要提交或回滚:

代码语言:javascript复制
session = Session(engine)

session.add(some_object())
session.add(some_other_object())

session.commit()  # commits

# will automatically begin again
result = session.execute(text("< some select statement >"))
session.add_all([more_objects, ...])
session.commit()  # commits

session.add(still_another_object)
session.flush()  # flush still_another_object
session.rollback()  # rolls back still_another_object

Session 本身具有一个 Session.close() 方法。如果 Session 是在尚未提交或回滚的事务内开始的,该方法将取消(即回滚)该事务,并清除 Session 对象状态中包含的所有对象。如果使用 Session 的方式不能保证调用 Session.commit()Session.rollback()(例如,不在上下文管理器或类似结构中),则可以使用 close 方法来确保释放所有资源:

代码语言:javascript复制
# expunges all objects, releases all transactions unconditionally
# (with rollback), releases all database connections back to their
# engines
session.close()

最后,会话的构建/关闭过程本身也可以通过上下文管理器运行。这是确保 Session 对象使用范围在一个固定块内的最佳方式。首先通过 Session 构造函数进行说明:

代码语言:javascript复制
with Session(engine) as session:
    session.add(some_object())
    session.add(some_other_object())

    session.commit()  # commits

    session.add(still_another_object)
    session.flush()  # flush still_another_object

    session.commit()  # commits

    result = session.execute(text("<some SELECT statement>"))

# remaining transactional state from the .execute() call is
# discarded

同样,sessionmaker 可以以相同的方式使用:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session() as session:
    with session.begin():
        session.add(some_object)
    # commits

# closes the Session

sessionmaker 本身包含一个 sessionmaker.begin() 方法,允许同时进行两个操作:

代码语言:javascript复制
with Session.begin() as session:
    session.add(some_object)
使用 SAVEPOINT

如果底层引擎支持,可以使用 Session.begin_nested() 方法来划定 SAVEPOINT 事务:

代码语言:javascript复制
Session = sessionmaker()

with Session.begin() as session:
    session.add(u1)
    session.add(u2)

    nested = session.begin_nested()  # establish a savepoint
    session.add(u3)
    nested.rollback()  # rolls back u3, keeps u1 and u2

# commits u1 and u2

每次调用 Session.begin_nested(),都会在当前数据库事务的范围内向数据库发出一个新的“BEGIN SAVEPOINT”命令(如果尚未开始,则开始一个),并返回一个类型为 SessionTransaction 的对象,代表这个 SAVEPOINT 的句柄。当调用该对象的 .commit() 方法时,会向数据库发出“RELEASE SAVEPOINT”,如果调用 .rollback() 方法,则会发出“ROLLBACK TO SAVEPOINT”。封闭的数据库事务仍在进行中。

Session.begin_nested() 通常用作上下文管理器,可以捕获特定的每个实例错误,并在该事务状态的部分发出回滚,而不会回滚整个事务,如下例所示:

代码语言:javascript复制
for record in records:
    try:
        with session.begin_nested():
            session.merge(record)
    except:
        print("Skipped record %s" % record)
session.commit()

当由 Session.begin_nested() 产生的上下文管理器完成时,“提交” savepoint,其中包括刷新所有挂起状态的常规行为。当出现错误时,保存点会被回滚,并且更改的对象的 Session 本地状态会过期。

这种模式非常适用于诸如使用 PostgreSQL 并捕获 IntegrityError 来检测重复行的情况;通常情况下,当出现此类错误时,PostgreSQL 会中止整个事务,但是使用 SAVEPOINT 时,外部事务会得以保留。在下面的示例中,将一系列数据持久化到数据库中,并且偶尔会跳过“重复的主键”记录,而无需回滚整个操作:

代码语言:javascript复制
from sqlalchemy import exc

with session.begin():
    for record in records:
        try:
            with session.begin_nested():
                obj = SomeRecord(id=record["identifier"], name=record["name"])
                session.add(obj)
        except exc.IntegrityError:
            print(f"Skipped record {record} - row already exists")

当调用 Session.begin_nested() 时,Session 首先会将当前所有挂起的状态刷新到数据库;这种情况会无条件发生,不管 Session.autoflush 参数的值是什么,该参数通常用于禁用自动刷新。这种行为的原因是,当此嵌套事务发生回滚时,Session 可以使在 SAVEPOINT 范围内创建的任何内存状态过期,同时确保在刷新这些过期对象时,SAVEPOINT 开始之前的对象图状态将可用于重新从数据库加载。

在 SQLAlchemy 的现代版本中,当由 Session.begin_nested() 启动的 SAVEPOINT 被回滚时,自 SAVEPOINT 创建以来已修改的内存对象状态会过期,但自 SAVEPOINT 开始后未修改的其他对象状态将保持不变。这样,后续操作可以继续使用其它未受影响的数据,而无需从数据库刷新。

另请参见

Connection.begin_nested() - 核心 SAVEPOINT API ### 会话级别与引擎级别的事务控制

在核心中的Connection和 ORM 中的_session.Session具有等效的事务语义,无论是在sessionmakerEngine的级别,还是在SessionConnection的级别。以下部分根据以下方案详细说明这些情况:

代码语言:javascript复制
ORM                                           Core
-----------------------------------------     -----------------------------------
sessionmaker                                  Engine
Session                                       Connection
sessionmaker.begin()                          Engine.begin()
some_session.commit()                         some_connection.commit()
with some_sessionmaker() as session:          with some_engine.connect() as conn:
with some_sessionmaker.begin() as session:    with some_engine.begin() as conn:
with some_session.begin_nested() as sp:       with some_connection.begin_nested() as sp:
边提交边进行

SessionConnection都具有Connection.commit()Connection.rollback()方法。使用 SQLAlchemy 2.0 风格的操作,这些方法在所有情况下都会影响最外层的事务。对于Session,假定Session.autobegin保持默认值True

Engine:

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.connect() as conn:
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
    conn.commit()

Session:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session() as session:
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
    session.commit()
一次开始

sessionmakerEngine都具有Engine.begin()方法,该方法将获取一个新对象来执行 SQL 语句(分别是SessionConnection),然后返回一个上下文管理器,用于维护该对象的开始/提交/回滚上下文。

引擎:

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.begin() as conn:
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
# commits and closes automatically

会话:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session.begin() as session:
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
# commits and closes automatically
嵌套事务

使用 SAVEPOINT 通过 Session.begin_nested()Connection.begin_nested() 方法时,必须使用返回的事务对象来提交或回滚 SAVEPOINT。 调用 Session.commit()Connection.commit() 方法将始终提交最外层事务;这是 SQLAlchemy 2.0 特定的行为,与 1.x 系列相反。

引擎:

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.begin() as conn:
    savepoint = conn.begin_nested()
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
    savepoint.commit()  # or rollback

# commits automatically

会话:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session.begin() as session:
    savepoint = session.begin_nested()
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
    savepoint.commit()  # or rollback
# commits automatically
```### 显式开始

`Session` 具有“自动开始”行为,这意味着一旦操作开始进行,它就会确保存在一个 `SessionTransaction` 来跟踪正在进行的操作。 当调用 `Session.commit()` 时,此事务将完成。

通常希望,特别是在框架集成中,控制“开始”操作发生的时间点。 为此,`Session` 使用“自动开始”策略,使得可以直接调用 `Session.begin()` 方法,以便为尚未启动事务的 `Session` 调用:

```py
Session = sessionmaker(bind=engine)
session = Session()
session.begin()
try:
    item1 = session.get(Item, 1)
    item2 = session.get(Item, 2)
    item1.foo = "bar"
    item2.bar = "foo"
    session.commit()
except:
    session.rollback()
    raise

上述模式更常用地使用上下文管理器调用:

代码语言:javascript复制
Session = sessionmaker(bind=engine)
session = Session()
with session.begin():
    item1 = session.get(Item, 1)
    item2 = session.get(Item, 2)
    item1.foo = "bar"
    item2.bar = "foo"

Session.begin() 方法和会话的“自动开始”过程使用相同的步骤序列开始事务。 这包括在发生时调用 SessionEvents.after_transaction_create() 事件;此挂钩被框架用于将其自己的事务处理过程与 ORM Session 集成。

对于支持两阶段操作的后端(目前支持 MySQL 和 PostgreSQL),会话可以被指示使用两阶段提交语义。这将协调跨数据库的事务提交,以便在所有数据库中要么提交事务,要么回滚事务。您还可以Session.prepare() 会话以与 SQLAlchemy 不管理的事务进行交互。要使用两阶段事务,请在会话上设置标志 twophase=True

代码语言:javascript复制
engine1 = create_engine("postgresql psycopg2://db1")
engine2 = create_engine("postgresql psycopg2://db2")

Session = sessionmaker(twophase=True)

# bind User operations to engine 1, Account operations to engine 2
Session.configure(binds={User: engine1, Account: engine2})

session = Session()

# .... work with accounts and users

# commit.  session will issue a flush to all DBs, and a prepare step to all DBs,
# before committing both transactions
session.commit()
```### 设置事务隔离级别 / DBAPI 自动提交

大多数 DBAPI 支持可配置的事务隔离级别的概念。传统上有四个级别:“READ UNCOMMITTED”,“READ COMMITTED”,“REPEATABLE READ” 和 “SERIALIZABLE”。这些通常应用于 DBAPI 连接在开始新事务之前,注意大多数 DBAPI 在首次发出 SQL 语句时会隐式开始此事务。

支持隔离级别的 DBAPI 通常也支持真正的 “自动提交” 概念,这意味着 DBAPI 连接本身将被置于非事务自动提交模式。这通常意味着数据库自动发出 “BEGIN” 的典型 DBAPI 行为不再发生,但它也可能包括其他指令。在使用此模式时,**DBAPI 在任何情况下都不使用事务**。SQLAlchemy 方法如 `.begin()`、`.commit()` 和 `.rollback()` 将静默传递。

SQLAlchemy 的方言支持在每个 `Engine` 或每个 `Connection` 基础上设置可设置的隔离模式,使用 `create_engine()` 层级以及 `Connection.execution_options()` 层级的标志。

当使用 ORM `Session` 时,它充当引擎和连接的 *facade*,但不直接暴露事务隔离。因此,为了影响事务隔离级别,我们需要根据情况对 `Engine` 或 `Connection` 采取行动。

另请参阅

设置事务隔离级别包括 DBAPI 自动提交 - 请确保查看 SQLAlchemy `Connection` 对象的隔离级别工作方式。

#### 为 Sessionmaker / Engine 设置隔离

要为特定的隔离级别全局设置`Session`或`sessionmaker`,第一种技术是可以针对所有情况构建一个具有特定隔离级别的`Engine`,然后将其用作`Session`和/或`sessionmaker`的连接源:

```py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

eng = create_engine(
    "postgresql psycopg2://scott:tiger@localhost/test",
    isolation_level="REPEATABLE READ",
)

Session = sessionmaker(eng)

另一个选项,如果同时存在具有不同隔离级别的两个引擎,是使用Engine.execution_options()方法,该方法将生成一个原始Engine的浅拷贝,该浅拷贝与父引擎共享相同的连接池。当操作将被分成“事务”和“自动提交”操作时,这通常是更可取的:

代码语言:javascript复制
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

eng = create_engine("postgresql psycopg2://scott:tiger@localhost/test")

autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")

transactional_session = sessionmaker(eng)
autocommit_session = sessionmaker(autocommit_engine)

在上述示例中,“eng”和"autocommit_engine"共享相同的方言和连接池。然而,当从autocommit_engine获取连接时,将设置“AUTOCOMMIT”模式。这两个sessionmaker对象“transactional_session”和“autocommit_session”在与数据库连接工作时继承这些特性。

autocommit_session仍然具有事务语义,包括Session.commit()Session.rollback()仍然认为自己在“提交”和“回滚”对象,然而事务将会默默地不存在。因此,通常情况下,尽管不是严格要求,使用 AUTOCOMMIT 隔离的会话应该以只读方式使用,即:

代码语言:javascript复制
with autocommit_session() as session:
    some_objects = session.execute(text("<statement>"))
    some_other_objects = session.execute(text("<statement>"))

# closes connection
设置单个会话的隔离级别

当我们创建一个新的Session,可以直接使用构造函数,也可以在调用由sessionmaker生成的可调用对象时,直接传递bind参数,覆盖预先存在的绑定。例如,我们可以从默认的sessionmaker创建我们的Session并传递一个设置为自动提交的引擎:

代码语言:javascript复制
plain_engine = create_engine("postgresql psycopg2://scott:tiger@localhost/test")

autocommit_engine = plain_engine.execution_options(isolation_level="AUTOCOMMIT")

# will normally use plain_engine
Session = sessionmaker(plain_engine)

# make a specific Session that will use the "autocommit" engine
with Session(bind=autocommit_engine) as session:
    # work with session
    ...

对于Sessionsessionmaker配置了多个binds的情况,我们可以重新完整指定binds参数,或者如果我们只想替换特定的 binds,则可以使用Session.bind_mapper()Session.bind_table()方法:

代码语言:javascript复制
with Session() as session:
    session.bind_mapper(User, autocommit_engine)
为个别事务设置隔离级别

关于隔离级别的一个关键警告是,在已经开始事务的Connection上不能安全地修改设置。数据库不能在进行中的事务中更改隔离级别,而一些 DBAPIs 和 SQLAlchemy 方言在这方面的行为不一致。

因此,最好使用一个提前绑定到具有所需隔离级别的引擎的Session。然而,通过在事务开始时使用Session.connection()方法可以影响每个连接的隔离级别:

代码语言:javascript复制
from sqlalchemy.orm import Session

# assume session just constructed
sess = Session(bind=engine)

# call connection() with options before any other operations proceed.
# this will procure a new connection from the bound engine and begin a real
# database transaction.
sess.connection(execution_options={"isolation_level": "SERIALIZABLE"})

# ... work with session in SERIALIZABLE isolation level...

# commit transaction.  the connection is released
# and reverted to its previous isolation level.
sess.commit()

# subsequent to commit() above, a new transaction may be begun if desired,
# which will proceed with the previous default isolation level unless
# it is set again.

在上面的例子中,我们首先使用构造函数或sessionmaker生成一个Session。然后,我们通过调用Session.connection()显式设置数据库级事务的开始,该方法提供了在数据库级事务开始之前将传递给连接的执行选项。事务使用此选定的隔离级别进行。当事务完成时,隔离级别会重置为其默认值,然后将连接返回到连接池。

Session.begin()方法也可以用于开始Session级事务;在调用该方法后,可以使用Session.connection()来设置每个连接的事务隔离级别:

代码语言:javascript复制
sess = Session(bind=engine)

with sess.begin():
    # call connection() with options before any other operations proceed.
    # this will procure a new connection from the bound engine and begin a
    # real database transaction.
    sess.connection(execution_options={"isolation_level": "SERIALIZABLE"})

    # ... work with session in SERIALIZABLE isolation level...

# outside the block, the transaction has been committed.  the connection is
# released and reverted to its previous isolation level.
使用事件跟踪事务状态

请参阅事务事件部分,了解有关会话事务状态更改的可用事件挂钩的概述。 ## 加入会话到外部事务(例如用于测试套件)

如果正在使用处于事务状态的 Connection(即已建立 Transaction),则可以通过将 Session 绑定到该 Connection 来使 Session 参与该事务。通常的理由是允许 ORM 代码自由地与 Session 一起工作,包括调用 Session.commit(),之后整个数据库交互都被回滚。

在 2.0 版本中更改:2.0 版本再次对“加入到外部事务”配方进行了改进;不再需要事件处理程序来“重置”嵌套事务。

该配方的工作方式是在事务内部建立一个 Connection,可选地建立一个 SAVEPOINT,然后将其传递给 Session 作为“bind”;Session.join_transaction_mode 参数传递了设置为 "create_savepoint",表示应该创建新的 SAVEPOINT 来实现 Session 的 BEGIN/COMMIT/ROLLBACK,这将使外部事务处于传递时的相同状态。

当测试拆解时,外部事务会被回滚,以便将测试中的任何数据更改还原:

代码语言:javascript复制
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from unittest import TestCase

# global application scope.  create Session class, engine
Session = sessionmaker()

engine = create_engine("postgresql psycopg2://...")

class SomeTest(TestCase):
    def setUp(self):
        # connect to the database
        self.connection = engine.connect()

        # begin a non-ORM transaction
        self.trans = self.connection.begin()

        # bind an individual Session to the connection, selecting
        # "create_savepoint" join_transaction_mode
        self.session = Session(
            bind=self.connection, join_transaction_mode="create_savepoint"
        )

    def test_something(self):
        # use the session in tests.

        self.session.add(Foo())
        self.session.commit()

    def test_something_with_rollbacks(self):
        self.session.add(Bar())
        self.session.flush()
        self.session.rollback()

        self.session.add(Foo())
        self.session.commit()

    def tearDown(self):
        self.session.close()

        # rollback - everything that happened with the
        # Session above (including calls to commit())
        # is rolled back.
        self.trans.rollback()

        # return connection to the Engine
        self.connection.close()

上述配方是 SQLAlchemy 自己的 CI 的一部分,以确保它仍然按预期工作。 ## 管理事务

在 1.4 版本中更改:会话事务管理已经进行了修改,使其更清晰、更易于使用。特别是,现在它具有“自动开始”操作,这意味着可以控制事务开始的时间点,而无需使用传统的“自动提交”模式。

Session 跟踪一次性的“虚拟”事务的状态,使用一个叫做 SessionTransaction 的对象。然后,该对象利用底层的 Engine 或引擎来启动使用 Connection 对象所需的真实连接级事务。

当需要时,这个“虚拟”事务会自动创建,或者可以使用 Session.begin() 方法手动开始。尽可能大程度地支持 Python 上下文管理器的使用,不仅在创建 Session 对象的级别上,还在维护 SessionTransaction 的范围上。

下面,假设我们从一个 Session 开始:

代码语言:javascript复制
from sqlalchemy.orm import Session

session = Session(engine)

现在我们可以使用上下文管理器在标记的事务中运行操作:

代码语言:javascript复制
with session.begin():
    session.add(some_object())
    session.add(some_other_object())
# commits transaction at the end, or rolls back if there
# was an exception raised

在上述上下文结束时,假设没有引发任何异常,任何待处理的对象都将被刷新到数据库,并且数据库事务将被提交。如果在上述块中引发了异常,则事务将被回滚。在这两种情况下,上述 Session 在退出块后都可以在后续事务中使用。

Session.begin() 方法是可选的,Session 也可以使用逐步提交的方法,在需要时自动开始事务;只需提交或回滚:

代码语言:javascript复制
session = Session(engine)

session.add(some_object())
session.add(some_other_object())

session.commit()  # commits

# will automatically begin again
result = session.execute(text("< some select statement >"))
session.add_all([more_objects, ...])
session.commit()  # commits

session.add(still_another_object)
session.flush()  # flush still_another_object
session.rollback()  # rolls back still_another_object

Session本身具有Session.close()方法。如果Session是在尚未提交或回滚的事务内开始的,则此方法将取消(即回滚)该事务,并且还将清除Session对象状态中包含的所有对象。如果Session的使用方式不保证调用Session.commit()Session.rollback()(例如不在上下文管理器或类似位置),则可以使用close方法确保释放所有资源:

代码语言:javascript复制
# expunges all objects, releases all transactions unconditionally
# (with rollback), releases all database connections back to their
# engines
session.close()

最后,会话构建/关闭过程本身也可以通过上下文管理器运行。这是确保Session对象使用范围在固定块内的最佳方法。首先通过Session构造函数进行说明:

代码语言:javascript复制
with Session(engine) as session:
    session.add(some_object())
    session.add(some_other_object())

    session.commit()  # commits

    session.add(still_another_object)
    session.flush()  # flush still_another_object

    session.commit()  # commits

    result = session.execute(text("<some SELECT statement>"))

# remaining transactional state from the .execute() call is
# discarded

同样,sessionmaker也可以以相同的方式使用:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session() as session:
    with session.begin():
        session.add(some_object)
    # commits

# closes the Session

sessionmaker本身包括一个sessionmaker.begin()方法,允许同时执行这两个操作:

代码语言:javascript复制
with Session.begin() as session:
    session.add(some_object)
使用 SAVEPOINT

如果底层引擎支持 SAVEPOINT 事务,则可以使用Session.begin_nested()方法来区分 SAVEPOINT 事务:

代码语言:javascript复制
Session = sessionmaker()

with Session.begin() as session:
    session.add(u1)
    session.add(u2)

    nested = session.begin_nested()  # establish a savepoint
    session.add(u3)
    nested.rollback()  # rolls back u3, keeps u1 and u2

# commits u1 and u2

每次调用Session.begin_nested()时,都会在当前数据库事务的范围内(如果尚未开始,则开始一个新的事务)向数据库发送新的“BEGIN SAVEPOINT”命令,并返回一个类型为SessionTransaction的对象,该对象表示对此 SAVEPOINT 的句柄。当调用该对象的.commit()方法时,将向数据库发出“RELEASE SAVEPOINT”命令;如果调用.rollback()方法,则发出“ROLLBACK TO SAVEPOINT”命令。封闭的数据库事务保持进行中。

Session.begin_nested()通常用作上下文管理器,其中可以捕获特定的每个实例错误,与事务状态的部分回滚一起使用,而不是回滚整个事务,如下例所示:

代码语言:javascript复制
for record in records:
    try:
        with session.begin_nested():
            session.merge(record)
    except:
        print("Skipped record %s" % record)
session.commit()

当由Session.begin_nested()生成的上下文管理器完成时,它“提交”了保存点,其中包括刷新所有待定状态的通常行为。当发生错误时,保存点被回滚,并且被更改的对象的Session本地状态会被过期。

这种模式非常适合诸如使用 PostgreSQL 并捕获IntegrityError以检测重复行的情况;当出现此类错误时,PostgreSQL 通常会中止整个事务,但是在使用 SAVEPOINT 时,外部事务会被维持。在下面的示例中,一组数据被持久化到数据库中,偶尔会跳过“重复的主键”记录,而不会回滚整个操作:

代码语言:javascript复制
from sqlalchemy import exc

with session.begin():
    for record in records:
        try:
            with session.begin_nested():
                obj = SomeRecord(id=record["identifier"], name=record["name"])
                session.add(obj)
        except exc.IntegrityError:
            print(f"Skipped record {record} - row already exists")

当调用Session.begin_nested()时,Session首先将当前所有待定状态刷新到数据库;这是无条件发生的,不管Session.autoflush参数的值如何,该参数通常用于禁用自动刷新。这种行为的理由是,当在这个嵌套事务上发生回滚时,Session可以使在 SAVEPOINT 范围内创建的任何内存状态过期,同时确保当这些过期对象被刷新时,SAVEPOINT 开始之前的对象图状态可重新从数据库加载。

在现代版本的 SQLAlchemy 中,当由Session.begin_nested()发起的 SAVEPOINT 被回滚时,自从创建 SAVEPOINT 以来被修改的内存对象状态会被过期,然而自 SAVEPOINT 开始以来未被改变的其他对象状态会被保留。这样,后续操作可以继续使用未受影响的数据,而无需从数据库中刷新。

另请参阅

Connection.begin_nested() - 核心 SAVEPOINT API ### 会话级别 vs. 引擎级别的事务控制

在核心中的连接和 ORM 中的_session.Session具有等效的事务语义,都在sessionmaker引擎的级别,以及会话连接的级别。以下各节根据以下方案详细说明了这些情况:

代码语言:javascript复制
ORM                                           Core
-----------------------------------------     -----------------------------------
sessionmaker                                  Engine
Session                                       Connection
sessionmaker.begin()                          Engine.begin()
some_session.commit()                         some_connection.commit()
with some_sessionmaker() as session:          with some_engine.connect() as conn:
with some_sessionmaker.begin() as session:    with some_engine.begin() as conn:
with some_session.begin_nested() as sp:       with some_connection.begin_nested() as sp:
边做边提交

会话连接都具有Connection.commit()Connection.rollback()方法。使用 SQLAlchemy 2.0 风格的操作,这些方法在所有情况下都会影响最外层的事务。对于会话,假定Session.autobegin保持默认值True

引擎

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.connect() as conn:
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
    conn.commit()

会话

代码语言:javascript复制
Session = sessionmaker(engine)

with Session() as session:
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
    session.commit()
单次开始

sessionmaker引擎都具有Engine.begin()方法,该方法将获取一个用于执行 SQL 语句的新对象(分别是会话连接),然后返回一个上下文管理器,该管理器将为该对象维护一个开始/提交/回滚的上下文。

引擎:

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.begin() as conn:
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
# commits and closes automatically

会话:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session.begin() as session:
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
# commits and closes automatically
嵌套事务

当使用 SAVEPOINT 通过 Session.begin_nested()Connection.begin_nested() 方法时,返回的事务对象必须用于提交或回滚 SAVEPOINT。调用 Session.commit()Connection.commit() 方法将始终提交最外层事务;这是 SQLAlchemy 2.0 特定于行为的,与 1.x 系列相反。

引擎:

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.begin() as conn:
    savepoint = conn.begin_nested()
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
    savepoint.commit()  # or rollback

# commits automatically

会话:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session.begin() as session:
    savepoint = session.begin_nested()
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
    savepoint.commit()  # or rollback
# commits automatically
```### 显式开始

`Session` 具有“自动开始”行为,这意味着一旦操作开始进行,它就会确保存在一个用于跟踪正在进行的操作的 `SessionTransaction`。当调用 `Session.commit()` 时,该事务将被完成。

通常希望特别是在框架集成中,控制“开始”操作发生的时机。为此,`Session` 使用“自动开始”策略,使得可以直接调用 `Session.begin()` 方法来为尚未启动事务的 `Session` 开始事务:

```py
Session = sessionmaker(bind=engine)
session = Session()
session.begin()
try:
    item1 = session.get(Item, 1)
    item2 = session.get(Item, 2)
    item1.foo = "bar"
    item2.bar = "foo"
    session.commit()
except:
    session.rollback()
    raise

上述模式更惯用地使用上下文管理器调用:

代码语言:javascript复制
Session = sessionmaker(bind=engine)
session = Session()
with session.begin():
    item1 = session.get(Item, 1)
    item2 = session.get(Item, 2)
    item1.foo = "bar"
    item2.bar = "foo"

Session.begin() 方法和会话的“自动开始”过程使用相同的步骤序列开始事务。这包括当 SessionEvents.after_transaction_create() 事件发生时调用;此钩子被框架用于将其自己的事务处理过程与 ORM Session 集成。 ### 启用两阶段提交

对于支持两阶段操作的后端(当前为 MySQL 和 PostgreSQL),可以指示会话使用两阶段提交语义。这将协调跨数据库的事务提交,以便在所有数据库中提交或回滚事务。您还可以Session.prepare() 会话以与 SQLAlchemy 未管理的事务进行交互。要使用两阶段事务,请在会话上设置标志 twophase=True

代码语言:javascript复制
engine1 = create_engine("postgresql psycopg2://db1")
engine2 = create_engine("postgresql psycopg2://db2")

Session = sessionmaker(twophase=True)

# bind User operations to engine 1, Account operations to engine 2
Session.configure(binds={User: engine1, Account: engine2})

session = Session()

# .... work with accounts and users

# commit.  session will issue a flush to all DBs, and a prepare step to all DBs,
# before committing both transactions
session.commit()
```### 设置事务隔离级别 / DBAPI AUTOCOMMIT

大多数 DBAPI 支持可配置的事务隔离级别概念。传统上有四个级别:“READ UNCOMMITTED”、“READ COMMITTED”、“REPEATABLE READ”和“SERIALIZABLE”。这些通常在 DBAPI 连接开始新事务之前应用,需要注意的是,大多数 DBAPI 在首次发出 SQL 语句时会隐式开始此事务。

支持隔离级别的 DBAPI 通常还支持真实的“自动提交”概念,这意味着 DBAPI 连接本身将被放置在非事务自动提交模式中。这通常意味着自动向数据库发出“BEGIN”的典型 DBAPI 行为不再发生,但也可能包括其他指令。在使用此模式时,**DBAPI 在任何情况下都不使用事务**。SQLAlchemy 方法如`.begin()`、`.commit()` 和 `.rollback()` 会静默通过。

SQLAlchemy 的方言支持在每个`Engine` 或每个`Connection` 的基础上设置隔离模式,使用`create_engine()` 层次上以及 `Connection.execution_options()` 层次上的标志。

当使用 ORM `Session` 时,它充当引擎和连接的*外观*,但不直接暴露事务隔离。因此,为了影响事务隔离级别,我们需要在适当时对`Engine` 或 `Connection` 进行操作。

另请参阅

设置事务隔离级别,包括 DBAPI 自动提交 - 一定要查看 SQLAlchemy `Connection` 对象级别的隔离级别是如何工作的。

#### 为 Sessionmaker / Engine 设置隔离级别

要为 `Session` 或 `sessionmaker` 设置特定的隔离级别,全局首选技巧是可以始终根据特定的隔离级别构建一个 `Engine`,然后将其用作 `Session` 和/或 `sessionmaker` 的连接源:

```py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

eng = create_engine(
    "postgresql psycopg2://scott:tiger@localhost/test",
    isolation_level="REPEATABLE READ",
)

Session = sessionmaker(eng)

另一个选项,如果同时存在具有不同隔离级别的两个引擎,是使用 Engine.execution_options() 方法,该方法将产生原始 Engine 的浅拷贝,该拷贝与父引擎共享相同的连接池。当操作将被分为“事务性”和“自动提交”操作时,通常最好使用此方法:

代码语言:javascript复制
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

eng = create_engine("postgresql psycopg2://scott:tiger@localhost/test")

autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")

transactional_session = sessionmaker(eng)
autocommit_session = sessionmaker(autocommit_engine)

在上述情况中,“eng” 和 "autocommit_engine" 共享相同的方言和连接池。但是,当从 autocommit_engine 获取连接时,将设置“AUTOCOMMIT”模式。当这两个 sessionmaker 对象“transactional_session” 和 “autocommit_session" 与数据库连接一起工作时,它们会继承这些特性。

autocommit_session” 仍然具有事务语义,包括 Session.commit()Session.rollback() 仍然将其自己视为“提交”和“回滚”对象,但是事务将会默默消失。因此,通常情况下,虽然不是严格要求,但 AUTOCOMMIT 隔离级别的会话应该以只读方式使用,也就是:

代码语言:javascript复制
with autocommit_session() as session:
    some_objects = session.execute(text("<statement>"))
    some_other_objects = session.execute(text("<statement>"))

# closes connection
设置单独会话的隔离级别

当我们创建一个新的 Session,无论是直接使用构造函数还是调用由 sessionmaker 生成的可调用函数时,我们都可以直接传递 bind 参数,覆盖预先存在的绑定。例如,我们可以从默认的 sessionmaker 创建我们的 Session,并传递一个设置为自动提交的引擎:

代码语言:javascript复制
plain_engine = create_engine("postgresql psycopg2://scott:tiger@localhost/test")

autocommit_engine = plain_engine.execution_options(isolation_level="AUTOCOMMIT")

# will normally use plain_engine
Session = sessionmaker(plain_engine)

# make a specific Session that will use the "autocommit" engine
with Session(bind=autocommit_engine) as session:
    # work with session
    ...

对于配置了多个“绑定”(Sessionsessionmaker)的情况,我们可以重新完全指定binds参数,或者如果我们只想替换特定的绑定,则可以使用Session.bind_mapper()Session.bind_table()方法:

代码语言:javascript复制
with Session() as session:
    session.bind_mapper(User, autocommit_engine)
为单个事务设置隔离级别

关于隔离级别的一个关键注意事项是,不能在已经开始事务的Connection上安全地修改设置。数据库不能更改正在进行的事务的隔离级别,并且一些 DBAPIs 和 SQLAlchemy 方言在这个领域的行为不一致。

因此,最好使用一个与所需隔离级别的引擎直接绑定的Session。然而,可以通过在事务开始时使用Session.connection()方法来影响每个连接的隔离级别:

代码语言:javascript复制
from sqlalchemy.orm import Session

# assume session just constructed
sess = Session(bind=engine)

# call connection() with options before any other operations proceed.
# this will procure a new connection from the bound engine and begin a real
# database transaction.
sess.connection(execution_options={"isolation_level": "SERIALIZABLE"})

# ... work with session in SERIALIZABLE isolation level...

# commit transaction.  the connection is released
# and reverted to its previous isolation level.
sess.commit()

# subsequent to commit() above, a new transaction may be begun if desired,
# which will proceed with the previous default isolation level unless
# it is set again.

在上面的例子中,我们首先使用构造函数或sessionmaker生成一个Session。然后,我们通过调用Session.connection()显式设置数据库级别事务的开始,该方法提供了将传递给连接的执行选项,在开始数据库级别事务之前进行设置。事务使用所选的隔离级别进行。事务完成后,将在将连接返回到连接池之前将连接上的隔离级别重置为其默认值。

Session.begin()方法也可以用于开始Session级别的事务;在调用该方法后,可以使用Session.connection()设置每个连接的事务隔离级别:

代码语言:javascript复制
sess = Session(bind=engine)

with sess.begin():
    # call connection() with options before any other operations proceed.
    # this will procure a new connection from the bound engine and begin a
    # real database transaction.
    sess.connection(execution_options={"isolation_level": "SERIALIZABLE"})

    # ... work with session in SERIALIZABLE isolation level...

# outside the block, the transaction has been committed.  the connection is
# released and reverted to its previous isolation level.
使用事件跟踪事务状态

请参阅事务事件部分,了解会话事务状态更改的可用事件挂钩的概述。

使用保存点

如果底层引擎支持 SAVEPOINT 事务,则可以使用Session.begin_nested()方法进行分割:

代码语言:javascript复制
Session = sessionmaker()

with Session.begin() as session:
    session.add(u1)
    session.add(u2)

    nested = session.begin_nested()  # establish a savepoint
    session.add(u3)
    nested.rollback()  # rolls back u3, keeps u1 and u2

# commits u1 and u2

每次调用Session.begin_nested()时,都会在当前数据库事务的范围内(如果尚未开始,则开始一个)向数据库发出新的“BEGIN SAVEPOINT”命令,并返回一个SessionTransaction类型的对象,该对象表示对此保存点的句柄。当调用此对象的.commit()方法时,将向数据库发出“RELEASE SAVEPOINT”,如果调用.rollback()方法,则会发出“ROLLBACK TO SAVEPOINT”。外层数据库事务仍然在进行中。

Session.begin_nested()通常用作上下文管理器,其中可以捕获特定的每个实例错误,在此事务状态的一部分上发出回滚,而无需回滚整个事务,就像下面的示例中一样:

代码语言:javascript复制
for record in records:
    try:
        with session.begin_nested():
            session.merge(record)
    except:
        print("Skipped record %s" % record)
session.commit()

当由Session.begin_nested()生成的上下文管理器完成时,它会“提交”保存点,其中包括刷新所有待处理状态的通常行为。当出现错误时,保存点会被回滚,并且对已更改的对象的Session的状态将被过期。

此模式非常适合于使用 PostgreSQL 并捕获IntegrityError以检测重复行的情况;当引发此类错误时,PostgreSQL 通常会中止整个事务,但是当使用 SAVEPOINT 时,外部事务会得以保留。在下面的示例中,将一系列数据持久化到数据库中,偶尔会跳过“重复主键”记录,而不会回滚整个操作:

代码语言:javascript复制
from sqlalchemy import exc

with session.begin():
    for record in records:
        try:
            with session.begin_nested():
                obj = SomeRecord(id=record["identifier"], name=record["name"])
                session.add(obj)
        except exc.IntegrityError:
            print(f"Skipped record {record} - row already exists")

当调用Session.begin_nested()时,Session首先会将当前所有待定状态刷新到数据库;无论Session.autoflush参数的值是什么,这都是无条件的,通常可以用来禁用自动刷新。这种行为的原因是当此嵌套事务上发生回滚时,Session可以使在保存点范围内创建的任何内存状态过期,同时确保在刷新这些过期对象时,保存点开始前的对象图状态将可用于重新从数据库加载。

在现代版本的 SQLAlchemy 中,当由Session.begin_nested()初始化的保存点被回滚时,自从保存点创建以来被修改的内存对象状态将会被过期,但是其他自保存点开始时未改变的对象状态将会被保留。这样做是为了让后续操作可以继续使用那些未受影响的数据,而无需从数据库中刷新。

另请参阅

Connection.begin_nested() - 核心保存点 API

会话级与引擎级事务控制

Core 中的Connection和 ORM 中的_session.Session都具有等效的事务语义,无论是在sessionmakerEngine之间,还是在SessionConnection之间。以下各节详细说明了这些情景,基于以下方案:

代码语言:javascript复制
ORM                                           Core
-----------------------------------------     -----------------------------------
sessionmaker                                  Engine
Session                                       Connection
sessionmaker.begin()                          Engine.begin()
some_session.commit()                         some_connection.commit()
with some_sessionmaker() as session:          with some_engine.connect() as conn:
with some_sessionmaker.begin() as session:    with some_engine.begin() as conn:
with some_session.begin_nested() as sp:       with some_connection.begin_nested() as sp:
边做边提交

SessionConnection 均提供了 Connection.commit()Connection.rollback() 方法。使用 SQLAlchemy 2.0 风格的操作时,这些方法在所有情况下都会影响最外层的事务。对于 Session,假定 Session.autobegin 保持默认值 True

Engine:

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.connect() as conn:
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
    conn.commit()

Session:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session() as session:
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
    session.commit()
只初始化一次

sessionmakerEngine 均提供了 Engine.begin() 方法,该方法将获取一个新对象来执行 SQL 语句(分别是 SessionConnection),然后返回一个上下文管理器,用于维护该对象的开始/提交/回滚上下文。

引擎:

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.begin() as conn:
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
# commits and closes automatically

会话:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session.begin() as session:
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
# commits and closes automatically
嵌套事务

当使用 SAVEPOINT 通过 Session.begin_nested()Connection.begin_nested() 方法时,必须使用返回的事务对象来提交或回滚 SAVEPOINT。调用 Session.commit()Connection.commit() 方法将始终提交最外层的事务;这是 SQLAlchemy 2.0 特有的行为,与 1.x 系列相反。

引擎:

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.begin() as conn:
    savepoint = conn.begin_nested()
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
    savepoint.commit()  # or rollback

# commits automatically

会话:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session.begin() as session:
    savepoint = session.begin_nested()
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
    savepoint.commit()  # or rollback
# commits automatically
边提交边执行

SessionConnection均提供Connection.commit()Connection.rollback()方法。使用 SQLAlchemy 2.0 风格的操作,这些方法在所有情况下都会影响到最外层的事务。对于Session,假定Session.autobegin保持其默认值为True

Engine

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.connect() as conn:
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
    conn.commit()

Session

代码语言:javascript复制
Session = sessionmaker(engine)

with Session() as session:
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
    session.commit()
开始一次

sessionmakerEngine均提供Engine.begin()方法,该方法将获取一个新对象以执行 SQL 语句(分别是SessionConnection),然后返回一个上下文管理器,用于维护该对象的开始/提交/回滚上下文。

引擎:

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.begin() as conn:
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
# commits and closes automatically

会话:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session.begin() as session:
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
# commits and closes automatically
嵌套事务

使用通过Session.begin_nested()Connection.begin_nested()方法创建的 SAVEPOINT 时,必须使用返回的事务对象提交或回滚 SAVEPOINT。调用Session.commit()Connection.commit()方法将始终提交最外层的事务;这是 SQLAlchemy 2.0 特定行为,与 1.x 系列相反。

引擎:

代码语言:javascript复制
engine = create_engine("postgresql psycopg2://user:pass@host/dbname")

with engine.begin() as conn:
    savepoint = conn.begin_nested()
    conn.execute(
        some_table.insert(),
        [
            {"data": "some data one"},
            {"data": "some data two"},
            {"data": "some data three"},
        ],
    )
    savepoint.commit()  # or rollback

# commits automatically

会话:

代码语言:javascript复制
Session = sessionmaker(engine)

with Session.begin() as session:
    savepoint = session.begin_nested()
    session.add_all(
        [
            SomeClass(data="some data one"),
            SomeClass(data="some data two"),
            SomeClass(data="some data three"),
        ]
    )
    savepoint.commit()  # or rollback
# commits automatically
显式开始

Session 具有“自动开始”行为,这意味着一旦开始执行操作,它就会确保存在一个 SessionTransaction 来跟踪正在进行的操作。当调用 Session.commit() 时,此事务将被完成。

通常情况下,特别是在框架集成中,控制“开始”操作发生的时间点是可取的。为此,Session使用“自动开始”策略,以便可以直接调用 Session.begin() 方法来启动一个尚未开始事务的 Session

代码语言:javascript复制
Session = sessionmaker(bind=engine)
session = Session()
session.begin()
try:
    item1 = session.get(Item, 1)
    item2 = session.get(Item, 2)
    item1.foo = "bar"
    item2.bar = "foo"
    session.commit()
except:
    session.rollback()
    raise

上述模式通常使用上下文管理器更具惯用性:

代码语言:javascript复制
Session = sessionmaker(bind=engine)
session = Session()
with session.begin():
    item1 = session.get(Item, 1)
    item2 = session.get(Item, 2)
    item1.foo = "bar"
    item2.bar = "foo"

Session.begin() 方法和会话的“自动开始”过程使用相同的步骤序列来开始事务。这包括当它发生时调用 SessionEvents.after_transaction_create() 事件;此钩子由框架使用,以便将其自己的事务处理集成到 ORM Session 的事务处理中。

启用两阶段提交

对于支持两阶段操作的后端(目前是 MySQL 和 PostgreSQL),可以指示会话使用两阶段提交语义。这将协调跨数据库的事务提交,以便在所有数据库中要么提交事务,要么回滚事务。您还可以为与 SQLAlchemy 未管理的事务交互的会话 Session.prepare() 。要使用两阶段事务,请在会话上设置标志 twophase=True

代码语言:javascript复制
engine1 = create_engine("postgresql psycopg2://db1")
engine2 = create_engine("postgresql psycopg2://db2")

Session = sessionmaker(twophase=True)

# bind User operations to engine 1, Account operations to engine 2
Session.configure(binds={User: engine1, Account: engine2})

session = Session()

# .... work with accounts and users

# commit.  session will issue a flush to all DBs, and a prepare step to all DBs,
# before committing both transactions
session.commit()
设置事务隔离级别 / DBAPI 自动提交

大多数 DBAPI 支持可配置的事务隔离级别的概念。传统上,这些级别是“READ UNCOMMITTED”、“READ COMMITTED”、“REPEATABLE READ”和“SERIALIZABLE”。这些通常在 DBAPI 连接开始新事务之前应用,注意大多数 DBAPI 在首次发出 SQL 语句时会隐式地开始此事务。

支持隔离级别的 DBAPI 通常也支持真正的“自动提交”概念,这意味着 DBAPI 连接本身将被放置到非事务自动提交模式中。这通常意味着数据库自动不再发出“BEGIN”,但也可能包括其他指令。使用此模式时,DBAPI 在任何情况下都不使用事务。SQLAlchemy 方法如.begin().commit().rollback()会悄无声息地传递。

SQLAlchemy 的方言支持在每个Engine或每个Connection 上设置可设置的隔离模式,使用的标志既可以在create_engine()级别,也可以在Connection.execution_options()级别。

当使用 ORM Session 时,它充当引擎和连接的门面,但不直接暴露事务隔离。因此,为了影响事务隔离级别,我们需要在适当的时候对EngineConnection进行操作。

也请参阅

设置事务隔离级别,包括 DBAPI 自动提交 - 一定要查看 SQLAlchemy Connection 对象级别上隔离级别的工作方式。

设置会话/引擎范围的隔离级别

要全局设置一个具有特定隔离级别的Sessionsessionmaker,第一种技术是可以在所有情况下构造一个具有特定隔离级别的Engine,然后将其用作Session和/或sessionmaker的连接来源:

代码语言:javascript复制
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

eng = create_engine(
    "postgresql psycopg2://scott:tiger@localhost/test",
    isolation_level="REPEATABLE READ",
)

Session = sessionmaker(eng)

另一个选项,如果同时存在两个具有不同隔离级别的引擎,可以使用Engine.execution_options()方法,该方法将生成原始Engine的浅拷贝,该浅拷贝与父引擎共享相同的连接池。当操作将被分为“事务”和“自动提交”操作时,通常更可取:

代码语言:javascript复制
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

eng = create_engine("postgresql psycopg2://scott:tiger@localhost/test")

autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")

transactional_session = sessionmaker(eng)
autocommit_session = sessionmaker(autocommit_engine)

在上面的例子中,“eng”和“autocommit_engine”共享相同的方言和连接池。然而,当从autocommit_engine获取连接时,将设置“AUTOCOMMIT”模式。然后,这两个sessionmaker对象“transactional_session”和“autocommit_session”在与数据库连接一起工作时继承这些特性。

autocommit_session保持事务语义,包括Session.commit()Session.rollback()仍然认为自己是“提交”和“回滚”对象,但事务将会静默不存在。因此,通常情况下,尽管不是严格要求,但一个具有 AUTOCOMMIT 隔离级别的 Session 应该以只读方式使用,即:

代码语言:javascript复制
with autocommit_session() as session:
    some_objects = session.execute(text("<statement>"))
    some_other_objects = session.execute(text("<statement>"))

# closes connection
为单个 Session 设置隔离级别

当我们创建一个新的Session时,可以直接传递bind参数,覆盖预先存在的绑定,无论是直接使用构造函数还是调用由sessionmaker产生的可调用对象。例如,我们可以从默认的sessionmaker创建我们的Session并传递一个设置为自动提交的引擎:

代码语言:javascript复制
plain_engine = create_engine("postgresql psycopg2://scott:tiger@localhost/test")

autocommit_engine = plain_engine.execution_options(isolation_level="AUTOCOMMIT")

# will normally use plain_engine
Session = sessionmaker(plain_engine)

# make a specific Session that will use the "autocommit" engine
with Session(bind=autocommit_engine) as session:
    # work with session
    ...

对于配置有多个bindsSessionsessionmaker的情况,我们可以重新指定完整的binds参数,或者如果我们只想替换特定的绑定,我们可以使用Session.bind_mapper()Session.bind_table()方法:

代码语言:javascript复制
with Session() as session:
    session.bind_mapper(User, autocommit_engine)
为单个事务设置隔离级别

关于隔离级别的一个关键注意事项是,不能在已经启动事务的 Connection 上安全地修改设置。数据库无法更改正在进行的事务的隔离级别,并且一些 DBAPI 和 SQLAlchemy 方言在这个领域的行为不一致。

因此最好使用一个最初绑定到具有所需隔离级别的引擎的 Session。但是,通过在事务开始时使用 Session.connection() 方法,可以影响每个连接的隔离级别:

代码语言:javascript复制
from sqlalchemy.orm import Session

# assume session just constructed
sess = Session(bind=engine)

# call connection() with options before any other operations proceed.
# this will procure a new connection from the bound engine and begin a real
# database transaction.
sess.connection(execution_options={"isolation_level": "SERIALIZABLE"})

# ... work with session in SERIALIZABLE isolation level...

# commit transaction.  the connection is released
# and reverted to its previous isolation level.
sess.commit()

# subsequent to commit() above, a new transaction may be begun if desired,
# which will proceed with the previous default isolation level unless
# it is set again.

在上面的示例中,我们首先使用构造函数或 sessionmaker 生成一个 Session。然后,通过调用 Session.connection() 明确设置数据库级事务的开始,该方法提供了将传递给连接的执行选项,在开始数据库级事务之前。事务使用此选择的隔离级别进行。当事务完成时,连接上的隔离级别将重置为默认值,然后将连接返回到连接池。

Session.begin() 方法也可用于开始 Session 级事务;在此调用之后调用 Session.connection() 可以用于设置每个连接的事务隔离级别:

代码语言:javascript复制
sess = Session(bind=engine)

with sess.begin():
    # call connection() with options before any other operations proceed.
    # this will procure a new connection from the bound engine and begin a
    # real database transaction.
    sess.connection(execution_options={"isolation_level": "SERIALIZABLE"})

    # ... work with session in SERIALIZABLE isolation level...

# outside the block, the transaction has been committed.  the connection is
# released and reverted to its previous isolation level.
为 Sessionmaker / Engine 设置隔离级别

要为 Sessionsessionmaker 全局设置特定的隔离级别,第一种技术是可以在所有情况下构建一个针对特定隔离级别的 Engine,然后将其用作 Session 和/或 sessionmaker 的连接来源:

代码语言:javascript复制
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

eng = create_engine(
    "postgresql psycopg2://scott:tiger@localhost/test",
    isolation_level="REPEATABLE READ",
)

Session = sessionmaker(eng)

另一个选项,如果同时有两个具有不同隔离级别的引擎,则可以使用Engine.execution_options()方法,它将生成原始Engine的浅拷贝,与父引擎共享相同的连接池。当操作将分成“事务”和“自动提交”操作时,这通常是首选:

代码语言:javascript复制
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

eng = create_engine("postgresql psycopg2://scott:tiger@localhost/test")

autocommit_engine = eng.execution_options(isolation_level="AUTOCOMMIT")

transactional_session = sessionmaker(eng)
autocommit_session = sessionmaker(autocommit_engine)

在上述示例中,“eng”和“autocommit_engine”都共享相同的方言和连接池。然而,当从autocommit_engine获取连接时,将设置“AUTOCOMMIT”模式。然后,当这两个sessionmaker对象“transactional_session”和“autocommit_session”与数据库连接一起工作时,它们继承了这些特征。

autocommit_session仍然具有事务语义,包括当它们从autocommit_engine获取时,Session.commit()Session.rollback()仍然认为自己是“提交”和“回滚”对象,但事务将默默地不存在。因此,通常,虽然不是严格要求,但具有 AUTOCOMMIT 隔离的会话应该以只读方式使用,即:

代码语言:javascript复制
with autocommit_session() as session:
    some_objects = session.execute(text("<statement>"))
    some_other_objects = session.execute(text("<statement>"))

# closes connection
为单个会话设置隔离

当我们创建一个新的Session时,可以直接传递bind参数,覆盖预先存在的绑定。例如,我们可以从默认的sessionmaker创建我们的Session,并传递设置为自动提交的引擎:

代码语言:javascript复制
plain_engine = create_engine("postgresql psycopg2://scott:tiger@localhost/test")

autocommit_engine = plain_engine.execution_options(isolation_level="AUTOCOMMIT")

# will normally use plain_engine
Session = sessionmaker(plain_engine)

# make a specific Session that will use the "autocommit" engine
with Session(bind=autocommit_engine) as session:
    # work with session
    ...

对于配置了多个bindsSessionsessionmaker的情况,我们可以重新完全指定binds参数,或者如果我们只想替换特定的绑定,我们可以使用Session.bind_mapper()Session.bind_table()方法:

代码语言:javascript复制
with Session() as session:
    session.bind_mapper(User, autocommit_engine)
为单个事务设置隔离

关于隔离级别的一个关键警告是,在已经开始事务的 Connection 上无法安全地修改设置。数据库无法更改正在进行的事务的隔离级别,并且一些 DBAPI 和 SQLAlchemy 方言在这个领域的行为不一致。

因此,最好使用一个明确绑定到具有所需隔离级别的引擎的 Session。但是,可以通过在事务开始时使用 Session.connection() 方法来影响每个连接的隔离级别:

代码语言:javascript复制
from sqlalchemy.orm import Session

# assume session just constructed
sess = Session(bind=engine)

# call connection() with options before any other operations proceed.
# this will procure a new connection from the bound engine and begin a real
# database transaction.
sess.connection(execution_options={"isolation_level": "SERIALIZABLE"})

# ... work with session in SERIALIZABLE isolation level...

# commit transaction.  the connection is released
# and reverted to its previous isolation level.
sess.commit()

# subsequent to commit() above, a new transaction may be begun if desired,
# which will proceed with the previous default isolation level unless
# it is set again.

在上面的例子中,我们首先使用构造函数或 sessionmaker 来生成一个 Session。然后,我们通过调用 Session.connection() 显式设置数据库级事务的开始,该方法提供将在开始数据库级事务之前传递给连接的执行选项。事务会以此选定的隔离级别继续进行。当事务完成时,连接上的隔离级别将被重置为其默认值,然后连接将返回到连接池。

Session.begin() 方法也可用于开始 Session 级别的事务;在此调用之后调用 Session.connection() 可用于设置每个连接的事务隔离级别:

代码语言:javascript复制
sess = Session(bind=engine)

with sess.begin():
    # call connection() with options before any other operations proceed.
    # this will procure a new connection from the bound engine and begin a
    # real database transaction.
    sess.connection(execution_options={"isolation_level": "SERIALIZABLE"})

    # ... work with session in SERIALIZABLE isolation level...

# outside the block, the transaction has been committed.  the connection is
# released and reverted to its previous isolation level.
使用事件跟踪事务状态

请参阅 事务事件 部分,了解会话事务状态更改的可用事件挂钩的概述。

将会话加入到外部事务(例如用于测试套件)

如果正在使用的Connection已经处于事务状态(即已建立Transaction),则可以通过将Session绑定到该Connection来使Session参与该事务。通常的理由是一个测试套件允许 ORM 代码自由地与Session一起工作,包括能够调用Session.commit(),之后整个数据库交互都被回滚。

从版本 2.0 开始更改:在 2.0 中,“加入外部事务”配方再次得到了改进;不再需要事件处理程序来“重置”嵌套事务。

该配方通过在事务内部建立Connection(可选地建立 SAVEPOINT),然后将其传递给Session作为“bind”来实现;传递了Session.join_transaction_mode参数,设置为"create_savepoint",这表示应创建新的 SAVEPOINT 以实现Session的 BEGIN/COMMIT/ROLLBACK,这将使外部事务保持与传递时相同的状态。

当测试拆卸时,外部事务将被回滚,以便撤消测试期间的任何数据更改:

代码语言:javascript复制
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from unittest import TestCase

# global application scope.  create Session class, engine
Session = sessionmaker()

engine = create_engine("postgresql psycopg2://...")

class SomeTest(TestCase):
    def setUp(self):
        # connect to the database
        self.connection = engine.connect()

        # begin a non-ORM transaction
        self.trans = self.connection.begin()

        # bind an individual Session to the connection, selecting
        # "create_savepoint" join_transaction_mode
        self.session = Session(
            bind=self.connection, join_transaction_mode="create_savepoint"
        )

    def test_something(self):
        # use the session in tests.

        self.session.add(Foo())
        self.session.commit()

    def test_something_with_rollbacks(self):
        self.session.add(Bar())
        self.session.flush()
        self.session.rollback()

        self.session.add(Foo())
        self.session.commit()

    def tearDown(self):
        self.session.close()

        # rollback - everything that happened with the
        # Session above (including calls to commit())
        # is rolled back.
        self.trans.rollback()

        # return connection to the Engine
        self.connection.close()

上述配方是 SQLAlchemy 自身 CI 的一部分,以确保其按预期工作。 ession.connection()` 显式设置数据库级事务的开始,该方法提供将在开始数据库级事务之前传递给连接的执行选项。事务会以此选定的隔离级别继续进行。当事务完成时,连接上的隔离级别将被重置为其默认值,然后连接将返回到连接池。

Session.begin() 方法也可用于开始 Session 级别的事务;在此调用之后调用 Session.connection() 可用于设置每个连接的事务隔离级别:

代码语言:javascript复制
sess = Session(bind=engine)

with sess.begin():
    # call connection() with options before any other operations proceed.
    # this will procure a new connection from the bound engine and begin a
    # real database transaction.
    sess.connection(execution_options={"isolation_level": "SERIALIZABLE"})

    # ... work with session in SERIALIZABLE isolation level...

# outside the block, the transaction has been committed.  the connection is
# released and reverted to its previous isolation level.
使用事件跟踪事务状态

请参阅 事务事件 部分,了解会话事务状态更改的可用事件挂钩的概述。

将会话加入到外部事务(例如用于测试套件)

如果正在使用的Connection已经处于事务状态(即已建立Transaction),则可以通过将Session绑定到该Connection来使Session参与该事务。通常的理由是一个测试套件允许 ORM 代码自由地与Session一起工作,包括能够调用Session.commit(),之后整个数据库交互都被回滚。

从版本 2.0 开始更改:在 2.0 中,“加入外部事务”配方再次得到了改进;不再需要事件处理程序来“重置”嵌套事务。

该配方通过在事务内部建立Connection(可选地建立 SAVEPOINT),然后将其传递给Session作为“bind”来实现;传递了Session.join_transaction_mode参数,设置为"create_savepoint",这表示应创建新的 SAVEPOINT 以实现Session的 BEGIN/COMMIT/ROLLBACK,这将使外部事务保持与传递时相同的状态。

当测试拆卸时,外部事务将被回滚,以便撤消测试期间的任何数据更改:

代码语言:javascript复制
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from unittest import TestCase

# global application scope.  create Session class, engine
Session = sessionmaker()

engine = create_engine("postgresql psycopg2://...")

class SomeTest(TestCase):
    def setUp(self):
        # connect to the database
        self.connection = engine.connect()

        # begin a non-ORM transaction
        self.trans = self.connection.begin()

        # bind an individual Session to the connection, selecting
        # "create_savepoint" join_transaction_mode
        self.session = Session(
            bind=self.connection, join_transaction_mode="create_savepoint"
        )

    def test_something(self):
        # use the session in tests.

        self.session.add(Foo())
        self.session.commit()

    def test_something_with_rollbacks(self):
        self.session.add(Bar())
        self.session.flush()
        self.session.rollback()

        self.session.add(Foo())
        self.session.commit()

    def tearDown(self):
        self.session.close()

        # rollback - everything that happened with the
        # Session above (including calls to commit())
        # is rolled back.
        self.trans.rollback()

        # return connection to the Engine
        self.connection.close()

上述配方是 SQLAlchemy 自身 CI 的一部分,以确保其按预期工作。

0 人点赞