SQLx Patterns
Czym jest SQLx?
Dział zatytułowany „Czym jest SQLx?”SQLx to async SQL toolkit dla Rust z compile-time verified queries. Vista używa SQLx zamiast tradycyjnego ORM.
Kluczowe cechy
Dział zatytułowany „Kluczowe cechy”| Cecha | Opis |
|---|---|
| Compile-time verification | Błędy SQL wykrywane podczas kompilacji |
| Type-safe | Automatyczne mapowanie typów Rust ↔ SQL |
| Async | Pełna integracja z Tokio |
| No ORM overhead | Raw SQL z pełną kontrolą |
Query Macros
Dział zatytułowany „Query Macros”query! - sprawdzanie compile-time
Dział zatytułowany „query! - sprawdzanie compile-time”// Kompilator sprawdzi:// 1. Czy tabela istnieje// 2. Czy kolumny istnieją// 3. Czy typy się zgadzają
let visits = sqlx::query!( "SELECT visit_id, patient_id, soap_subjective FROM visits WHERE user_id = ? AND date_time > ?", user_id, since_date).fetch_all(&pool).await?;
// Dostęp do pól - sprawdzony compile-timefor row in visits { println!("{}: {}", row.visit_id, row.soap_subjective.unwrap_or_default());}query_as! - mapowanie na struct
Dział zatytułowany „query_as! - mapowanie na struct”#[derive(sqlx::FromRow)]pub struct Visit { pub visit_id: String, pub patient_id: String, pub user_id: String, pub date_time: String, pub visit_status: String, pub soap_subjective: Option<String>, // ...}
// Automatyczne mapowanie na structlet visits = sqlx::query_as!( Visit, "SELECT * FROM visits WHERE user_id = ? ORDER BY date_time DESC LIMIT ? OFFSET ?", user_id, limit, offset).fetch_all(&pool).await?;Connection Pool
Dział zatytułowany „Connection Pool”Konfiguracja
Dział zatytułowany „Konfiguracja”pub struct Database { pub pool: SqlitePool,}
impl Database { pub async fn new(database_url: &str) -> Result<Self, sqlx::Error> { let pool = SqlitePoolOptions::new() .max_connections(10) // Max concurrent connections .acquire_timeout(Duration::from_secs(30)) .idle_timeout(Duration::from_secs(600)) // 10 minutes .connect(database_url) .await?;
Ok(Database { pool }) }}SQLite Pragmas
Dział zatytułowany „SQLite Pragmas”// Wykonywane przy inicjalizacji połączeniaPRAGMA journal_mode = WAL; // Write-Ahead LoggingPRAGMA synchronous = NORMAL; // Balans bezpieczeństwo/wydajnośćPRAGMA foreign_keys = ON; // Wymuszaj FK constraintsPRAGMA busy_timeout = 3000; // 3s timeout przy blokadachTransactions
Dział zatytułowany „Transactions”Podstawowa transakcja
Dział zatytułowany „Podstawowa transakcja”pub async fn create_visit_with_recording( pool: &SqlitePool, visit_data: CreateVisitRequest, recording_data: CreateRecordingRequest,) -> Result<(Visit, Recording), Error> { // Rozpocznij transakcję let mut tx = pool.begin().await?;
// 1. Utwórz wizytę let visit = sqlx::query_as!(Visit, "INSERT INTO visits (...) VALUES (...) RETURNING *", // ... parameters ).fetch_one(&mut *tx).await?;
// 2. Utwórz nagranie powiązane z wizytą let recording = sqlx::query_as!(Recording, "INSERT INTO recordings (visit_id, ...) VALUES (?, ...) RETURNING *", visit.visit_id, // ... parameters ).fetch_one(&mut *tx).await?;
// 3. Zatwierdź transakcję (lub rollback przy błędzie) tx.commit().await?;
Ok((visit, recording))}Transakcja z rollback
Dział zatytułowany „Transakcja z rollback”pub async fn update_visit_with_audit( pool: &SqlitePool, visit_id: &str, updates: VisitUpdate, user_id: &str,) -> Result<Visit, Error> { let mut tx = pool.begin().await?;
// Aktualizacja wizyty let result = sqlx::query!( "UPDATE visits SET soap_subjective = ?, version = version + 1 WHERE visit_id = ? AND version = ?", updates.soap_subjective, visit_id, updates.expected_version ).execute(&mut *tx).await?;
// Sprawdź optimistic locking if result.rows_affected() == 0 { // Rollback następuje automatycznie gdy tx wychodzi ze scope return Err(Error::ConcurrentModification); }
// Audit trail sqlx::query!( "INSERT INTO audit_trail (audit_id, user_id, action, resource_type, resource_id) VALUES (?, ?, 'update', 'visit', ?)", uuid::Uuid::new_v4().to_string(), user_id, visit_id ).execute(&mut *tx).await?;
tx.commit().await?;
// Pobierz zaktualizowany rekord get_visit_by_id(pool, visit_id).await}Optimistic Locking
Dział zatytułowany „Optimistic Locking”Pattern
Dział zatytułowany „Pattern”-- Tabela visits używa version fieldUPDATE visits SET soap_subjective = ?, version = version + 1, updated_at = CURRENT_TIMESTAMPWHERE visit_id = ? AND version = ?;-- Jeśli affected_rows = 0, ktoś inny edytował - conflict!Implementacja Rust
Dział zatytułowany „Implementacja Rust”pub async fn update_visit_soap( pool: &SqlitePool, visit_id: &str, soap_data: SoapUpdate, expected_version: i32,) -> Result<Visit, Error> { let result = sqlx::query!( "UPDATE visits SET soap_subjective = ?, version = version + 1 WHERE visit_id = ? AND version = ?", soap_data.subjective, visit_id, expected_version ).execute(pool).await?;
if result.rows_affected() == 0 { return Err(Error::ConcurrentModification); }
// Pobierz zaktualizowany rekord get_visit_by_id(pool, visit_id).await}Pagination Pattern
Dział zatytułowany „Pagination Pattern”#[tauri::command]pub async fn get_visits_paginated( db: State<'_, Database>, user_id: String, page: i64, limit: i64, filters: VisitFilters,) -> Result<PaginatedVisits, String> { let offset = (page - 1) * limit;
let visits = sqlx::query_as!( Visit, r#" SELECT * FROM visits WHERE user_id = ? AND ($1 IS NULL OR visit_type = $1) AND ($2 IS NULL OR date_time >= $2) ORDER BY date_time DESC LIMIT ? OFFSET ? "#, user_id, filters.visit_type, filters.start_date, limit, offset ).fetch_all(&db.pool).await?;
let total = get_visits_count(&db, &user_id, &filters).await?;
Ok(PaginatedVisits { visits, pagination: PaginationInfo { page, limit, total, total_pages: (total + limit - 1) / limit, } })}Lokalizacja
Dział zatytułowany „Lokalizacja”src-tauri/src/database/├── mod.rs # Database struct, pool initialization├── models.rs # Rust structs (sqlx::FromRow)├── schema.rs # SCHEMA_STATEMENTS (CREATE TABLE)└── migrations.rs # Migration logicPrzykład modelu
Dział zatytułowany „Przykład modelu”#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]pub struct User { pub user_id: String, // snake_case - tak samo w JSON pub name: String, pub email: String, pub password_hash: Option<String>, pub roles: String, // JSON array jako TEXT pub active: bool, pub biometric_enabled: i32, // SQLite INTEGER -> Rust i32 pub created_at: String, pub updated_at: String,}SQLx CLI
Dział zatytułowany „SQLx CLI”Przygotowanie metadanych
Dział zatytułowany „Przygotowanie metadanych”# Generuj metadata dla compile-time verificationcd src-tauricargo sqlx prepare -- --all-features
# Tworzy plik .sqlx/ z informacjami o schemacieSprawdzenie migracji
Dział zatytułowany „Sprawdzenie migracji”# Sprawdź czy wszystkie queries są validcargo sqlx prepare --checkError Handling
Dział zatytułowany „Error Handling”Typowe błędy SQLx
Dział zatytułowany „Typowe błędy SQLx”match result { Ok(data) => Ok(data), Err(sqlx::Error::RowNotFound) => { Err("Record not found".to_string()) } Err(sqlx::Error::Database(db_err)) => { if db_err.is_unique_violation() { Err("Duplicate entry".to_string()) } else if db_err.is_foreign_key_violation() { Err("Referenced record does not exist".to_string()) } else { Err(format!("Database error: {}", db_err)) } } Err(e) => Err(format!("Unexpected error: {}", e))}Busy timeout handling
Dział zatytułowany „Busy timeout handling”// SQLite może zwrócić SQLITE_BUSY przy concurrent writes// busy_timeout=3000ms obsługuje większość przypadków// Dla krytycznych operacji - retry logic:
let mut attempts = 0;loop { match execute_query(&pool).await { Ok(result) => return Ok(result), Err(e) if e.to_string().contains("database is locked") && attempts < 3 => { attempts += 1; tokio::time::sleep(Duration::from_millis(100 * attempts)).await; } Err(e) => return Err(e), }}