Migrer de pyodbc vers mssql-python

Le pilote mssql-python est le pilote Python de première partie de Microsoft pour Microsoft SQL. Si vous préférez une option de pilote maintenue par Microsoft, elle propose :

  • Aucune dépendance externe à un pilote ODBC.
  • Pool de connexions intégré.
  • Prise en charge moderne de Python 3.10 et versions ultérieures.
  • Authentification Microsoft Entra native.

Principales différences

Fonctionnalité pyodbc mssql-python
Style de paramètre qmark (?) qmark (?) et pyformat (%(name)s)
Pilote ODBC requis Oui Non
Regroupement de connexions Externe Intégré
Minimum Python 3.6 3.10
callproc() Soutenu Non implémenté
Validation automatique par défaut Off Off

Étapes de base de la migration

Les étapes suivantes couvrent les changements clés pour migrer une application pyodbc vers mssql-python.

1. Mise à jour des importations

Remplacez l’importation pyodbc par mssql_python:

Avant (pyodbc) :

import pyodbc

Après (mssql-python) :

import mssql_python

2. Mettre à jour les chaînes de connexion

Supprimez le DRIVER= mot-clé et mettez à jour la méthode d’authentification :

Avant (pyodbc, nécessite un pilote ODBC) :

conn = pyodbc.connect(
    "DRIVER={ODBC Driver 18 for SQL Server};"
    "SERVER=localhost;"
    "DATABASE=AdventureWorks2022;"
    "Trusted_Connection=yes;"
)

Après (mssql-python, aucun pilote requis, avec l’authentification Microsoft Entra) :

conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryDefault;"
    "Encrypt=yes;"
)

3. Conservez vos requêtes telles quelles

Le pilote mssql-python prend en charge à la fois ? (qmark) et %(name)s (pyformat) comme styles de paramètres. Vos requêtes existantes ? fonctionnent sans modifications :

Avant (pyodbc) :

cursor.execute("SELECT * FROM Production.Product WHERE ProductID = ? AND Color = ?", (1, "Red"))

Après (mssql-python, même requête) :

cursor.execute("SELECT * FROM Production.Product WHERE ProductID = ? AND Color = ?", (1, "Red"))

4. Conservez executemany tel quel

Les appels existants utilisant des tuples executemany et des marqueurs ? fonctionnent sans aucune modification :

Avant (pyodbc) :

cursor.execute("CREATE TABLE #MigrateDemo (ID INT, Name NVARCHAR(50))")
data = [(1, "Alice"), (2, "Bob"), (3, "Carol")]
cursor.executemany("INSERT INTO #MigrateDemo (ID, Name) VALUES (?, ?)", data)

Après (mssql-python, même code) :

cursor.execute("DROP TABLE IF EXISTS #MigrateDemo")
cursor.execute("CREATE TABLE #MigrateDemo (ID INT, Name NVARCHAR(50))")
data = [(1, "Alice"), (2, "Bob"), (3, "Carol")]
cursor.executemany("INSERT INTO #MigrateDemo (ID, Name) VALUES (?, ?)", data)

Migration des procédures stockées

Le pilote mssql-python n’implémente pas callproc(). Les sections suivantes montrent comment utiliser EXECUTE à la place.

Utilisez EXECUTE pour les procédures stockées

Le pilote pyodbc prend en charge callproc(), mais le pilote mssql-python ne le fait pas. Utilisez EXECUTE à la place :

Avant (pyodbc) :

cursor.callproc("dbo.uspGetEmployeeManagers", (5,))
results = cursor.fetchall()

Après (mssql-python) :

cursor.execute(
    "EXECUTE dbo.uspGetEmployeeManagers @BusinessEntityID = %(id)s",
    {"id": 5}
)
results = cursor.fetchall()
print(f"Got {len(results)} rows")

Paramètres de sortie

Utilisez des variables T-SQL pour capturer les valeurs de sortie au lieu de vous fier aux callproc() paramètres de sortie :

Avant (pyodbc, en utilisant le callproc) :

params = (category_id, pyodbc.SQL_INTEGER)
cursor.callproc("dbo.GetProductCount", params)
count = params[1].value

Après (mssql-python, utilisant des variables T-SQL) :

cursor.execute(
    """
    DECLARE @count INT;
    SELECT @count = COUNT(*) FROM Production.Product
    WHERE ProductSubcategoryID = %(cat_id)s;
    SELECT @count AS ProductCount;
    """,
    {"cat_id": 1}
)
product_count = cursor.fetchval()
print(f"Product count: {product_count}")

Migrations spécifiques à chaque fonctionnalité

Les sections suivantes couvrent des caractéristiques spécifiques de pyodbc et leurs équivalents mssql-python.

