Объекты строк в mssql-python

Драйвер mssql-python возвращает строки в виде Row объектов из операций выборки. Эти объекты обеспечивают гибкие паттерны доступа:

  • Доступ к атрибуту (row.ColumnName) — самый читаемый выбор для именованных столбцов. Используйте его, когда в вашем запросе есть известный и стабильный список столбцов.
  • Доступ к строковому ключу (row['ColumnName']) обеспечивает словарный доступ к столбцам по названию, что полезно при программном поиске столбцов или когда имена столбцов содержат пробелы или специальные символы.
  • Доступ к индексу (row[0]) полезен для динамических запросов, где имена столбцов неизвестны на момент разработки или при обработке SELECT * результатов.
  • Распаковка кортежей (a, b, c = row) — самый лаконичный выбор для корпусов петель с небольшим фиксированным количеством столбцов.

Доступ к атрибутам

Доступ к значениям столбцов напрямую по названию:

import mssql_python

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

cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID = 1")
row = cursor.fetchone()

print(row.ProductID)     # 1
print(row.Name)          # 'Adjustable Race'
print(row.ListPrice)     # 0.00

Учет регистра

Доступ к именам столбцов чувствителен к регистру и совпадает с названиями столбцов, возвращаемых SQL Server. Если ваша база данных использует несогласованный корпус, используйте SQL-псевдонимы AS для нормализации имён или включите lowercase настройки модуля (см. конфигурацию модуля):

cursor.execute("SELECT FirstName, LastName, EmailPromotion FROM Person.Person WHERE BusinessEntityID < 10")
row = cursor.fetchone()

print(row.FirstName)      # Works
print(row.LastName)       # Works
print(row.EmailPromotion) # Works
print(row.firstname)      # AttributeError - wrong case

Псевдонимы колонок

Используйте SQL-псевдонимы для создания дружественных имён атрибутов:

cursor.execute("""
    SELECT 
        p.ProductID,
        p.Name,
        c.Name AS Category
    FROM Production.Product p
    JOIN Production.ProductSubcategory c ON p.ProductSubcategoryID = c.ProductSubcategoryID
    WHERE p.ProductSubcategoryID IS NOT NULL
""")

for row in cursor:
    print(f"{row.Name} ({row.Category})")

Доступ к индексу

Значения доступа по индексу столбцов на основе нуля:

cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID < 5")
row = cursor.fetchone()

print(row[0])  # ProductID
print(row[1])  # Name
print(row[2])  # ListPrice

Отрицательная индексация

Драйвер поддерживает отрицательную индексацию в стиле Python:

cursor.execute("""
    SELECT Demo.A, Demo.B, Demo.C, Demo.D
    FROM (VALUES (1, 2, 3, 4)) AS Demo(A, B, C, D)
""")
row = cursor.fetchone()

print(row[-1])  # Last column (D)
print(row[-2])  # Second to last (C)

Доступ к строковому ключу

Доступ к значениям столбцов по названию с помощью синтаксиса словарного стиля:

cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID = 1")
row = cursor.fetchone()

print(row['ProductID'])   # 1
print(row['Name'])        # 'Adjustable Race'
print(row['ListPrice'])   # Decimal('0.00')

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

column_name = 'ListPrice'
value = row[column_name]  # Programmatic column access

Нарезка

Извлекайте несколько значений с помощью срезов:

cursor.execute("""
    SELECT Demo.A, Demo.B, Demo.C, Demo.D, Demo.E
    FROM (VALUES (1, 2, 3, 4, 5)) AS Demo(A, B, C, D, E)
""")
row = cursor.fetchone()

print(row[1:4])    # Columns B, C, D (indices 1, 2, 3)
print(row[:2])     # First two columns (A, B)
print(row[2:])     # From C to end

Распаковка кортежей

Распаковывайте значения строк напрямую в переменные:

cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID < 10")

for product_id, name, price in cursor:
    print(f"#{product_id}: {name} - ${price}")

Частичная распаковка

Используйте * для захвата оставшихся значений:

cursor.execute("""
    SELECT TOP 2 ProductID, Name, ProductNumber, Color, Size, Weight
    FROM Production.Product
    WHERE ProductNumber IS NOT NULL
    ORDER BY ProductID
""")

for product_id, name, *rest in cursor:
    print(f"{product_id}: {name}, extra columns: {rest}")

Длина строки и итерация

Работайте с размерностями строк и просматривайте значения столбцов:

Получите количество столбцов

Используйте len() для получения количества столбцов в строке:

cursor.execute("SELECT * FROM Production.Product WHERE ProductID < 5")
row = cursor.fetchone()

print(len(row))  # Number of columns

Итерация по значениям

Проведите значения столбцов по порядку:

cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID < 5")
row = cursor.fetchone()

for value in row:
    print(value)

Проверьте, существует ли столбец

Используйте hasattr() для проверки наличия названия столбца:

# Use hasattr to check for column name
if hasattr(row, 'DiscountPrice'):
    print(f"Discount: {row.DiscountPrice}")
else:
    print("No discount available")

Конвертация в встроенные типы

Объекты строк можно преобразовать в стандартные типы Python для интеграции с другими библиотеками и API:

