Chiama le stored procedure con mssql-python

Il driver mssql-python non implementa il callproc() metodo della specifica DB-API 2.0. La chiamata a callproc() genera NotSupportedError. Invece, si utilizza la sequenza di escape ODBC {CALL ...} con metodi standard di esecuzione delle query.

Esecuzione di base delle stored procedure

Senza parametri

Esegui una procedura memorizzata utilizzando la sequenza di {CALL} fuga. Questo esempio chiama la sp_databases procedura di sistema memorizzata, che non prende parametri e restituisce una riga per database:

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)

Con parametri di ingresso

Passa i parametri usando segnaposto posizionali o denominati:

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

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

Parametri di output

Dichiarare e recuperare i parametri di output

Le stored procedure di SQL Server possono restituire valori tramite parametri di output. Usare le variabili Transact-SQL (T-SQL) per acquisire i valori di output, quindi recuperarli con un'istruzione 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}")

Parametri di uscita multipli

Acquisire più valori di output dichiarando variabili separate. Lo stesso schema funziona con qualsiasi procedura memorizzata che abbia OUTPUT parametri:

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}")

Valori restituiti

Cattura valore di ritorno della procedura memorizzata

Esegui una procedura memorizzata che restituisce risultati:

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")

Valore di ritorno con parametri di uscita

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}")

Set di risultati

Set di risultati singoli

Esegui una procedura archiviata che restituisce un solo insieme di risultati:

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

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

Più set di risultati

Per ogni risultato a cui avanzi, nextset() aggiorna rowcount. Un risultato di modifica dei dati riporta il proprio conteggio di righe interessate e contiene description is None; non chiamare metodi di fetch per quel risultato. Un risultato che restituisce righe inizia con rowcount == -1 prima di eseguire il fetch. SET NOCOUNT ON sopprime i risultati delle righe interessate. Per un batch che miscela questi tipi di risultati, vedi Monitorare ogni risultato in un batch.

Alcune stored procedure restituiscono più set di risultati. Usare nextset() per spostarsi tra di essi:

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}")

Controlla altri set di risultati

Iterare su tutti gli insiemi di risultati restituiti da una stored procedure usando 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

Transazioni con procedure memorizzate

Controllo esplicito delle transazioni

Racchiudi più chiamate di stored procedure in una transazione per garantire l'atomicità:

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}")

Lascia che la stored procedure gestisca la transazione

Se la stored procedure gestisce le proprie transazioni:

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")

Gestione degli errori

Rilevare errori nella stored procedure

Gestire le eccezioni sollevate dalle stored procedure o dalle istruzioni 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}")

Cattura le dichiarazioni PRINT e i messaggi informativi

Le istruzioni di SQL Server PRINT e RAISERROR con gravità inferiore a 11 vengono registrate in cursor.messages dopo l'esecuzione. Ogni voce è una (message_type, message_text) tupla. Quando una PRINT viene eseguita prima di un set di risultati, occupa un set di risultati senza righe proprio, quindi leggi cursor.messages prima, poi chiama nextset() per raggiungere le righe:

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}")

Quando una procedura archiviata emette messaggi PRINT in più set di risultati, leggere cursor.messages dopo execute() e di nuovo dopo ogni chiamata a nextset(), in modo che i messaggi di ogni set di risultati vengano acquisiti:

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}")

Per l'API completa cursor.messages , vedi Gestione cursore.

Procedure consigliate

Usa parametri nominati

I parametri nominati sono più chiari e mantengono l'indipendenza dell'ordine:

# 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"))

Gestire i parametri di output nullabili

Verifica i valori NULL quando recuperi i parametri di output dalle stored procedure:

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")

Usare SET NOCOUNT ON nelle procedure archiviate

Per migliori prestazioni e una gestione dei risultati più pulita, assicurati che le tue stored procedure includano:

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

Esempio: flusso di lavoro completo

Ecco un esempio pratico che chiama una stored procedure, recupera i valori di output e gestisce gli errori:

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)

Recuperare le chiavi generate usando OUTPUT INSERTED

Per recuperare un valore identità da un INSERT (con o senza procedura memorizzata), usare OUTPUT INSERTED invece di SCOPE_IDENTITY(). Questo approccio restituisce il valore nello stesso insieme di risultati:

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}")

Questo pattern funziona per qualsiasi tabella con una colonna identità e non richiede una procedura memorizzata.

Funzionalità non supportate

callproc()

Il mssql-python conducente aumenta NotSupportedError se chiami cursor.callproc(). Usa cursor.execute("{CALL ...}") o cursor.execute("EXECUTE ...") invece, come mostrato in questo articolo.

Parametri a valori di tabella (TVP)

Il mssql-python driver non supporta parametri a valori di tabella. Per la matrice completa di supporto alle funzionalità, vedi ciclo di vita del supporto. Se devi passare un insieme di righe a una procedura memorizzata, usa queste alternative:

  • Inserisci prima in una tabella temporanea, poi fai leggere la procedura memorizzata da essa.
  • Usa bulkcopy() per caricare i dati in una tabella di staging.
  • Passare una stringa JSON ed eseguirne il parsing con OPENJSON all'interno della procedura.