Вызов хранимых процедур с помощью mssql-python

Драйвер mssql-python не реализует метод callproc() из спецификации DB-API 2.0. Вызов callproc() вызывает NotSupportedError. Вместо этого используйте последовательность escape ODBC {CALL ...} со стандартными методами выполнения запросов.

Базовое выполнение сохранённой процедуры

Без параметров

Выполните сохранённую процедуру, используя {CALL} последовательность escape. В этом примере вызывается системная хранимая процедура sp_databases, которая не принимает параметров и возвращает по одной строке для каждой базы данных:

import mssql_python

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

cursor.execute("{CALL sp_databases}")

for row in cursor:
    print(row.DATABASE_NAME, row.DATABASE_SIZE)

С входными параметрами

Передавайте параметры с помощью позиционных или именованных заполнителей:

# Single parameter
cursor.execute(
    "{CALL dbo.uspGetManagerEmployees(?)}", (16,)
)

for row in cursor:
    print(row.FirstName, row.LastName)

Выходные параметры

Объявить и получить выходные параметры

Хранимые процедуры SQL Server могут возвращать значения через выходные параметры. Используйте переменные Transact-SQL (T-SQL) для сохранения выходных значений, затем извлеките их с помощью оператора SELECT:

cursor.execute("""
    DECLARE @total_out MONEY;
    SELECT @total_out = SUM(TotalDue)
    FROM Sales.SalesOrderHeader
    WHERE CustomerID = %(customer_id)s;
    SELECT @total_out AS TotalAmount;
""", {"customer_id": 29825})

row = cursor.fetchone()
total = row.TotalAmount
print(f"Customer total: ${total}")

Множественные параметры выхода

Фиксируйте несколько выходных значений, объявляя отдельные переменные. Тот же паттерн работает с любой хранящейся процедурой, имеющей OUTPUT параметры:

cursor.execute("""
    DECLARE @total_orders INT, @total_spent MONEY;
    SELECT @total_orders = COUNT(*), @total_spent = SUM(TotalDue)
    FROM Sales.SalesOrderHeader
    WHERE CustomerID = %(cust_id)s;
    SELECT @total_orders AS OrderCount, @total_spent AS TotalSpent;
""", {"cust_id": 29825})

stats = cursor.fetchone()
print(f"Orders: {stats.OrderCount}, Total spent: ${stats.TotalSpent}")

Возвращаемые значения

Получить возвращаемое значение хранимой процедуры

Выполните сохранённую процедуру, которая возвращает результаты:

cursor.execute("EXECUTE dbo.uspGetEmployeeManagers @BusinessEntityID = %(emp_id)s", {"emp_id": 5})

rows = cursor.fetchall()
if rows:
    print(f"Found {len(rows)} managers in chain")
    for row in rows:
        print(f"  Manager: {row.FirstName} {row.LastName}")
else:
    print("No managers found")

Возвращаемое значение с параметрами вывода

cursor.execute("""
    DECLARE @return_value INT, @message NVARCHAR(500);
    SELECT @return_value = CASE WHEN COUNT(*) > 0 THEN 0 ELSE 1 END,
           @message = CASE WHEN COUNT(*) > 0 THEN N'Customer found' ELSE N'Customer not found' END
    FROM Sales.Customer WHERE CustomerID = %(cust_id)s;
    SELECT @return_value AS ReturnCode, @message AS Message;
""", {"cust_id": 29825})

result = cursor.fetchone()
print(f"Return code: {result.ReturnCode}, Message: {result.Message}")

Наборы результатов

Набор однорезультатных результатов

Выполните сохранённую процедуру, которая возвращает один набор результатов:

cursor.execute(
    "{CALL dbo.uspGetBillOfMaterials(?, ?)}",
    (800, "2026-01-01")
)

customers = cursor.fetchall()
for row in customers:
    print(f"{row.ProductAssemblyID}: {row.ComponentDesc}")

Множество результирующих наборов

Некоторые хранимые процедуры возвращают несколько результирующих наборов. Используйте nextset() для перехода между ними:

cursor.execute("""
    SELECT TOP 1 SalesOrderID, OrderDate, TotalDue
    FROM Sales.SalesOrderHeader WHERE CustomerID = 29825;
    SELECT TOP 3 Name, ListPrice
    FROM Production.Product WHERE ListPrice > 0
    ORDER BY ListPrice DESC;
""")