Преобразование в кортеж

Преобразуем строку в кортеж с помощью tuple() конструктора:

cursor.execute("SELECT ProductID, Name FROM Production.Product WHERE ProductID = 1")
row = cursor.fetchone()

row_tuple = tuple(row)
print(row_tuple)  # (1, 'Adjustable Race')

Преобразовать в список

Преобразуйте строку в список с помощью list() конструктора:

row_list = list(row)
print(row_list)  # [1, 'Adjustable Race']

Конвертировать в словарь

Преобразуйте строку в словарь, когда нужно сериализировать её в JSON, передайте в шаблонный движок или объедините с другими данными. Постройте словарь из cursor.description и значений строк:

cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID = 1")
row = cursor.fetchone()

# Create dict from description and values
columns = [col[0] for col in cursor.description]
row_dict = dict(zip(columns, row))
print(row_dict)  # {'ProductID': 1, 'Name': 'Adjustable Race', 'ListPrice': Decimal('0.00')}

Вспомогательная функция для преобразования диктовки

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

def rows_to_dicts(cursor):
    """Convert fetched rows to list of dictionaries."""
    columns = [col[0] for col in cursor.description]
    return [dict(zip(columns, row)) for row in cursor.fetchall()]

cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID < 10")
products = rows_to_dicts(cursor)
for p in products:
    print(p['Name'])

Работа с обнулируемыми значениями

Драйвер возвращает значения NULL в формате Python None:

cursor.execute("SELECT FirstName, MiddleName, LastName FROM Person.Person WHERE BusinessEntityID = 1")
row = cursor.fetchone()

if row.MiddleName is None:
    full_name = f"{row.FirstName} {row.LastName}"
else:
    full_name = f"{row.FirstName} {row.MiddleName} {row.LastName}"

Работа с cursor.description

Доступ к метаданным столбцов вместе с данными строк:

cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID < 5")

# Column information
for col in cursor.description:
    print(f"Name: {col[0]}, Type: {col[1]}")

# Fetch with metadata
row = cursor.fetchone()
for i, col in enumerate(cursor.description):
    print(f"{col[0]}: {row[i]}")

Распространенные шаблоны

Вот практические шаблоны работы с объектами Row в реальных приложениях:

Строки процессов с именованным доступом

Обработка строк с использованием доступа к атрибутам для читаемого, поддерживаемого кода:

def process_orders(conn):
    cursor = conn.cursor()
    cursor.execute("""
        SELECT SalesOrderID, CustomerID, OrderDate, TotalDue 
        FROM Sales.SalesOrderHeader 
        WHERE Status = 5
    """)
    
    for order in cursor:
        print(f"Order #{order.SalesOrderID}")
        print(f"  Customer: {order.CustomerID}")
        print(f"  Date: {order.OrderDate}")
        print(f"  Total: ${order.TotalDue:.2f}")

Строить объекты из строк

Для приложений с доменной моделью сопоставьте строки с классами данных или типизированными объектами. Это отображение даёт автозаполнение IDE, проверку типов и чёткую границу между строками базы данных и логикой приложения.

from dataclasses import dataclass
from datetime import date
from decimal import Decimal

@dataclass
class Product:
    id: int
    name: str
    price: Decimal
    created: date

def get_products(conn) -> list[Product]:
    cursor = conn.cursor()
    cursor.execute("SELECT ProductID, Name, ListPrice, SellStartDate FROM Production.Product WHERE ProductID < 10")
    
    return [
        Product(
            id=row.ProductID,
            name=row.Name,
            price=row.ListPrice,
            created=row.SellStartDate
        )
        for row in cursor
    ]

Экспорт в JSON

Сериализуйте объекты строк в JSON с помощью пользовательской обработки типов для значений datetime и десятичных:

import json
from datetime import date, datetime
from decimal import Decimal

def json_serializer(obj):
    """Custom serializer for non-JSON types."""
    if isinstance(obj, (date, datetime)):
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return float(obj)
    raise TypeError(f"Type {type(obj)} not serializable")

def export_to_json(cursor, filename):
    columns = [col[0] for col in cursor.description]
    rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
    
    with open(filename, 'w') as f:
        json.dump(rows, f, default=json_serializer, indent=2)

cursor.execute("SELECT ProductID, Name, ListPrice FROM Production.Product WHERE ProductID < 10")
export_to_json(cursor, "products.json")

Объединение в группы

Сгруппируйте строки по значению столбца и соберите их в словарь для анализа или отображения:

from collections import defaultdict

cursor.execute("""
    SELECT c.Name AS CategoryName, p.Name AS ProductName, p.ListPrice 
    FROM Production.Product p
    JOIN Production.ProductSubcategory c ON p.ProductSubcategoryID = c.ProductSubcategoryID
    WHERE p.ProductSubcategoryID IS NOT NULL
    ORDER BY c.Name
""")

products_by_category = defaultdict(list)
for row in cursor:
    products_by_category[row.CategoryName].append({
        'name': row.ProductName,
        'price': row.ListPrice
    })

for category, products in products_by_category.items():
    print(f"\n{category}:")
    for p in products:
        print(f"  - {p['name']}: ${p['price']}")