【问题标题】:PyODBC: how to replicate behavior of batch separator (GO) in a transaction?PyODBC:如何在事务中复制批处理分隔符 (GO) 的行为?
【发布时间】:2021-10-31 07:53:56
【问题描述】:

背景

我维护一个 Python 应用程序,该应用程序自动将 SQL 模式迁移(添加/删除表和列、调整数据等)应用到我们的数据库 (SQL2016)。每次迁移都是通过 PyODBC 在事务中执行的,以便在出现问题时可以回滚。有时迁移需要一个或多个batch statements (GO) 才能正确执行。由于GOnot actually a T-SQL command but rather a special keyword in SSMS,我一直在GO 上拆分每个SQL 迁移,并在同一个事务中分别执行每个SQL 片段。

import pyodbc
import re

conn_args = {
    'driver': '{ODBC Driver 17 for SQL Server}',
    'hostname': 'MyServer',
    'port': 1298,
    'server': r'MyServer\MyInstance',
    'database': 'MyDatabase',
    'user': 'MyUser',
    'password': '********',
    'autocommit': False,
}
connection = pyodbc.connect(**conn_args)
cursor = connection.cursor()

sql = '''
    ALTER TABLE MyTable ADD NewForeignKeyID INT NULL FOREIGN KEY REFERENCES MyParentTable(ID)
    GO
    UPDATE MyTable
    SET NewForeignKeyID = 1
'''
sql_fragments = re.split(r'^\s*GO;?\s*$', sql, flags=re.IGNORECASE|re.MULTILINE)

for sql_frag in sql_fragments:
    cursor.execute(sql_frag)
    # Wait for the command to complete.  This is necessary for some database system commands
    # (backup, restore, etc).  Probably not necessary for schema migrations, but included
    # for completeness.
    while cursor.nextset():
        pass

connection.commit()

问题

SQL 语句批处理没有像我预期的那样执行。当在 SSMS 中执行上述模式迁移时,它会成功。在 Python 中执行时,第一批(添加外键)执行得很好,但第二批(设置外键值)失败,因为它不知道新的外键。

('42S22', "[42S22] [FreeTDS][SQL Server]Invalid column name 'NewForeignKeyID'. (207) (SQLExecDirectW)")

目标

在 PyODBC 中的单个事务中执行 SQL 语句批次的层次结构(即每个语句批次取决于前一个批次)。

我的尝试

  • 搜索PyODBC documentation 以获取有关 PyODBC 如何支持或不支持批处理语句/GO 命令的信息。未找到参考资料。

  • 搜索 StackOverflow 和 Google 以了解如何在 PyODBC 中批处理语句。

  • 在 SQL 片段执行之间引入一个小睡眠,以防出现某种竞争条件。似乎不太可能成为解决方案,并且没有改变行为。

  • 我考虑过将每批语句分离成一个单独的事务,在下一批执行之前提交,但这会降低/消除我们自动回滚失败的架构迁移的能力。

  • 编辑:我刚刚找到this question,这正是我想要做的。但是,在测试(在 SSMS 中)the answer that recommends using EXEC 时,我发现 第二个 EXEC 命令(设置值)失败,因为它不知道新的外键。我不擅长测试,它确实成功了。此解决方案可能有效,但并不理想,因为 EXEC 与参数不兼容。此外,如果跨片段使用变量,这将不起作用。

     BEGIN TRAN
         EXEC('ALTER TABLE MyTable ADD NewForeignKeyID INT NULL FOREIGN KEY REFERENCES MyParentTable(ID)')
         EXEC('UPDATE MyTable SET NewForeignKeyID = 1')
     ROLLBACK TRAN
    
     Invalid column name 'FK_TestID'.
    

【问题讨论】:

  • 我刚刚在 SSMS 中尝试了您的 BEGIN TRAN / EXEC / … 块,它在 SQL Server 2017 和默认事务隔离(“READ COMMITTED”,IIRC)中对我来说很好。当事务提交(而不是回滚)时,该列被添加并且 UPDATE 成功。
  • 它实际上是否正确拆分它?你有re.split(r'^\s*GO;?\s*$' 不应该是re.split(r'\s*GO;?\s*' 否则它最多只会拆分一次,因为文本只有一个开头和结尾
  • @GordThompson 我刚刚重新测试了我的 EXEC 块,现在可以正常工作了。我一定是之前打错了什么。 EXEC 不是一个理想的解决方案,因为它与参数不兼容,但如果这是我唯一的选择,它可能会起作用。我还注意到,在我的示例代码中,我正在执行整个 sql 而不是 sql_frag,所以我会解决这个问题。 @Charlieface 好问题,但是是的,它正在正确拆分。查看re.MULTILINE 的文档。
  • 另外,EXEC 方法与跨多个片段使用的变量不兼容。
  • 如果您的 Python 脚本正在从文本文件中读取 SQL 代码(包括 GO 关键字),那么您可能会考虑只使用该文本文件作为输入来运行 sqlcmd 实用程序。

标签: python sql sql-server pyodbc


【解决方案1】:

如果您正在从文本文件(例如由 SSMS 中的脚本对象生成的)读取 SQL 语句,那么您可以使用 Python 的 subprocess 模块来运行 sqlcmd 实用程序,将该文件作为输入(@ 987654324@)。最简单的形式看起来像

server = "localhost"
port = 49242
uid = "scott"
pwd = "tiger^5HHH"
database = "myDb"
script_file = r"C:\__tmp\batch_test.sql"
"""contents of the above file:
DROP TABLE IF EXISTS so69020084;
CREATE TABLE so69020084 (src varchar(10), var_value varchar(10));
INSERT INTO so69020084 (src, var_value) VALUES ('1st batch', 'foo');
GO
INSERT INTO so69020084 (src, var_value) VALUES ('2nd batch', 'bar');
GO
"""

import subprocess

cmd = [
    "sqlcmd",
    "-S", f"{server},{port}",
    "-U", uid,
    "-P", pwd,
    "-d", database,
    "-i", script_file,
]
subprocess.run(cmd)

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-01-09
  • 1970-01-01
  • 2013-05-29
  • 1970-01-01
  • 2011-02-25
  • 2023-03-10
  • 1970-01-01
相关资源
最近更新 更多