Миграция с pyodbc на mssql-python

Драйвер mssql-python — это оригинальный Python-драйвер Microsoft для Microsoft SQL. Если вы предпочитаете опцию драйвера, поддерживаемого Microsoft, он предлагает:

  • Нет внешних зависимостей от драйвера ODBC.
  • Встроенный пул соединений.
  • Поддержка современного Python 3.10+.
  • Нативная аутентификация Microsoft Entra.

Основные отличия

Функция pyodbc mssql-python
Стиль параметров qmark (?) qmark (?) и pyformat (%(name)s)
Требуется драйвер ODBC Yes нет
Пулинг соединений External Built-in
Минимальная версия Python 3.6 3.10
callproc() Поддерживается Не реализовано
Автоматическое подтверждение по умолчанию Off Off

Основные шаги миграции

Следующие шаги охватывают ключевые изменения для миграции приложения pyodbc на mssql-python.

1. Обновление импорта

Замените pyodbc импорт на mssql_python:

До (pyodbc):

import pyodbc

После (mssql-python):

import mssql_python

2. Обновить строки соединения

Удалите ключевое DRIVER= слово и обновите метод аутентификации:

Раньше (pyodbc, требуется драйвер ODBC):

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

После (mssql-python, драйвер не требуется, с использованием аутентификации Microsoft Entra):

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

3. Оставляйте свои запросы без изменений

Драйвер mssql-python поддерживает оба стиля параметров: ? (qmark) и %(name)s (pyformat). Ваши существующие ? запросы работают без изменений:

До (pyodbc):

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

После (mssql-python, тот же запрос):

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

4. Оставьте executemany как есть

Существующие executemany вызовы с кортежами и ? маркерами работают без изменений:

До (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)

После (mssql-python, тот же код):

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)

Миграция хранящихся процедур

Драйвер mssql-python не реализует callproc(). В следующих разделах показано, как использовать EXECUTE вместо него.

Используйте EXECUTE для хранящихся процедур

Драйвер pyodbc поддерживает callproc(), а драйвер mssql-python — нет. Используйте EXECUTE вместо этого:

До (pyodbc):

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

После (mssql-python):

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

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

Используйте переменные T-SQL для получения выходных значений вместо использования параметров вывода callproc():

До (pyodbc, с использованием callproc):

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

После (mssql-python, с использованием переменных 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}")

Миграции для конкретных функций

Следующие разделы охватывают конкретные функции pyodbc и их аналоги на mssql-python.

Строки подключения

Ключевое слово pyodbc Ключевое слово mssql-python Примечания.
DRIVER={...} Не требуется Драйвер ODBC встроен.
SERVER= Server= Нет изменений в поведении.
DATABASE= Database= Нет изменений в поведении.
Trusted_Connection= Trusted_Connection= Нет изменений в поведении.
UID= / PWD= UID= / PWD= Нет изменений в поведении.
Authentication= Authentication= Принимает те же ценности.

Автокоммит

Поведение автокоммита идентично в обоих драйверах:

pyodbc:

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

mssql-python:

conn.autocommit = True

Пакетные вставки

Чтобы ускорить большие INSERT партии, пользователи pyodbc устанавливают fast_executemany = True. Драйвер mssql-python уже оптимизирует executemany для параметризованных пакетов, поэтому умеренные вставки не требуют специального флага. При больших объёмах загрузки данных используйте bulkcopy(), который потоково передаёт строки по протоколу массового копирования и работает гораздо быстрее, чем выполнение отдельных операторов INSERT. Описание полного процесса см. в разделе «Использование массового копирования».

pyodbc:

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

После (mssql-python), средние пакеты с 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()

После (mssql-python) большие загрузки с bulkcopy (предпочтительно):

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

Фабрика строк

Драйвер mssql-python возвращает объекты Row, поддерживающие доступ к атрибутам по умолчанию, без необходимости использовать пользовательскую фабрику строк:

PYODBC (Custom Row Factory):

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 (по умолчанию доступ к атрибутам):

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

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

Драйвер mssql-python использует ту же иерархию исключений, что и pyodbc, поэтому большинство обработчиков исключений требуют только изменения имени модуля.

Иерархия исключений

Имена классов исключений напрямую соответствуют друг другу в разных драйверах:

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

Сведения об ошибке

Оба драйвера раскрывают детали ошибок через аргументы исключений:

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

Пулинг соединений

Драйвер mssql-python по умолчанию включает пул соединений, поэтому внешние библиотеки пула больше не требуются.

Удалить внешнее объединение ресурсов

Если вы использовали внешний пул подключений с pyodbc, в драйвере mssql-python эта возможность уже встроена:

До (внешнего пула 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()

После (встроенное пулирование mssql-python):

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

Конфигурация пула

Переопределите стандартный размер пула и тайм-аут с помощью mssql_python.pooling():

import mssql_python

mssql_python.pooling()

Пример завершения миграции

Ниже показана та же функция, записанная с помощью pyodbc и затем перезаписанная с помощью mssql-python.

До (pyodbc)

В этой версии используется строка подключения pyodbc с ключевым словом 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

После (mssql-python)

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

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

Единственные изменения — оператор import и строка подключения (ключевое слово DRIVER не требуется). Каждый запрос, параметр, шаблон выборки и доступ к строкам остаются идентичными.

Тестирование миграции

Перед завершением миграции выполните те же запросы к обоим драйверам и сравните результаты, чтобы подтвердить эквивалентное поведение.

Проверьте эквивалентное поведение

Используйте функцию сравнения, которая выполняет один и тот же запрос к обоим драйверам и подтверждает совпадение результатов:

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

Checklist

  • [ ] Обновить импорт из pyodbc в mssql_python.
  • [ ] Снимите DRIVER= с соединительных строк.
  • [ ] Сохраняйте существующие ? запросы параметров (они работают as-is).
  • [ ] Используйте инструкции EXECUTE для вызовов хранимых процедур.
  • [ ] Удалите конфигурацию внешнего пулирования соединений.
  • [ ] Обновить имена классов для обработки исключений.
  • [ ] Проверьте все запросы и хранящиеся процедуры.
  • [ ] Проверьте обработку типов данных (особенно десятичных и дат).
  • [ ] Удалить драйвер ODBC из требований к развертыванию.