Appeler des procédures stockées avec mssql-python

Le pilote mssql-python n’implémente pas la callproc() méthode de la spécification DB-API 2.0. L’appel à callproc() lève NotSupportedError. Utilisez plutôt la séquence d’échappement ODBC {CALL ...} avec les méthodes d’exécution de requêtes standard.

Exécution de base d’une procédure stockée

Sans paramètres

Exécutez une procédure stockée en utilisant la séquence d’échappement {CALL} . Cet exemple appelle la sp_databases procédure stockée système, qui ne prend aucun paramètre et renvoie une ligne par base de données :

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)

Avec des paramètres d’entrée

Transmettre des paramètres à l'aide de paramètres de position ou de paramètres nommés :

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

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

Paramètres de sortie

Déclarer et récupérer les paramètres de sortie

Les procédures stockées SQL Server peuvent retourner des valeurs via des paramètres de sortie. Utilisez Transact-SQL variables (T-SQL) pour capturer les valeurs de sortie, puis récupérez-les avec une SELECT instruction :

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

Paramètres de sortie multiples

Capturez plusieurs valeurs de sortie en déclarant des variables distinctes. Le même schéma fonctionne avec toute procédure stockée qui possède OUTPUT des paramètres :

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

Valeurs de retour

Capturer la valeur de retour d’une procédure stockée

Exécutez une procédure stockée qui restitue les résultats :

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

Valeur de retour avec paramètres de sortie

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

Ensembles de résultats

Ensemble de résultats unique

Exécuter une procédure stockée qui retourne un seul ensemble de résultats :

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

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

Ensembles de résultats multiples

Pour chaque résultat auquel vous accédez, nextset() rafraîchit rowcount. Un résultat de modification de données rapporte son propre nombre de lignes affectées et dispose de description is None ; n’appelez pas les méthodes de récupération pour ce résultat. Un résultat produisant des lignes commence par rowcount == -1 avant la récupération. SET NOCOUNT ON supprime les résultats des lignes affectées. Pour un lot qui mélange ces types de résultats, voir Suivre chaque résultat dans un lot.

Certaines procédures stockées retournent plusieurs jeux de résultats. Utilisez nextset() pour naviguer de l’un à l’autre :

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

Vérifiez s’il existe d’autres jeux de résultats

Parcourir tous les ensembles de résultats renvoyés par une procédure stockée à l’aide de 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

Transactions avec procédures stockées

Contrôle explicite des transactions

Inclure dans un wrapper plusieurs appels de procédures stockées dans une transaction pour garantir 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}")

Laissez la procédure stockée gérer la transaction

Si la procédure stockée gère ses propres transactions :

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

Gestion des erreurs

Détecter les erreurs de procédure stockée

Gérer les exceptions générées par les procédures stockées ou les instructions 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}")

Capturer les instructions PRINT et les messages informatifs

Les instructions SQL Server PRINT et RAISERROR avec une gravité inférieure à 11 sont enregistrées dans cursor.messages après l’exécution. Chaque entrée est un (message_type, message_text) tuple. Lorsqu’un PRINT s’exécute avant un ensemble de résultats, il occupe son propre ensemble de résultats sans rangées, donc lis cursor.messages d’abord, puis appelle nextset() pour atteindre les lignes :

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

Lorsqu’une procédure stockée émet PRINT des messages à travers plusieurs ensembles de résultats, lisez cursor.messages après execute() et après chaque nextset() appel afin que les messages de chaque ensemble de résultats soient capturés :

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

Pour l’API complète cursor.messages , voir Gestion du curseur.

Bonnes pratiques

Utiliser des paramètres nommés

Les paramètres nommés sont plus clairs et maintiennent l’indépendance de l’ordre :

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

Gérer les paramètres de sortie annulables

Vérifiez les valeurs NULL lors de la récupération des paramètres de sortie des procédures stockées :

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

Utiliser SET NOCOUNT ON dans les procédures stockées

Pour de meilleures performances et une gestion des résultats plus propre, assurez-vous que vos procédures stockées incluent :

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

Exemple : flux de travail complet

Voici un exemple pratique qui appelle une procédure stockée, récupère les valeurs de sortie et gère les erreurs :

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)

Obtenez des clés générées en utilisant OUTPUT INSERTED

Pour récupérer une valeur identité à partir d’un INSERT (avec ou sans procédure stockée), utilisez OUTPUT INSERTED au lieu de SCOPE_IDENTITY(). Cette approche renvoie la valeur dans le même ensemble de résultats :

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

Ce schéma fonctionne pour toute table avec une colonne d’identité et ne nécessite pas de procédure stockée.

Fonctionnalités non prises en charge

callproc()

Le pilote mssql-python lève NotSupportedError si vous appelez cursor.callproc(). Utilisez cursor.execute("{CALL ...}") ou cursor.execute("EXECUTE ...") plutôt, comme montré dans cet article.

Paramètres à valeurs de table (TVP)

Le mssql-python pilote ne prend pas en charge les paramètres à valeurs de table. Pour la matrice complète de support des fonctionnalités, voir cycle de vie du support. Si vous devez transmettre un ensemble de lignes à une procédure stockée, utilisez ces alternatives :

  • Insérez d’abord les données dans une table temporaire, puis demandez à la procédure stockée de les lire depuis celle-ci.
  • Utilisez bulkcopy() pour charger des données dans une table intermédiaire.
  • Passez une chaîne JSON pour l’analyser avec OPENJSON dans la procédure.