Buduj łańcuchy połączeń programatycznie

Wiele aplikacji musi dynamicznie budować łańcuchy połączeń, zamiast przechowywać je jako statyczne wartości konfiguracyjne. Wybierz podejście dopasowane do Twojego wdrożenia:

  • Zmienne środowiskowe: Najlepsze dla kontenerów, CI/CD i aplikacji 12-składnikowych. Proste i szeroko wspierane.
  • Pliki konfiguracyjne JSON/YAML: Najlepsze rozwiązanie dla aplikacji z wieloma środowiskami (deweloperskim, przedprodukcyjnym, produkcyjnym), które wymagają ustrukturyzowanej konfiguracji.
  • Azure Key Vault: Najlepszy do wdrożeń produkcyjnych, gdzie sekrety muszą być centralnie zarządzane i audytowane.
  • Klasa Builder: Najlepsze dla bibliotek lub frameworków, które muszą konstruować ciągi połączeń z wejścia użytkownika z automatycznym escapingiem.

Podstawowa konstrukcja strun

Użyj struny f

Struny f są powszechnym podejściem do szybkich skryptów i prototypów. Unikaj tego wzorca, gdy wartości pochodzą z danych wejściowych użytkownika, ponieważ złośliwa wartość taka jak mydb;Server=evil.com może zmienić cel połączenia:

import mssql_python

server = "<server>.database.windows.net"
database = "<database>"

connection_string = f"Server={server};Database={database};Authentication=ActiveDirectoryDefault;Encrypt=yes;"

conn = mssql_python.connect(connection_string)

Użyj funkcji join

Podejście to join rozdziela pary klucz-wartość na wywołanie funkcji w stylu słownikowym, które jest łatwiejsze do odczytania i utrzymania niż długi ciąg f. Dodatkowo automatycznie odfiltrowuje wartości None, dzięki czemu możesz przekazywać opcjonalne parametry bez dodatkowej logiki warunkowej:

def build_connection_string(**kwargs) -> str:
    """Build connection string from keyword arguments."""
    return ";".join(f"{key}={value}" for key, value in kwargs.items() if value is not None)

conn_str = build_connection_string(
    Server="<server>.database.windows.net",
    Database="<database>",
    Authentication="ActiveDirectoryDefault",
    Encrypt="yes"
)

conn = mssql_python.connect(conn_str)

Klasa konstruktora parametrów połączenia

Klasa builder zapewnia API płynne z automatycznym escapingiem. To podejście jest przydatne w bibliotekach lub aplikacjach wielodzierżawczych, gdzie parametry połączenia pochodzą z różnych źródeł:

import mssql_python

class ConnectionStringBuilder:
    """Builder for SQL Server connection strings."""

    def __init__(self):
        self._params = {}

    def server(self, value: str) -> "ConnectionStringBuilder":
        self._params["Server"] = value
        return self

    def database(self, value: str) -> "ConnectionStringBuilder":
        self._params["Database"] = value
        return self

    def trusted_connection(self) -> "ConnectionStringBuilder":
        self._params["Trusted_Connection"] = "yes"
        return self

    def sql_auth(self, username: str, password: str) -> "ConnectionStringBuilder":
        self._params["UID"] = username
        self._params["PWD"] = password
        return self

    def entra_default(self) -> "ConnectionStringBuilder":
        self._params["Authentication"] = "ActiveDirectoryDefault"
        return self

    def entra_msi(self, client_id: str = None) -> "ConnectionStringBuilder":
        self._params["Authentication"] = "ActiveDirectoryMSI"
        if client_id:
            self._params["UID"] = client_id
        return self

    def encrypt(self, value: bool = True) -> "ConnectionStringBuilder":
        self._params["Encrypt"] = "yes" if value else "no"
        return self

    def trust_server_certificate(self, value: bool = True) -> "ConnectionStringBuilder":
        self._params["TrustServerCertificate"] = "yes" if value else "no"
        return self

    def connect_timeout(self, seconds: int) -> "ConnectionStringBuilder":
        self._timeout = seconds
        return self

    def build(self) -> str:
        """Build the connection string."""
        return ";".join(f"{k}={v}" for k, v in self._params.items())

    def connect(self) -> mssql_python.Connection:
        """Build and connect."""
        return mssql_python.connect(self.build(), timeout=getattr(self, '_timeout', 0))