# First result set: order header
order = cursor.fetchone()
print(f"Order: {order.SalesOrderID}, Date: {order.OrderDate}")

cursor.nextset()

# Second result set: products
print("Products:")
for item in cursor:
    print(f"  {item.Name}: ${item.ListPrice}")

Проверьте наличие дополнительных наборов результатов

Пройтись по всем наборам результатов, возвращаемым сохранённой процедурой, используя nextset():

cursor.execute("SELECT TOP 3 ProductID, Name FROM Production.Product; SELECT TOP 3 FirstName, LastName FROM Person.Person")

result_set_num = 1
while True:
    print(f"--- Result Set {result_set_num} ---")
    for row in cursor:
        print(row)
    
    if not cursor.nextset():
        break
    result_set_num += 1

Транзакции с хранящимися процедурами

Явное управление транзакциями

Оберните несколько вызовов хранимых процедур в транзакцию для обеспечения атомарности:

conn.autocommit = False

try:
    cursor.execute("{CALL dbo.DebitAccount(?, ?)}", (1001, 100.00))
    
    cursor.execute("{CALL dbo.CreditAccount(?, ?)}", (1002, 100.00))
    
    conn.commit()
    print("Transfer completed")
except mssql_python.DatabaseError as e:
    conn.rollback()
    print(f"Transfer failed: {e}")

Пусть хранимая процедура управляет транзакцией

Если сохранённая процедура обрабатывает собственные транзакции:

conn.autocommit = True  # Let SP manage transactions

cursor.execute("""
    DECLARE @result INT;
    EXECUTE @result = dbo.TransferFunds 
        @FromAccount = %(from_acc)s,
        @ToAccount = %(to_acc)s,
        @Amount = %(amount)s;
    SELECT @result AS TransferResult;
""", {"from_acc": 1001, "to_acc": 1002, "amount": 100.00})

result = cursor.fetchone()
if result.TransferResult == 0:
    print("Transfer successful")

Обработка ошибок

Ошибки обнаружения сохранённой процедуры

Обрабатывайте исключения, вызванные хранящимися процедурами или операторами Transact-SQL:

try:
    cursor.execute("{CALL dbo.DangerousProcedure}")
except mssql_python.ProgrammingError as e:
    # Handle SQL errors raised by RAISERROR or THROW
    print(f"Stored procedure error: {e}")
except mssql_python.DatabaseError as e:
    # Handle other database errors
    print(f"Database error: {e}")

Захват операторов PRINT и информационных сообщений

Операторы SQL Server PRINT и RAISERROR с уровнем серьезности ниже 11 фиксируются в cursor.messages после выполнения. Каждая запись представляет собой кортеж (message_type, message_text). Когда PRINT предшествует набору результатов, он занимает отдельный набор результатов без строк, поэтому сначала считайте cursor.messages, а затем вызовите nextset(), чтобы перейти к строкам:

cursor.execute("PRINT 'Operation complete'; SELECT 1 AS Status")

for msg_type, msg_text in cursor.messages:
    print(f"Server message: {msg_text}")

cursor.nextset()
row = cursor.fetchone()
print(f"Status: {row.Status}")

Когда хранимая процедура выдаёт сообщения PRINT в нескольких наборах результатов, считывайте cursor.messages после execute(), а затем снова после каждого вызова nextset(), чтобы получать сообщения из каждого набора результатов:

cursor.execute("""
    PRINT 'Starting first result set';
    SELECT TOP 3 ProductID, Name FROM Production.Product;
    PRINT 'Starting second result set';
    SELECT TOP 3 FirstName, LastName FROM Person.Person;
""")

all_messages = []
while True:
    all_messages.extend(cursor.messages)
    if cursor.description:
        for row in cursor:
            print(row)
    if not cursor.nextset():
        break

for _, text in all_messages:
    print(f"Server: {text}")

Полный cursor.messages API смотрите в разделе «Управление курсорами».

Лучшие практики

Используйте именованные параметры

Названные параметры более чёткие и сохраняют независимость порядка:

# Recommended: {CALL} with positional parameters
cursor.execute(
    "{CALL dbo.uspGetBillOfMaterials(?, ?)}",
    (800, "2026-01-01")
)

