【问题标题】:Prevent SQLAlchemy from automatically setting IDENTITY_INSERT防止 SQLAlchemy 自动设置 IDENTITY_INSERT
【发布时间】:2019-08-31 18:21:19
【问题描述】:

我有一个带有自动递增主键的 SQL Server 表,我正在使用 SQLAlchemy 对其进行操作。如果我创建包含主键的该表模型的实例而不是将其关闭并调用session.add(instance),SQLAlchemy 会自动向数据库发出SET IDENTITY_INSERT [table] ON 并且插入成功。

有没有办法自己控制这个?如果我尝试插入特定的主键而不是让它自动递增,我宁愿得到一个错误。如果我真的需要设置主键,我宁愿明确告诉它。

这是模型:

Base = declarative_base()

class Tub(Base):
    __tablename__ = 'Tub'

    id = Column('ID', Integer, primary_key=True, autoincrement=True)
    tare_weight = Column('TareWeight', Float(53), nullable=False)

这是一个插入示例:

t = Tub(id=20, tare_weight=200)
session.add(t)
session.commit()

这是生成的 SQL:

BEGIN
SET IDENTITY_INSERT [Tub] ON
INSERT INTO [Tub] ([ID], [TareWeight]) VALUES (20, 200)
SET IDENTITY_INSERT [Tub] OFF
COMMIT

更新:

我意识到的另一个相关情况是:

t = Tub(id=20, tare_weight=200)
session.merge(t)
session.commit()

如果 tub 已经存在于数据库中,我希望更新它的权重。如果没有,我希望插入失败,因为它包含显式主键。