Chaînes de connexion

Mot-clé pyodbc mot-clé mssql-python Notes
DRIVER={...} Non nécessaire Le pilote ODBC est intégré en interne.
SERVER= Server= Aucun changement de comportement.
DATABASE= Database= Aucun changement de comportement.
Trusted_Connection= Trusted_Connection= Aucun changement de comportement.
UID= / PWD= UID= / PWD= Aucun changement de comportement.
Authentication= Authentication= Accepte les mêmes valeurs.

Validation automatique

Le comportement d’autocommit est identique dans les deux pilotes :

pyodbc :

conn.autocommit = True
pyodbc.connect(connection_string, autocommit=True)

MSSQL-python :

conn.autocommit = True

Insertions en masse

Pour accélérer les lots volumineux de INSERT, les utilisateurs de pyodbc définissent fast_executemany = True. Le pilote mssql-python optimise executemany déjà pour les lots paramétrés, donc les insertions modérées n’ont pas besoin de drapeau spécial. Pour les charges de données importantes, préférez bulkcopy(), qui diffuse les lignes via le protocole de copiage en masse et est beaucoup plus rapide que l’émission d’instructions individuelles INSERT . Pour le flux de travail complet, voir Utiliser la copie en masse.

pyodbc :

cursor.fast_executemany = True
cursor.executemany(query, data)

Après (mssql-python), lots de taille modérée avec executemany:

cursor.execute("DROP TABLE IF EXISTS #BulkTarget")
cursor.execute("CREATE TABLE #BulkTarget (ID INT, Name NVARCHAR(50))")
data = [(i, f"Item {i}") for i in range(100)]
cursor.executemany("INSERT INTO #BulkTarget (ID, Name) VALUES (?, ?)", data)
conn.commit()

Après (mssql-python), charges importantes avec bulkcopy (préférable) :

cursor.execute("IF OBJECT_ID('##BulkTarget') IS NOT NULL DROP TABLE ##BulkTarget")
cursor.execute("CREATE TABLE ##BulkTarget (ID INT, Name NVARCHAR(50))")
conn.commit()  # Commit DDL before bulkcopy
data = [(i, f"Item {i}") for i in range(100)]
result = cursor.bulkcopy("##BulkTarget", data)
print(f"Bulk copied {result['rows_copied']} rows")
cursor.execute("DROP TABLE ##BulkTarget")
conn.commit()

Fabrique de ligne

Le pilote mssql-python renvoie par défaut des objets Row permettant l’accès aux attributs, sans nécessiter de fabrique de lignes personnalisée :

pyodbc (fabrique de lignes personnalisée) :

def namedtuple_row_factory(cursor):
    from collections import namedtuple
    columns = [col[0] for col in cursor.description]
    Row = namedtuple("Row", columns)
    return Row

mssql-python (accès par défaut aux attributs) :

cursor.execute("SELECT Name, ListPrice FROM Production.Product")
row = cursor.fetchone()
print(row.Name)   # Attribute access works directly
print(row[0])     # Index access also works

Gestion des erreurs

Le pilote mssql-python utilise la même hiérarchie d’exception que pyodbc, donc la plupart des gestionnaires d’exceptions ne nécessitent qu’un changement de nom de module.

Hiérarchie d’exceptions

Les noms des classes d’exception correspondent directement entre les pilotes :

pyodbc :

try:
    cursor.execute(query)
except pyodbc.Error as e:
    pass
except pyodbc.DatabaseError as e:
    pass
except pyodbc.OperationalError as e:
    pass

MSSQL-python :

try:
    cursor.execute("SELECT TOP 1 * FROM Production.Product")
    print(cursor.fetchone())
except mssql_python.Error as e:
    pass
except mssql_python.DatabaseError as e:
    pass
except mssql_python.OperationalError as e:
    pass

Détails de l’erreur

Les deux conducteurs exposent les détails des erreurs à travers des arguments d’exception :

pyodbc :

try:
    cursor.execute(query)
except pyodbc.Error as e:
    sqlstate = e.args[0]
    message = e.args[1]

MSSQL-python :

try:
    cursor.execute("SELECT TOP 1 * FROM NonExistentTable_XYZ")
except mssql_python.Error as e:
    # Error message contains SQLSTATE and details
    print(str(e))

Regroupement de connexions

Le pilote mssql-python inclut par défaut le pooling de connexions, donc les bibliothèques de pooling externes ne sont plus nécessaires.

Supprimer la mise en commun externe

Si vous avez utilisé un pooling externe avec pyodbc, le pilote mssql-python l’a intégré :

Avant (pool externe pyodbc) :

from dbutils.pooled_db import PooledDB