# Also valid: EXECUTE with named T-SQL parameters
cursor.execute("""
    EXECUTE dbo.uspGetBillOfMaterials
        @StartProductID = ?,
        @CheckDate = ?
""", (800, "2026-01-01"))

Обработка нулевых выходных параметров

Проверьте значения NULL при извлечении выходных параметров из хранящихся процедур:

cursor.execute("""
    DECLARE @optional_value NVARCHAR(100);
    SELECT @optional_value = Color FROM Production.Product WHERE ProductID = %(id)s;
    SELECT @optional_value AS OutputValue;
""", {"id": 1})

result = cursor.fetchone()
if result.OutputValue is not None:
    print(f"Value: {result.OutputValue}")
else:
    print("No value returned")

Используйте SET NOCOUNT ON в хранимых процедурах

Для лучшей производительности и более чистой обработки результатов убедитесь, что ваши хранящиеся процедуры включают:

CREATE PROCEDURE dbo.MyProcedure
AS
BEGIN
    SET NOCOUNT ON;  -- Prevents "n rows affected" messages
    -- procedure logic
END

Пример: Полный рабочий процесс

Вот практический пример, который вызывает сохранённую процедуру, получает выходные значения и обрабатывает ошибки:

import mssql_python

def get_employee_report(manager_id: int) -> dict:
    """Look up a manager's employees and compute average vacation hours."""
    conn = mssql_python.connect(connection_string)
    conn.autocommit = False
    cursor = conn.cursor()

    try:
        # Get manager info
        cursor.execute("""
            SELECT BusinessEntityID, JobTitle
            FROM HumanResources.Employee
            WHERE BusinessEntityID = %(mgr)s
        """, {"mgr": manager_id})
        mgr = cursor.fetchone()
        print(f"Manager {mgr.BusinessEntityID}: {mgr.JobTitle}")

        # Get direct reports via stored procedure
        cursor.execute("{CALL dbo.uspGetManagerEmployees(?)}", (manager_id,))
        employees = cursor.fetchall()
        print(f"Found {len(employees)} employee(s)")

        # Compute average vacation hours
        cursor.execute("""
            DECLARE @avg_hours INT;
            SELECT @avg_hours = AVG(VacationHours)
            FROM HumanResources.Employee;
            SELECT @avg_hours AS AvgVacation;
        """)
        avg = cursor.fetchone().AvgVacation
        print(f"Avg vacation hours: {avg}")

        conn.commit()
        return {"manager": mgr.JobTitle, "reports": len(employees), "avg_vacation": avg}

    except mssql_python.DatabaseError as e:
        conn.rollback()
        raise
    finally:
        cursor.close()
        conn.close()

# Usage
result = get_employee_report(manager_id=16)

Получите сгенерированные ключи с помощью OUTPUT INSERTED

Чтобы получить тождественное значение из INSERT (с хранящейся процедурой или без), используйте OUTPUT INSERTED вместо SCOPE_IDENTITY(). Этот подход возвращает значение в том же наборе результатов:

cursor.execute("""
    INSERT INTO Production.ProductCategory (Name)
    OUTPUT INSERTED.ProductCategoryID
    VALUES (%(name)s)
""", {"name": "Custom Parts"})

new_id = cursor.fetchval()
print(f"New category ID: {new_id}")

Этот паттерн работает для любой таблицы с столбцем идентичности и не требует сохранённой процедуры.

Неподдерживаемые функции

callproc()

Драйвер mssql-python вызывает NotSupportedError, если вы вызываете cursor.callproc(). Используйте cursor.execute("{CALL ...}") или cursor.execute("EXECUTE ...") вместо этого, как показано в этой статье.

Параметры, представленные в виде таблицы (TVP)

Табличные параметры не поддерживаются в текущей версии (1.11.0) mssql-python. Если нужно передать набор строк в сохранённую процедуру, используйте альтернативы:

  • Сначала вставьте её в временную таблицу, затем пусть сохранённая процедура будет прочитана из неё.
  • Используйте bulkcopy(), чтобы загрузить данные в промежуточную таблицу.
  • Передайте строку JSON и разберите её с помощью OPENJSON внутри процедуры.