【问题讨论】:

    标签: python sql-server sqlalchemy


    【解决方案1】:

    根据SQLAlchemy 1.3 Documentation

    对于不需要默认生成IDENTITY 的情况,请在第一个整数主键列上为Column.autoincrement 标志指定False

    m = MetaData()
    t = Table('t', m,
            Column('id', Integer, primary_key=True, autoincrement=False),
            Column('x', Integer))
    m.create_all(engine)
    

    【讨论】:

    • 我确实希望该列自动递增,这就是我在数据库中定义它的方式。问题是当我不小心包含主键列的值时,SQLAlchemy 通过调用SET IDENTITY_INSERT ON 自动覆盖列的标识属性。我不喜欢这种自动、无声的覆盖,我宁愿让它抛出一个错误。
    • 奇怪的是,SQLAlchemy 假设默认情况下每个带有primary_key=TrueInteger 列都必须是autoincrement - 这绝对是不是一个安全的选择。可以很容易地根据数据库元数据检查它以证明/反驳它,这一事实使它变得更加陌生。
    【解决方案2】:

    为什么不直接重写构造函数?

    class Tub(Base):
        __tablename__ = 'Tub'
    
        id = Column('ID', Integer, primary_key=True, autoincrement=True)
        tare_weight = Column('TareWeight', Float(53), nullable=False)
    
        def __init__(self, id=None, **kwargs):
            if id is not None:
                raise ValueError("Hey, don't pass in `id`!")
                # or you could even just ignore that it was passed in and issue a warning.
            super().__init__(**kwargs)
    

    如果你想将同样的方法应用到很多类,你可以实现一个 mixin:

    import warnings
    
    class AutoIncrMixin:
        id = Column('ID', Integer, primary_key=True, autoincrement=True)
    
        def __init__(self, id=None, **kwargs):
            if id is not None:
                warnings.warn("supplied `id` value was ignored")
            super().__init__(**kwargs)
    
    class Tub(Base, AutoIncrMixin):
        tare_weight = ...
    

    【讨论】:

    • 为了完整起见,此解决方案还应防止创建没有 id 的 Tub,然后将 tub.id 设置为其他内容。但不幸的是,即使有了这种增强,我认为我不能使用这种解决方法,因为我仍然需要创建具有显式 id 的 Tubs 的能力。用例是我希望能够通过以下步骤从数据库中更新现有的 Tub:1)将 JSON 从 PUT/PATCH 请求提供给 marshmallow-sqlalchemy 以生成 Tub 对象,2)从URL,3)调用session.merge()更新数据库中对应的Tub。
    • session.add() 一样,我不希望session.merge() 自动覆盖数据库列的自动递增特性来强制插入一个带有显式ID 的Tub桌子。我只想对 SQLAlchemy 说,“全面来说,永远不要自动禁用自动递增行为来进行插入。”
    • 虽然这个答案不是我可以在我当前的应用程序中使用的东西,但我决定 +1,因为它看起来仍然很有用。关于我的评论,它还应该禁止从构造函数外部设置主键(例如tub = Tub(...); tub.id = 5),我认为将 id 列包装在属性中可能会起作用,就像在这个答案中一样:stackoverflow.com/a/2317843/5405967。如果它确实有效,它应该消除对特殊构造函数的需要。我还没有尝试实现它,但我想我会为最终阅读本文的人提及这个想法。
    【解决方案3】:

    从长远来看,您可以进行完整的猴子修补。我不认为它可以解决你所有的问题,但它确实阻止了SET IDENTITY_INSERT 的使用。你可以修改MSExecutionContexts pre_execpost_exec。例如:

    from sqlalchemy import engine
    from sqlalchemy.dialects.mssql.base import MSExecutionContext
    
    def pre_exec(self):
        if self.isinsert:
            tbl = self.compiled.statement.table
            seq_column = tbl._autoincrement_column
            insert_has_sequence = seq_column is not None
    
            self._select_lastrowid = (
                not self.compiled.inline
                and insert_has_sequence
                and not self.compiled.returning
                and not self._enable_identity_insert
                and not self.executemany
            )
    
    
    def post_exec(self):
        conn = self.root_connection
        if self._select_lastrowid:
            if self.dialect.use_scope_identity:
                conn._cursor_execute(
                    self.cursor,
                    "SELECT scope_identity() AS lastrowid",
                    (),
                    self,
                )
            else:
                conn._cursor_execute(
                    self.cursor, "SELECT @@identity AS lastrowid", (), self
                )
            # fetchall() ensures the cursor is consumed without closing it
            row = self.cursor.fetchall()[0]
            self._lastrowid = int(row[0])
    
        if (
            self.isinsert or self.isupdate or self.isdelete
        ) and self.compiled.returning:
            self._result_proxy = engine.FullyBufferedResultProxy(self)
    
    
    MSExecutionContext.pre_exec = pre_exec
    MSExecutionContext.post_exec = post_exec
    

    我使用猴子的粗略示例修补了利用_enable_identity_insert 功能的功能,并删除了这些部分。现在每次尝试专门INSERTing 一个 ID 都会给你以下错误:

    sqlalchemy.exc.IntegrityError: (pyodbc.IntegrityError) ('23000', "[23000] [Microsoft][SQL Server Native Client 11.0][SQL Server]Cannot insert explicit value for identity column in table 'Tub' when IDENTITY_INSERT 设置为 OFF。(544) (SQLExecDirectW)")

    您现在的问题可能是您完全依赖自动增量功能,但它可能对某些人有用,或者您找到其他方法来绕过此功能。

    【讨论】:

    • 哇,这太棒了/激烈。我试了一下,它对于add()merge() 都适用于__table_args__ = {'implicit_returning': False} 的表。但是,出于好奇,我禁用了表的触发器并删除了该表的 args 行,它停止了工作。现在,在self._result_proxy = engine.FullyBufferedResultProxy(self) 线上,我得到AttributeError: 'Engine' object has no attribute 'FullyBufferedResultProxy'。另外,用@@identity 代替OUTPUT 有什么用?这不危险吗?
    • 为了澄清,我测试了应该成功或失败的添加和合并。使用implicit_returning=False,所有四个方案都可以正常工作。使用implicit_returning=True,除了其中一种情况外,所有情况仍然有效。导致错误的是由于不涉及显式主键而预计会成功的添加:session.add(Tub(tare_weight=100)); session.commit()
    • @MarredCheese 啊,我明白了。它需要from sqlalchemy import engine,所以可能会使用别名以避免与现有的engine 发生冲突。这对你有什么作用?
    • 添加 from sqlalchemy import engine 有效。不错的工作。我会接受这个答案,因为我怀疑会发布任何更简单的东西。我真的希望有一些简单的配置选项来控制这种行为,而不是需要猴子补丁核心功能。对我来说,默认行为包括静默覆盖数据库规则似乎显然是错误的。
    【解决方案4】:

    我正在添加我自己的答案,以详细说明我的 cmets 到 @SuperShoot 的答案。我认为,如果您打算首先阻止主键获取值的解决方法,您应该通过 property 而不是构造函数:

    class Tub(Base):
        __tablename__ = 'Tub'
    
        __id = Column('ID', Integer, primary_key=True, autoincrement=True)
        tare_weight = Column('TareWeight', Float(53), nullable=False)
    
        @property
        def id(self):
            return self.__id
    

    这会阻止你做这两件事,而不仅仅是第一件事:

    tub = Tub(id=5, tare_weight=100)
    
    or
    
    tub = Tub(tare_weight=100)
    tub.id = 5
    

    AttributeError: 无法设置属性

    这样做将停止显式主键插入,因为id 永远不会得到值。缺点是您不能创建对象然后调用session.merge() 来发出UPDATE 查询。

    ...除了 Python 允许您在确实需要时作弊:

    tub = Tub(_Tub__id=5, tare_weight=100)
    
    or
    
    tub = Tub(tare_weight=100)
    tub._Tub__id = 5
    

    【讨论】:

    • 您的 cmets 关于要求它与合并一起工作是我没有更新我的答案以包含此模式的确切原因。您是否查看过事件挂钩,看看是否有可以挂钩的东西?我一直想看看但太忙了。
    • @SuperShoot 是的,我想。不用担心。 (我想肯定是因为我在你的名字前加上了@,所以它一定是对你进行了 ping 操作。我什至没有意识到这是答案中的一件事 - 很酷。)就这个身份插入问题而言,我决定继续前进,因为它并不重要- 只是让我感到困惑。
    • 没有通知,只是打开一个标签页,其中包含我观看的标签提要。很高兴你把它整理得令你满意。好奇,你实现了猴子补丁解决方案吗?
    • @SuperShoot 有了猴子补丁,看起来复杂性/维护负担似乎是不必要的,只是为了处理让我烦恼的事情。对于不需要合并并且什么都不做的情况,我可能只做这个属性的事情。
    猜你喜欢
    • 2012-12-31
    • 1970-01-01
    • 2013-06-09
    • 1970-01-01
    • 2012-05-16
    • 2017-11-14
    • 1970-01-01
    • 2010-09-13
    • 2010-10-12
    相关资源
    最近更新 更多