SQLAlchemy flush()和获取插入ID?

186

我想要做这样的事情:

f = Foo(bar='x')
session.add(f)
session.flush()

# do additional queries using f.id before commit()
print f.id # should be not None

session.commit()

但是当我尝试时,f.idNone。怎样才能让它工作?


2
你能否使用 echo=True 初始化 SA 引擎,并查看在 flush 时执行的 SQL 吗?你所描述的 应该 能够工作并给出 id,但可能存在其他问题导致 f.id 为 None。 - Pavel Repin
10个回答

204

我刚刚遇到了同样的问题,在测试后发现这些答案都不够好。目前,或者说从 sqlalchemy .6 版本开始,有一个非常简单的解决方法(虽然我不知道在之前的版本中是否存在):

session.refresh()

因此,您的代码应该类似于:

f = Foo(bar=x)
session.add(f)
session.flush()
# At this point, the object f has been pushed to the DB, 
# and has been automatically assigned a unique primary key id

f.id
# is None

session.refresh(f)
# refresh updates given object in the session with its state in the DB
# (and can also only refresh certain attributes - search for documentation)

f.id
# is the automatically assigned primary key ID given in the database.

那就是如何做到的。


10
这个回答接近我需要的答案,但我收到了以下错误信息:InvalidRequestError: Could not refresh instance '<....>'. 刷新之后,该实例似乎不存在了。非常感激任何帮助。 - PlaidFan
1
你刚刚救了我的命。我不认为我以后还会用Django的ORM。我的个人看法是,flush()指令并不像文档中所述那样起作用。 - Marc
3
更新,我不得不使用sessionmaker(autoflush=True)这个组合,加上refresh()函数让我得到了行ID。#grrr - Marc
8
如果你不理解flush()commit()之间的区别,这里有一个很好的解释:https://dev59.com/zm855IYBdhLWcg3wrGdY#4202016。 - Epoc
3
与其使用flush(),建议使用commit(),并紧接着使用session.refresh(f)来更新。这对我有效,并且我使用的是SQLAlchemy版本 0.6.7 - Ricky Levi
3
Seams版本1.3.1不需要session.refresh(f) - Nelson G.

102

您的示例代码应该可以直接运行。假设f.id是自动生成的主键列,那么SQLAlchemy应该已经为其提供了一个值。在生成主键属性时,它们会立即在flush()过程中填充,并且不需要调用commit()。因此,答案可能涉及以下一个或多个方面:

  1. 您的映射细节
  2. 使用后端的任何奇怪特性(例如,SQLite不会为组合主键生成整数值)
  3. 在打开echo选项时,产生的SQL语句

2
你说得对,在shell中快速检查后,发现它填充了主键字段的值。我需要调查一下为什么在实践中没有工作。 - Eloff

39

谢谢大家。我通过修改列映射解决了我的问题。对我来说,autoincrement=True 是必需的。

原始内容:

id = Column('ID', Integer, primary_key=True, nullable=False)

修改后:

id = Column('ID', Integer, primary_key=True, autoincrement=True, nullable=True)
然后
session.flush()  
print(f.id)

没问题!


3
主键不应该是可空的。 - Maarti

10
在过去的几个小时/几天/其他时间里,我一直在努力让上述建议起作用。最初,我编写了所有的插入函数如下:
_add = User(id, user_name, email, ...)

在圆括号中的所有项都是变量,可以是 None、"user a"、"a@example.com" 等。

这是我的用户表:

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, autoincrement=True)
    user_name = Column(String(50), unique=True, nullable=False)
    email = Column(String(100), unique=True, nullable=False)

SQLAlchemy可以正确处理_add查询,因为它会插入具有自动递增ID的记录。同时,id列没有设置默认值,这是应该的。

我已经以各种方式尝试了上述所有选项(包括提交/不提交、刷新/不刷新、刷新/不刷新、先执行一个操作再执行另一个操作、在语句之间设置超时等),甚至几次改变整个应用程序/数据库交互。但在所有情况下,“_add.id”要么返回0,要么返回“实例''已被删除或其行不存在”。

