【发布时间】:2021-10-31 07:53:56
【问题描述】:
背景
我维护一个 Python 应用程序,该应用程序自动将 SQL 模式迁移(添加/删除表和列、调整数据等)应用到我们的数据库 (SQL2016)。每次迁移都是通过 PyODBC 在事务中执行的,以便在出现问题时可以回滚。有时迁移需要一个或多个batch statements (GO) 才能正确执行。由于GO 是not 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