# Usage examples
# Microsoft Entra authentication (recommended)
conn = (ConnectionStringBuilder()
    .server("<server>.database.windows.net")
    .database("<database>")
    .entra_default()
    .encrypt()
    .connect())

# Azure with managed identity
conn = (ConnectionStringBuilder()
    .server("<server>.database.windows.net")
    .database("<database>")
    .entra_msi()
    .encrypt()
    .connect())

Konfiguracja oparta na środowisku

Ze zmiennych środowiskowych

Odczytywanie parametrów połączenia ze zmiennych środowiskowych pozwala trzymać dane uwierzytelniające poza kodem źródłowym i sprawdza się zarówno w lokalnym środowisku programistycznym, jak i w kontenerach oraz potokach CI/CD. Funkcja sprawdza, którą metodę uwierzytelniania użyć na podstawie ustawionych zmiennych:

import os
import mssql_python

def get_connection_from_env() -> mssql_python.Connection:
    """Build connection from environment variables."""
    server = os.environ.get("SQL_SERVER")
    database = os.environ.get("SQL_DATABASE")

    if not server or not database:
        raise ValueError("SQL_SERVER and SQL_DATABASE environment variables required")

    # Check for authentication method
    if os.environ.get("SQL_USE_MSI", "").lower() == "true":
        # Azure Managed Identity
        conn_str = f"Server={server};Database={database};Authentication=ActiveDirectoryMSI;Encrypt=yes;"
    elif os.environ.get("SQL_TRUSTED_CONNECTION", "").lower() == "true":
        # Windows authentication
        conn_str = f"Server={server};Database={database};Trusted_Connection=yes;Encrypt=yes;"
    else:
        # SQL authentication
        username = os.environ.get("SQL_USERNAME")
        password = os.environ.get("SQL_PASSWORD")
        if not username or not password:
            raise ValueError("SQL_USERNAME and SQL_PASSWORD required for SQL authentication")
        conn_str = f"Server={server};Database={database};UID={username};PWD={password};Encrypt=yes;"

    return mssql_python.connect(conn_str)

# Usage
conn = get_connection_from_env()

Z użyciem python-dotenv

Pakiet python-dotenv wczytuje pary klucz-wartość z pliku .env do zmiennych środowiskowych, dzięki czemu kod korzysta z poświadczeń w ten sam sposób zarówno w środowisku lokalnym, jak i produkcyjnym. Plik .env nie trafia do systemu kontroli wersji (dodaj go do .gitignore), a wdrożone środowiska udostępniają te same zmienne za pośrednictwem platformowego magazynu sekretów.

Zainstaluj za pomocą pip install python-dotenv.

Utwórz plik .env w katalogu głównym projektu z parametrami połączenia:

# .env - add this file to .gitignore
SQL_SERVER=<server>.database.windows.net
SQL_DATABASE=<database>
SQL_USE_MSI=true

Następnie załaduj i użyj tych wartości w swoim skrypcie:

from dotenv import load_dotenv
import os
import mssql_python

# Load .env file into os.environ (no-op if the file doesn't exist)
load_dotenv()

server = os.getenv("SQL_SERVER")
database = os.getenv("SQL_DATABASE")

if not server or not database:
    raise ValueError("SQL_SERVER and SQL_DATABASE must be set in .env or as environment variables")

use_msi = os.getenv("SQL_USE_MSI", "false").lower() == "true"

if use_msi:
    conn_str = f"Server={server};Database={database};Authentication=ActiveDirectoryMSI;Encrypt=yes;"
else:
    conn_str = f"Server={server};Database={database};Authentication=ActiveDirectoryDefault;Encrypt=yes;"

