SqlAlchemy-2-0-中文文档-十一-
SqlAlchemy 2.0 中文文档(十一)
状态管理
原文:
docs.sqlalchemy.org/en/20/orm/session_state_management.html
对象状态简介
知道对象在会话中可能具有的状态是有帮助的:
-
Transient - 一个不在会话中的实例,也没有保存到数据库;即它没有数据库标识。这样的对象与 ORM 的唯一关系是其类与一个
Mapper
相关联。 -
Pending - 当您
Session.add()
一个瞬态实例时,它变为待定状态。它实际上还没有被刷新到数据库中,但在下一次刷新时会被刷新到数据库中。 -
Persistent - 存在于会话中并在数据库中具有记录的实例。您可以通过刷新使待定实例变为持久实例,或者通过查询数据库获取现有实例(或将其他会话中的持久实例移动到您的本地会话中)来获取持久实例。
-
Deleted - 在刷新中已被删除的实例,但事务尚未完成。处于此状态的对象基本上处于“待定”状态的相反状态;当会话的事务提交时,对象将移动到分离状态。或者,当会话的事务回滚时,删除的对象将返回到持久状态。
-
Detached - 一个对应于数据库中的记录,但目前不在任何会话中的实例。分离的对象将包含一个数据库标识标记,但是由于它没有与会话关联,因此无法确定此数据库标识是否实际存在于目标数据库中。分离的对象通常可以安全使用,但它们无法加载未加载的属性或先前标记为“过期”的属性。
深入了解所有可能的状态转换,请参阅对象生命周期事件部分,其中描述了每个转换以及如何以编程方式跟踪每个转换。
获取对象的当前状态
您可以随时使用inspect()
函数在任何映射对象上查看实际状态;此函数将返回管理对象的内部 ORM 状态的相应InstanceState
对象。InstanceState
提供了其他访问器,包括指示对象持久状态的布尔属性,包括:
-
InstanceState.transient
-
InstanceState.pending
-
InstanceState.persistent
-
InstanceState.deleted
-
InstanceState.detached
例如:
>>> from sqlalchemy import inspect
>>> insp = inspect(my_object)
>>> insp.persistent
True
另请参阅
映射实例的检查 - InstanceState
的更多示例 ## 会话属性
Session
本身在某种程度上就像一个集合。可以使用迭代器接口访问所有已存在的项目:
for obj in session:
print(obj)
并且可以使用常规的“包含”语义来测试存在性:
if obj in session:
print("Object is present")
会话还跟踪所有新创建的(即待处理的)对象,自上次加载或保存以来发生了更改的所有对象(即“脏对象”),以及标记为已删除的所有对象:
# pending objects recently added to the Session
session.new
# persistent objects which currently have changes detected
# (this collection is now created on the fly each time the property is called)
session.dirty
# persistent objects that have been marked as deleted via session.delete(obj)
session.deleted
# dictionary of all persistent objects, keyed on their
# identity key
session.identity_map
(文档:Session.new
, Session.dirty
, Session.deleted
, Session.identity_map
). ## 会话引用行为
会话内的对象是弱引用的。这意味着当它们在外部应用程序中取消引用时,它们也从Session
中消失,并且受 Python 解释器的垃圾收集影响。这种情况的例外包括待处理的对象、标记为已删除的对象或具有待处理更改的持久对象。在完全刷新后,这些集合都为空,并且所有对象再次成为弱引用。
为了使Session
中的对象保持强引用,通常只需要一个简单的方法。外部管理强引用行为的示例包括将对象加载到以其主键为键的本地字典中,或者在它们需要保持引用的时间段内加载到列表或集合中。如果需要,这些集合可以与 Session
关联,方法是将它们放入 Session.info
字典中。
也可以采用基于事件的方法。一个简单的方法可以为所有对象在持久状态下保持“强引用”行为,具体如下:
from sqlalchemy import event
def strong_reference_session(session):
@event.listens_for(session, "pending_to_persistent")
@event.listens_for(session, "deleted_to_persistent")
@event.listens_for(session, "detached_to_persistent")
@event.listens_for(session, "loaded_as_persistent")
def strong_ref_object(sess, instance):
if "refs" not in sess.info:
sess.info["refs"] = refs = set()
else:
refs = sess.info["refs"]
refs.add(instance)
@event.listens_for(session, "persistent_to_detached")
@event.listens_for(session, "persistent_to_deleted")
@event.listens_for(session, "persistent_to_transient")
def deref_object(sess, instance):
sess.info["refs"].discard(instance)
在上面的示例中,我们拦截了 SessionEvents.pending_to_persistent()
、SessionEvents.detached_to_persistent()
、SessionEvents.deleted_to_persistent()
和 SessionEvents.loaded_as_persistent()
事件钩子,以便拦截对象在进入持久状态时的行为,并在对象离开持久状态时拦截 SessionEvents.persistent_to_detached()
和 SessionEvents.persistent_to_deleted()
事件钩子。
上述函数可用于任何 Session
,以在每个Session
基础上提供强引用行为:
from sqlalchemy.orm import Session
my_session = Session()
strong_reference_session(my_session)
对于任何 sessionmaker
,也可能被调用:
from sqlalchemy.orm import sessionmaker
maker = sessionmaker()
strong_reference_session(maker)
``` ## 合并
`Session.merge()` 将状态从外部对象传输到会话中的新实例或已存在的实例。它还将传入的数据与数据库状态进行对比,生成一个历史流,该流将被应用于下一次刷新,或者可以被设置为生成简单的状态“传输”,而不生成变更历史或访问数据库。使用方法如下:
```py
merged_object = session.merge(existing_object)
给定一个实例时,它遵循以下步骤:
-
它检查实例的主键。如果存在,则尝试在本地标识映射中定位该实例。如果
load=True
标志保持默认设置,则还会检查数据库是否存在此主键,如果在本地找不到,则检查数据库是否存在此主键。 -
如果给定实例没有主键,或者给定的主键找不到实例,则创建一个新实例。
-
然后将给定实例的状态复制到定位的/新创建的实例上。对于源实例上存在的属性值,该值将转移到目标实例上。对于源实例上不存在的属性值,目标实例上的相应属性将从内存中过期,这会丢弃目标实例的该属性的任何本地存在值,但不会对该属性的数据库持久化值进行直接修改。
如果
load=True
标志保持默认设置,此复制过程会发出事件,并且将为源对象上的每个属性加载目标对象的未加载集合,以便可以根据数据库中存在的内容来协调传入状态。如果传递load
为False
,则传入的数据将直接“标记”,而不产生任何历史记录。 -
操作会根据
merge
级联(请参阅级联)传播到相关对象和集合。 -
返回新实例。
使用Session.merge()
,给定的“源”实例不会被修改,也不会与目标Session
关联,并且仍然可以与任意数量的其他Session
对象合并。Session.merge()
对于获取任何类型的对象结构的状态而无需考虑其来源或当前会话关联,并将其状态复制到新会话中非常有用。以下是一些示例:
-
从文件读取对象结构并希望将其保存到数据库的应用程序可能会解析文件,构建结构,然后使用
Session.merge()
将其保存到数据库中,确保文件中的数据用于构造结构的每个元素的主键。稍后,当文件发生更改时,可以重新运行相同的过程,生成稍微不同的对象结构,然后可以再次进行merge
,并且Session
将自动更新数据库以反映这些更改,通过主键从数据库加载每个对象,然后使用新状态更新其状态。 -
一个应用程序将对象存储在一个内存缓存中,由许多
Session
对象同时共享。每次从缓存中检索对象时,都会使用Session.merge()
创建它的本地副本,以便在每个请求它的Session
中。缓存的对象保持分离状态;只有它的状态被移动到本地于各个Session
对象的副本中。在缓存用例中,通常使用
load=False
标志来消除对象状态与数据库之间的开销。还有一个“批量”版本的Session.merge()
称为Query.merge_result()
,它被设计用于与缓存扩展的Query
对象一起使用 - 请参阅 Dogpile Caching 部分。 -
一个应用程序想要将一系列对象的状态转移到由工作线程或其他并发系统维护的
Session
中。Session.merge()
将每个要放入新Session
中的对象复制一份。操作结束时,父线程/进程保留了其开始的对象,而线程/工作程序可以继续使用这些对象的本地副本。在“线程/进程之间传输”用例中,应用程序可能希望同时使用
load=False
标志,以避免开销和冗余的 SQL 查询,因为数据正在传输。
合并提示
Session.merge()
是一个非常有用的方法,适用于许多目的。然而,它处理的是瞬态/分离对象和持久化对象之间复杂的边界,以及状态的自动转移。这里可能出现的各种各样的场景通常需要对对象状态更加谨慎的处理。合并的常见问题通常涉及到传递给Session.merge()
的对象的一些意外状态。
让我们以用户和地址对象的典型示例为例:
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
name = mapped_column(String(50), nullable=False)
addresses = relationship("Address", backref="user")
class Address(Base):
__tablename__ = "address"
id = mapped_column(Integer, primary_key=True)
email_address = mapped_column(String(50), nullable=False)
user_id = mapped_column(Integer, ForeignKey("user.id"), nullable=False)
假设一个具有一个地址的User
对象,已经持久化:
>>> u1 = User(name="ed", addresses=[Address(email_address="ed@ed.com")])
>>> session.add(u1)
>>> session.commit()
现在我们创建了一个在会话之外的对象a1
,我们希望将其合并到现有的Address
上:
>>> existing_a1 = u1.addresses[0]
>>> a1 = Address(id=existing_a1.id)
如果我们这样说会有一个意外的情况:
>>> a1.user = u1
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.orm.exc.FlushError: New instance <Address at 0x1298f50>
with identity key (<class '__main__.Address'>, (1,)) conflicts with
persistent instance <Address at 0x12a25d0>
为什么会这样?我们没有正确处理级联。将a1.user
分配给持久对象级联到User.addresses
的反向引用,并使我们的a1
对象挂起,就好像我们已经添加了它一样。现在我们的会话中有两个Address
对象:
>>> a1 = Address()
>>> a1.user = u1
>>> a1 in session
True
>>> existing_a1 in session
True
>>> a1 is existing_a1
False
上面,我们的a1
已经在会话中挂起。随后的Session.merge()
操作实际上什么都不做。级联可以通过relationship.cascade
选项在relationship()
上配置,尽管在这种情况下,这意味着从User.addresses
关系中删除了save-update
级联 - 而且通常,那种行为非常方便。这里的解决方案通常是不将a1.user
分配给已经存在于目标会话中的对象。
relationship()
的cascade_backrefs=False
选项也将阻止通过a1.user = u1
分配将Address
添加到会话中。
更多关于级联操作的细节请参阅级联。
另一个意外状态的例子:
>>> a1 = Address(id=existing_a1.id, user_id=u1.id)
>>> a1.user = None
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.exc.IntegrityError: (IntegrityError) address.user_id
may not be NULL
上面,将user
的分配优先于user_id
的外键分配,最终导致user_id
应用了None
,导致失败。
大多数Session.merge()
问题可以通过首先检查 - 对象是否过早地在会话中?
>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> assert a1 not in session
>>> a1 = session.merge(a1)
或者对象上有我们不想要的状态吗?检查__dict__
是一个快速检查的方法:
>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> a1.user
>>> a1.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x1298d10>,
'user_id': 1,
'id': 1,
'user': None}
>>> # we don't want user=None merged, remove it
>>> del a1.user
>>> a1 = session.merge(a1)
>>> # success
>>> session.commit()
清除
Expunge 将对象从会话中删除,将持久实例发送到脱机状态,将待处理实例发送到瞬态状态:
session.expunge(obj1)
要删除所有项目,请调用Session.expunge_all()
(此方法以前称为clear()
)。
刷新 / 过期
过期意味着数据库持久化数据存储在一系列对象属性中被清除,这样当下次访问这些属性时,将发出一个 SQL 查询,该查询将从数据库中刷新数据。
当我们谈论数据的过期时,我们通常是指处于持久状态的对象。例如,如果我们像这样加载一个对象:
user = session.scalars(select(User).filter_by(name="user1").limit(1)).first()
上述的User
对象是持久的,并且具有一系列存在的属性;如果我们查看它的__dict__
,我们会看到已加载的状态:
>>> user.__dict__
{
'id': 1, 'name': u'user1',
'_sa_instance_state': <...>,
}
其中id
和name
是数据库中的列。_sa_instance_state
是 SQLAlchemy 内部使用的非数据库持久化值(它引用了实例的InstanceState
。虽然与本节直接相关,但如果我们想要获取它,我们应该使用inspect()
函数来访问它)。
此时,我们User
对象中的状态与加载的数据库行的状态相匹配。但是在使用诸如Session.expire()
之类的方法使对象过期后,我们会看到状态被删除:
>>> session.expire(user)
>>> user.__dict__
{'_sa_instance_state': <...>}
我们看到,虽然内部的“状态”仍然存在,但与id
和name
列对应的值已经消失。如果我们要访问其中一列并观察 SQL,我们会看到这样的情况:
>>> print(user.name)
SELECT user.id AS user_id, user.name AS user_name
FROM user
WHERE user.id = ?
(1,)
user1
上面,在访问已过期的属性user.name
时,ORM 启动了一个惰性加载以从数据库中检索最新状态,通过向这个用户引用的用户行发出一个 SELECT。之后,__dict__
再次被填充:
>>> user.__dict__
{
'id': 1, 'name': u'user1',
'_sa_instance_state': <...>,
}
注意
虽然我们正在查看__dict__
的内容,以便了解 SQLAlchemy 对对象属性的处理,但我们不应直接修改__dict__
的内容,至少不应修改 SQLAlchemy ORM 正在维护的属性(SQLA 领域之外的其他属性没问题)。这是因为 SQLAlchemy 使用描述符来跟踪我们对对象所做的更改,当我们直接修改__dict__
时,ORM 将无法跟踪到我们做出的更改。
Session.expire()
和Session.refresh()
的另一个关键行为是,对象上的所有未刷新的更改都将被丢弃。也就是说,如果我们要修改User
上的属性:
>>> user.name = "user2"
但是当我们在调用Session.expire()
之前没有调用Session.flush()
时,我们挂起的值'user2'
将被丢弃:
>>> session.expire(user)
>>> user.name
'user1'
Session.expire()
方法可用于将实例的所有 ORM 映射属性标记为“过期”:
# expire all ORM-mapped attributes on obj1
session.expire(obj1)
它还可以传递一个字符串属性名称列表,指定要标记为过期的特定属性:
# expire only attributes obj1.attr1, obj1.attr2
session.expire(obj1, ["attr1", "attr2"])
Session.expire_all()
方法允许我们一次性对Session
中包含的所有对象调用Session.expire()
:
session.expire_all()
Session.refresh()
方法具有类似的接口,但是不是使过期,而是立即发出对象行的 SELECT:
# reload all attributes on obj1
session.refresh(obj1)
Session.refresh()
还接受一个字符串属性名称的列表,但与Session.expire()
不同,它期望至少一个名称是列映射属性的名称:
# reload obj1.attr1, obj1.attr2
session.refresh(obj1, ["attr1", "attr2"])
提示
通常更灵活的刷新替代方法是使用 ORM 的填充现有内容功能,适用于使用select()
进行 2.0 风格查询以及在 1.x 风格查询中的Query.populate_existing()
方法。使用此执行选项,语句结果集中返回的所有 ORM 对象都将使用来自数据库的数据进行刷新:
stmt = (
select(User)
.execution_options(populate_existing=True)
.where((User.name.in_(["a", "b", "c"])))
)
for user in session.execute(stmt).scalars():
print(user) # will be refreshed for those columns that came back from the query
查看填充现有内容以获取更多详细信息。
实际加载内容
当标记为Session.expire()
或使用Session.refresh()
加载的对象时,所发出的 SELECT 语句因多种因素而异,包括:
-
仅从列映射属性加载过期属性。虽然可以将任何类型的属性标记为过期,包括
relationship()
- 映射属性,但访问过期的relationship()
属性将仅为该属性发出加载,使用标准的基于关系的惰性加载。即使过期,基于列的属性也不会作为此操作的一部分加载,而是在访问任何基于列的属性时加载。 -
通过
relationship()
映射的属性不会在访问过期的基于列的属性时加载。 -
关于关系,
Session.refresh()
在属性不是列映射的情况下比Session.expire()
更为严格。调用Session.refresh()
并传递一个只包括关系映射属性的名称列表将会引发错误。无论如何,非急切加载的relationship()
属性都不会包含在任何刷新操作中。 -
通过
relationship.lazy
参数配置为“急切加载”的relationship()
属性将在Session.refresh()
的情况下加载,如果未指定任何属性名称,或者如果它们的名称包含在要刷新的属性列表中。 -
配置为
deferred()
的属性通常不会在过期属性加载期间或刷新期间加载。当直接访问未加载的属性或者作为延迟属性组的一部分访问该组中的未加载属性时,配置为deferred()
的未加载属性将自行加载。 -
对于在访问时加载的过期属性,连接继承表映射将发出一个通常只包含那些存在未加载属性的表的 SELECT。在这里的操作足够复杂,以仅加载父表或子表,例如,如果最初过期的列的子集仅包含其中一个表或另一个表。
-
当在连接继承表映射上使用
Session.refresh()
时,所发出的 SELECT 与在目标对象的类上使用Session.query()
时的类似。这通常是映射的一部分设置的所有表。
何时过期或刷新
Session
在会话引用的事务结束时自动使用过期功能。这意味着,每当调用 Session.commit()
或 Session.rollback()
,会话中的所有对象都会过期,使用与 Session.expire_all()
方法相当的功能。其原因在于事务的结束是一个标志性的点,在此点上不再有可用于了解数据库当前状态的上下文,因为任意数量的其他事务可能正在影响它。只有当新事务开始时,我们才能再次访问数据库的当前状态,在此时可能已经发生了任意数量的更改。
当希望强制对象重新从数据库加载其数据时,应使用 Session.expire()
和 Session.refresh()
方法,当已知数据的当前状态可能过时时。这样做的原因可能包括:
-
一些 SQL 在 ORM 对象处理范围之外的事务中被发出,比如如果使用
Session.execute()
方法发出了Table.update()
构造; -
如果应用程序试图获取已知在并发事务中已修改的数据,并且已知正在生效的隔离规则允许该数据可见。
第二条警告很重要,即“也已知在生效的隔离规则下,这些数据可见。”这意味着不能假设在另一个数据库连接上发生的更新在本地已经可见;在许多情况下,它是不可见的。这就是为什么如果想要在正在进行的事务之间使用 Session.expire()
或 Session.refresh()
查看数据,就必须了解正在生效的隔离行为。
另见
Session.expire()
Session.expire_all()
Session.refresh()
填充现有 - 允许任何 ORM 查询在 SELECT 语句的结果中刷新对象,就像它们通常加载一样,刷新标识映射中所有匹配的对象。
隔离 - 隔离的词汇解释,其中包括指向维基百科的链接。
SQLAlchemy 会话深入解析 - 一个关于对象生命周期的深入讨论的视频 + 幻灯片,包括数据过期的角色。## 快速对象状态介绍
了解实例在会话中可能具有的状态是有帮助的:
-
瞬时 - 一个不在会话中并且没有保存到数据库的实例;即它没有数据库标识。这样的对象与 ORM 的唯一关系是其类与一个
Mapper
相关联。 -
待定 - 当你
Session.add()
一个瞬时实例时,它变为待定状态。它实际上还没有被刷新到数据库,但在下一次刷新时会被刷新到数据库。 -
持久 - 存在于会话中并且在数据库中有记录的实例。您可以通过刷新使待定实例变为持久实例,或通过查询数据库获取现有实例(或将其他会话中的持久实例移动到您的本地会话)来获得持久实例。
-
已删除 - 在刷新中已删除的实例,但事务尚未完成。处于这种状态的对象基本上与“待定”状态相反;当会话的事务提交时,对象将移至分离状态。另外,当会话的事务回滚时,已删除的对象将回到持久状态。
-
分离 - 一个实例,它对应于或以前对应于数据库中的记录,但当前不在任何会话中。分离的对象将包含一个数据库标识标记,但由于它没有关联到会话,因此不知道此数据库标识实际上是否存在于目标数据库中。分离的对象通常是安全的使用,除了它们无法加载未加载的属性或以前标记为“过期”的属性。
深入研究所有可能的状态转换,请参阅 对象生命周期事件 部分,该部分描述了每个转换以及如何以编程方式跟踪每个转换。
获取对象的当前状态
任何映射对象的实际状态都可以随时使用映射实例上的 inspect()
函数查看;此函数将返回管理对象的内部 ORM 状态的相应 InstanceState
对象。InstanceState
提供了其他访问器,其中包括指示对象持久性状态的布尔属性,包括:
-
InstanceState.transient
-
InstanceState.pending
-
InstanceState.persistent
-
InstanceState.deleted
-
InstanceState.detached
例如:
>>> from sqlalchemy import inspect
>>> insp = inspect(my_object)
>>> insp.persistent
True
另请参阅
映射实例的检查 - 更多有关 InstanceState
的示例
获取对象的当前状态
任何映射对象的实际状态都可以随时使用映射实例上的 inspect()
函数查看;此函数将返回管理对象的内部 ORM 状态的相应 InstanceState
对象。InstanceState
提供了其他访问器,其中包括指示对象持久性状态的布尔属性,包括:
-
InstanceState.transient
-
InstanceState.pending
-
InstanceState.persistent
-
InstanceState.deleted
-
InstanceState.detached
例如:
>>> from sqlalchemy import inspect
>>> insp = inspect(my_object)
>>> insp.persistent
True
另请参阅
映射实例的检查 - 更多有关 InstanceState
的示例
会话属性
Session
本身的行为有点像一个类似集合的集合。可以使用迭代器接口访问所有存在的项目:
for obj in session:
print(obj)
并且可以使用常规的“包含”语义进行测试:
if obj in session:
print("Object is present")
会话还会跟踪所有新创建的(即待处理的)对象,所有自上次加载或保存以来发生更改的对象(即“脏对象”),以及所有被标记为已删除的对象:
# pending objects recently added to the Session
session.new
# persistent objects which currently have changes detected
# (this collection is now created on the fly each time the property is called)
session.dirty
# persistent objects that have been marked as deleted via session.delete(obj)
session.deleted
# dictionary of all persistent objects, keyed on their
# identity key
session.identity_map
(文档:Session.new
,Session.dirty
,Session.deleted
,Session.identity_map
)。
会话引用行为
会话中的对象是弱引用的。这意味着当它们在外部应用程序中取消引用时,它们也会从Session
中失去作用域,并且由 Python 解释器进行垃圾回收。这种情况的例外包括待处理对象、标记为已删除的对象或具有待处理更改的持久对象。在完全刷新后,这些集合都为空,并且所有对象再次是弱引用的。
使Session
中的对象保持强引用通常只需要简单的方法。外部管理的强引用行为示例包括将对象加载到以其主键为键的本地字典中,或者在它们需要保持引用的时间段内加载到列表或集合中。如果需要,这些集合可以与Session
关联,方法是将它们放入Session.info
字典中。
还可以使用基于事件的方法。以下是一个提供了所有对象在持久化状态下保持“强引用”行为的简单方案:
from sqlalchemy import event
def strong_reference_session(session):
@event.listens_for(session, "pending_to_persistent")
@event.listens_for(session, "deleted_to_persistent")
@event.listens_for(session, "detached_to_persistent")
@event.listens_for(session, "loaded_as_persistent")
def strong_ref_object(sess, instance):
if "refs" not in sess.info:
sess.info["refs"] = refs = set()
else:
refs = sess.info["refs"]
refs.add(instance)
@event.listens_for(session, "persistent_to_detached")
@event.listens_for(session, "persistent_to_deleted")
@event.listens_for(session, "persistent_to_transient")
def deref_object(sess, instance):
sess.info["refs"].discard(instance)
上述,我们拦截了SessionEvents.pending_to_persistent()
,SessionEvents.detached_to_persistent()
,SessionEvents.deleted_to_persistent()
和SessionEvents.loaded_as_persistent()
事件钩子,以拦截对象进入 persistent 状态转换时的情况,以及SessionEvents.persistent_to_detached()
和SessionEvents.persistent_to_deleted()
钩子以拦截对象离开持久状态时的情况。
上述函数可针对任何Session
进行调用,以在每个Session
上提供强引用行为:
from sqlalchemy.orm import Session
my_session = Session()
strong_reference_session(my_session)
也可以针对任何sessionmaker
进行调用:
from sqlalchemy.orm import sessionmaker
maker = sessionmaker()
strong_reference_session(maker)
合并
Session.merge()
将外部对象的状态转移到会话中的新实例或已存在的实例中。 它还将传入的数据与数据库状态进行对比,生成将应用于下一个刷新的历史流,或者可以产生简单的状态“转移”而不产生更改历史或访问数据库。 使用方法如下:
merged_object = session.merge(existing_object)
当给定一个实例时,它按以下步骤执行:
-
它检查实例的主键。 如果存在,它会尝试在本地标识映射中定位该实例。 如果将
load=True
标志保留为其默认值,则还会检查数据库以获取该主键(如果未在本地找到)。 -
如果给定实例没有主键,或者无法找到给定主键的实例,则会创建一个新实例。
-
然后,给定实例的状态将被复制到定位/新创建的实例上。对于源实例上存在的属性值,该值将转移到目标实例。对于源实例上不存在的属性值,目标实例上的相应属性将从内存中过期,这将丢弃目标实例的该属性的任何局部存在值,但不会直接修改该属性的数据库持久化值。
如果
load=True
标志保持其默认状态,则此复制过程会触发事件,并且将为源对象上的每个属性加载目标对象的未加载集合,以便对入站状态进行与数据库中存在的内容进行对比。如果传递了load
为False
,则传入的数据将直接“盖章”,而不产生任何历史记录。 -
操作会级联到相关对象和集合,如
merge
级联所示(见级联)。 -
返回新实例。
使用Session.merge()
,给定的“源”实例不会被修改,也不会与目标Session
关联,并且仍然可用于与任意数量的其他Session
对象合并。Session.merge()
对于将任何类型的对象结构的状态复制到新会话中而不考虑其来源或当前会话关联很有用。以下是一些示例:
-
从文件读取对象结构并希望将其保存到数据库的应用程序可能会解析文件,构建结构,然后使用
Session.merge()
将其保存到数据库,确保使用文件中的数据来制定结构的每个元素的主键。稍后,当文件发生更改时,可以重新运行相同的过程,生成稍微不同的对象结构,然后可以再次进行合并,并且Session
将自动更新数据库以反映这些更改,通过主键从数据库加载每个对象,然后使用给定的新状态更新其状态。 -
一个应用程序将对象存储在一个内存缓存中,被许多
Session
对象同时共享。每次从缓存中检索对象时,都会使用Session.merge()
在请求该对象的每个Session
中创建一个本地副本。缓存的对象保持分离状态;只有其状态被移动到各自局部的Session
对象的副本中。在缓存用例中,通常使用
load=False
标志来消除对象状态与数据库之间的协调开销。还有一个与缓存扩展Query
对象一起工作的“批量”版本的Session.merge()
,称为Query.merge_result()
,请参见 Dogpile Caching 部分。 -
一个应用程序希望将一系列对象的状态转移到由工作线程或其他并发系统维护的
Session
中。Session.merge()
会为要放入这个新Session
的每个对象创建一个副本。操作结束时,父线程/进程保留其开始的对象,而线程/工作线程可以继续使用这些对象的本地副本。在“线程/进程之间传输”用例中,应用程序可能还想使用
load=False
标志,以避免在数据传输时产生额外开销和冗余的 SQL 查询。
合并提示
Session.merge()
是一个非常有用的方法,适用于许多目的。然而,它处理的是瞬态/分离对象和持久对象之间复杂的边界,以及状态的自动传输。这里可能出现的各种场景通常需要更加谨慎地处理对象的状态。合并常见问题通常涉及传递给Session.merge()
的对象的一些意外状态。
让我们使用用户和地址对象的典型示例:
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
name = mapped_column(String(50), nullable=False)
addresses = relationship("Address", backref="user")
class Address(Base):
__tablename__ = "address"
id = mapped_column(Integer, primary_key=True)
email_address = mapped_column(String(50), nullable=False)
user_id = mapped_column(Integer, ForeignKey("user.id"), nullable=False)
假设一个具有一个Address
的User
对象,已经是持久的:
>>> u1 = User(name="ed", addresses=[Address(email_address="ed@ed.com")])
>>> session.add(u1)
>>> session.commit()
现在我们创建a1
,一个在会话之外的对象,我们希望将其合并到现有的Address
上:
>>> existing_a1 = u1.addresses[0]
>>> a1 = Address(id=existing_a1.id)
如果我们这样说,将会出现一个意外情况:
>>> a1.user = u1
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.orm.exc.FlushError: New instance <Address at 0x1298f50>
with identity key (<class '__main__.Address'>, (1,)) conflicts with
persistent instance <Address at 0x12a25d0>
为什么会这样?我们没有仔细处理级联。将a1.user
分配给持久对象,级联到了User.addresses
的反向引用,并使我们的a1
对象挂起,就像我们已经将其添加一样。现在我们在会话中有两个Address
对象:
>>> a1 = Address()
>>> a1.user = u1
>>> a1 in session
True
>>> existing_a1 in session
True
>>> a1 is existing_a1
False
在上面的例子中,我们的a1
已经在会话中处于挂起状态。随后的Session.merge()
操作实际上什么也没做。级联可以通过relationship()
上的relationship.cascade
选项进行配置,尽管在这种情况下,这意味着从User.addresses
关系中删除save-update
级联 - 通常,这种行为非常方便。在这里的解决方案通常是不将a1.user
分配给目标会话中已经持久化的对象。
relationship()
的cascade_backrefs=False
选项还将通过a1.user = u1
赋值阻止将Address
添加到会话中。
有关级联操作的进一步细节,请参见 Cascades。
另一个意外状态的例子:
>>> a1 = Address(id=existing_a1.id, user_id=u1.id)
>>> a1.user = None
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.exc.IntegrityError: (IntegrityError) address.user_id
may not be NULL
在上面的例子中,user
的赋值优先于user_id
的外键赋值,其最终结果是将None
应用于user_id
,导致失败。
大多数Session.merge()
问题可以通过首先检查对象是否过早出现在会话中来检查。
>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> assert a1 not in session
>>> a1 = session.merge(a1)
或者对象上有我们不想要的状态吗?检查__dict__
是快速检查的一种方式:
>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> a1.user
>>> a1.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x1298d10>,
'user_id': 1,
'id': 1,
'user': None}
>>> # we don't want user=None merged, remove it
>>> del a1.user
>>> a1 = session.merge(a1)
>>> # success
>>> session.commit()
合并提示
Session.merge()
对于许多目的非常有用。但是,它处理的是瞬态/游离对象与持久对象之间复杂的边界,以及状态的自动转移。这里可能出现的各种各样的场景通常需要更加谨慎的对象状态处理。与合并相关的常见问题通常涉及到传递给Session.merge()
的对象的一些意外状态。
让我们使用用户和地址对象的典型示例:
class User(Base):
__tablename__ = "user"
id = mapped_column(Integer, primary_key=True)
name = mapped_column(String(50), nullable=False)
addresses = relationship("Address", backref="user")
class Address(Base):
__tablename__ = "address"
id = mapped_column(Integer, primary_key=True)
email_address = mapped_column(String(50), nullable=False)
user_id = mapped_column(Integer, ForeignKey("user.id"), nullable=False)
假设已经持久化了一个具有一个Address
的User
对象:
>>> u1 = User(name="ed", addresses=[Address(email_address="ed@ed.com")])
>>> session.add(u1)
>>> session.commit()
现在我们创建了一个在会话之外的对象a1
,我们希望将其合并到现有的Address
上:
>>> existing_a1 = u1.addresses[0]
>>> a1 = Address(id=existing_a1.id)
如果我们这样说,就会出现意外的情况:
>>> a1.user = u1
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.orm.exc.FlushError: New instance <Address at 0x1298f50>
with identity key (<class '__main__.Address'>, (1,)) conflicts with
persistent instance <Address at 0x12a25d0>
为什么会这样?我们没有小心处理级联。将 a1.user
分配给持久对象级联到了 User.addresses
的反向引用,并使我们的 a1
对象处于待定状态,就好像我们已经将其添加了一样。现在我们会话中有 两个 Address
对象。
>>> a1 = Address()
>>> a1.user = u1
>>> a1 in session
True
>>> existing_a1 in session
True
>>> a1 is existing_a1
False
在上面,我们的 a1
已经在会话中处于待定状态。随后的 Session.merge()
操作实质上什么也不做。级联可以通过 relationship()
上的 relationship.cascade
选项进行配置,尽管在这种情况下,它意味着从 User.addresses
关系中删除了 save-update
级联 - 通常,这种行为非常方便。在这种情况下,解决方案通常是不将 a1.user
分配给目标会话中已经持久的对象。
relationship()
的 cascade_backrefs=False
选项还会防止通过 a1.user = u1
赋值将 Address
添加到会话中。
关于级联操作的进一步细节请参见 级联。
另一个意外状态的例子:
>>> a1 = Address(id=existing_a1.id, user_id=u1.id)
>>> a1.user = None
>>> a1 = session.merge(a1)
>>> session.commit()
sqlalchemy.exc.IntegrityError: (IntegrityError) address.user_id
may not be NULL
在上面,user
的赋值优先于 user_id
的外键赋值,结果是 None
被应用于 user_id
,导致失败。
大多数 Session.merge()
问题可以通过首先检查对象是否过早出现在会话中来检查。
>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> assert a1 not in session
>>> a1 = session.merge(a1)
或者对象上是否有我们不希望的状态?检查 __dict__
是一种快速检查的方法:
>>> a1 = Address(id=existing_a1, user_id=user.id)
>>> a1.user
>>> a1.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x1298d10>,
'user_id': 1,
'id': 1,
'user': None}
>>> # we don't want user=None merged, remove it
>>> del a1.user
>>> a1 = session.merge(a1)
>>> # success
>>> session.commit()
Expunging
Expunge 从会话中移除一个对象,将持久实例发送到分离状态,将待定实例发送到瞬态状态:
session.expunge(obj1)
要删除所有项目,请调用 Session.expunge_all()
(此方法以前称为 clear()
)。
刷新 / 到期
到期 意味着数据库持久化的数据被擦除,当下次访问这些属性时,会发出一个 SQL 查询,该查询将从数据库刷新该数据。
当我们谈论数据的到期时,通常是指处于 持久 状态的对象。例如,如果我们加载一个对象如下:
user = session.scalars(select(User).filter_by(name="user1").limit(1)).first()
上述的 User
对象是持久的,并且具有一系列属性;如果我们查看它的 __dict__
,我们会看到加载的状态:
>>> user.__dict__
{
'id': 1, 'name': u'user1',
'_sa_instance_state': <...>,
}
其中 id
和 name
指的是数据库中的那些列。 _sa_instance_state
是 SQLAlchemy 内部使用的非数据库持久化值(它指的是实例的InstanceState
。虽然与本节无直接关系,但如果我们想访问它,应该使用inspect()
函数来访问它)。
此时,我们的 User
对象中的状态与加载的数据库行的状态相匹配。但是在使用诸如Session.expire()
这样的方法使对象过期后,我们发现状态已被删除:
>>> session.expire(user)
>>> user.__dict__
{'_sa_instance_state': <...>}
我们发现,虽然内部的“状态”仍然存在,但对应于 id
和 name
列的值已经消失了。如果我们尝试访问其中一个列,并且正在观察 SQL,我们会看到这样的情况:
>>> print(user.name)
SELECT user.id AS user_id, user.name AS user_name
FROM user
WHERE user.id = ?
(1,)
user1
上面,访问过期属性 user.name
后,ORM 启动了一个延迟加载以从数据库中检索最新的状态,通过发出一个 SELECT 来检索与此用户相关联的用户行。然后,__dict__
再次被填充:
>>> user.__dict__
{
'id': 1, 'name': u'user1',
'_sa_instance_state': <...>,
}
注意
当我们窥视 __dict__
以了解 SQLAlchemy 处理对象属性的一些情况时,我们不应直接修改 __dict__
的内容,至少在 SQLAlchemy ORM 维护的属性方面不应该这样做(SQLA 领域之外的其他属性没问题)。这是因为 SQLAlchemy 使用描述符来跟踪我们对对象所做的更改,当我们直接修改 __dict__
时,ORM 将无法跟踪到我们做了什么更改。
Session.expire()
和Session.refresh()
的另一个关键行为是,对象上的所有未刷新的更改都将被丢弃。也就是说,如果我们要修改我们的 User
上的属性:
>>> user.name = "user2"
但是当我们在首先调用Session.flush()
之前调用Session.expire()
时,我们挂起的值 'user2'
就会被丢弃:
>>> session.expire(user)
>>> user.name
'user1'
Session.expire()
方法可用于标记实例的所有 ORM 映射属性为“过期”状态:
# expire all ORM-mapped attributes on obj1
session.expire(obj1)
它还可以传递一个字符串属性名称的列表,指示特定属性将被标记为过期:
# expire only attributes obj1.attr1, obj1.attr2
session.expire(obj1, ["attr1", "attr2"])
Session.expire_all()
方法允许我们一次性对 Session
中包含的所有对象调用 Session.expire()
:
session.expire_all()
Session.refresh()
方法具有类似的接口,但不是过期,而是立即发出对象行的 SELECT:
# reload all attributes on obj1
session.refresh(obj1)
Session.refresh()
也接受一个字符串属性名称列表,但与 Session.expire()
不同,它希望至少有一个名称是列映射属性的名称:
# reload obj1.attr1, obj1.attr2
session.refresh(obj1, ["attr1", "attr2"])
提示
通常更灵活的刷新方法是使用 ORM 的 Populate Existing 功能,该功能适用于具有 select()
的 2.0 样式 查询以及 Query.populate_existing()
方法的 1.x 样式 查询。使用这种执行选项,语句结果集中返回的所有 ORM 对象都将使用数据库中的数据进行刷新:
stmt = (
select(User)
.execution_options(populate_existing=True)
.where((User.name.in_(["a", "b", "c"])))
)
for user in session.execute(stmt).scalars():
print(user) # will be refreshed for those columns that came back from the query
有关详细信息,请参见 Populate Existing。
实际加载的内容
当标记为 Session.expire()
或使用 Session.refresh()
加载的对象时,发出的 SELECT 语句基于几个因素而变化:
-
过期属性的加载仅从列映射属性触发。虽然可以将任何类型的属性标记为过期,包括
relationship()
- 映射属性,但访问过期的relationship()
属性将仅为该属性发出加载,使用标准的关联导向延迟加载。即使已过期,列导向属性也不会作为此操作的一部分加载,而是在访问任何列导向属性时加载。 -
响应于访问过期的基于列的属性时,
relationship()
-映射属性不会加载。 -
关于关系,与
Session.expire()
相比,Session.refresh()
对于未映射到列的属性更加严格。调用Session.refresh()
并传递仅包括关系映射属性的名称列表实际上会引发错误。无论如何,非“eager loading”relationship()
属性都不会包含在任何刷新操作中。 -
relationship()
属性通过relationship.lazy
参数配置为“eager loading”时,如果未指定属性名称,或者属性名称包含在要刷新的属性列表中,则在Session.refresh()
的情况下加载。 -
配置为
deferred()
的属性通常不会在过期属性加载或刷新期间加载。当直接访问未加载的属性时,或者如果未加载的属性是一组“deferred”属性的一部分,其中访问了该组中的未加载属性,则deferred()
属性将自行加载。 -
对于在访问时加载的过期属性,连接继承表映射将发出一个 SELECT,该 SELECT 通常仅包括那些存在未加载属性的表。在这里的操作足够复杂,可以仅加载父表或子表,例如,如果最初过期的列的子集仅涵盖这两个表中的一个。
-
当在连接继承表映射上使用
Session.refresh()
时,发出的 SELECT 将类似于在目标对象的类上使用Session.query()
时的情况。这通常是作为映射的一部分设置的所有表。
何时过期或刷新
Session
在会话结束时自动使用到期特性。意思是,每当调用 Session.commit()
或 Session.rollback()
时,Session
中的所有对象都会过期,使用与 Session.expire_all()
方法相当的特性。其理由是事务的结束是一个界定点,在此之后没有更多的上下文可用于了解数据库的当前状态,因为可能有任意数量的其他事务正在影响它。只有在新事务开始时,我们才能再次访问数据库的当前状态,在这一点上可能发生了任意数量的更改。
当希望强制对象从数据库重新加载其数据时,使用 Session.expire()
和 Session.refresh()
方法,在这些情况下,已知当前数据可能过时。 这样做的原因可能包括:
-
在 ORM 对象处理范围之外的事务内发出了一些 SQL,例如,如果使用
Session.execute()
方法发出了Table.update()
构造; -
如果应用程序试图获取在并发事务中已知已被修改的数据,并且已知正在生效的隔离规则允许将此数据可见。
第二条要点是“已知正在生效的隔离规则允许将此数据可见。” 这意味着不能假设在另一个数据库连接上发生的 UPDATE 在本地这里就可见;在许多情况下,不会是这样。 这就是为什么如果希望在正在进行的事务之间使用 Session.expire()
或 Session.refresh()
来查看数据,对生效的隔离行为有所了解是必要的原因。
另请参阅
Session.expire()
Session.expire_all()
Session.refresh()
填充现有对象 - 允许任何 ORM 查询像通常加载对象一样刷新对象,针对与 SELECT 语句的结果相匹配的所有匹配对象在标识映射中进行刷新。
隔离 - 隔离的词汇解释,其中包括指向维基百科的链接。
SQLAlchemy 会话深入解析 - 一个视频+幻灯片,深入讨论对象生命周期,包括数据过期的作用。
实际加载情况
当一个对象被标记为Session.expire()
或通过Session.refresh()
加载时,所发出的 SELECT 语句会基于几个因素而变化,包括:
-
过期属性的加载仅仅是从基于列的属性触发的。虽然任何类型的属性都可以被标记为过期,包括
relationship()
- 映射属性,但访问过期的relationship()
属性将仅对该属性进行加载,使用标准的基于关系的惰性加载。基于列的属性,即使过期,也不会作为此操作的一部分加载,而是在访问任何基于列的属性时加载。 -
relationship()
- 映射属性在访问到过期的基于列的属性时不会加载。 -
关于关系,
Session.refresh()
比Session.expire()
更严格,因为它不会加载那些非列映射的属性。调用Session.refresh()
并传递一个只包括关系映射属性的名称列表将会引发错误。在任何情况下,非急加载的relationship()
属性都不会包含在任何刷新操作中。 -
通过
relationship.lazy
参数配置为“急加载”的relationship()
属性在Session.refresh()
的情况下会加载,如果未指定属性名称,或者如果它们的名称包含在要刷新的属性列表中。 -
配置为
deferred()
的属性通常不会在过期属性加载或刷新期间加载。一个未加载的属性如果是deferred()
,那么当直接访问时或者作为“组”中的未加载属性之一在组中的其他未加载属性被访问时会加载。 -
对于在访问时加载的已过期属性,一个联合继承表映射将会发出一个 SELECT 语句,通常只包括那些存在未加载属性的表。这里的操作足够复杂,可以只加载父表或子表,例如,如果原始已过期的列子集仅涵盖其中一个表。
-
当在联合继承表映射上使用
Session.refresh()
时,所发出的 SELECT 将类似于在目标对象的类上使用Session.query()
时的情况。这通常是设置为映射的一部分的所有表。
何时过期或刷新
当Session
引用的事务结束时,会自动使用到期特性。这意味着,无论何时调用Session.commit()
或Session.rollback()
,Session
中的所有对象都将过期,使用与Session.expire_all()
方法相当的特性。其理由在于,事务结束是一个标记点,此时不再有上下文可用以知晓数据库的当前状态,因为任何数量的其他事务可能正在影响它。只有当新事务启动时,我们才能再次访问数据库的当前状态,在这时可能已经发生了任意数量的更改。
当希望强制对象从数据库重新加载数据时,可以使用Session.expire()
和Session.refresh()
方法,这种情况下已知当前数据状态可能过时。 这样做的原因可能包括:
-
在 ORM 对象处理范围之外的事务中发出了一些 SQL,比如使用
Session.execute()
方法发出了Table.update()
构造; -
如果应用程序试图获取已知在并发事务中已被修改的数据,并且已知生效的隔离规则允许查看这些数据。
第二个要点是“已知生效的隔离规则允许查看这些数据。” 这意味着不能假设在另一个数据库连接上发生的 UPDATE 在本地这里就能看到;在许多情况下,是不会看到的。 这就是为什么如果想要在进行中的事务之间查看数据,就需要了解生效的隔离行为,从而使用Session.expire()
或Session.refresh()
。
另请参阅
Session.expire()
Session.expire_all()
Session.refresh()
填充现有对象 - 允许任何 ORM 查询刷新对象,就像它们通常加载一样,根据 SELECT 语句的结果刷新标识映射中的所有匹配对象。
隔离 - 隔离的词汇解释,包括指向维基百科的链接。
SQLAlchemy 会话深入解析 - 一个视频+幻灯片,深入讨论对象生命周期,包括数据过期的作用。
级联
映射器支持在relationship()
构造上配置可配置级联行为的概念。这涉及到相对于特定Session
上执行的操作应如何传播到由该关系引用的项目(例如“子”对象),并且受到relationship.cascade
选项的影响。
级联的默认行为仅限于所谓的 save-update 和 merge 设置的级联。级联的典型“替代”设置是添加 delete 和 delete-orphan 选项;这些设置适用于只有在附加到其父对象时才存在的相关对象,并且在其他情况下将被删除。
使用relationship()
上的relationship.cascade
选项配置级联行为:
class Order(Base):
__tablename__ = "order"
items = relationship("Item", cascade="all, delete-orphan")
customer = relationship("User", cascade="save-update")
要在反向引用上设置级联,可以使用相同的标志与backref()
函数一起使用,该函数最终将其参数反馈到relationship()
中:
class Item(Base):
__tablename__ = "item"
order = relationship(
"Order", backref=backref("items", cascade="all, delete-orphan")
)
relationship.cascade
的默认值为save-update, merge
。此参数的典型替代设置为all
或更常见的是all, delete-orphan
。all
符号是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
,其中包含两个相关对象address1
、address2
:
>>> user1 = User()
>>> address1, address2 = Address(), Address()
>>> user1.addresses = [address1, address2]
如果我们将user1
添加到Session
中,它也会隐式添加address1
、address2
:
>>> sess = Session()
>>> sess.add(user1)
>>> address1 in sess
True
save-update
级联也会影响已经存在于Session
中的对象的属性操作。如果我们将第三个对象address3
添加到user1.addresses
集合中,它将成为该Session
的状态的一部分:
>>> address3 = Address()
>>> user1.addresses.append(address3)
>>> address3 in sess
True
当从集合中移除一个项目或将对象从标量属性中解除关联时,save-update
级联可能会表现出令人惊讶的行为。在某些情况下,被孤立的对象仍然可能被拉入原父级的Session
中;这是为了使刷新过程可以适当地处理相关对象。这种情况通常只会在一个对象从一个Session
中移除并添加到另一个对象时出现:
>>> 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_populates
或relationship.backref
参数创建相互引用的两个独立的relationship()
对象时,save-update
级联是单向的。
当一个未关联Session
的对象被赋给与关联Session
相关的父对象的属性或集合时,它将自动添加到同一Session
中。然而,反向操作不会产生此效果;当一个未关联Session
的对象被赋给与关联Session
相关的子对象时,不会自动将该父对象添加到Session
中。这种行为的总体主题称为“级联反向引用”,并代表了从 SQLAlchemy 2.0 开始标准化的行为变更。
以示例说明,假设给定了一个Order
对象的映射,它与一系列Item
对象通过关系Order.items
和Item.order
双向关联:
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
对象并将其附加到该Order
的Order.items
集合中,Item
将自动级联到相同的Session
中:
>>> 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.items
和Item.order
的双向性意味着附加到Order.items
也会赋值给Item.order
。同时,save-update
级联允许将Item
对象添加到与父Order
已关联的相同Session
中。
然而,如果上述操作以反向方向执行,即赋值Item.order
而不是直接附加到Order.item
,则级联操作不会自动进行,即使对象赋值Order.items
和Item.order
与上一个示例中的状态相同:
>>> 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
中:
>>> 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
级联:
class User(Base):
# ...
addresses = relationship("Address", cascade="all, delete")
如果使用上述映射,我们有一个User
对象和两个相关的Address
对象:
>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1, address2 = user1.addresses
如果我们标记user1
进行删除,在刷新操作进行后,address1
和address2
也将被删除:
>>> 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
来解除user1
与address1
和address2
的关联。使用以下映射:
class User(Base):
# ...
addresses = relationship("Address")
在删除父User
对象时,address
中的行不会被删除,而是被解除关联:
>>> 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。delete
和delete-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"
:
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"
设置在两个关系上都配置了,则级联操作将继续通过所有Parent
和Child
对象进行级联,加载遇到的每个children
和parents
集合并删除所有连接的内容。通常不希望“delete”级联双向配置。
另请参阅
从多对多表中删除行
使用外键 ON DELETE 处理多对多关系 ### 使用 ORM 关系的外键 ON DELETE 级联处理
SQLAlchemy 的“delete”级联行为与数据库 FOREIGN KEY
约束的 ON DELETE
特性重叠。SQLAlchemy 允许使用 ForeignKey
和 ForeignKeyConstraint
构造配置这些模式级 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
:
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")
当删除父行时,上述配置的行为如下:
-
应用程序调用
session.delete(my_parent)
,其中my_parent
是Parent
的一个实例。 -
当
Session
下次将更改刷新到数据库时,my_parent.children
集合中的所有 当前加载的 项目都将由 ORM 删除,这意味着为每条记录发出一个DELETE
语句。 -
如果
my_parent.children
集合是未加载的,则不会发出DELETE
语句。如果在此relationship()
上未设置relationship.passive_deletes
标志,则将为未加载的Child
对象发出SELECT
语句。 -
然后会为
my_parent
行本身发出DELETE
语句。 -
数据库级别的
ON DELETE CASCADE
设置确保将删除所有引用受影响的parent
行的child
中的行。 -
由
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
,如下所示:
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
对象的操作如下:
-
使用
Session.delete()
标记要删除的Parent
对象。 -
当发生刷新时,如果未加载
Parent.children
集合,则 ORM 将首先发出 SELECT 语句,以加载与Parent.children
对应的Child
对象。 -
然后,将发出针对与该父行对应的
association
中的行的DELETE
语句。 -
对于每个受此立即删除影响的
Child
对象,由于配置了passive_deletes=True
,工作单元将不需要尝试为每个Child.parents
集合发出 SELECT 语句,因为假定将删除association
中的相应行。 -
对于从
Parent.children
加载的每个Child
对象,都会发出DELETE
语句。 ## delete-orphan
delete-orphan
级联会为delete
级联添加行为,这样当子对象与父对象取消关联时,子对象将被标记为删除,而不仅仅是在父对象被标记为删除时。当处理一个与父对象“拥有”关系的相关对象时,这是一种常见的特性,该关系具有 NOT NULL 外键,因此从父集合中删除项目会导致其被删除。
delete-orphan
级联意味着每个子对象一次只能有一个父对象,并且在绝大多数情况下,它只配置在一对多关系上。对于设置在多对一或多对多关系上的非常罕见的情况,可以通过配置relationship.single_parent
参数来强制“多”端一次只允许一个对象,该参数建立了 Python 端验证,确保对象一次只与一个父对象关联,但这大大限制了“多”关系的功能,通常不是所需的。
另请参阅
对于关系
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
关联的集合中:
>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True
当上述会话提交时,所有属性都将过期。下一次访问 user.addresses
将重新加载集合,显示所需的状态:
>>> session.commit()
>>> address in user.addresses
False
拦截 Session.delete()
并自动调用其过期的方法有一种方法;请参阅 ExpireRelationshipOnFKChange 查看详情。然而,通常的做法是在集合内删除项目时直接放弃使用 Session.delete()
,而是使用级联行为自动调用删除操作,因为将对象从父集合中删除的结果。delete-orphan
级联可以实现这一点,如下例所示:
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
,该标志会触发一个断言,即此相关对象不应同时与任何其他父对象共享:
class User(Base):
# ...
preference = relationship(
"Preference", cascade="all, delete-orphan", single_parent=True
)
如果上面的情况下,一个假设的Preference
对象从一个User
中移除,它将在 flush 时被删除:
some_user.preference = None
session.flush() # will delete the Preference object
另请参阅
有关级联的详细信息,请参阅 Cascades。## save-update
save-update
级联表示当通过Session.add()
将对象放入Session
时,通过此relationship()
与之关联的所有对象也应该被添加到同一个Session
中。假设我们有一个对象user1
,它有两个相关对象address1
、address2
:
>>> user1 = User()
>>> address1, address2 = Address(), Address()
>>> user1.addresses = [address1, address2]
如果我们将user1
添加到Session
中,它也会隐式添加address1
、address2
:
>>> sess = Session()
>>> sess.add(user1)
>>> address1 in sess
True
save-update
级联也会影响已经存在于Session
中的对象的属性操作。如果我们向user1.addresses
集合添加第三个对象address3
,它将成为该Session
的状态的一部分:
>>> address3 = Address()
>>> user1.addresses.append(address3)
>>> address3 in sess
True
当从集合中删除项目或将对象与标量属性取消关联时,save-update
级联可能会表现出令人惊讶的行为。在某些情况下,被孤立的对象仍然可能被拉入原父对象的Session
;这是为了 flush 进程能够适当处理该相关对象。这种情况通常只会在对象从一个Session
中移除并添加到另一个Session
时出现:
>>> 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_populates
或relationship.backref
参数创建两个相互引用的relationship()
对象时。
当将一个未与Session
关联的对象分配给与Session
关联的父对象的属性或集合时,该对象将自动添加到同一个Session
中。然而,反向操作不会产生这种效果;当分配一个未与Session
关联的对象时,分配给一个与Session
关联的子对象,不会自动将父对象添加到Session
中。这种行为的总体主题被称为“级联反向引用”,代表了作为 SQLAlchemy 2.0 的标准化行为的变化。
为了说明,假设有一系列通过关系Order.items
和Item.order
与Item
对象双向关联的Order
对象的映射:
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
对象并附加到该Order
的Order.items
集合中,Item
将自动级联到相同的Session
中:
>>> 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.items
和Item.order
的双向性意味着附加到Order.items
也会赋值给Item.order
。同时,save-update
级联允许将Item
对象添加到与父Order
已关联的同一个Session
中。
然而,如果上述操作在反向方向进行,即将Item.order
赋值而不是直接附加到Order.item
,则级联操作不会自动进行到Session
中,即使对象赋值Order.items
和Item.order
的状态与前面的示例相同:
>>> 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
中:
>>> 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_populates
或relationship.backref
参数创建相互引用的两个单独的relationship()
对象时。
一个未与Session
相关联的对象,当分配给与Session
相关联的父对象的属性或集合时,将自动添加到相同的Session
中。但是,相反的操作不会产生这种效果;一个未与Session
相关联的对象,其中一个与Session
相关联的子对象被分配,将不会自动将该父对象添加到Session
中。此行为的整体主题称为“级联反向引用”,并代表了作为 SQLAlchemy 2.0 的标准化行为的变化。
为了说明,假设有一系列通过关系Order.items
和Item.order
与Item
对象双向关联的Order
对象的映射:
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
对象并将其附加到该Order
的Order.items
集合中,那么Item
将自动级联到相同的Session
中:
>>> 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.items
和Item.order
的双向性意味着附加到Order.items
也会赋值给Item.order
。同时,save-update
级联允许将Item
对象添加到与父Order
已关联的相同Session
中。
但是,如果上述操作是以相反方向执行的,即将Item.order
赋值而不是直接附加到Order.item
,则即使对象分配Order.items
和Item.order
的状态与前面的示例相同,也不会自动进入到Session
的级联操作中:
>>> 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
中:
>>> 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
:
class User(Base):
# ...
addresses = relationship("Address", cascade="all, delete")
如果使用上述映射,我们有一个User
对象和两个相关的Address
对象:
>>> user1 = sess1.scalars(select(User).filter_by(id=1)).first()
>>> address1, address2 = user1.addresses
如果我们标记user1
进行删除,在刷新操作进行后,address1
和address2
也将被删除:
>>> 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
来解除user1
与address1
和address2
的关联。使用以下映射:
class User(Base):
# ...
addresses = relationship("Address")
在删除父User
对象时,address
中的行不会被删除,而是被解除关联:
>>> 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"
设置:
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"
设置在两个关系上都配置了,则级联操作将继续通过所有Parent
和Child
对象,加载遇到的每个children
和parents
集合,并删除所有连接的内容。通常不希望将“删除”级联配置为双向。
另见
从多对多表中删除行
使用外键 ON DELETE 与多对多关系 ### 使用 ORM 关系的外键 ON DELETE 级联
SQLAlchemy 的“delete”级联行为与数据库FOREIGN KEY
约束的ON DELETE
特性重叠。SQLAlchemy 允许使用ForeignKey
和ForeignKeyConstraint
构造配置这些模式级 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
:
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")
当删除父行时,上述配置的行为如下:
-
应用程序调用
session.delete(my_parent)
,其中my_parent
是Parent
类的一个实例。 -
当
Session
下次将更改刷新到数据库时,my_parent.children
集合中的当前加载的所有项目都将被 ORM 删除,这意味着为每个记录发出一个DELETE
语句。 -
如果
my_parent.children
集合未加载,则不会发出任何DELETE
语句。如果在这个relationship()
上未设置relationship.passive_deletes
标志,则会发出一个用于未加载的Child
对象的SELECT
语句。 -
然后为
my_parent
行本身发出一个DELETE
语句。 -
数据库级别的
ON DELETE CASCADE
设置确保了所有引用受影响的parent
行的child
行也被删除。 -
由
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
,如下所示:
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
对象的过程如下:
-
使用
Session.delete()
标记要删除的Parent
对象。 -
当刷新发生时,如果未加载
Parent.children
集合,则 ORM 将首先发出 SELECT 语句以加载与Parent.children
对应的Child
对象。 -
然后会为对应于该父行的
association
中的行发出DELETE
语句。 -
对于由此立即删除受影响的每个
Child
对象,因为配置了passive_deletes=True
,工作单元不需要尝试为每个Child.parents
集合发出 SELECT 语句,因为假设将删除association
中对应的行。 -
然后对从
Parent.children
加载的每个Child
对象发出DELETE
语句。 ### 使用删除级联处理多对多关系
cascade="all, delete"
选项与多对多关系同样有效,即使用 relationship.secondary
指示关联表的关系。当删除父对象并因此取消关联其相关对象时,工作单元进程通常会删除关联表中的行,但保留相关对象。当与 cascade="all, delete"
结合使用时,将为子行本身执行额外的 DELETE
语句。
以下示例将多对多的示例调整为示例,以说明在关联的一侧上设置 cascade="all, delete"
。
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"
设置被配置在两个关系上,那么级联操作将继续通过所有 Parent
和 Child
对象进行级联,加载遇到的每个 children
和 parents
集合,并删除所有连接的内容。通常不希望双向配置“delete”级联。
另请参阅
从多对多表中删除行
使用外键 ON DELETE 处理多对多关系
使用 ORM 关系中的外键 ON DELETE 级联
SQLAlchemy 的“delete”级联的行为与数据库FOREIGN KEY
约束的ON DELETE
特性重叠。SQLAlchemy 允许使用 ForeignKey
和 ForeignKeyConstraint
构造配置这些模式级别的 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
:
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")
当删除父行时,上述配置的行为如下:
-
应用程序调用
session.delete(my_parent)
,其中my_parent
是Parent
的实例。 -
当
Session
下次将更改刷新到数据库时,my_parent.children
集合中的所有当前加载的项目都将被 ORM 删除,这意味着为每个记录发出了一个DELETE
语句。 -
如果
my_parent.children
集合未加载,则不会发出DELETE
语句。 如果在此relationship()
上未设置relationship.passive_deletes
标志,那么将会发出一个针对未加载的Child
对象的SELECT
语句。 -
针对
my_parent
行本身发出了一个DELETE
语句。 -
数据库级别的
ON DELETE CASCADE
设置确保了所有引用受影响的parent
行的child
中的行也被删除。 -
由
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
,如下所示:
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
对象的过程如下:
-
使用
Session.delete()
标记要删除的Parent
对象。 -
当发生刷新时,如果未加载
Parent.children
集合,则 ORM 首先会发出 SELECT 语句,以加载与Parent.children
对应的Child
对象。 -
然后会为与该父行对应的
association
中的行发出DELETE
语句。 -
对于受此即时删除影响的每个
Child
对象,因为配置了passive_deletes=True
,工作单元不需要尝试为每个Child.parents
集合发出 SELECT 语句,因为假设将删除association
中的相应行。 -
然后会为从
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
关联的集合中:
>>> address = user.addresses[1]
>>> session.delete(address)
>>> session.flush()
>>> address in user.addresses
True
当以上会话提交时,所有属性都会过期。对user.addresses
的下一次访问将重新加载集合,显示所需的状态:
>>> session.commit()
>>> address in user.addresses
False
有一个拦截Session.delete()
并自动调用此过期的配方;参见ExpireRelationshipOnFKChange。然而,删除集合中的项目的通常做法是直接放弃使用Session.delete()
,而是使用级联行为自动调用删除作为从父集合中删除对象的结果。delete-orphan
级联实现了这一点,如下例所示:
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
,它调用一个断言,指出这个相关对象不会同时与任何其他父对象共享:
class User(Base):
# ...
preference = relationship(
"Preference", cascade="all, delete-orphan", single_parent=True
)
上面,如果一个假设的Preference
对象从一个User
中移除,它将在刷新时被删除:
some_user.preference = None
session.flush() # will delete the Preference object
另请参阅
级联以获取级联的详细信息。
事务和连接管理
事务管理
在 1.4 版本中更改:会话事务管理已经修订为更清晰、更易于使用。特别是,它现在具有“自动开始”操作,这意味着可以控制事务开始的时间点,而不使用传统的“自动提交”模式。
Session
一次跟踪单个“虚拟”事务的状态,使用一个称为SessionTransaction
的对象。然后,此对象利用绑定到Session
对象的基础Engine
或引擎,根据需要使用Connection
对象开始真实的连接级事务。
此“虚拟”事务在需要时会自动创建,或者可以使用Session.begin()
方法启动。尽可能地支持 Python 上下文管理器的使用,既在创建Session
对象的层面,也在维护SessionTransaction
的范围方面。
假设我们从Session
开始:
from sqlalchemy.orm import Session
session = Session(engine)
我们现在可以使用上下文管理器在一个确定的事务中运行操作:
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
也可以使用按需自动开始事务的“随时提交”方法;这些只需要提交或回滚:
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
方法来确保释放所有资源:
# expunges all objects, releases all transactions unconditionally
# (with rollback), releases all database connections back to their
# engines
session.close()
最后,会话的构建/关闭过程本身也可以通过上下文管理器运行。这是确保 Session
对象使用范围在一个固定块内的最佳方式。首先通过 Session
构造函数进行说明:
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
可以以相同的方式使用:
Session = sessionmaker(engine)
with Session() as session:
with session.begin():
session.add(some_object)
# commits
# closes the Session
sessionmaker
本身包含一个 sessionmaker.begin()
方法,允许同时进行两个操作:
with Session.begin() as session:
session.add(some_object)
使用 SAVEPOINT
如果底层引擎支持,可以使用 Session.begin_nested()
方法来划定 SAVEPOINT 事务:
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()
通常用作上下文管理器,可以捕获特定的每个实例错误,并在该事务状态的部分发出回滚,而不会回滚整个事务,如下例所示:
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 时,外部事务会得以保留。在下面的示例中,将一系列数据持久化到数据库中,并且偶尔会跳过“重复的主键”记录,而无需回滚整个操作:
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
具有等效的事务语义,无论是在sessionmaker
与Engine
的级别,还是在Session
与Connection
的级别。以下部分根据以下方案详细说明这些情况:
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:
边提交边进行
Session
和Connection
都具有Connection.commit()
和Connection.rollback()
方法。使用 SQLAlchemy 2.0 风格的操作,这些方法在所有情况下都会影响最外层的事务。对于Session
,假定Session.autobegin
保持默认值True
。
Engine
:
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
:
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
都具有Engine.begin()
方法,该方法将获取一个新对象来执行 SQL 语句(分别是Session
和Connection
),然后返回一个上下文管理器,用于维护该对象的开始/提交/回滚上下文。
引擎:
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
会话:
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 系列相反。
引擎:
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
会话:
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
上述模式更常用地使用上下文管理器调用:
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
:
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
的浅拷贝,该浅拷贝与父引擎共享相同的连接池。当操作将被分成“事务”和“自动提交”操作时,这通常是更可取的:
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 隔离的会话应该以只读方式使用,即:
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
并传递一个设置为自动提交的引擎:
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
...
对于Session
或sessionmaker
配置了多个binds
的情况,我们可以重新完整指定binds
参数,或者如果我们只想替换特定的 binds,则可以使用Session.bind_mapper()
或Session.bind_table()
方法:
with Session() as session:
session.bind_mapper(User, autocommit_engine)
为个别事务设置隔离级别
关于隔离级别的一个关键警告是,在已经开始事务的Connection
上不能安全地修改设置。数据库不能在进行中的事务中更改隔离级别,而一些 DBAPIs 和 SQLAlchemy 方言在这方面的行为不一致。
因此,最好使用一个提前绑定到具有所需隔离级别的引擎的Session
。然而,通过在事务开始时使用Session.connection()
方法可以影响每个连接的隔离级别:
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()
来设置每个连接的事务隔离级别:
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,这将使外部事务处于传递时的相同状态。
当测试拆解时,外部事务会被回滚,以便将测试中的任何数据更改还原:
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
开始:
from sqlalchemy.orm import Session
session = Session(engine)
现在我们可以使用上下文管理器在标记的事务中运行操作:
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
也可以使用逐步提交的方法,在需要时自动开始事务;只需提交或回滚:
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
方法确保释放所有资源:
# expunges all objects, releases all transactions unconditionally
# (with rollback), releases all database connections back to their
# engines
session.close()
最后,会话构建/关闭过程本身也可以通过上下文管理器运行。这是确保Session
对象使用范围在固定块内的最佳方法。首先通过Session
构造函数进行说明:
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
也可以以相同的方式使用:
Session = sessionmaker(engine)
with Session() as session:
with session.begin():
session.add(some_object)
# commits
# closes the Session
sessionmaker
本身包括一个sessionmaker.begin()
方法,允许同时执行这两个操作:
with Session.begin() as session:
session.add(some_object)
使用 SAVEPOINT
如果底层引擎支持 SAVEPOINT 事务,则可以使用Session.begin_nested()
方法来区分 SAVEPOINT 事务:
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()
通常用作上下文管理器,其中可以捕获特定的每个实例错误,与事务状态的部分回滚一起使用,而不是回滚整个事务,如下例所示:
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 时,外部事务会被维持。在下面的示例中,一组数据被持久化到数据库中,偶尔会跳过“重复的主键”记录,而不会回滚整个操作:
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
与引擎
的级别,以及会话
与连接
的级别。以下各节根据以下方案详细说明了这些情况:
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
。
引擎
:
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 = 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 语句的新对象(分别是会话
和连接
),然后返回一个上下文管理器,该管理器将为该对象维护一个开始/提交/回滚的上下文。
引擎:
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
会话:
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 系列相反。
引擎:
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
会话:
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
上述模式更惯用地使用上下文管理器调用:
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
:
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
的浅拷贝,该拷贝与父引擎共享相同的连接池。当操作将被分为“事务性”和“自动提交”操作时,通常最好使用此方法:
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 隔离级别的会话应该以只读方式使用,也就是:
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
,并传递一个设置为自动提交的引擎:
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
...
对于配置了多个“绑定”(Session
或sessionmaker
)的情况,我们可以重新完全指定binds
参数,或者如果我们只想替换特定的绑定,则可以使用Session.bind_mapper()
或Session.bind_table()
方法:
with Session() as session:
session.bind_mapper(User, autocommit_engine)
为单个事务设置隔离级别
关于隔离级别的一个关键注意事项是,不能在已经开始事务的Connection
上安全地修改设置。数据库不能更改正在进行的事务的隔离级别,并且一些 DBAPIs 和 SQLAlchemy 方言在这个领域的行为不一致。
因此,最好使用一个与所需隔离级别的引擎直接绑定的Session
。然而,可以通过在事务开始时使用Session.connection()
方法来影响每个连接的隔离级别:
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()
设置每个连接的事务隔离级别:
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()
方法进行分割:
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()
通常用作上下文管理器,其中可以捕获特定的每个实例错误,在此事务状态的一部分上发出回滚,而无需回滚整个事务,就像下面的示例中一样:
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 时,外部事务会得以保留。在下面的示例中,将一系列数据持久化到数据库中,偶尔会跳过“重复主键”记录,而不会回滚整个操作:
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
都具有等效的事务语义,无论是在sessionmaker
与Engine
之间,还是在Session
与Connection
之间。以下各节详细说明了这些情景,基于以下方案:
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:
边做边提交
Session
和 Connection
均提供了 Connection.commit()
和 Connection.rollback()
方法。使用 SQLAlchemy 2.0 风格的操作时,这些方法在所有情况下都会影响最外层的事务。对于 Session
,假定 Session.autobegin
保持默认值 True
。
Engine
:
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
:
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
均提供了 Engine.begin()
方法,该方法将获取一个新对象来执行 SQL 语句(分别是 Session
和 Connection
),然后返回一个上下文管理器,用于维护该对象的开始/提交/回滚上下文。
引擎:
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
会话:
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 系列相反。
引擎:
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
会话:
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
和Connection
均提供Connection.commit()
和Connection.rollback()
方法。使用 SQLAlchemy 2.0 风格的操作,这些方法在所有情况下都会影响到最外层的事务。对于Session
,假定Session.autobegin
保持其默认值为True
。
Engine
:
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
:
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
均提供Engine.begin()
方法,该方法将获取一个新对象以执行 SQL 语句(分别是Session
和Connection
),然后返回一个上下文管理器,用于维护该对象的开始/提交/回滚上下文。
引擎:
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
会话:
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 系列相反。
引擎:
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
会话:
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
:
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
上述模式通常使用上下文管理器更具惯用性:
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
:
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
时,它充当引擎和连接的门面,但不直接暴露事务隔离。因此,为了影响事务隔离级别,我们需要在适当的时候对Engine
或Connection
进行操作。
也请参阅
设置事务隔离级别,包括 DBAPI 自动提交 - 一定要查看 SQLAlchemy Connection
对象级别上隔离级别的工作方式。
设置会话/引擎范围的隔离级别
要全局设置一个具有特定隔离级别的Session
或sessionmaker
,第一种技术是可以在所有情况下构造一个具有特定隔离级别的Engine
,然后将其用作Session
和/或sessionmaker
的连接来源:
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
的浅拷贝,该浅拷贝与父引擎共享相同的连接池。当操作将被分为“事务”和“自动提交”操作时,通常更可取:
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 应该以只读方式使用,即:
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
并传递一个设置为自动提交的引擎:
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
...
对于配置有多个binds
的Session
或sessionmaker
的情况,我们可以重新指定完整的binds
参数,或者如果我们只想替换特定的绑定,我们可以使用Session.bind_mapper()
或Session.bind_table()
方法:
with Session() as session:
session.bind_mapper(User, autocommit_engine)
为单个事务设置隔离级别
关于隔离级别的一个关键注意事项是,不能在已经启动事务的 Connection
上安全地修改设置。数据库无法更改正在进行的事务的隔离级别,并且一些 DBAPI 和 SQLAlchemy 方言在这个领域的行为不一致。
因此最好使用一个最初绑定到具有所需隔离级别的引擎的 Session
。但是,通过在事务开始时使用 Session.connection()
方法,可以影响每个连接的隔离级别:
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()
可以用于设置每个连接的事务隔离级别:
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 设置隔离级别
要为 Session
或 sessionmaker
全局设置特定的隔离级别,第一种技术是可以在所有情况下构建一个针对特定隔离级别的 Engine
,然后将其用作 Session
和/或 sessionmaker
的连接来源:
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
的浅拷贝,与父引擎共享相同的连接池。当操作将分成“事务”和“自动提交”操作时,这通常是首选:
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 隔离的会话应该以只读方式使用,即:
with autocommit_session() as session:
some_objects = session.execute(text("<statement>"))
some_other_objects = session.execute(text("<statement>"))
# closes connection
为单个会话设置隔离
当我们创建一个新的Session
时,可以直接传递bind
参数,覆盖预先存在的绑定。例如,我们可以从默认的sessionmaker
创建我们的Session
,并传递设置为自动提交的引擎:
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
...
对于配置了多个binds
的Session
或sessionmaker
的情况,我们可以重新完全指定binds
参数,或者如果我们只想替换特定的绑定,我们可以使用Session.bind_mapper()
或Session.bind_table()
方法:
with Session() as session:
session.bind_mapper(User, autocommit_engine)
为单个事务设置隔离
关于隔离级别的一个关键警告是,在已经开始事务的 Connection
上无法安全地修改设置。数据库无法更改正在进行的事务的隔离级别,并且一些 DBAPI 和 SQLAlchemy 方言在这个领域的行为不一致。
因此,最好使用一个明确绑定到具有所需隔离级别的引擎的 Session
。但是,可以通过在事务开始时使用 Session.connection()
方法来影响每个连接的隔离级别:
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()
可用于设置每个连接的事务隔离级别:
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,这将使外部事务保持与传递时相同的状态。
当测试拆卸时,外部事务将被回滚,以便撤消测试期间的任何数据更改:
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 的一部分,以确保其按预期工作。
附加持久化技术
原文:
docs.sqlalchemy.org/en/20/orm/persistence_techniques.html
将 SQL 插入/更新表达式嵌入到刷新中
此功能允许将数据库列的值设置为 SQL 表达式而不是文字值。这对于原子更新、调用存储过程等特别有用。您所要做的就是将表达式分配给属性:
class SomeClass(Base):
__tablename__ = "some_table"
# ...
value = mapped_column(Integer)
someobject = session.get(SomeClass, 5)
# set 'value' attribute to a SQL expression adding one
someobject.value = SomeClass.value + 1
# issues "UPDATE some_table SET value=value+1"
session.commit()
这种技术对于 INSERT 和 UPDATE 语句都适用。在刷新/提交操作之后,上述someobject
的value
属性将会过期,因此在下次访问时,新生成的值将从数据库加载。
该功能还具有条件支持,可以与主键列一起使用。对于支持 RETURNING 的后端(包括 Oracle、SQL Server、MariaDB 10.5、SQLite 3.35),还可以将 SQL 表达式分配给主键列。这允许对 SQL 表达式进行评估,并允许在 INSERT 时修改主键值的服务器端触发器成功地由 ORM 作为对象的主键的一部分检索:
class Foo(Base):
__tablename__ = "foo"
pk = mapped_column(Integer, primary_key=True)
bar = mapped_column(Integer)
e = create_engine("postgresql+psycopg2://scott:tiger@localhost/test", echo=True)
Base.metadata.create_all(e)
session = Session(e)
foo = Foo(pk=sql.select(sql.func.coalesce(sql.func.max(Foo.pk) + 1, 1)))
session.add(foo)
session.commit()
在 PostgreSQL 上,上述Session
将发出以下 INSERT:
INSERT INTO foo (foopk, bar) VALUES
((SELECT coalesce(max(foo.foopk) + %(max_1)s, %(coalesce_2)s) AS coalesce_1
FROM foo), %(bar)s) RETURNING foo.foopk
新版本 1.3 中:现在可以在 ORM 刷新期间将 SQL 表达式传递给主键列;如果数据库支持 RETURNING,或者正在使用 pysqlite,ORM 将能够将服务器生成的值作为主键属性的值检索出来。 ## 在会话中使用 SQL 表达式
SQL 表达式和字符串可以通过Session
在其事务上下文中执行。最简单的方法是使用Session.execute()
方法,它以与Engine
或Connection
相同的方式返回一个CursorResult
:
Session = sessionmaker(bind=engine)
session = Session()
# execute a string statement
result = session.execute(text("select * from table where id=:id"), {"id": 7})
# execute a SQL expression construct
result = session.execute(select(mytable).where(mytable.c.id == 7))
由Session
持有的当前Connection
可通过Session.connection()
方法访问:
connection = session.connection()
上述示例涉及绑定到单个Engine
或Connection
的Session
。要使用绑定到多个引擎或根本没有绑定(即依赖于绑定元数据)的Session
执行语句,Session.execute()
和Session.connection()
都接受一个绑定参数字典Session.execute.bind_arguments
,其中可能包括“mapper”,该参数传递了一个映射类或Mapper
实例,用于定位所需引擎的适当上下文:
Session = sessionmaker()
session = Session()
# need to specify mapper or class when executing
result = session.execute(
text("select * from table where id=:id"),
{"id": 7},
bind_arguments={"mapper": MyMappedClass},
)
result = session.execute(
select(mytable).where(mytable.c.id == 7), bind_arguments={"mapper": MyMappedClass}
)
connection = session.connection(MyMappedClass)
从 1.4 版本开始更改:Session.execute()
中的mapper
和clause
参数现在作为发送给Session.execute.bind_arguments
参数的字典的一部分传递。之前的参数仍然被接受,但此用法已被弃用。 ## 强制在具有默认值的列上使用 NULL
ORM 将任何从未在对象上设置过的属性视为“默认”情况;该属性将在 INSERT 语句中被省略:
class MyObject(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
data = mapped_column(String(50), nullable=True)
obj = MyObject(id=1)
session.add(obj)
session.commit() # INSERT with the 'data' column omitted; the database
# itself will persist this as the NULL value
在 INSERT 中省略一列意味着该列将设置为 NULL 值,除非该列设置了默认值,此时默认值将被保留。这既适用于纯 SQL 角度的服务器端默认值,也适用于 SQLAlchemy 在客户端和服务器端默认值下的插入行为:
class MyObject(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
data = mapped_column(String(50), nullable=True, server_default="default")
obj = MyObject(id=1)
session.add(obj)
session.commit() # INSERT with the 'data' column omitted; the database
# itself will persist this as the value 'default'
然而,在 ORM 中,即使将 Python 值None
明确地分配给对象,这与从未分配值一样处理:
class MyObject(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
data = mapped_column(String(50), nullable=True, server_default="default")
obj = MyObject(id=1, data=None)
session.add(obj)
session.commit() # INSERT with the 'data' column explicitly set to None;
# the ORM still omits it from the statement and the
# database will still persist this as the value 'default'
上述操作将将服务器默认值"default"
持久化到data
列中,而不是 SQL NULL,即使传递了None
;这是 ORM 的一个长期行为,许多应用程序将其视为一种假设。
那么如果我们想要实际将 NULL 放入这一列中,即使该列有默认值呢?有两种方法。一种是在每个实例级别上,我们使用null
SQL 构造来分配属性:
from sqlalchemy import null
obj = MyObject(id=1, data=null())
session.add(obj)
session.commit() # INSERT with the 'data' column explicitly set as null();
# the ORM uses this directly, bypassing all client-
# and server-side defaults, and the database will
# persist this as the NULL value
null
SQL 构造总是直接在目标 INSERT 语句中转换为 SQL NULL 值。
如果我们希望能够使用 Python 值 None
并且尽管存在列默认值,也希望将其持久化为 NULL,则可以在 ORM 中使用一个 Core 级别的修改器 TypeEngine.evaluates_none()
进行配置,该修改器指示 ORM 应该将值 None
与任何其他值一样对待,并将其传递,而不是将其视为“缺失”的值而省略它:
class MyObject(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
data = mapped_column(
String(50).evaluates_none(), # indicate that None should always be passed
nullable=True,
server_default="default",
)
obj = MyObject(id=1, data=None)
session.add(obj)
session.commit() # INSERT with the 'data' column explicitly set to None;
# the ORM uses this directly, bypassing all client-
# and server-side defaults, and the database will
# persist this as the NULL value
``` ## 获取服务器生成的默认值
如在服务器调用的 DDL-显式默认表达式和标记隐式生成的值、时间戳和触发列章节中介绍的,Core 支持数据库列的概念,即数据库自身在 INSERT 语句中生成一个值,以及在较少见的情况下,在 UPDATE 语句中生成一个值。ORM 功能支持此类列,以便在刷新时能够获取这些新生成的值。在服务器生成的主键列的情况下,这种行为是必需的,因为一旦对象被持久化,ORM 就必须知道对象的主键。
在绝大多数情况下,由数据库自动生成值的主键列是简单的整数列,数据库实现为所谓的“自增”列,或者从与列关联的序列中生成。SQLAlchemy Core 中的每个数据库方言都支持一种检索这些主键值的方法,这种方法通常是 Python DBAPI 本地的,并且一般情况下这个过程是自动的。关于这一点还有更多的文档资料在 `Column.autoincrement`。
对于不是主键列或不是简单自增整数列的服务器生成列,ORM 要求这些列用适当的 `server_default` 指令标记,以允许 ORM 检索此值。然而,并非所有方法都支持所有后端,因此必须小心使用适当的方法。要回答的两个问题是,1\. 这一列是否是主键,2\. 数据库是否支持 RETURNING 或等效方法,如“OUTPUT inserted”;这些是在调用 INSERT 或 UPDATE 语句时同时返回服务器生成的值的 SQL 短语。RETURNING 目前由 PostgreSQL、Oracle、MariaDB 10.5、SQLite 3.35 和 SQL Server 支持。
### 情况 1:非主键,支持 RETURNING 或等效方法
在这种情况下,列应标记为 `FetchedValue` 或具有显式的 `Column.server_default`。当执行 INSERT 语句时,ORM 将自动将这些列添加到 RETURNING 子句中,假设 `Mapper.eager_defaults` 参数设置为 `True`,或者如果将其默认设置为 `"auto"`,对于同时支持 RETURNING 和 insertmanyvalues 的方言:
```py
class MyModel(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
# server-side SQL date function generates a new timestamp
timestamp = mapped_column(DateTime(), server_default=func.now())
# some other server-side function not named here, such as a trigger,
# populates a value into this column during INSERT
special_identifier = mapped_column(String(50), server_default=FetchedValue())
# set eager defaults to True. This is usually optional, as if the
# backend supports RETURNING + insertmanyvalues, eager defaults
# will take place regardless on INSERT
__mapper_args__ = {"eager_defaults": True}
上面的 INSERT 语句未为“timestamp”或“special_identifier”指定客户端端显式值时,将在 RETURNING 子句中包括“timestamp”和“special_identifier”列,以便立即可用。在 PostgreSQL 数据库中,上述表的 INSERT 如下所示:
INSERT INTO my_table DEFAULT VALUES RETURNING my_table.id, my_table.timestamp, my_table.special_identifier
从版本 2.0.0rc1 更改:Mapper.eager_defaults
参数现在默认为新设置 "auto"
,它将自动使用 RETURNING 在 INSERT 时获取服务器生成的默认值,如果后备数据库同时支持 RETURNING 和 insertmanyvalues。
注意
Mapper.eager_defaults
的 "auto"
值仅适用于 INSERT 语句。即使可用,UPDATE 语句也不会使用 RETURNING,除非 Mapper.eager_defaults
设置为 True
。这是因为 UPDATE 没有等价的“insertmanyvalues”功能,因此 UPDATE RETURNING 将要求为每个要 UPDATE 的行单独发出 UPDATE 语句。
情况 2:表中包含触发器生成的值,这些值与 RETURNING 不兼容
Mapper.eager_defaults
的 "auto"
设置意味着支持 RETURNING 的后端通常会在 INSERT 语句中使用 RETURNING 来检索新生成的默认值。但是,存在使用触发器生成的服务器生成值的限制,无法使用 RETURNING:
-
SQL Server 不允许在 INSERT 语句中使用 RETURNING 来检索触发器生成的值;该语句将失败。
-
SQLite 在将 RETURNING 与触发器结合使用时存在限制,因此 RETURNING 子句将不会具有 INSERTed 值可用
-
其他后端可能在与触发器一起使用 RETURNING 或其他类型的服务器生成的值时存在限制。
要禁用 RETURNING 用于这些值的使用,不仅包括服务器生成的默认值,还要确保 ORM 永远不会对特定表使用 RETURNING,请为映射的Table
指定Table.implicit_returning
参数为False
。使用声明性映射,看起来像这样:
class MyModel(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(primary_key=True)
data: Mapped[str] = mapped_column(String(50))
# assume a database trigger populates a value into this column
# during INSERT
special_identifier = mapped_column(String(50), server_default=FetchedValue())
# disable all use of RETURNING for the table
__table_args__ = {"implicit_returning": False}
在使用 pyodbc 驱动程序的 SQL Server 上,上述表的 INSERT 不会使用 RETURNING,并将使用 SQL Server scope_identity()
函数检索新生成的主键值:
INSERT INTO my_table (data) VALUES (?); select scope_identity()
另请参阅
INSERT 行为 - SQL Server 方言获取新生成的主键值的方法的背景
情况 3:非主键、不支持或不需要 RETURNING 或等效功能
这种情况与上述情况 1 相同,除了我们通常不想使用Mapper.eager_defaults
,因为在没有支持 RETURNING 的情况下,它的当前实现是发出每行一个 SELECT,这是不高效的。因此,在下面的映射中省略了该参数:
class MyModel(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
timestamp = mapped_column(DateTime(), server_default=func.now())
# assume a database trigger populates a value into this column
# during INSERT
special_identifier = mapped_column(String(50), server_default=FetchedValue())
在使用上述映射的记录在不包括 RETURNING 或“insertmanyvalues”支持的后端上 INSERT 之后, “timestamp”和“special_identifier”列将保持为空,并且将在刷新后首次访问时通过第二个 SELECT 语句获取,例如它们被标记为“过期”时。
如果Mapper.eager_defaults
显式提供了一个值为True
,并且后端数据库不支持 RETURNING 或等效功能,则 ORM 将在 INSERT 语句之后立即发出一个 SELECT 语句以获取新生成的值;如果没有可用的 RETURNING,则 ORM 目前无法批量选择许多新插入的行。这通常是不希望的,因为它会在刷新过程中添加额外的 SELECT 语句,这些语句可能是不必要的。将上述映射与Mapper.eager_defaults
标志设置为 True 一起针对 MySQL(不是 MariaDB)使用会在刷新时产生如下 SQL:
INSERT INTO my_table () VALUES ()
-- when eager_defaults **is** used, but RETURNING is not supported
SELECT my_table.timestamp AS my_table_timestamp, my_table.special_identifier AS my_table_special_identifier
FROM my_table WHERE my_table.id = %s
未来的 SQLAlchemy 版本可能会试图改进在没有 RETURNING 的情况下的急切默认值的效率,以便在单个 SELECT 语句中批量处理多行。
情况 4:支持主键、RETURNING 或等效功能
具有服务器生成值的主键列必须在 INSERT 后立即获取;ORM 只能访问具有主键值的行,因此如果主键由服务器生成,则 ORM 需要一种在 INSERT 后立即检索该新值的方法。
如上所述,对于整数的“自增”列,以及标记为Identity
的列和诸如 PostgreSQL SERIAL 等特殊构造,这些类型由 Core 自动处理;数据库包括用于获取“最后插入的 id”的函数,在不支持 RETURNING 的情况下,以及在支持 RETURNING 的情况下 SQLAlchemy 将使用它。
例如,在 Oracle 中使用标记为Identity
的列,RETURNING 会自动用于获取新的主键值:
class MyOracleModel(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(Identity(), primary_key=True)
data: Mapped[str] = mapped_column(String(50))
上述模型在 Oracle 上的 INSERT 如下所示:
INSERT INTO my_table (data) VALUES (:data) RETURNING my_table.id INTO :ret_0
SQLAlchemy 为“data”字段渲染了一个 INSERT,但只在 RETURNING 子句中包括了“id”,以便进行“id”的服务器端生成,并立即返回新值。
对于由服务器端函数或触发器生成的非整数值,以及来自表本身之外的构造的整数值,包括显式序列和触发器,必须在表元数据中标记服务器默认生成。再次以 Oracle 为例,我们可以用上面类似的表来说明使用 Sequence
构造命名的显式序列:
class MyOracleModel(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(Sequence("my_oracle_seq"), primary_key=True)
data: Mapped[str] = mapped_column(String(50))
在 Oracle 上,这个版本的模型的 INSERT 如下所示:
INSERT INTO my_table (id, data) VALUES (my_oracle_seq.nextval, :data) RETURNING my_table.id INTO :ret_0
在上面的例子中,SQLAlchemy 为主键列渲染了 my_sequence.nextval
,以便用于新的主键生成,并且还使用 RETURNING 立即获取新值。
如果数据源不是由简单的 SQL 函数或 Sequence
表示,例如在使用触发器或产生新值的数据库特定数据类型时,可以通过在列定义中使用 FetchedValue
来指示存在生成值的默认值。以下是一个使用 SQL Server TIMESTAMP 列作为主键的模型;在 SQL Server 上,这种数据类型会自动生成新值,因此在表元数据中通过为 Column.server_default
参数指示 FetchedValue
来指示:
class MySQLServerModel(Base):
__tablename__ = "my_table"
timestamp: Mapped[datetime.datetime] = mapped_column(
TIMESTAMP(), server_default=FetchedValue(), primary_key=True
)
data: Mapped[str] = mapped_column(String(50))
上述表在 SQL Server 上的 INSERT 如下所示:
INSERT INTO my_table (data) OUTPUT inserted.timestamp VALUES (?)
情况 5:主键,不支持 RETURNING 或等价的功能
在这个区域,我们正在为像 MySQL 这样的数据库生成行,其中一些生成默认值的方法是在服务器上进行的,但超出了数据库通常的自动增量程序。在这种情况下,我们必须确保 SQLAlchemy 可以“预执行”默认值,这意味着它必须是一个显式的 SQL 表达式。
注意
本节将以 MySQL 的 datetime 值为例,说明多个配方,因为此后端的 datetime 数据类型具有额外的特殊要求,这些要求对于说明很有用。但请记住,MySQL 对于用作主键的任何自动生成的数据类型都需要一个显式的“预执行”默认生成器,而不是通常的单列自增整数值。
具有 DateTime 主键的 MySQL
使用 MySQL 的DateTime
列的例子,我们使用“NOW()”SQL 函数添加了一个显式的预执行支持的默认值:
class MyModel(Base):
__tablename__ = "my_table"
timestamp = mapped_column(DateTime(), default=func.now(), primary_key=True)
在上述例子中,我们选择“NOW()”函数来向列传递日期时间值。由上述生成的 SQL 如下:
SELECT now() AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
('2018-08-09 13:08:46',)
具有 TIMESTAMP 主键的 MySQL
当在 MySQL 中使用TIMESTAMP
数据类型时,MySQL 通常会自动将服务器端默认值与此数据类型关联起来。但是,当我们将其用作主键时,核心无法检索新生成的值,除非我们自己执行函数。由于 MySQL 上的TIMESTAMP
实际上存储了一个二进制值,因此我们需要在使用“NOW()”时添加额外的“CAST”,以便检索到可以持久化到列中的二进制值:
from sqlalchemy import cast, Binary
class MyModel(Base):
__tablename__ = "my_table"
timestamp = mapped_column(
TIMESTAMP(), default=cast(func.now(), Binary), primary_key=True
)
除了选择“NOW()”函数之外,在上面的例子中,我们还额外使用Binary
数据类型结合cast()
,以便返回的值是二进制的。从上面插入的 SQL 看起来像:
SELECT CAST(now() AS BINARY) AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
(b'2018-08-09 13:08:46',)
另请参阅
列插入/更新默认值
关于急切获取客户端调用的用于 INSERT 或 UPDATE 的 SQL 表达式的注意事项
前面的例子表明了使用Column.server_default
创建包含默认生成函数的表格的 DDL。
SQLAlchemy 还支持非 DDL 服务器端默认值,如文档中所述客户端调用的 SQL 表达式; 这些“客户端调用的 SQL 表达式”是使用Column.default
和Column.onupdate
参数设置的。
当 Mapper.eager_defaults
设置为 "auto"
或 True
时,这些 SQL 表达式目前受到 ORM 中与真正的服务器端默认值相同的限制;除非将 FetchedValue
指令与 Column
关联,否则当使用 RETURNING 时,它们不会被自动获取,尽管这些表达式不是 DDL 服务器默认值,而是由 SQLAlchemy 本身积极渲染的。这个限制可能在未来的 SQLAlchemy 版本中得到解决。
FetchedValue
结构可以同时应用于 Column.server_default
或 Column.server_onupdate
,以及与 Column.default
和 Column.onupdate
一起使用的 SQL 表达式,例如下面的示例,其中 func.now()
结构用作客户端调用的 SQL 表达式,用于 Column.default
和 Column.onupdate
。为了让 Mapper.eager_defaults
的行为包括使用 RETURNING 时获取这些值,需要使用 FetchedValue
来确保进行获取:
class MyModel(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
created = mapped_column(
DateTime(), default=func.now(), server_default=FetchedValue()
)
updated = mapped_column(
DateTime(),
onupdate=func.now(),
server_default=FetchedValue(),
server_onupdate=FetchedValue(),
)
__mapper_args__ = {"eager_defaults": True}
与上述类似的映射,ORM 渲染的 INSERT 和 UPDATE 的 SQL 将在 RETURNING 子句中包含 created
和 updated
:
INSERT INTO my_table (created) VALUES (now()) RETURNING my_table.id, my_table.created, my_table.updated
UPDATE my_table SET updated=now() WHERE my_table.id = %(my_table_id)s RETURNING my_table.updated
``` ## 使用 INSERT、UPDATE 和 ON CONFLICT(即 upsert)返回 ORM 对象
SQLAlchemy 2.0 包括增强功能,用于发出几种类型的 ORM-enabled INSERT、UPDATE 和 upsert 语句。请查看文档 ORM-Enabled INSERT, UPDATE, and DELETE statements。有关 upsert,请参见 ORM “upsert” Statements。
### 使用 PostgreSQL ON CONFLICT 与 RETURNING 返回 upserted ORM 对象
本节已移至 ORM "upsert"语句。## 分区策略(例如,每个会话多个数据库后端)
### 简单的垂直分区
垂直分区将不同的类、类层次结构或映射表配置到多个数据库中,方法是通过使用`Session`的`Session.binds`参数。该参数接收一个包含任意 ORM 映射类、映射层次结构中的任意类(如声明基类或混合类)、`Table`对象和`Mapper`对象的字典作为键,然后通常引用`Engine`或不太常见的是`Connection`对象作为目标。每当`Session`需要代表特定类型的映射类发出 SQL 以定位适当的数据库连接源时,都会查询该字典:
```py
engine1 = create_engine("postgresql+psycopg2://db1")
engine2 = create_engine("postgresql+psycopg2://db2")
Session = sessionmaker()
# bind User operations to engine 1, Account operations to engine 2
Session.configure(binds={User: engine1, Account: engine2})
session = Session()
在上述情况下,针对任何一个类的 SQL 操作都将使用与该类相关联的Engine
。该功能在读取和写入操作中都是全面的;针对映射到engine1
的实体的Query
(通过查看请求项列表中的第一个实体来确定)将使用engine1
来运行查询。刷新操作将基于每个类使用两个引擎,因为它会刷新User
和Account
类型的对象。
在更常见的情况下,通常有基类或混合类可用于区分目的地不同数据库连接的操作。Session.binds
参数可以容纳任何任意的 Python 类作为键,如果发现它在特定映射类的__mro__
(Python 方法解析顺序)中,则将使用它。假设有两个声明基类代表两个不同的数据库连接:
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Session
class BaseA(DeclarativeBase):
pass
class BaseB(DeclarativeBase):
pass
class User(BaseA): ...
class Address(BaseA): ...
class GameInfo(BaseB): ...
class GameStats(BaseB): ...
Session = sessionmaker()
# all User/Address operations will be on engine 1, all
# Game operations will be on engine 2
Session.configure(binds={BaseA: engine1, BaseB: engine2})
以上,从BaseA
和BaseB
继承的类将根据它们是否从任何一个超类继承来路由它们的 SQL 操作到两个引擎之一。在一个类从多个“绑定”超类继承的情况下,将选择目标类层次结构中最高的超类来表示应该使用哪个引擎。
另请参见
Session.binds
多引擎会话的事务协调
使用多个绑定引擎时的一个注意事项是,当提交操作在一个后端成功提交后,可能在另一个后端失败。这是一个不一致性问题,在关系型数据库中通过使用“两阶段事务”解决,它在提交序列中增加了一个额外的“准备”步骤,允许多个数据库在实际完成事务之前同意提交。
由于 DBAPI 中的支持有限,SQLAlchemy 在跨后端的两阶段事务方面的支持也有限。通常,它被认为与 PostgreSQL 后端很好地配合使用,而与 MySQL 后端配合使用的程度较低。但是,当后端支持时,Session
完全能够利用两阶段事务功能,方法是在 sessionmaker
或 Session
中设置 Session.use_twophase
标志。请参阅启用两阶段提交以获取示例。
自定义垂直分区
通过重写 Session.get_bind()
方法可以构建更全面的基于规则的类级别分区。下面我们展示一个自定义的 Session
,它提供以下规则:
-
刷新操作以及批量的“更新”和“删除”操作都发送到名为
leader
的引擎。 -
所有子类为
MyOtherClass
的对象操作都发生在other
引擎上。 -
所有其他类的读操作都发生在
follower1
或follower2
数据库的随机选择上。
engines = {
"leader": create_engine("sqlite:///leader.db"),
"other": create_engine("sqlite:///other.db"),
"follower1": create_engine("sqlite:///follower1.db"),
"follower2": create_engine("sqlite:///follower2.db"),
}
from sqlalchemy.sql import Update, Delete
from sqlalchemy.orm import Session, sessionmaker
import random
class RoutingSession(Session):
def get_bind(self, mapper=None, clause=None):
if mapper and issubclass(mapper.class_, MyOtherClass):
return engines["other"]
elif self._flushing or isinstance(clause, (Update, Delete)):
# NOTE: this is for example, however in practice reader/writer
# splits are likely more straightforward by using two distinct
# Sessions at the top of a "reader" or "writer" operation.
# See note below
return engines["leader"]
else:
return engines[random.choice(["follower1", "follower2"])]
上述的 Session
类通过 class_
参数插入到 sessionmaker
中:
Session = sessionmaker(class_=RoutingSession)
这种方法可以与多个 MetaData
对象结合使用,使用类似使用声明性 __abstract__
关键字的方法,详细描述在 abstract。
注意
虽然上面的示例说明了将特定的 SQL 语句路由到所谓的“领导者”或“跟随者”数据库的方法,这可能不是一个实际的方法,因为它导致了在同一操作中读写之间的不协调事务行为。实际上,最好根据正在进行的整体操作/事务,提前将 Session
构造为“读取器”或“写入器”会话。这样,将要写入数据的操作也将在同一事务范围内发出其读取查询。参见 为 Sessionmaker / Engine 设置隔离级别 中的示例,该示例设置了一个用于使用自动提交连接的“只读”操作的 sessionmaker
,另一个用于“写入”操作,其中包括 DML / COMMIT。
另请参阅
在 SQLAlchemy 中实现 Django 风格的数据库路由器 - 关于更全面的Session.get_bind()
的博客文章。
水平分区
水平分区将单个表(或一组表)的行分布到多个数据库中。SQLAlchemy Session
包含对这个概念的支持,但要完全使用它,需要使用 Session
和 Query
的子类。这些子类的基本版本可在 水平分片 ORM 扩展中找到。一个使用示例在这里:水平分片。 ## 批量操作
传统特性
SQLAlchemy 2.0 已将“批量插入”和“批量更新”功能集成到 2.0 风格的 Session.execute()
方法中,直接使用了 Insert
和 Update
构造。参见文档 ORM-Enabled INSERT, UPDATE, and DELETE statements,包括传统的会话批量 INSERT 方法 ,该文档说明了从旧方法迁移到新方法的示例。 ## 将 SQL 插入/更新表达式嵌入到刷新中
此功能允许将数据库列的值设置为 SQL 表达式,而不是文字值。对于原子更新、调用存储过程等特别有用。您所做的一切就是将表达式分配给属性:
class SomeClass(Base):
__tablename__ = "some_table"
# ...
value = mapped_column(Integer)
someobject = session.get(SomeClass, 5)
# set 'value' attribute to a SQL expression adding one
someobject.value = SomeClass.value + 1
# issues "UPDATE some_table SET value=value+1"
session.commit()
此技术对于 INSERT 和 UPDATE 语句均有效。在 flush/commit 操作之后,上述 someobject
上的 value
属性将过期,因此在下次访问时,新生成的值将从数据库加载。
该功能还具有条件支持,可与主键列一起使用。对于支持 RETURNING 的后端(包括 Oracle、SQL Server、MariaDB 10.5、SQLite 3.35),还可以将 SQL 表达式分配给主键列。这不仅允许评估 SQL 表达式,还允许检索任何在插入时修改主键值的服务器端触发器作为对象主键的一部分成功地检索到 ORM:
class Foo(Base):
__tablename__ = "foo"
pk = mapped_column(Integer, primary_key=True)
bar = mapped_column(Integer)
e = create_engine("postgresql+psycopg2://scott:tiger@localhost/test", echo=True)
Base.metadata.create_all(e)
session = Session(e)
foo = Foo(pk=sql.select(sql.func.coalesce(sql.func.max(Foo.pk) + 1, 1)))
session.add(foo)
session.commit()
在 PostgreSQL 上,上述 Session
将发出以下 INSERT:
INSERT INTO foo (foopk, bar) VALUES
((SELECT coalesce(max(foo.foopk) + %(max_1)s, %(coalesce_2)s) AS coalesce_1
FROM foo), %(bar)s) RETURNING foo.foopk
从版本 1.3 新增:在 ORM flush 期间,SQL 表达式现在可以传递到主键列;如果数据库支持 RETURNING,或者正在使用 pysqlite,则 ORM 将能够将服务器生成的值检索为主键属性的值。
使用 SQL 表达式与会话
SQL 表达式和字符串可以通过其事务上下文在 Session
中执行。这最容易通过 Session.execute()
方法来实现,该方法以与 Engine
或 Connection
相同的方式返回一个 CursorResult
:
Session = sessionmaker(bind=engine)
session = Session()
# execute a string statement
result = session.execute(text("select * from table where id=:id"), {"id": 7})
# execute a SQL expression construct
result = session.execute(select(mytable).where(mytable.c.id == 7))
Session
当前持有的 Connection
可以通过 Session.connection()
方法访问:
connection = session.connection()
上面的示例涉及到绑定到单个 Engine
或 Connection
的 Session
。要使用绑定到多个引擎或根本没有绑定到引擎的 Session
执行语句,Session.execute()
和 Session.connection()
都接受一个绑定参数字典 Session.execute.bind_arguments
,其中可能包括 “mapper”,该参数传递了一个映射类或 Mapper
实例,用于定位所需引擎的正确上下文:
Session = sessionmaker()
session = Session()
# need to specify mapper or class when executing
result = session.execute(
text("select * from table where id=:id"),
{"id": 7},
bind_arguments={"mapper": MyMappedClass},
)
result = session.execute(
select(mytable).where(mytable.c.id == 7), bind_arguments={"mapper": MyMappedClass}
)
connection = session.connection(MyMappedClass)
从版本 1.4 开始变更:Session.execute()
的 mapper
和 clause
参数现在作为字典的一部分发送,作为 Session.execute.bind_arguments
参数。以前的参数仍然被接受,但此用法已被弃用。
强制将 NULL 值放入具有默认值的列
ORM 认为对象上从未设置的任何属性都是“默认”情况;该属性将从 INSERT 语句中省略:
class MyObject(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
data = mapped_column(String(50), nullable=True)
obj = MyObject(id=1)
session.add(obj)
session.commit() # INSERT with the 'data' column omitted; the database
# itself will persist this as the NULL value
如果在 INSERT 中省略了某列,则该列将被设置为 NULL 值,除非该列设置了默认值,在这种情况下,默认值将被保留。这适用于纯 SQL 视角下具有服务器端默认值的情况,也适用于 SQLAlchemy 的插入行为,无论是客户端默认值还是服务器端默认值:
class MyObject(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
data = mapped_column(String(50), nullable=True, server_default="default")
obj = MyObject(id=1)
session.add(obj)
session.commit() # INSERT with the 'data' column omitted; the database
# itself will persist this as the value 'default'
然而,在 ORM 中,即使将 Python 值 None
显式地分配给对象,这也被视为相同,就像从未分配过值一样:
class MyObject(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
data = mapped_column(String(50), nullable=True, server_default="default")
obj = MyObject(id=1, data=None)
session.add(obj)
session.commit() # INSERT with the 'data' column explicitly set to None;
# the ORM still omits it from the statement and the
# database will still persist this as the value 'default'
上述操作将持久化到 data
列的服务器默认值为 "default"
,而不是 SQL NULL,即使传递了 None
;这是 ORM 的长期行为,许多应用程序都将其视为假设。
那么,如果我们想要在这列中实际放入 NULL 值,即使该列有默认值呢?有两种方法。一种是在每个实例级别上,我们使用 null
SQL 构造分配属性:
from sqlalchemy import null
obj = MyObject(id=1, data=null())
session.add(obj)
session.commit() # INSERT with the 'data' column explicitly set as null();
# the ORM uses this directly, bypassing all client-
# and server-side defaults, and the database will
# persist this as the NULL value
null
SQL 结构总是将 SQL NULL 值直接包含在目标 INSERT 语句中。
如果我们希望能够使用 Python 值 None
并且将其作为 NULL 持久化,尽管存在列默认值,我们可以在 ORM 中使用 Core 级别的修饰符 TypeEngine.evaluates_none()
进行配置,该修饰符指示 ORM 应该将值 None
与任何其他值一样对待并将其传递,而不是将其省略为“丢失”的值:
class MyObject(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
data = mapped_column(
String(50).evaluates_none(), # indicate that None should always be passed
nullable=True,
server_default="default",
)
obj = MyObject(id=1, data=None)
session.add(obj)
session.commit() # INSERT with the 'data' column explicitly set to None;
# the ORM uses this directly, bypassing all client-
# and server-side defaults, and the database will
# persist this as the NULL value
获取服务器生成的默认值
正如在章节 Server-invoked DDL-Explicit Default Expressions 和 Marking Implicitly Generated Values, timestamps, and Triggered Columns 中介绍的,Core 支持数据库列的概念,其中数据库本身在 INSERT 时生成值,在不太常见的情况下,在 UPDATE 语句中生成值。ORM 功能支持这些列,以便能够在刷新时获取这些新生成的值。在服务器生成的主键列的情况下,由于 ORM 必须在对象持久化后知道其主键,因此需要这种行为。
在绝大多数情况下,由数据库自动生成值的主键列都是简单的整数列,这些列由数据库实现为所谓的“自增”列,或者是与列关联的序列。SQLAlchemy Core 中的每个数据库方言都支持一种检索这些主键值的方法,通常是原生于 Python DBAPI,并且通常这个过程是自动的。关于这一点,有更多的文档说明在 Column.autoincrement
中。
对于不是主键列或不是简单自增整数列的服务器生成列,ORM 要求这些列使用适当的 server_default
指令标记,以允许 ORM 检索此值。然而,并不是所有方法都受到所有后端的支持,因此必须注意使用适当的方法。要回答的两个问题是,1. 此列是否是主键列,2. 数据库是否支持 RETURNING 或等效操作,如 “OUTPUT inserted”;这些是 SQL 短语,它们在调用 INSERT 或 UPDATE 语句时同时返回服务器生成的值。RETURNING 目前由 PostgreSQL、Oracle、MariaDB 10.5、SQLite 3.35 和 SQL Server 支持。
情况 1:非主键,支持 RETURNING 或等效操作
在这种情况下,列应标记为FetchedValue
,或者用显式的Column.server_default
。ORM 在执行 INSERT 语句时将自动将这些列添加到 RETURNING 子句中,假设 Mapper.eager_defaults
参数设置为 True
,或者对于同时支持 RETURNING 和 insertmanyvalues 的方言,保持其默认设置为 "auto"
:
class MyModel(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
# server-side SQL date function generates a new timestamp
timestamp = mapped_column(DateTime(), server_default=func.now())
# some other server-side function not named here, such as a trigger,
# populates a value into this column during INSERT
special_identifier = mapped_column(String(50), server_default=FetchedValue())
# set eager defaults to True. This is usually optional, as if the
# backend supports RETURNING + insertmanyvalues, eager defaults
# will take place regardless on INSERT
__mapper_args__ = {"eager_defaults": True}
在上面的示例中,如果客户端未为“timestamp”或“special_identifier”指定显式值,则 INSERT 语句将在 RETURNING 子句中包含“timestamp”和“special_identifier”列,以便立即使用。在 PostgreSQL 数据库中,上述表的 INSERT 如下所示:
INSERT INTO my_table DEFAULT VALUES RETURNING my_table.id, my_table.timestamp, my_table.special_identifier
从版本 2.0.0rc1 起更改:Mapper.eager_defaults
参数现在默认为新设置 "auto"
,如果后端数据库同时支持 RETURNING 和 insertmanyvalues,将自动使用 RETURNING 获取 INSERT 上生成的默认值。
注意
Mapper.eager_defaults
的 "auto"
值仅适用于 INSERT 语句。即使可用,UPDATE 语句也不会使用 RETURNING,除非 Mapper.eager_defaults
设置为 True
。这是因为 UPDATE 没有等效的“insertmanyvalues”特性,因此 UPDATE RETURNING 将要求对每个要更新的行分别发出 UPDATE 语句。
情况 2:表包含不兼容于 RETURNING 的触发器生成的值
Mapper.eager_defaults
的 "auto"
设置意味着支持 RETURNING 的后端通常会在 INSERT 语句中使用 RETURNING 以检索新生成的默认值。但是,使用触发器生成的服务器值存在限制,使得无法使用 RETURNING:
-
SQL Server 不允许在 INSERT 语句中使用 RETURNING 来检索触发器生成的值;该语句将失败。
-
SQLite 在将 RETURNING 与触发器组合使用时存在限制,因此 RETURNING 子句将不会包含插入的值
-
其他后端可能在与触发器一起使用 RETURNING,或者其他类型的服务器生成值时存在限制。
要禁用对这些值的 RETURNING 使用,不仅包括服务器生成的默认值,还要确保 ORM 永远不会与特定表使用 RETURNING,请为映射的 Table
指定 Table.implicit_returning
为 False
。使用声明性映射如下所示:
class MyModel(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(primary_key=True)
data: Mapped[str] = mapped_column(String(50))
# assume a database trigger populates a value into this column
# during INSERT
special_identifier = mapped_column(String(50), server_default=FetchedValue())
# disable all use of RETURNING for the table
__table_args__ = {"implicit_returning": False}
在使用 pyodbc 驱动程序的 SQL Server 上,对上述表的 INSERT 不会使用 RETURNING,并将使用 SQL Server 的 scope_identity()
函数来检索新生成的主键值:
INSERT INTO my_table (data) VALUES (?); select scope_identity()
另请参阅
INSERT 行为 - 关于 SQL Server 方言获取新生成主键值的方法的背景信息
情况 3:非主键,不支持或不需要 RETURNING 或等效功能
该情况与上述情况 1 相同,但通常我们不希望使用 Mapper.eager_defaults
,因为在没有 RETURNING 支持的情况下,其当前实现是为每行发出一个 SELECT,这不是高效的。因此,在下面的映射中省略了该参数:
class MyModel(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
timestamp = mapped_column(DateTime(), server_default=func.now())
# assume a database trigger populates a value into this column
# during INSERT
special_identifier = mapped_column(String(50), server_default=FetchedValue())
在不包含 RETURNING 或“insertmanyvalues”支持的后端上插入具有上述映射的记录后,"timestamp" 和 "special_identifier" 列将保持为空,并且在刷新后首次访问时,例如标记为“过期”时,将通过第二个 SELECT 语句获取。
如果 Mapper.eager_defaults
明确提供了值 True
,并且后端数据库不支持 RETURNING 或等效功能,则 ORM 将在 INSERT 语句后立即发出 SELECT 语句以获取新生成的值;如果没有 RETURNING 可用,ORM 目前无法批量选择许多新插入的行。这通常是不希望的,因为它会向刷新过程添加额外的 SELECT 语句,这些语句可能是不必要的。在 MySQL(而不是 MariaDB)上使用上述映射与将 Mapper.eager_defaults
标志设置为 True 会导致刷新时生成以下 SQL:
INSERT INTO my_table () VALUES ()
-- when eager_defaults **is** used, but RETURNING is not supported
SELECT my_table.timestamp AS my_table_timestamp, my_table.special_identifier AS my_table_special_identifier
FROM my_table WHERE my_table.id = %s
未来的 SQLAlchemy 版本可能会在没有 RETURNING 的情况下,通过批量处理单个 SELECT 语句中的多行来提高急切默认值的效率。
情况 4:主键,支持 RETURNING 或等效功能
具有服务器生成值的主键列必须在 INSERT 后立即获取;ORM 只能访问具有主键值的行,因此如果主键由服务器生成,则 ORM 需要一种在 INSERT 后立即检索该新值的方法。
如上所述,对于整数“自动增量”列,以及标记有 Identity
和特殊构造(如 PostgreSQL SERIAL)的列,Core 会自动处理这些类型;数据库包括用于获取“最后插入 id”的函数,在不支持 RETURNING 的情况下,以及支持 RETURNING 的情况下 SQLAlchemy 将使用该函数。
例如,在 Oracle 中,如果将列标记为 Identity
,则自动使用 RETURNING 获取新的主键值:
class MyOracleModel(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(Identity(), primary_key=True)
data: Mapped[str] = mapped_column(String(50))
如上所述,上述模型在 Oracle 上的 INSERT 如下所示:
INSERT INTO my_table (data) VALUES (:data) RETURNING my_table.id INTO :ret_0
SQLAlchemy 为“data”字段渲染了一个 INSERT,但在 RETURNING 子句中仅包含了“id”,以便在服务器端生成“id”,并立即返回新值。
对于由服务器端函数或触发器生成的非整数值,以及来自表格本身之外的结构(包括显式序列和触发器)的整数值,必须在表格元数据中标记服务器默认生成。再次以 Oracle 为例,我们可以举例说明一个类似上述的表格,使用 Sequence
构造命名一个显式序列:
class MyOracleModel(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(Sequence("my_oracle_seq"), primary_key=True)
data: Mapped[str] = mapped_column(String(50))
在 Oracle 上,对于此模型的 INSERT 如下所示:
INSERT INTO my_table (id, data) VALUES (my_oracle_seq.nextval, :data) RETURNING my_table.id INTO :ret_0
在上述情况中,SQLAlchemy 为主键列渲染了 my_sequence.nextval
,以便用于新的主键生成,并且还使用 RETURNING 立即获取新值。
如果数据源不是由简单的 SQL 函数或 Sequence
表示,例如在使用触发器或生成新值的数据库特定数据类型时,可以通过在列定义中使用 FetchedValue
来指示值生成默认值的存在。下面是一个使用 SQL Server TIMESTAMP 列作为主键的模型;在 SQL Server 上,此数据类型会自动生成新值,因此在表格元数据中通过为 Column.server_default
参数指示 FetchedValue
来表示这一点:
class MySQLServerModel(Base):
__tablename__ = "my_table"
timestamp: Mapped[datetime.datetime] = mapped_column(
TIMESTAMP(), server_default=FetchedValue(), primary_key=True
)
data: Mapped[str] = mapped_column(String(50))
在 SQL Server 上,对于上述表格的 INSERT 如下所示:
INSERT INTO my_table (data) OUTPUT inserted.timestamp VALUES (?)
情况 5:不支持主键、RETURNING 或等效功能。
在此领域,我们正在为 MySQL 等数据库生成行,其中服务器上正在发生一些默认生成的手段,但这些手段不在数据库的通常自增例程中。在这种情况下,我们必须确保 SQLAlchemy 可以“预先执行”默认值,这意味着它必须是一个明确的 SQL 表达式。
注意
本节将说明涉及 MySQL 日期时间值的多个配方,因为该后端的日期时间数据类型具有额外的特殊要求,这些要求对于说明非常有用。但是请注意,除了通常的单列自增整数值之外,MySQL 需要为任何用作主键的自动生成数据类型显式的“预执行”默认生成器。
具有 DateTime 主键的 MySQL
以 MySQL 的DateTime
列为例,我们使用“NOW()”SQL 函数添加了一个明确的预执行支持的默认值:
class MyModel(Base):
__tablename__ = "my_table"
timestamp = mapped_column(DateTime(), default=func.now(), primary_key=True)
在上述情况下,我们选择“NOW()”函数以向列传递日期时间值。上述生成的 SQL 是:
SELECT now() AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
('2018-08-09 13:08:46',)
具有 TIMESTAMP 主键的 MySQL
当使用 MySQL 的TIMESTAMP
数据类型时,MySQL 通常会自动将服务器端默认与该数据类型关联起来。但是,当我们将其用作主键时,Core 无法检索新生成的值,除非我们自己执行该函数。由于 MySQL 上的TIMESTAMP
实际上存储了一个二进制值,因此我们需要在“NOW()”的使用中添加一个额外的“CAST”,以便检索到可以持久化到列中的二进制值:
from sqlalchemy import cast, Binary
class MyModel(Base):
__tablename__ = "my_table"
timestamp = mapped_column(
TIMESTAMP(), default=cast(func.now(), Binary), primary_key=True
)
以上,在选择“NOW()”函数的同时,我们还使用了Binary
数据类型结合cast()
,以便返回的值是二进制的。在 INSERT 中从上述渲染的 SQL 如下所示:
SELECT CAST(now() AS BINARY) AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
(b'2018-08-09 13:08:46',)
另见
列插入/更新默认值
注意事项:对于用于 INSERT 或 UPDATE 的急切提取客户端调用的 SQL 表达式
上述示例指示了使用Column.server_default
创建包含其 DDL 中的默认生成函数的表。
SQLAlchemy 也支持非 DDL 服务器端的默认设置,如客户端调用的 SQL 表达式文档中所述;这些“客户端调用的 SQL 表达式”是使用Column.default
和Column.onupdate
参数设置的。
目前,ORM 中的这些 SQL 表达式受到与真正的服务器端默认值相同的限制;当 Mapper.eager_defaults
设置为 "auto"
或 True
时,除非 FetchedValue
指令与 Column
相关联,否则它们不会被 RETURNING 急切地获取,尽管这些表达式不是 DDL 服务器默认值,并且由 SQLAlchemy 本身主动渲染。这个限制可能在未来的 SQLAlchemy 版本中得到解决。
FetchedValue
构造可以同时应用于 Column.server_default
或 Column.server_onupdate
,就像下面的例子中使用 func.now()
构造作为客户端调用的 SQL 表达式用于 Column.default
和 Column.onupdate
一样。为了使 Mapper.eager_defaults
的行为包括在可用时使用 RETURNING 获取这些值,Column.server_default
和 Column.server_onupdate
与 FetchedValue
一起使用以确保获取发生:
class MyModel(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
created = mapped_column(
DateTime(), default=func.now(), server_default=FetchedValue()
)
updated = mapped_column(
DateTime(),
onupdate=func.now(),
server_default=FetchedValue(),
server_onupdate=FetchedValue(),
)
__mapper_args__ = {"eager_defaults": True}
使用类似上面的映射,ORM 渲染的 INSERT 和 UPDATE 的 SQL 将在 RETURNING 子句中包括created
和updated
:
INSERT INTO my_table (created) VALUES (now()) RETURNING my_table.id, my_table.created, my_table.updated
UPDATE my_table SET updated=now() WHERE my_table.id = %(my_table_id)s RETURNING my_table.updated
情况 1:非主键,支持 RETURNING 或等效功能
在这种情况下,应将列标记为FetchedValue
或具有显式的Column.server_default
。如果Mapper.eager_defaults
参数设置为True
,或者对于支持 RETURNING 以及 insertmanyvalues 的方言,默认设置为"auto"
,ORM 将在执行 INSERT 语句时自动将这些列添加到 RETURNING 子句中:
class MyModel(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
# server-side SQL date function generates a new timestamp
timestamp = mapped_column(DateTime(), server_default=func.now())
# some other server-side function not named here, such as a trigger,
# populates a value into this column during INSERT
special_identifier = mapped_column(String(50), server_default=FetchedValue())
# set eager defaults to True. This is usually optional, as if the
# backend supports RETURNING + insertmanyvalues, eager defaults
# will take place regardless on INSERT
__mapper_args__ = {"eager_defaults": True}
在上述情况下,未在客户端指定“timestamp”或“special_identifier”的显式值的 INSERT 语句将包括“timestamp”和“special_identifier”列在 RETURNING 子句中,以便立即使用。在 PostgreSQL 数据库上,上述表的 INSERT 将如下所示:
INSERT INTO my_table DEFAULT VALUES RETURNING my_table.id, my_table.timestamp, my_table.special_identifier
从版本 2.0.0rc1 开始更改:Mapper.eager_defaults
参数现在默认为新设置"auto"
,如果支持 RETURNING 以及 insertmanyvalues 的后端数据库,则会自动使用 RETURNING 来获取 INSERT 时的服务器生成默认值。
注意
Mapper.eager_defaults
的"auto"
值仅适用于 INSERT 语句。即使可用,UPDATE 语句也不会使用 RETURNING,除非将Mapper.eager_defaults
设置为True
。这是因为 UPDATE 没有等效的“insertmanyvalues”特性,因此 UPDATE RETURNING 将要求为每个被 UPDATE 的行分别发出 UPDATE 语句。
情况 2:表包含与 RETURNING 不兼容的触发器生成的值
Mapper.eager_defaults
的"auto"
设置意味着支持 RETURNING 的后端通常会在 INSERT 语句中使用 RETURNING 来检索新生成的默认值。但是,存在使用触发器生成的服务器生成值的限制,因此不能使用 RETURNING:
-
SQL Server 不允许在 INSERT 语句中使用 RETURNING 来检索触发器生成的值;该语句将失败。
-
SQLite 在与触发器结合使用 RETURNING 时存在限制,因此 RETURNING 子句将无法获取已插入的值。
-
其他后端可能在与触发器或其他类型的服务器生成值结合使用 RETURNING 时存在限制。
要禁用用于此类值的 RETURNING 的使用,包括不仅用于服务器生成的默认值而且确保 ORM 永远不会使用 RETURNING 与特定表,指定 Table.implicit_returning
为 False
对于映射的 Table
。使用声明性映射,看起来像这样:
class MyModel(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(primary_key=True)
data: Mapped[str] = mapped_column(String(50))
# assume a database trigger populates a value into this column
# during INSERT
special_identifier = mapped_column(String(50), server_default=FetchedValue())
# disable all use of RETURNING for the table
__table_args__ = {"implicit_returning": False}
在使用 pyodbc 驱动程序的 SQL Server 上,对于上述表的 INSERT 不会使用 RETURNING,并且将使用 SQL Server scope_identity()
函数来检索新生成的主键值:
INSERT INTO my_table (data) VALUES (?); select scope_identity()
另请参阅
INSERT 行为 - 关于 SQL Server 方言获取新生成的主键值的方法的背景
情况 3:非主键,不支持或不需要 RETURNING 或等效功能
该情况与上面的情况 1 相同,只是我们通常不想使用 Mapper.eager_defaults
,因为在没有 RETURNING 支持的情况下,其当前实现是发出每行一个 SELECT,这是不高效的。因此,在下面的映射中省略了该参数:
class MyModel(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
timestamp = mapped_column(DateTime(), server_default=func.now())
# assume a database trigger populates a value into this column
# during INSERT
special_identifier = mapped_column(String(50), server_default=FetchedValue())
在上述映射中插入记录后,在不包括 RETURNING 或“insertmanyvalues”支持的后端上,“timestamp” 和 “special_identifier” 列将保持为空,并且在刷新后首次访问时将通过第二个 SELECT 语句获取,例如,它们被标记为“过期”时。
如果 Mapper.eager_defaults
明确提供了值 True
,并且后端数据库不支持 RETURNING 或等效功能,则 ORM 将在 INSERT 语句后立即发出 SELECT 语句,以获取新生成的值;如果没有可用的 RETURNING,ORM 目前无法批量选择许多新插入的行。这通常是不可取的,因为它会向刷新过程添加额外的 SELECT 语句,这些语句可能是不需要的。使用上述映射,针对 MySQL(不是 MariaDB)将 Mapper.eager_defaults
标志设置为 True 在刷新时会产生类似以下的 SQL:
INSERT INTO my_table () VALUES ()
-- when eager_defaults **is** used, but RETURNING is not supported
SELECT my_table.timestamp AS my_table_timestamp, my_table.special_identifier AS my_table_special_identifier
FROM my_table WHERE my_table.id = %s
未来的 SQLAlchemy 版本可能会在没有 RETURNING 的情况下寻求改进急切默认值的效率,以在单个 SELECT 语句中批量处理多行。
情况 4:主键,支持 RETURNING 或等效功能
具有服务器生成值的主键列必须在 INSERT 后立即获取;ORM 只能访问具有主键值的行,因此如果主键由服务器生成,则 ORM 需要一种在 INSERT 后立即检索该新值的方法。
如上所述,对于整数“自增”列,以及标记为Identity
的列和特殊构造,例如 PostgreSQL 的 SERIAL,这些类型将由核心自动处理;数据库包括获取“最后插入的 id”函数,其中不支持 RETURNING,而在支持 RETURNING 的情况下,SQLAlchemy 将使用它。
例如,使用 Oracle 并将列标记为Identity
,RETURNING 将自动用于获取新的主键值:
class MyOracleModel(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(Identity(), primary_key=True)
data: Mapped[str] = mapped_column(String(50))
如上模型在 Oracle 上的 INSERT 如下所示:
INSERT INTO my_table (data) VALUES (:data) RETURNING my_table.id INTO :ret_0
SQLAlchemy 渲染“data”字段的 INSERT,但仅在 RETURNING 子句中包含“id”,以便在服务器端生成“id”并立即返回新值。
对于由服务器端函数或触发器生成的非整数值,以及来自表本身之外的构造的整数值,包括显式序列和触发器,必须在表元数据中标记服务器默认生成。再次以 Oracle 为例,我们可以用Sequence
构造说明一个类似的表:
class MyOracleModel(Base):
__tablename__ = "my_table"
id: Mapped[int] = mapped_column(Sequence("my_oracle_seq"), primary_key=True)
data: Mapped[str] = mapped_column(String(50))
Oracle 上此模型的 INSERT 如下所示:
INSERT INTO my_table (id, data) VALUES (my_oracle_seq.nextval, :data) RETURNING my_table.id INTO :ret_0
在上述情况下,SQLAlchemy 渲染my_sequence.nextval
用于主键列的新主键生成,并且还使用 RETURNING 立即获取新值。
如果数据源不是由简单的 SQL 函数或Sequence
表示,例如使用触发器或生成新值的数据库特定数据类型,可以通过在列定义中使用FetchedValue
来指示值生成的默认值。下面是一个使用 SQL Server TIMESTAMP 列作为主键的模型;在 SQL Server 上,此数据类型会自动生成新值,因此在表元数据中通过为Column.server_default
参数指定FetchedValue
来指示:
class MySQLServerModel(Base):
__tablename__ = "my_table"
timestamp: Mapped[datetime.datetime] = mapped_column(
TIMESTAMP(), server_default=FetchedValue(), primary_key=True
)
data: Mapped[str] = mapped_column(String(50))
SQL Server 上上述表的 INSERT 如下所示:
INSERT INTO my_table (data) OUTPUT inserted.timestamp VALUES (?)
情况 5:不支持主键、RETURNING 或等效项。
在这个领域,我们为像 MySQL 这样的数据库生成行,其中服务器上正在发生某种默认生成的方法,但是超出了数据库的通常自动增量例程。在这种情况下,我们必须确保 SQLAlchemy 可以“预执行”默认值,这意味着它必须是一个显式的 SQL 表达式。
注
本节将说明 MySQL 中涉及日期时间值的多个示例,因为此后端的日期时间数据类型具有有用的额外特殊要求。但是请记住,MySQL 对于用作主键的任何自动生成的数据类型都需要明确的“预执行”默认生成器,除了通常的单列自增整数值。
MySQL 使用 DateTime 主键
使用 MySQL 的DateTime
列作为例子,我们使用“NOW()”SQL 函数添加一个明确的预执行支持的默认值:
class MyModel(Base):
__tablename__ = "my_table"
timestamp = mapped_column(DateTime(), default=func.now(), primary_key=True)
在上面的例子中,我们选择“NOW()”函数将日期时间值传递给列。由上述生成的 SQL 是:
SELECT now() AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
('2018-08-09 13:08:46',)
MySQL 使用 TIMESTAMP 主键
当在 MySQL 中使用TIMESTAMP
数据类型时,MySQL 通常会自动将服务器端默认值与此数据类型关联起来。但是,当我们将其用作主键时,Core 无法检索到新生成的值,除非我们自己执行该函数。由于 MySQL 上的TIMESTAMP
实际上存储的是二进制值,因此我们需要在“NOW()”的使用中添加额外的“CAST”,以便检索到可持久化到列中的二进制值:
from sqlalchemy import cast, Binary
class MyModel(Base):
__tablename__ = "my_table"
timestamp = mapped_column(
TIMESTAMP(), default=cast(func.now(), Binary), primary_key=True
)
上述,除了选择“NOW()”函数外,我们还额外利用Binary
数据类型与cast()
结合使用,以便返回值是二进制的。在 INSERT 中生成的 SQL 如下所示:
SELECT CAST(now() AS BINARY) AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
(b'2018-08-09 13:08:46',)
另请参阅
列的 INSERT/UPDATE 默认值
MySQL 使用 DateTime 主键
使用 MySQL 的DateTime
列作为例子,我们使用“NOW()”SQL 函数添加一个明确的预执行支持的默认值:
class MyModel(Base):
__tablename__ = "my_table"
timestamp = mapped_column(DateTime(), default=func.now(), primary_key=True)
在上面的例子中,我们选择“NOW()”函数将日期时间值传递给列。由上述生成的 SQL 是:
SELECT now() AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
('2018-08-09 13:08:46',)
MySQL 使用 TIMESTAMP 主键
当在 MySQL 中使用TIMESTAMP
数据类型时,MySQL 通常会自动将服务器端默认值与此数据类型关联起来。但是,当我们将其用作主键时,Core 无法检索到新生成的值,除非我们自己执行该函数。由于 MySQL 上的TIMESTAMP
实际上存储的是二进制值,因此我们需要在“NOW()”的使用中添加额外的“CAST”,以便检索到可持久化到列中的二进制值:
from sqlalchemy import cast, Binary
class MyModel(Base):
__tablename__ = "my_table"
timestamp = mapped_column(
TIMESTAMP(), default=cast(func.now(), Binary), primary_key=True
)
在上面的示例中,除了选择“NOW()”函数外,我们还使用Binary
数据类型结合cast()
,以使返回的值是二进制的。在 INSERT 中从上面渲染的 SQL 如下所示:
SELECT CAST(now() AS BINARY) AS anon_1
INSERT INTO my_table (timestamp) VALUES (%s)
(b'2018-08-09 13:08:46',)
另请参阅
列插入/更新默认值
关于急切获取用于 INSERT 或 UPDATE 的客户端调用的 SQL 表达式的注意事项
前面的示例表明了使用Column.server_default
创建包含默认生成函数的表的方法。
SQLAlchemy 也支持非 DDL 服务器端默认值,如客户端调用的 SQL 表达式文档所述;这些“客户端调用的 SQL 表达式”是使用Column.default
和Column.onupdate
参数设置的。
这些 SQL 表达式目前受 ORM 中与真正的服务器端默认值发生的相同限制的约束;当Mapper.eager_defaults
设置为"auto"
或True
时,它们不会被急切地获取到 RETURNING 中,除非FetchedValue
指令与Column
关联,即使这些表达式不是 DDL 服务器默认值,而是由 SQLAlchemy 本身主动渲染的。这个限制可能在未来的 SQLAlchemy 版本中得到解决。
FetchedValue
构造可以同时应用于 Column.server_default
或 Column.server_onupdate
,与 Column.default
和 Column.onupdate
一起使用 SQL 表达式,例如下面的示例中,func.now()
构造被用作客户端调用的 SQL 表达式,用于 Column.default
和 Column.onupdate
。为了使 Mapper.eager_defaults
的行为包括使用 RETURNING 在可用时获取这些值,需要使用 Column.server_default
和 Column.server_onupdate
与 FetchedValue
以确保获取发生:
class MyModel(Base):
__tablename__ = "my_table"
id = mapped_column(Integer, primary_key=True)
created = mapped_column(
DateTime(), default=func.now(), server_default=FetchedValue()
)
updated = mapped_column(
DateTime(),
onupdate=func.now(),
server_default=FetchedValue(),
server_onupdate=FetchedValue(),
)
__mapper_args__ = {"eager_defaults": True}
与上述类似的映射,ORM 渲染的 INSERT 和 UPDATE 的 SQL 将在 RETURNING 子句中包括 created
和 updated
:
INSERT INTO my_table (created) VALUES (now()) RETURNING my_table.id, my_table.created, my_table.updated
UPDATE my_table SET updated=now() WHERE my_table.id = %(my_table_id)s RETURNING my_table.updated
使用 INSERT、UPDATE 和 ON CONFLICT(即 upsert)返回 ORM 对象
SQLAlchemy 2.0 包括增强功能,用于发出几种类型的启用 ORM 的 INSERT、UPDATE 和 upsert 语句。查看文档 ORM-Enabled INSERT, UPDATE, and DELETE statements 以获取文档。有关 upsert,请参见 ORM “upsert” Statements。
使用 PostgreSQL ON CONFLICT 与 RETURNING 返回 upserted ORM 对象
本节已移至 ORM “upsert” Statements。
使用 PostgreSQL ON CONFLICT 与 RETURNING 返回 upserted ORM 对象
本节已移至 ORM “upsert” Statements。
分区策略(例如,每个会话使用多个数据库后端)
简单的垂直分区
垂直分区通过配置Session
的Session.binds
参数,将不同的类、类层次结构或映射表放置在多个数据库中。此参数接收一个包含任意组合的 ORM 映射类、映射层次结构中的任意类(如声明性基类或混合类)、Table
对象和Mapper
对象作为键的字典,然后通常引用Engine
或较不常见的 Connection
对象作为目标。每当Session
需要代表特定类型的映射类发出 SQL 以定位适当的数据库连接源时,就会查询该字典:
engine1 = create_engine("postgresql+psycopg2://db1")
engine2 = create_engine("postgresql+psycopg2://db2")
Session = sessionmaker()
# bind User operations to engine 1, Account operations to engine 2
Session.configure(binds={User: engine1, Account: engine2})
session = Session()
在上面,针对任一类的 SQL 操作都将使用与该类链接的Engine
。该功能涵盖了读写操作;针对映射到engine1
的实体的 Query
(通过查看请求的项目列表中的第一个实体确定)将使用engine1
来运行查询。刷新操作将根据每个类使用两个引擎,因为它刷新了User
和Account
类型的对象。
在更常见的情况下,通常有基础类或混合类可用于区分不同数据库连接的操作。Session.binds
参数可以容纳任何任意的 Python 类作为键,如果发现它在特定映射类的__mro__
(Python 方法解析顺序)中,则会使用该键。假设两个声明性基类分别表示两个不同的数据库连接:
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Session
class BaseA(DeclarativeBase):
pass
class BaseB(DeclarativeBase):
pass
class User(BaseA): ...
class Address(BaseA): ...
class GameInfo(BaseB): ...
class GameStats(BaseB): ...
Session = sessionmaker()
# all User/Address operations will be on engine 1, all
# Game operations will be on engine 2
Session.configure(binds={BaseA: engine1, BaseB: engine2})
在上面,从BaseA
和BaseB
继承的类将根据它们是否继承自任何超类来将它们的 SQL 操作路由到两个引擎中的一个。对于从多个“绑定”超类继承的类的情况,将选择目标类层次结构中最高的超类来表示应使用哪个引擎。
另请参阅
Session.binds
为多引擎会话协调事务
使用多个绑定引擎的一个注意事项是,如果提交操作在一个后端成功提交后在另一个后端失败,则可能会出现问题。这是一个一致性问题,在关系数据库中通过“两阶段事务”解决,它在提交序列中添加了一个额外的“准备”步骤,允许多个数据库在实际完成事务之前同意提交。
由于 DBAPI 中的支持有限,SQLAlchemy 对跨后端的两阶段事务的支持也有限。通常来说,它已知与 PostgreSQL 后端一起工作良好,与 MySQL 后端一起工作效果较差。然而,当后端支持时,Session
完全能够利用两阶段事务功能,方法是在 sessionmaker
或 Session
中设置 Session.use_twophase
标志。参见启用两阶段提交以获取示例。
自定义垂直分区
更全面的基于规则的类级分区可以通过覆盖 Session.get_bind()
方法来构建。下面我们演示了一个自定义的 Session
,它提供以下规则:
-
刷新操作以及批量的“更新”和“删除”操作都传递到名为
leader
的引擎。 -
所有子类为
MyOtherClass
的对象的操作都发生在other
引擎上。 -
对于所有其他类的读取操作都发生在随机选择的
follower1
或follower2
数据库上。
engines = {
"leader": create_engine("sqlite:///leader.db"),
"other": create_engine("sqlite:///other.db"),
"follower1": create_engine("sqlite:///follower1.db"),
"follower2": create_engine("sqlite:///follower2.db"),
}
from sqlalchemy.sql import Update, Delete
from sqlalchemy.orm import Session, sessionmaker
import random
class RoutingSession(Session):
def get_bind(self, mapper=None, clause=None):
if mapper and issubclass(mapper.class_, MyOtherClass):
return engines["other"]
elif self._flushing or isinstance(clause, (Update, Delete)):
# NOTE: this is for example, however in practice reader/writer
# splits are likely more straightforward by using two distinct
# Sessions at the top of a "reader" or "writer" operation.
# See note below
return engines["leader"]
else:
return engines[random.choice(["follower1", "follower2"])]
上述Session
类是通过 sessionmaker
的 class_
参数来插入的:
Session = sessionmaker(class_=RoutingSession)
这种方法可以与多个 MetaData
对象结合使用,使用类似于使用声明性 __abstract__
关键字的方法,如 abstract 中所述。
注意
上述示例说明了根据 SQL 语句是否期望写入数据将特定 SQL 语句路由到所谓的“主”或“从”数据库,但这可能不是一个实用的方法,因为它会导致在同一操作中读取和写入之间存在不协调的事务行为。实践中,最好在整个操作/事务进行的基础上,提前构建Session
作为“读取者”或“写入者”会话。这样,将要写入数据的操作也会在同一个事务范围内发出其读取查询。有关在sessionmaker
中设置“只读”操作的配方,使用自动提交连接,以及用于包含 DML/COMMIT 的“写入”操作的另一个配方,请参阅为 Sessionmaker / Engine 设置隔离级别的示例。
另请参阅
SQLAlchemy 中的 Django 风格数据库路由器 - 关于Session.get_bind()
更全面示例的博文
水平分区
水平分区将单个表(或一组表)的行分区到多个数据库中。SQLAlchemy 的Session
包含对这个概念的支持,但要完全使用它,需要使用Session
和Query
子类。这些子类的基本版本可在水平分区 ORM 扩展中找到。一个使用示例位于:水平分区。
简单的垂直分区
垂直分区将不同的类、类层次结构或映射表配置到多个数据库中,通过配置Session
的Session.binds
参数。该参数接收一个字典,其中包含任意组合的 ORM 映射类、映射层次结构内的任意类(例如声明基类或混合类)、Table
对象和Mapper
对象作为键,这些键通常引用Engine
或更少见的情况下引用Connection
对象作为目标。每当Session
需要代表特定类型的映射类发出 SQL 以定位数据库连接的适当源时,就会查询该字典:
engine1 = create_engine("postgresql+psycopg2://db1")
engine2 = create_engine("postgresql+psycopg2://db2")
Session = sessionmaker()
# bind User operations to engine 1, Account operations to engine 2
Session.configure(binds={User: engine1, Account: engine2})
session = Session()
在上述情况下,针对任一类的 SQL 操作将使用与该类链接的Engine
。该功能在读写操作中都是全面的;针对映射到engine1
的实体的Query
(通过查看请求的项目列表中的第一个实体来确定)将使用engine1
来运行查询。刷新操作将基于每个类使用两个引擎,因为它会刷新User
和Account
类型的对象。
在更常见的情况下,通常有基类或混合类可用于区分命令操作的目标数据库连接。Session.binds
参数可以接受任何任意的 Python 类作为键,如果在特定映射类的__mro__
(Python 方法解析顺序)中找到,则会使用该键。假设有两个声明基类代表两个不同的数据库连接:
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Session
class BaseA(DeclarativeBase):
pass
class BaseB(DeclarativeBase):
pass
class User(BaseA): ...
class Address(BaseA): ...
class GameInfo(BaseB): ...
class GameStats(BaseB): ...
Session = sessionmaker()
# all User/Address operations will be on engine 1, all
# Game operations will be on engine 2
Session.configure(binds={BaseA: engine1, BaseB: engine2})
在上述情况下,从BaseA
和BaseB
继承的类将根据它们是否继承自其中任何一个超类而将其 SQL 操作路由到两个引擎中的一个。对于从多个“绑定”超类继承的类,将选择目标类层次结构中最高的超类来表示应该使用哪个引擎。
另请参阅
Session.binds
多引擎会话的事务协调
在使用多个绑定引擎的情况下,有一个需要注意的地方是,在一个提交操作在一个后端成功提交后,另一个后端可能失败。这是一个一致性问题,在关系型数据库中通过“两阶段事务”解决,该事务将一个额外的“准备”步骤添加到提交序列中,允许多个数据库在实际完成事务之前同意提交。
由于 DBAPI 的支持有限,SQLAlchemy 对跨后端的两阶段事务的支持也有限。最典型的是,它在 PostgreSQL 后端上运行良好,并且在 MySQL 后端上的支持较少。但是,当后端支持时,Session
完全能够利用两阶段事务功能,方法是在sessionmaker
或Session
中设置Session.use_twophase
标志。参见启用两阶段提交以获取示例。
自定义垂直分区
可以通过重写Session.get_bind()
方法来构建更全面的基于规则的类级分区。以下是一个自定义Session
的示例,提供以下规则:
-
刷新操作以及批量“更新”和“删除”操作将传送到名为
leader
的引擎。 -
所有子类化
MyOtherClass
的对象操作都发生在other
引擎上。 -
所有其他类的读取操作都在
follower1
或follower2
数据库的随机选择上进行。
engines = {
"leader": create_engine("sqlite:///leader.db"),
"other": create_engine("sqlite:///other.db"),
"follower1": create_engine("sqlite:///follower1.db"),
"follower2": create_engine("sqlite:///follower2.db"),
}
from sqlalchemy.sql import Update, Delete
from sqlalchemy.orm import Session, sessionmaker
import random
class RoutingSession(Session):
def get_bind(self, mapper=None, clause=None):
if mapper and issubclass(mapper.class_, MyOtherClass):
return engines["other"]
elif self._flushing or isinstance(clause, (Update, Delete)):
# NOTE: this is for example, however in practice reader/writer
# splits are likely more straightforward by using two distinct
# Sessions at the top of a "reader" or "writer" operation.
# See note below
return engines["leader"]
else:
return engines[random.choice(["follower1", "follower2"])]
上述Session
类是通过向sessionmaker
传递class_
参数来插入的:
Session = sessionmaker(class_=RoutingSession)
这种方法可以与多个MetaData
对象结合使用,例如使用声明性的__abstract__
关键字的方法,如在 abstract 中所述。
注意
尽管上面的示例说明了将特定的 SQL 语句路由到基于语句是否期望写入数据的所谓 “leader” 或 “follower” 数据库,但这可能不是一种实际的方法,因为它导致在同一操作中读取和写入之间的不协调事务行为。实际上,最好是根据正在进行的整体操作 / 事务,提前将 Session
构造为 “读取者” 或 “写入者” 会话。这样,将要写入数据的操作也会在同一个事务范围内发出其读取查询。请参阅 为 Sessionmaker / Engine 设置隔离 中的示例,该示例设置了一个用于 “只读” 操作的 sessionmaker
,使用自动提交连接,另一个用于包含 DML / COMMIT 的 “写入” 操作。
另请参阅
SQLAlchemy 中的 Django 风格数据库路由器 - 有关 Session.get_bind()
的更全面示例的博客文章
水平分区
水平分区将单个表(或一组表)的行跨多个数据库进行分区。SQLAlchemy Session
包含对此概念的支持,但要充分利用它,需要使用 Session
和 Query
的子类。这些子类的基本版本在 水平分片 ORM 扩展中可用。使用示例位于:水平分片。
批量操作
遗留特性
SQLAlchemy 2.0 将 Session
的“批量插入”和“批量更新”功能集成到了 2.0 风格的 Session.execute()
方法中,直接使用了 Insert
和 Update
构造。请参阅 ORM 启用的 INSERT、UPDATE 和 DELETE 语句 文档,包括 遗留 Session 批量 INSERT 方法 ,其中说明了从旧方法迁移到新方法的示例。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异