Rijobjecten in mssql-python

De mssql-python-driver retourneert rijgegevens als Row objecten uit fetch-operaties. Deze objecten bieden flexibele toegangspatronen:

  • Attribuuttoegang (row.ColumnName) is de meest leesbare keuze voor benoemde kolommen. Gebruik het wanneer je query een bekende, stabiele kolomlijst heeft.
  • String key access (row['ColumnName']) biedt woordenboekachtige toegang tot kolommen op naam, handig wanneer je programmatische kolomzoekopdracht nodig hebt of wanneer kolomnamen spaties of speciale tekens bevatten.
  • Indextoegang (row[0]) is nuttig voor dynamische queries waarbij kolomnamen niet bekend zijn tijdens de ontwikkeling of bij het verwerken SELECT * van resultaten.
  • Tuple unpacking (a, b, c = row) is de meest beknopte keuze voor luslichamen met een klein, vast aantal kolommen.
  • Mapping access (row._mapping) levert een alleen-lezen mapping van kolomnamen naar waarden terug. Gebruik het om een rij om te zetten naar een woordenboek of om de rij door te geven aan code die een mapping verwacht.

Attribuuttoegang

Raadpleeg kolomwaarden direct op naam:

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

Hoofdlettergevoelig

Kolomnaamtoegang is hoofdlettergevoelig en komt overeen met de kolomnamen die door SQL Server worden teruggegeven. Als je database inconsistente casing gebruikt, gebruik dan SQL-aliasen AS om namen te normaliseren, of schakel de lowercase module-instelling in (zie Moduleconfiguratie):

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

Kolomaliasen

Gebruik SQL-aliassen om vriendelijke attribuutnamen te maken:

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

Indextoegang

Toegangswaarden via nul-gebaseerde kolomindex:

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

Negatieve indexering

De driver ondersteunt negatieve indexering in Python-stijl:

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)

Toegang tot de string-sleutel

Raadpleeg kolomwaarden op naam met behulp van woordenboekachtige syntaxis:

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

Dit is handig wanneer kolomnamen spaties of speciale tekens bevatten, of wanneer je kolommen programmatisch moet benaderen:

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

Snijden

Haal meerdere waarden uit met slices:

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

Tuple uitpakken

Pak rijwaarden direct uit in variabelen:

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

Gedeeltelijke uitpakken

Gebruik * om de resterende waarden vast te leggen:

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

Rijlengte en iteratie

Werk met rijafmetingen en herhaal kolomwaarden:

Krijg kolomaantal

Gebruik len() om het aantal kolommen in een rij te krijgen:

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

print(len(row))  # Number of columns

Itereren over waarden

Loop kolomwaarden in volgorde door:

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

for value in row:
    print(value)

Controleer of er een kolom bestaat

Gebruik hasattr() om te testen op de aanwezigheid van een kolomnaam:

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

Converteer naar ingebouwde types

Rijobjecten kunnen worden omgezet naar standaard Python-types voor integratie met andere bibliotheken en API's:

Converteer naar tuple

Zet een rij om in een tuple met de tuple() constructor:

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

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

Omzetten naar lijst

Zet een rij om in een lijst met de list() constructor:

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

Converteren naar woordenlijst

Converteer een rij naar een woordenboek wanneer je deze moet serialiseren naar JSON, geef hem door aan een template-engine, of voeg hem samen met andere data. De row._mapping eigenschap geeft een alleen-lezen mapping van kolomnamen terug naar waarden in de kolomvolgorde van het resultaat. Geef het door dict() om een regulier woordenboek te krijgen:

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

Het eigendom vertoont _mapping de volgende gedragingen:

  • De mapping is alleen-lezen. Het toewijzen aan een sleutel verhoogt TypeError. Converteer het eerst met dict() als je waarden moet veranderen.
  • Sleutels zijn de kolomnamen uit cursor.description. Belangrijke opzoekopdrachten zijn hoofdlettergevoelig. Als je de lowercase module-instelling inschakelt, zijn de toetsen kleine letters en komt de mapping alleen overeen met die kleine letters. Attribuut- en string-toetstoegang op de rij zelf wordt hoofdlettergevoelig wanneer lowercase aan staat, maar row._mapping opzoekingen niet.
  • Als de resultaatset dubbele kolomnamen heeft, behoudt de mapping de laatste waarde voor elke naam. Gebruik Transact-SQL (T-SQL) AS aliassen om elke kolom een unieke naam te geven, of lees de waarden per index.
  • De eigenschap gaat voor een resultaatkolom met de naam _mapping, dus row._mapping geeft altijd de afbeelding terug. Lees die column met row['_mapping'].

Hulpfunctie voor dictconversie

Maak een herbruikbare hulpfunctie om alle opgehaalde rijen om te zetten in woordenboeken:

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

Werk met nullbare waarden

De driver geeft NULL-waarden terug als PythonNone:

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

Werk met cursor.description

Toegang tot kolommetadata naast rijgegevens:

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

Algemene patronen

Hier zijn praktische patronen voor het werken met Row-objecten in toepassingen in de echte wereld:

Procesrijen met benoemde toegang

Procesrijen met attribuuttoegang voor leesbare, onderhoudbare code:

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

Bouw objecten uit rijen

Voor toepassingen met een domeinmodel koppel je rijen aan dataklassen of getypeerde objecten. Deze mapping geeft je IDE-autocompletie, typecontrole en een duidelijke grens tussen databaserijen en applicatielogica.

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
    ]

Export naar JSON

Serialiseer rijobjecten naar JSON met aangepaste typebehandeling voor datetime en decimale waarden:

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

Samenvoegen in groepen

Groepeer rijen op kolomwaarde en verzamel ze in een woordenboek voor analyse of weergave:

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