让Python等待存储过程执行完成
我有一个Python脚本,使用pyodbc来调用MSSQL的存储过程,代码大概是这样的:
cursor.execute("exec MyProcedure @param1 = '" + myparam + "'")
我在一个循环里调用这个存储过程,发现有时候它会在上一次还没执行完的时候就被再次调用。我知道这一点是因为如果我在执行的那一行后面加上这一行:
time.sleep(1)
那么一切就正常了。
有没有更优雅、耗时更少的方法来表示“等到执行完成再继续”呢?
更新(Divij的解决方案):这个代码现在对我来说不管用:
from tornado import gen
import pyodbc
@gen.engine
def func(*args, **kwargs):
# connect to db
cnxn_str = """
Driver={SQL Server Native Client 11.0};
Server=172.16.111.235\SQLEXPRESS;
Database=CellTestData2;
UID=sa;
PWD=Welcome!;
"""
cnxn = pyodbc.connect(cnxn_str)
cnxn.autocommit = True
cursor = cnxn.cursor()
for _ in range(5):
yield gen.Task(cursor.execute, 'exec longtest')
return
func()
5 个回答
我觉得我的方法有点粗糙,但同时也更容易理解:
cursor = connection.cursor()
SQLCommand = ("IF EXISTS(SELECT 1 FROM msdb.dbo.sysjobs J JOIN
msdb.dbo.sysjobactivity A ON A.job_id = J.job_id WHERE J.name ='dbo.SPNAME' AND
A.run_requested_date IS NOT NULL AND A.stop_execution_date IS NULL) select 'The job is
running!' ELSE select 'The job is not running.'")
cursor.execute(SQLCommand)
results = cursor.fetchone()
sresult= str(results)
while "The job is not running" in sresult:
time.sleep(1)
cursor.execute(SQLCommand)
results = cursor.fetchone()
sresult= str(results)
当“SPNAME”在jobactivity表中返回“这个任务没有在运行”时,就等1秒钟,然后再检查一次结果。这个方法适用于SQL任务,对于存储过程(SP),应该在另一个表中进行类似的检查。
Python没有内置的方法可以让你等待一个异步调用完成。不过,你可以使用Tornado的IOLoop来实现这个功能。Tornado的gen
接口允许你把一个函数调用注册为一个Task
,并在这个调用执行完后再继续执行函数中的下一行代码。下面是一个使用gen
和gen.Task
的例子。
from tornado import gen
@gen.engine
def func(*args, **kwargs)
for _ in range(5):
yield gen.Task(async_function_call, arg1, arg2)
return
在这个例子中,func
的执行会在async_function_call
完成后继续。这样,后续对async_function_call
的调用就不会重叠,而且你也不需要用time.sleep
来暂停主进程的执行。
我找到了一种解决方案,不需要“静音”你的存储过程,也不需要对它们进行任何修改。根据pyodbc的维基页面:
nextset()
这个方法会让光标跳到下一个可用的结果集,丢弃当前结果集中的任何剩余行。如果没有更多的结果集,这个方法会返回False。否则,它会返回True,之后调用获取数据的方法时会返回下一个结果集中的行。
这个方法主要用于那些返回多个结果的存储过程。
如果你想在程序继续执行之前等待存储过程完成,可以在执行存储过程的代码后使用以下代码。
slept = 0
while cursor.nextset():
if slept >= TIMEOUT:
break
time.sleep(1)
slept += 1
你也可以把time.sleep()
的值从1
秒改成稍微少于1秒,以减少额外的等待时间,但我不建议每秒调用很多次。
下面是一个完整的程序,展示了如何实现这段代码:
import time
import pyodbc
connection = pyodbc.connect('DRIVER={SQL Server};SERVER=<hostname>;PORT=1433;DATABASE=<database name>;UID=<database user>;PWD=password;CHARSET=UTF-8;')
cursor = connection.cursor()
TIMEOUT = 20 # Max number of seconds to wait for procedure to finish execution
params = ['value1', 2, 'value3']
cursor.execute("BEGIN EXEC dbo.sp_StoredProcedureName ?, ?, ? END", *params)
# here's where the magic happens with the nextset() function
slept = 0
while cursor.nextset():
if slept >= TIMEOUT:
break
time.sleep(1)
slept += 1
cursor.close()
connection.close()
我知道这个问题有点旧,但我刚花了几个小时试图弄明白如何让我的Python代码等待MSSQL上的存储过程完成。
问题不在于异步调用。
解决这个问题的关键是确保你的存储过程在运行完成之前不返回任何消息。否则,PYODBC会把存储过程的第一条消息当作结束信号。
运行你的存储过程时使用 SET NOCOUNT ON
。同时,确保你可能用来调试的任何 PRINT
语句或 RAISERROR
都被静音。
可以在你的存储过程中添加一个BIT类型的参数,比如 @muted
,只有当它的值是 0
时才输出调试消息。
在我的具体案例中,我执行了一个存储过程来处理一个加载的表,而我的应用程序在存储过程完成之前就退出并关闭了游标,因为我收到了行计数和调试消息。
所以总结一下,可以这样做:
cursor.execute('SET NOCOUNT ON; EXEC schema.proc @muted = 1')
这样PYODBC就会等待存储过程完成。
这是我的解决方法:
在数据库里,我创建了一个叫做 RunningStatus
的表,这个表只有一个字段 status
,它的类型是 bit
,并且只有一行,最开始设置为 0。
在我的存储过程开始时,我执行这一行
update RunningStatus set status = 1;
在存储过程结束时,
update RunningStatus set status = 0;
在我的 Python 脚本中,我打开一个新的连接和游标,连接到同一个数据库。在我的 execute
语句之后,我简单地添加
while 1:
q = status_check_cursor.execute('select status from RunningStatus').fetchone()
if q[0] == 0:
break
你需要创建一个新的连接和游标,因为从旧的连接发出的任何调用都会打断存储过程,并可能导致 status
永远不会回到 0。
这个方法有点不太完美,但对我来说效果很好!