【问题标题】:How can I switch two fields of a unique row within one commit using SQLAlchemy?如何使用 SQLAlchemy 在一次提交中切换唯一行的两个字段?
【发布时间】:2012-02-24 23:09:34
【问题描述】:

假设有一个具有唯一名称的对象。现在要切换两个对象的名称:

这是布局:

import sqlalchemy as sa
import sqlalchemy.orm as orm
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class MyObject(Base):
  __tablename__ = 'my_objects'
  id = sa.Column(sa.Integer, primary_key=True)
  name = sa.Column(sa.Text, unique=True)

if __name__ == "__main__":
  engine = sa.create_engine('sqlite:///:memory:', echo=True)
  Session = orm.sessionmaker(bind=engine)
  Base.metadata.create_all(engine)
  session = Session()

我想这样做:

a = MyObject(name="Max")
b = MyObject(name="Moritz")
session.add_all([a, b])
session.commit()

# Now: switch names!
tmp = a.name
a.name = b.name
b.name = tmp
session.commit()

这会引发IntegrityError。有没有办法在一次提交中切换这些字段而不会出现此错误?

【问题讨论】:

    标签: python sqlalchemy unique-constraint


    【解决方案1】:

    Python 允许这种语法(使用元组):

    a.name, b.name = b.name, a.name
    

    这样切换两个普通参数绝对没问题,但没有在你的情况下测试过,也许你可以试一试?

    【讨论】:

    • 只要我不提交可以正常工作的数据库。问题似乎出在数据库上。
    • 我似乎也没有找到等效的单语句 SQL 来执行此操作。也许这是 SQLAlchemy 没有考虑到的。
    【解决方案2】:

    您在名称字段中提供了unique=True,因此当您尝试提交时,它将运行更新查询,它会引发错误。

    这种情况是当您更改名称时它将在内存中设置。但是当它尝试运行更新查询时,旧记录已经存在同名,所以它会给出IntegrityError

    改名的方法是

    a = MyObject(name="Max")
    b = MyObject(name="Moritz")
    session.add_all([a, b])
    session.commit()
    
    # Now: switch names!
    atmp = a.name
    btemp = b.name
    
    a.name = a.name+btemp # Temp set the any random name
    session.commit()
    
    b.name = atemp
    a.name = btemp
    session.commit() # Run the update query for update the record.
    

    【讨论】:

    • 谢谢。实际上,它似乎是直接通过 SQLAlchemy 传递的基本 SQL 属性。这是一个相关问题的链接:stackoverflow.com/questions/644/…
    • 出于某种原因,我还必须将 b.name 设置为一个随机值(除了 a.name),因为在某些情况下,第二个 session.commit() 会引发 IntegrityError。
    • 第一次提交应该是一个flush(),否则你就违反了ACID语义(你的改变不是原子的)。
    【解决方案3】:

    更纯粹的选择是删除 a,重命名 b,然后重新添加重命名的:

    session.delete(a)
    sqlalchemy.orm.session.make_transient(a)
    a.name, b.name = b.name, a.name
    session.flush()
    session.add(a)
    session.commit()
    

    【讨论】:

      【解决方案4】:

      感谢Philipp der Rautenberg's comment,我找到了更好的解决方案,它适用于 PostgreSQL,但不适用于 SQLite。

      有关其工作原理的更多信息,请参见 this answerPostgreSQL docsSQLAlchemy docs

      这个想法是首先将唯一约束声明为deferrable,然后通过在initially中使用它来将其设置为DEFERRED

      import sqlalchemy as sa
      import sqlalchemy.orm as orm
      from sqlalchemy.ext.declarative import declarative_base
      
      Base = declarative_base()
      
      class MyObject(Base):
          __tablename__ = 'my_object'
          id = sa.Column(sa.Integer, primary_key=True)
          name = sa.Column(sa.Text)
          name_unique_constraint = sa.UniqueConstraint(name, deferrable=True, initially='DEFERRED')
      
      
      if __name__ == "__main__":
          engine = sa.create_engine('postgresql+psycopg2://user:password@hostname/database_name', echo=True)
          Session = orm.sessionmaker(bind=engine)
          Base.metadata.create_all(engine)
          session = Session()
      
          a = MyObject(name="Max")
          b = MyObject(name="Moritz")
          session.add_all([a, b])
          session.commit()
      
          # Now: switch names!
          a.name, b.name = b.name, a.name
      
          session.add_all([a, b]) # this line seems to be necessary to raise exception if swap woudn't be possible
          session.commit()
      

      或者用明确的sql语句:

      db.execute('SET CONSTRAINTS ALL DEFERRED')
      

      db.execute('SET CONSTRAINTS uq_my_object_name DEFERRED')
      

      但请记住在使用SET CONSTRAINTS 时:

      此命令仅更改当前事务中约束的行为。

      【讨论】:

        猜你喜欢
        • 2015-06-30
        • 1970-01-01
        • 2012-05-28
        • 1970-01-01
        • 1970-01-01
        • 2023-03-22
        • 2015-09-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多