pool = PooledDB(pyodbc, 5, driver="{ODBC Driver 18 for SQL Server}",
                server="your_server", database="your_database",
                uid="your_username", pwd="your_password")
conn = pool.connection()

Après (pooling intégré mssql-python) :

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

Configurer le pool

Supplantez la taille du pool par défaut et le délai d’expiration par :mssql_python.pooling()

import mssql_python

mssql_python.pooling()

Exemple de migration complet

Ce qui suit montre la même fonction écrite avec pyodbc puis réécrite avec mssql-python.

Avant (pyodbc)

Cette version utilise la chaîne de connexion pyodbc avec le mot-clé DRIVER :

import pyodbc
from datetime import date

def get_orders(customer_id: int, start_date: date):
    conn = pyodbc.connect(
        "DRIVER={ODBC Driver 18 for SQL Server};"
        "SERVER=localhost;"
        "DATABASE=AdventureWorks2022;"
        "Trusted_Connection=yes;"
    )
    cursor = conn.cursor()

    cursor.execute("""
        SELECT SalesOrderID, OrderDate, TotalDue
        FROM Sales.SalesOrderHeader
        WHERE CustomerID = ? AND OrderDate >= ?
        ORDER BY OrderDate DESC
    """, (customer_id, start_date))

    orders = []
    for row in cursor:
        orders.append({
            "id": row.SalesOrderID,
            "date": row.OrderDate,
            "total": row.TotalDue
        })

    cursor.close()
    conn.close()
    return orders

Après (mssql-python)

Cette version supprime le DRIVER mot-clé. Toutes les requêtes, paramètres et schémas d’accès aux lignes restent identiques :

import mssql_python
from datetime import date

def get_orders(customer_id: int, start_date: date):
    conn = mssql_python.connect(
        "Server=localhost;"
        "Database=AdventureWorks2022;"
        "Trusted_Connection=yes;"
    )
    cursor = conn.cursor()

    cursor.execute("""
        SELECT SalesOrderID, OrderDate, TotalDue
        FROM Sales.SalesOrderHeader
        WHERE CustomerID = ? AND OrderDate >= ?
        ORDER BY OrderDate DESC
    """, (customer_id, start_date))

    orders = []
    for row in cursor:
        orders.append({
            "id": row.SalesOrderID,
            "date": row.OrderDate,
            "total": row.TotalDue
        })

    cursor.close()
    conn.close()
    return orders

Les seuls changements concernent l’instruction d’importation et la chaîne de connexion (aucun mot-clé DRIVER n’est nécessaire). Chaque requête, chaque paramètre, chaque schéma d’extraction et chaque accès aux lignes restent identiques.

Test de migration

Avant de terminer la migration, effectuez les mêmes requêtes sur les deux pilotes et comparez les résultats pour confirmer un comportement équivalent.

Vérifier le comportement équivalent

Utilisez une fonction de comparaison qui exécute la même requête sur les deux pilotes et affirme que les résultats correspondent :

import pyodbc
import mssql_python

def compare_results(pyodbc_conn_str: str, mssql_conn_str: str, query: str):
    """Compare results from both drivers."""
    # pyodbc query
    pyodbc_conn = pyodbc.connect(pyodbc_conn_str)
    pyodbc_cursor = pyodbc_conn.cursor()
    pyodbc_cursor.execute(query)
    pyodbc_results = pyodbc_cursor.fetchall()
    pyodbc_conn.close()
    
    # mssql-python query
    mssql_conn = mssql_python.connect(mssql_conn_str)
    mssql_cursor = mssql_conn.cursor()
    mssql_cursor.execute(query)
    mssql_results = mssql_cursor.fetchall()
    mssql_conn.close()
    
    # Compare
    assert len(pyodbc_results) == len(mssql_results)
    for p_row, m_row in zip(pyodbc_results, mssql_results):
        assert tuple(p_row) == tuple(m_row)
    
    print(f"Results match: {len(pyodbc_results)} rows")

Liste de contrôle

  • [ ] Mettez à jour les imports de pyodbc vers mssql_python.
  • [ ] Supprimez DRIVER= des chaînes de connexion.
  • [ ] Conservez les requêtes de paramètres existantes ? (elles fonctionnent as-is).
  • [ ] Utilisez les instructions EXECUTE pour les appels à des procédures stockées.
  • [ ] Supprimer la configuration du groupement de connexions externe.
  • [ ] Mettre à jour les noms des classes de gestion des exceptions.
  • [ ] Testez toutes les requêtes et procédures stockées.
  • [ ] Vérifier la gestion des types de données (en particulier les décimales et les dates).
  • [ ] Retirez le pilote ODBC des exigences de déploiement.