conn = mssql_python.connect(conn_str)

Wskazówka

load_dotenv() nie nadpisuje zmiennych, które już są ustawione w środowisku. W środowisku produkcyjnym ustaw te same nazwy zmiennych za pośrednictwem swojej platformy (na przykład w ustawieniach aplikacji w usłudze App Service lub jako zmienne środowiskowe kontenera) i całkowicie pomiń plik .env.

Konfiguracja oparta na plikach

Z konfiguracji JSON

Plik konfiguracyjny JSON pozwala definiować ustawienia połączeń dla wielu środowisk (program, staging, produkcja) w jednym miejscu. Funkcja odczytuje plik, wybiera środowisko docelowe i buduje string połączenia na podstawie ustawień strukturalnych:

import json
import io
import mssql_python

def load_connection_from_json(config_file, environment: str = "development") -> str:
    """Load connection settings from a JSON config file or file-like object."""
    config = json.load(config_file)

    env_config = config.get(environment, {})
    db_config = env_config.get("database", {})

    params = {
        "Server": db_config.get("server"),
        "Database": db_config.get("database"),
        "Encrypt": "yes" if db_config.get("encrypt", True) else "no",
    }

    auth_type = db_config.get("authentication", "sql")
    if auth_type == "msi":
        params["Authentication"] = "ActiveDirectoryMSI"
    elif auth_type == "default":
        params["Authentication"] = "ActiveDirectoryDefault"
    elif auth_type == "windows":
        params["Trusted_Connection"] = "yes"
    else:
        params["UID"] = db_config.get("username")
        params["PWD"] = db_config.get("password")

    return ";".join(f"{k}={v}" for k, v in params.items() if v)

# Example: load from an inline JSON config (in production, use open("config.json"))
sample_config = json.dumps({
    "development": {
        "database": {
            "server": "localhost",
            "database": "devdb",
            "authentication": "windows",
            "encrypt": False
        }
    },
    "production": {
        "database": {
            "server": "prod.database.windows.net",
            "database": "proddb",
            "authentication": "msi",
            "encrypt": True
        }
    }
})

conn_str = load_connection_from_json(io.StringIO(sample_config), "production")
print(f"Connection string: {conn_str}")

Z konfiguracji YAML

Pliki konfiguracyjne YAML są czytelną alternatywą dla JSON. Są one powszechnie używane w projektach Python i wdrożeniach Kubernetes. To podejście odczytuje ustawienia połączenia z ustrukturyzowanego pliku YAML i buduje parametry połączenia na podstawie typu uwierzytelniania zdefiniowanego w konfiguracji.

Zainstaluj pakiet, uruchamiając pip install pyyaml.

Utwórz plik database.yml w swoim projekcie:

database:
  server: <server>.database.windows.net
  name: <database>
  authentication: msi
  encrypt: true

Następnie załaduj i użyj tych ustawień w swoim skrypcie:

import yaml
import mssql_python

def load_from_yaml(config_path: str) -> mssql_python.Connection:
    """Load connection from YAML config."""
    with open(config_path) as f:
        config = yaml.safe_load(f)

    db = config["database"]

    parts = [
        f"Server={db['server']}",
        f"Database={db['name']}",
    ]

    if db.get("trusted_connection"):
        parts.append("Trusted_Connection=yes")
    elif db.get("authentication") == "msi":
        parts.append("Authentication=ActiveDirectoryMSI")
    else:
        parts.append(f"UID={db['username']}")
        parts.append(f"PWD={db['password']}")

    if db.get("encrypt", True):
        parts.append("Encrypt=yes")
    if db.get("trust_server_certificate"):
        parts.append("TrustServerCertificate=yes")

    return mssql_python.connect(";".join(parts))

conn = load_from_yaml("database.yml")

Integracja usługi Azure Key Vault

