Objets de ligne dans mssql-python

Le pilote mssql-python renvoie les données de ligne sous forme Row d’objets issus d’opérations de récupération. Ces objets offrent des schémas d’accès flexibles :

  • L’accès aux attributs (row.ColumnName) est le choix le plus lisible pour les colonnes nommées. Utilisez-le lorsque votre requête a une liste de colonnes connue et stable.
  • L’accès à la clé de chaîne (row['ColumnName']) permet un accès de type dictionnaire aux colonnes par nom, utile lorsque vous avez besoin d’une recherche programmatique de colonnes ou lorsque les noms de colonnes contiennent des espaces ou des caractères spéciaux.
  • L’accès à l’index (row[0]) est utile pour les requêtes dynamiques où les noms des colonnes ne sont pas connus au moment du développement ou lors du traitement SELECT * des résultats.
  • Le déballage de tuples (a, b, c = row) est le choix le plus concis pour les corps de boucles avec un petit nombre fixe de colonnes.
  • Mapper access (row._mapping) renvoie un mappage en lecture seule des noms de colonnes vers des valeurs. Utilisez-le pour convertir une ligne en dictionnaire ou pour passer la ligne à un code qui attend une correspondance.

Accès aux attributs

Accéder directement aux valeurs de la colonne par nom :

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

Respect de la casse

L’accès aux noms de colonnes est sensible aux cas majuscules et correspond aux noms de colonnes retournés par SQL Server. Si votre base de données utilise des casiers incohérents, utilisez des alias SQL AS pour normaliser les noms, ou activez le lowercase paramètre du module (voir Configuration du module) :

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

Alias de colonne

Utilisez des alias SQL pour créer des noms d’attributs amicaux :

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

Accès à l’index

Valeurs d’accès par indice de colonnes à base de zéros :

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

Indexation négative

Le pilote prend en charge l’indexation négative de style 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)

Accès à la clé de la chaîne

Accéder aux valeurs des colonnes par nom en utilisant une syntaxe de type dictionnaire :

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

C’est utile lorsque les noms de colonnes contiennent des espaces ou des caractères spéciaux, ou lorsque vous devez accéder aux colonnes de façon programmatique :

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

Découpage

Extraire plusieurs valeurs à l’aide de tranches :

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

Déballage de tuples

Décompressez directement les valeurs des lignes en variables :

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

Déchargement partiel

À utiliser * pour capturer les valeurs restantes :

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

Longueur de ligne et itération

Travaillez avec les dimensions de ligne et itérez à travers les valeurs des colonnes :

Obtenir le nombre de colonnes

Utilisez len() pour obtenir le nombre de colonnes consécutives :

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

print(len(row))  # Number of columns

Itère sur les valeurs

Bouclez les valeurs des colonnes dans l’ordre :

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

for value in row:
    print(value)

Vérifiez si une colonne existe

À utiliser hasattr() pour vérifier la présence d’un nom de colonne :

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

Convertir en types intégrés

Les objets lignes peuvent être convertis en types Python standards pour une intégration avec d’autres bibliothèques et API :

Convertir en tuple

Convertir une ligne en uplet à l’aide du tuple() constructeur :

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

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

Convertir en liste

Convertir une ligne en liste à l’aide du list() constructeur :

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

Convertir en dictionnaire

Convertissez une ligne en dictionnaire lorsque vous devez la sérialiser en JSON, la transférez à un moteur de templates, ou fusionnez-la avec d’autres données. La row._mapping propriété renvoie un mappage en lecture seule des noms de colonnes vers des valeurs dans l’ordre des colonnes par ensemble de résultats. Passez-le à dict() un dictionnaire classique :

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

row_dict = dict(row._mapping)
print(row_dict)  # {'ProductID': 1, 'Name': 'Adjustable Race', 'ListPrice': Decimal('0.0000')}

La _mapping propriété présente les comportements suivants :

  • Le mappage est en lecture seule. Attribuer à une clé augmente TypeError. Convertis-le d’abord si dict() tu dois changer les valeurs.
  • Les clés sont les noms des colonnes de cursor.description. Les recherches clés sont sensibles à la majuscule. Si vous activez le lowercase réglage du module, les touches sont en minuscules, et la correspondance correspond uniquement à ces touches minuscules. L’accès aux attributs et aux clés de chaîne sur la ligne elle-même devient insensible à la majuscule lorsqu’il lowercase est activé, mais row._mapping les recherches non.
  • Si l’ensemble de résultats comporte des noms de colonnes en double, la correspondance conserve la dernière valeur pour chaque nom. Utilisez des alias Transact-SQL (T-SQL) AS pour donner un nom unique à chaque colonne, ou lisez les valeurs par index.
  • La propriété prévaut sur une colonne de résultat nommée _mapping, donc row._mapping retourne toujours l’application. Lisez cette chronique avec row['_mapping'].

Fonction d’aide pour la conversion de dicts

Créez une fonction d’assistance réutilisable pour convertir toutes les lignes récupérées en dictionnaires :

def rows_to_dicts(cursor):
    """Convert fetched rows to list of dictionaries."""
    return [dict(row._mapping) 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'])

Travail avec des valeurs nulles

Le pilote retourne les valeurs NULL sous forme de 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}"

Travail avec cursor.description

Accéder aux métadonnées des colonnes aux côtés des lignes :

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

Modèles courants

Voici des schémas pratiques pour travailler avec des objets Row dans des applications réelles :

Lignes de processus avec accès nommé

Lignes de processus utilisant l’accès aux attributs pour un code lisible et maintenable :

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

Construire des objets à partir de lignes

Pour les applications avec un modèle de domaine, mapper les lignes en classes de données ou en objets typés. Cette correspondance vous offre l’autocomplétion de l’IDE, la vérification des types, et une frontière claire entre les lignes de la base de données et la logique applicative.

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
    ]

Exportation vers JSON

Sérialiser les objets de ligne en JSON avec une gestion personnalisée des types pour les valeurs de date-heure et décimales :

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):
    rows = [dict(row._mapping) 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")

Agréger en groupes

Regroupez les lignes par valeur de colonne et collectez-les dans un dictionnaire pour analyse ou affichage :

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