SQLAlchemy:尝试保存非唯一值后重新保存模型的唯一字段

7 投票
3 回答
6257 浏览
提问于 2025-04-17 03:02

在我的SQLAlchemy应用中,我有一个这样的模型:

from sqlalchemy import Column, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
from zope.sqlalchemy import ZopeTransactionExtension

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))

class MyModel(declarative_base()):
    # ...
    label = Column(String(20), unique=True)

    def save(self, force=False):
        DBSession.add(self)
        if force:
            DBSession.flush()

后来在代码中,对于每一个新的MyModel对象,我想随机生成一个label,如果生成的值已经在数据库里存在,就重新生成。
我尝试做以下操作:

# my_model is an object of MyModel
while True:
    my_model.label = generate_label()
    try:
        my_model.save(force=True)
    except IntegrityError:
        # label is not unique - will do one more iteration
        # (*)
        pass
    else:
        # my_model saved successfully - exit the loop
        break

但是当第一次生成的label不唯一,并且在第二次(或之后的)迭代中调用save()时,我遇到了这个错误:

 InvalidRequestError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original exception was: (IntegrityError) column url_label is not unique... 

当我在位置(*)添加DBSession.rollback()时,我得到了这个结果:

 ResourceClosedError: The transaction is closed

我该如何正确处理这种情况呢?
谢谢

3 个回答

2

我在用Pyramid框架写的网页应用中遇到了类似的问题。我找到了一种稍微不同的解决办法。

while True:
    try:
        my_model.label = generate_label()
        DBSession.flush()
        break
    except IntegrityError:
        # Rollback will recreate session:
        DBSession.rollback()
        # if my_model was in db it must be merged:
        my_model = DBSession.merge(my_model)

合并这部分非常重要,如果之前没有存储过my_model,合并就会导致会话为空,这样刷新就不会有任何效果。

2

数据库在告诉你一个事务失败的原因时,并没有统一的方式,这种信息也不容易被自动化程序获取。一般来说,你不能简单地尝试执行这个事务,然后因为某个特定的原因失败后再重试。

如果你知道有某种条件需要绕过(比如唯一性约束),你需要自己去检查这个约束。在使用sqlalchemy时,代码大概是这样的:

# Find a unique label
label = generate_label()
while DBsession.query(
        sqlalchemy.exists(sqlalchemy.orm.Query(Model)
                  .filter(Model.lable == label)
                  .statement)).scalar():
    label = generate_label()

# add that label to the model
my_model.label = label
DBSession.add(my_model)
DBSession.flush()

补充:还有一种回答方式是,你不应该自动重试这个事务;你可以返回一个HTTP状态码307 Temporary Redirect(并在重定向的URL中加点额外信息),这样事务就能真正从头开始。

5

如果你的 session 对象出现了回滚,基本上你需要创建一个新的会话,并在重新开始之前刷新你的模型。如果你在使用 zope.sqlalchemy,那么你应该使用 transaction.commit()transaction.abort() 来控制事务。所以你的循环大概会是这样的:

# you'll also need this import after your zope.sqlalchemy import statement
import transaction

while True:
    my_model.label = generate_label()
    try:
        transaction.commit()
    except IntegrityError:
        # need to use zope.sqlalchemy to clean things up
        transaction.abort()
        # recreate the session and re-add your object
        session = DBSession()
        session.add(my_model)
    else:
        break

我把 session 对象的使用从对象的 save 方法中提取出来了。我不太确定在你这样使用的类级别上, ScopedSession 是如何自我刷新的。个人认为,把 SqlAlchemy 的东西嵌入到你的模型中,实际上并不太符合 SqlAlchemyunit of work(工作单元)方法。

如果你的标签对象确实是一个生成的唯一值,那么我同意 TokenMacGuy 的看法,直接使用 uuid 值就可以了。

希望这对你有帮助。

撰写回答