刚才我想:“也许我应该稍微改变我的_add查询方式,通过为指定表定义列名来定义它”,如下所示:

_add = User(id=None, user_name=user_name, email=email, etc)

强调一下,注意:在_add查询中,需要包含id=user_name=email=。按照以下顺序执行这些语句,SQLAlchemy将返回插入的ID!
session.add(_add)
print(_add.id)    <-- returns None

session.flush()   <-- does **not** insert record into database, but does increment id,
                      waiting to be committed. Flush may be omitted, because
                      session.commit() unconditionally issues session.flush()*
print(_add.id)    <-- returns incremented id

session.commit()  <-- commit is needed to actually insert record into database
print(_add.id)    <-- returns incremented id

尽管答案已经给出,但我对_add查询中缺少的列名不清楚,因此我的懒惰是导致问题的原因。希望这能帮助有人避免相同的故障... SQLAlchemy文档

8
核心解决方案在其他更早的答案中已经提到,但这里使用了更新的异步API。
使用sqlalchemy==1.4(2.0样式),以下内容似乎可以工作:
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine

engine = create_async_engine(
        "postgresql+asyncpg://user:pass@localhost/db",
        echo=False,
    )


# expire_on_commit=False will prevent attributes from being expired
# after commit.
async_session = sessionmaker(
    engine, expire_on_commit=False, class_=AsyncSession,
)
# default kwarg autoflush=True


async with async_session() as session: 
    async with session.begin(): 
        f = Foo(bar='x')
        session.add(f)
        print(f.id)
        # None

        await session.flush()
        print(f.id)
        # not None
    # commits transaction, closes session

特别是,如果您忘记await调用session.flush,那么ID将不会存在(因为刷新操作还没有发生)。 - undefined

7
与 dpb 给出的答案不同,刷新并不是必要的。一旦您清除缓存,您就可以访问 id 字段,SQLAlchemy 会自动刷新在后端自动生成的 id。
我遇到了这个问题,并在一些调查后找到了确切的原因。我的模型是使用 integerfield 创建的,在我的表单中,id 被表示为 hiddenfield(因为我不想在表单中显示 id)。默认情况下,隐藏字段被表示为文本。一旦我将表单更改为带有 widget=hiddenInput() 的 integerfield,问题就解决了。

1
如我所述,只有refresh()对我有效。在进行数据迁移时,我需要在循环中使用行ID来填充FK。我尝试了每种组合的commit、flush、session hacking,只有refresh()起作用。我的数据非常干净,但我发现SQLA并不是真正好用的(至少有5个主要ORMS的相当经验)。我花了5个多小时来获取add()->commit()/flush()的行ID,最终还是成功了。 - Marc

2

我的代码是这样工作的:

f = Foo(bar="blabla")
session.add(f)
session.flush()
session.refresh(f, attribute_names=[columns name that you want retrieve]
# so now you can access the id inserted, for example
return f.id # id inserted will be returned

0

我曾经遇到过一个问题,就是在调用 session.add 方法之前将 0 赋值给了 id。尽管数据库正确地分配了 id,但在 session.flush() 后未能从会话中检索到正确的 id。


0
我在使用bulk_save_objects时遇到了同样的问题,即在创建对象后丢失id值。
幸运的是,bulk_save_objects中的return_defaults参数会将所有id添加到对象中,而无需调用flush或commit。
users: list[User] = [User(user_name=..., email=..., etc) for foo in bar]
session.bulk_save_objects(references, return_defaults=True)
assert users[0].id is not None # User.id is now populated.

请参阅SQLAlchemy文档::param return_defaults: 当为True时,缺少值的行将被插入一次一个,以便主键值可用。这些缺少值通常是整数主键默认值和序列。

-7
你应该尝试使用 session.save_or_update(f) 而不是 session.add(f)

3
自0.5版本以来,“save_or_update”已被弃用。应该使用“session.add()”。 - Pavel Repin

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