W przypadku wdrożeń produkcyjnych przechowywaj dane połączeń w Azure Key Vault, a nie w plikach konfiguracyjnych czy zmiennych środowiskowych. Key Vault zapewnia scentralizowane zarządzanie tajemnicami, audyt dostępu oraz automatyczną rotację. Zainstaluj wymagane pakiety, uruchamiając pip install azure-keyvault-secrets azure-identity. Pełny przegląd znajdziesz w Quickstart: Azure Key Vault secret client library for Python.

import os
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
import mssql_python

def get_connection_from_keyvault(vault_url: str) -> mssql_python.Connection:
    """Build connection using secrets from Azure Key Vault."""
    credential = DefaultAzureCredential()
    client = SecretClient(vault_url=vault_url, credential=credential)

    server = client.get_secret("sql-server").value
    database = client.get_secret("sql-database").value
    username = client.get_secret("sql-username").value
    password = client.get_secret("sql-password").value

    conn_str = f"Server={server};Database={database};UID={username};PWD={password};Encrypt=yes;"
    return mssql_python.connect(conn_str)

vault_url = os.environ.get("AZURE_KEY_VAULT_URL")
if vault_url:
    conn = get_connection_from_keyvault(vault_url)

Obsługa znaków specjalnych

Poprzedź średniki i nawiasy klamrowe znakiem ucieczki

Musisz uciekać z wartości parametry połączenia zawierające specjalne znaki. Owij wartość w napierkach {} i podwaj wszystkie wewnętrzne zamknięcia }:

def escape_value(value: str) -> str:
    """Escape special characters in connection string values."""
    if ";" in value or "{" in value or "}" in value:
        # Wrap in braces and escape internal braces
        value = value.replace("}", "}}")
        return "{" + value + "}"
    return value

# Password with semicolon
password = "my;complex;password"
escaped_password = escape_value(password)  # {my;complex;password}

conn_str = f"Server=<server>;Database=<database>;UID=<login>;PWD={escaped_password};"

Budowniczy z automatycznym ucieczką

Ta klasa builder automatycznie opakowuje każdą wartość, więc wywołujący nie muszą pamiętać reguł ucieczki. Używaj go, gdy parametry połączenia pochodzą z zewnętrznych wejść, takich jak formularze użytkownika, API konfiguracyjne lub tajne magazyny, gdzie wartości mogą zawierać średniki lub nawiasy:

class SafeConnectionStringBuilder:
    """Connection string builder with automatic escaping."""

    SPECIAL_CHARS = {";", "{", "}"}

    def __init__(self):
        self._params = {}

    def _escape(self, value: str) -> str:
        if any(c in value for c in self.SPECIAL_CHARS):
            value = value.replace("}", "}}")
            return "{" + value + "}"
        return value

    def set(self, key: str, value: str) -> "SafeConnectionStringBuilder":
        self._params[key] = self._escape(value)
        return self

    def build(self) -> str:
        return ";".join(f"{k}={v}" for k, v in self._params.items())

# Safely handles special characters
builder = SafeConnectionStringBuilder()
builder.set("Server", "<server>.database.windows.net")
builder.set("Database", "<database>")
builder.set("PWD", "pass;word{with}special")  # Automatically escaped

conn_str = builder.build()

Validation

Zanim użyjesz w aplikacji dynamicznie tworzonego ciągu połączenia, sprawdź, czy rzeczywiście umożliwia nawiązanie połączenia. Ta funkcja pomocnicza wykonuje próbne proste zapytanie i zwraca wartość logiczną:

import mssql_python

def validate_connection_string(conn_str: str) -> bool:
    """Validate a connection string by attempting to connect."""
    try:
        conn = mssql_python.connect(conn_str)
        cursor = conn.cursor()
        cursor.execute("SELECT 1")
        cursor.fetchone()
        conn.close()
        return True
    except mssql_python.Error as e:
        print(f"Connection failed: {e}")
        return False

# Test before using
conn_str = "Server=<server>.database.windows.net;Database=<database>;Authentication=ActiveDirectoryDefault;Encrypt=yes;"
if validate_connection_string(conn_str):
    print("Connection string is valid")