SQLAlchemy:flush()和commit()有什么区别?

662

flush()commit()在SQLAlchemy中有什么区别?

我已经阅读了文档,但并没有更清楚 - 它们似乎假定我已经具备了某些预先了解的知识。

我特别关注它们对内存使用的影响。 我正在从一系列文件(总共约500万行)向数据库加载一些数据,我的会话偶尔崩溃 - 这是一个大型数据库,并且机器内存不足。

我在想是否使用了太多的commit()而没有足够的flush()调用 - 但是如果我不真正理解它们之间的区别,那就很难判断!


30
因为“none the wiser”而点赞……没人喜欢SQLAlchemy文档。 :( - ThoPaz
8个回答

847

Session对象基本上是对数据库的更改(更新、插入、删除)的持续事务。这些操作在提交之前不会持久保存到数据库中(如果由于某种原因您的程序在事务过程中中止,则其中未提交的更改将丢失)。

Session对象使用session.add()注册事务操作,但在调用session.flush()之前还没有将它们传递给数据库。

session.flush()向数据库通信一系列操作(插入、更新、删除)。数据库将它们作为挂起操作保留在事务中。在数据库接收到当前事务的COMMIT之前,这些更改不会永久保存到磁盘上,也不会对其他事务可见(这就是session.commit()的作用)。

session.commit()将这些更改提交(持久化)到数据库中。

flush()始终作为调用commit()的一部分而被调用(1)。

当您使用Session对象查询数据库时,查询将返回来自数据库和它所持有的未提交事务的刷新部分的结果。默认情况下,Session对象autoflush其操作,但可以禁用此功能。

希望这个例子能让这个问题更加清晰:

#---
s = Session()

s.add(Foo('A')) # The Foo('A') object has been added to the session.
                # It has not been committed to the database yet,
                #   but is returned as part of a query.
print 1, s.query(Foo).all()
s.commit()

#---
s2 = Session()
s2.autoflush = False

s2.add(Foo('B'))
print 2, s2.query(Foo).all() # The Foo('B') object is *not* returned
                             #   as part of this query because it hasn't
                             #   been flushed yet.
s2.flush()                   # Now, Foo('B') is in the same state as
                             #   Foo('A') was above.
print 3, s2.query(Foo).all() 
s2.rollback()                # Foo('B') has not been committed, and rolling
                             #   back the session's transaction removes it
                             #   from the session.
print 4, s2.query(Foo).all()

#---
Output:
1 [<Foo('A')>]
2 [<Foo('A')>]
3 [<Foo('A')>, <Foo('B')>]
4 [<Foo('A')>]

3
对于不支持事务的数据库引擎(例如MyISAM),这也是错误的。由于没有正在进行的事务,因此刷新(flush)与提交(commit)之间的区别更小。 - underrun
1
@underrun 如果我在session.flush()之后执行session.query(),那么我能看到我的更改吗?假设我正在使用MyISAM。 - Frozen Flame
3
在使用flush()commit()时,是否是好的风格,还是应该让Alchemy来处理?我在某些情况下使用了flush(),因为后续查询需要获取新数据。 - Jens
@FrozenFlame 是的,因为数据在交易内对您可见。但是在其他交易中还没有出现。 - Kiran Jonnalagadda
1
@Jens 使用autoflush(默认为True)。它会在所有查询之前自动刷新,因此您不必每次都记住。 - Kiran Jonnalagadda
显示剩余4条评论

64

这并不是严格意义上的回答原问题,但有些人提到使用 session.autoflush = True,你就不必使用 session.flush()... 但这并不总是正确的。

如果您想在事务中间使用新创建对象的id,则必须调用session.flush()

# Given a model with at least this id
class AModel(Base):
   id = Column(Integer, primary_key=True)  # autoincrement by default on integer primary key

session.autoflush = True

a = AModel()
session.add(a)
a.id  # None
session.flush()
a.id  # autoincremented integer

这是因为autoflush不会自动填充id(尽管对象的查询会填充,有时会导致困惑,例如“为什么在这里可以工作但在那里不能?”但snapshoe已经涵盖了这部分)。


我认为一个相关而重要的方面并未被提及:

为什么不一直提交? - 答案是原子性

一个花哨的词来说:一系列操作必须全部成功执行,否则它们中的任何一个都将不起作用。

例如,如果您想创建/更新/删除某个对象(A),然后创建/更新/删除另一个对象(B),但如果(B)失败,则要恢复(A)。 这意味着这两个操作是原子的

因此,如果(B)需要(A)的结果,则应在(A)后调用flush,在(B)后调用commit

此外,如果session.autoflush为True,除了我上面提到的情况或Jimbo答案中的其他情况,您将不需要手动调用flush


38

当您需要模拟写入时,请使用flush,例如从自动增量计数器中获取主键ID。

john=Person(name='John Smith', parent=None)
session.add(john)
session.flush()

son=Person(name='Bill Smith', parent=john.id)

如果不进行flush操作,john.id将为null。

正如其他人所说,如果没有commit(),则这些更改都不会永久地存储到数据库中。


1
为了澄清自己和其他可能像我一样困惑的人:这不是持久化的,但自增部分是:flush 保留了 ID,因此即使您不提交,多次运行此代码也会导致不同的 ID(否则它无法工作)。 - bfontaine

33

为什么要刷新缓存而不是直接提交?

作为一个刚开始使用数据库和SQLAlchemy的人来说,之前的回答——flush()将SQL语句发送到数据库中,而commit()则将它们持久化——并没有让我很清楚。这些定义很有道理,但从定义上很难立即理解为什么你会使用刷新而不是直接提交。

由于提交总是会刷新(https://docs.sqlalchemy.org/en/13/orm/session_basics.html#committing),所以这两个听起来非常相似。我认为需要强调的问题是刷新不是永久的,可以被撤销,而提交是永久的,也就是说你不能要求数据库撤销最后一次提交(我想)。

@snapshoe强调,如果你想查询数据库并获得包括新添加对象的结果,你需要先刷新(或提交,提交将为您刷新)。也许这对一些人很有用,虽然我不确定为什么你会选择刷新而不是提交(除了可以被撤销这个微不足道的答案)。

在另一个例子中,我在本地数据库和远程服务器之间同步文档,如果用户决定取消操作,则所有的添加/更新/删除都应该被撤销(即不进行部分同步,只进行完全同步)。当更新单个文档时,我决定简单地删除旧记录并添加来自远程服务器的更新版本。结果发现,由于sqlalchemy的编写方式,提交时操作顺序不能得到保证。这导致了添加了重复的版本(在尝试删除旧版本之前),从而导致数据库无法满足唯一约束条件。为了解决这个问题,我使用了flush()以确保顺序正确,但如果后来同步过程失败,我仍然可以撤消这些操作。
参见我的帖子:Is there any order for add versus delete when committing in sqlalchemy 同样,有人想知道提交时是否维护添加顺序,即如果我添加object1, 然后添加object2,那么object1会在object2之前被添加到数据库吗?
参见:Does SQLAlchemy save order when adding objects to session? 总之,flush() 的一个用途是提供顺序保证(我认为),同时仍然允许您进行“撤消”选项,这是提交操作所不提供的。

自动刷新和自动提交

注意,可以使用自动刷新来确保查询作用于已更新的数据库,因为SQLAlchemy会在执行查询之前进行刷新。 https://docs.sqlalchemy.org/en/13/orm/session_api.html#sqlalchemy.orm.session.Session.params.autoflush

自动提交是另一种我不完全理解的东西,但听起来似乎不鼓励使用:https://docs.sqlalchemy.org/en/13/orm/session_api.html#sqlalchemy.orm.session.Session.params.autocommit

内存使用

现在,原始问题实际上想了解刷新与提交对内存的影响。由于持久化或不持久化是数据库提供的功能(我认为),因此仅刷新应足以卸载到数据库 - 虽然如果您不关心撤消,则提交不会有害(实际上可能有帮助-请参见下文)。

SQLAlchemy对已经刷新的对象使用弱引用:https://docs.sqlalchemy.org/en/13/orm/session_state_management.html#session-referencing-behavior

这意味着如果您没有在列表或字典等地方显式地持有一个对象,sqlalchemy就不会将其保存在内存中。

但是,对于数据库方面,你需要担心。假设没有提交而进行刷出操作会有一些内存代价来维护事务。再次说明,我对此很陌生,但下面的链接似乎确实表明了这一点:https://dev59.com/22Up5IYBdhLWcg3wdnd6#15305650

换句话说,提交应该会减少内存使用,尽管在这里可能存在内存和性能之间的权衡。换句话说,您可能不想每次进行单个数据库更改时都提交一次(因为性能原因),但是等待时间过长会增加内存使用。


1
“autocommit”是人们通常与数据库交互的简单直观方式。没有事务,或更准确地说,每个操作本身就是一个事务,会自动开始和提交(因此称为“autocommit”)。想象一下您如何通过cli客户端更新表而不发出“begin/commit”语句。实际上,您的查询将被包装在隐式事务中。在应用程序中不建议使用它,因为这会削弱关系型数据库管理系统最强的卖点之一:事务的ACID特性(即原子性、一致性、隔离性和持久性)的保证。 - Michael Ekoka
为什么要刷新缓存,如果可以提交呢?有时候你需要在数据库层面执行多个操作,但是如果其中任何一个操作失败,你希望回滚。例如,你需要保存用户的状态,然后检查数据库中所有用户是否具有相同的状态,如果所有用户的状态都是“已批准”,则将部门状态更改为“已批准”。这些操作彼此依赖,如果在所有步骤完成之前应用程序崩溃,则无法保存状态。 - Ramon Dias

10

除非您了解什么是数据库 事务,否则现有的答案并没有太多意义。(直到最近我自己也是如此。)

有时,您可能希望运行多个SQL语句,并将它们作为一个整体成功或失败。例如,如果您想要执行从A账户到B账户的银行转账,则需要执行两个查询语句,例如:

UPDATE accounts SET value = value - 100 WHERE acct = 'A'
UPDATE accounts SET value = value + 100 WHERE acct = 'B'

如果第一个查询成功,但第二个查询失败,这是不好的(原因很明显)。因此,我们需要一种方法将这两个查询“作为一个整体”处理。解决方案是从BEGIN语句开始,以COMMIT语句或ROLLBACK语句结束,例如:

BEGIN
UPDATE accounts SET value = value - 100 WHERE acct = 'A'
UPDATE accounts SET value = value + 100 WHERE acct = 'B'
COMMIT

这是一次单独的交易.

在SQLAlchemy的ORM中,它可能看起来像:

                                      # BEGIN issued here
acctA = session.query(Account).get(1) # SELECT issued here
acctB = session.query(Account).get(2) # SELECT issued here

acctA.value -= 100
acctB.value += 100

session.commit()                      # UPDATEs and COMMIT issued here 

如果您监视各种查询的执行时间,您会发现在调用 session.commit() 之前,UPDATE语句没有对数据库产生影响。

在某些情况下,您可能希望在发出COMMIT之前执行UPDATE语句。(也许数据库向对象发出一个自动递增的ID,并且您希望在提交之前获取它)。在这些情况下,您可以显式地使用 flush() 函数刷新会话。

                                      # BEGIN issued here
acctA = session.query(Account).get(1) # SELECT issued here
acctB = session.query(Account).get(2) # SELECT issued here

acctA.value -= 100
acctB.value += 100

session.flush()                       # UPDATEs issued here 
session.commit()                      # COMMIT issued here 

这篇帖子应该是对该主题的回答。 - undefined

5

简单定位:

  • commit执行实际更改(它们在数据库中变得可见)
  • flush进行虚拟更改(它们仅对您可见)

想象一下,数据库的工作方式就像git分支:

  • 首先,您必须了解在 transaction 过程中,您并不是在操作真实的数据库数据。
  • 相反,您会得到一个类似于新的 branch,然后在其中进行操作。
  • 如果您在某个时刻编写了 commit 命令,那么意味着:“merge 我的数据更改到主数据库数据中”。
  • 但是,如果您需要一些未来的数据,只有在 commit 后才能获取(例如插入到表中,并且您需要插入的 PKID),那么您可以使用 flush 命令,表示:““计算出未来的 PKID并为我保留它
  • 然后,您可以在代码中进一步使用该 PKID 值,并确保真实数据符合预期。
  • Commit 必须始终放在最后,以合并到主数据库数据中。

0
很多人仍然感到困惑,所以我试着加上自己的解释。
几乎每个人都知道commit是什么,但flush有些不清楚。
如果你使用sqlalchemy的Session来操作ORM对象,你应该明白Session不是你的数据库实体,而是sqlalchemy的抽象层。你对ORM对象的所有操作都会在session中"存储",直到你强制sqlalchemy执行真正的数据库查询。通常情况下,这会在session的上下文管理器__exit__被调用时发生,就在commit之前。但你也可以手动强制执行。
>>> from main import Session, User
>>> session = Session()

>>> usr1 = User(name="John")  # No SQL happen here
>>> usr1.name = "Kevin"       # Also no SQL happen here   
>>> usr1.id is None           # ID is DB-side autoincrement, so it undefined
True
>>> session.add(usr1)         # Probably you think that SQL INSERT sent, but no!
                              # User object just "attached" to session
>>> usr1.name = "Robert"      # Manipulate over ORM object, but no single SQL sent      

>>> session.flush()           # Forcing sqlalchemy sync session state to DB
                              # First real SQL issued only there
INFO sqlalchemy.engine.Engine select pg_catalog.version()
INFO sqlalchemy.engine.Engine [raw sql] {}
INFO sqlalchemy.engine.Engine select current_schema()
INFO sqlalchemy.engine.Engine [raw sql] {}
INFO sqlalchemy.engine.Engine show standard_conforming_strings
INFO sqlalchemy.engine.Engine [raw sql] {}
INFO sqlalchemy.engine.Engine BEGIN (implicit)
INFO sqlalchemy.engine.Engine INSERT INTO users (name) VALUES (%(name)s) RETURNING users.id
INFO sqlalchemy.engine.Engine [generated in 0.00009s] {'name': 'Robert'}

>>> session.commit()          # It sent COMMIT to DB
INFO sqlalchemy.engine.Engine COMMIT

# ----- Let's play other way -----

>>> usr2 = User(name="other user")      # No SQL 
>>> session.add(usr2)                   # Still no SQL
>>> session\
        .query(User)\
        .filter_by(name="other user")\
        .all()                          # Sqlalchemy flushing state before do SELECT
INFO sqlalchemy.engine.Engine INSERT INTO users (name) VALUES (%(name)s) RETURNING users.id
INFO sqlalchemy.engine.Engine [cached since 40.72s ago] {'name': 'other user'}
INFO sqlalchemy.engine.Engine SELECT users.id AS users_id, users.name AS users_name 
FROM users 
WHERE users.name = %(name_1)s
INFO sqlalchemy.engine.Engine [generated in 0.00016s] {'name_1': 'other user'}
[User(id=6, name=other user)]

>>> session.flush()                      # So "flush" do nothing - session state already flushed

>>> session.commit()                     # Commit sent COMMIT, as expected
INFO sqlalchemy.engine.Engine COMMIT

>>> session.close()

-1

commit()将这些更改记录到数据库中。flush()始终作为commit()(1)调用的一部分调用。当您使用Session对象查询数据库时,查询将返回从数据库和正在执行的未记录事务的红色部分中的结果。


3
这个回答实际上没有为已有的答案增加任何内容吗? - oskros

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接