# Rust i SQLx w 2026: Zapytania Sprawdzane w Czasie Kompilacji oraz Pytania Rekrutacyjne > Kompleksowy przewodnik po SQLx w Rust - zapytaniach sprawdzanych w czasie kompilacji, porównaniu z ORM-ami oraz praktycznych pytaniach na rozmowy kwalifikacyjne. - Published: 2026-09-12 - Updated: 2026-09-12 - Author: Anthony Fillion-Maillet - Tags: rust, sqlx, database, postgresql, tutorial - Reading time: 12 min --- Biblioteka SQLx zrewolucjonizowała sposób, w jaki programiści Rust wchodzą w interakcję z bazami danych. W przeciwieństwie do tradycyjnych ORM-ów, SQLx oferuje unikalne podejście: surowe zapytania SQL ze sprawdzaniem w czasie kompilacji. To połączenie zapewnia bezpieczeństwo typów bez poświęcania wydajności lub kontroli nad generowanym SQL. > SQLx 0.8+ wspiera PostgreSQL, MySQL, MariaDB oraz SQLite z pełnym asynchronicznym wsparciem poprzez tokio lub async-std. Sprawdzanie zapytań w czasie kompilacji wykrywa błędy składni SQL oraz niezgodności typów przed uruchomieniem aplikacji. ## Dlaczego SQLx Zamiast Tradycyjnych ORM-ów Ekosystem Rust oferuje kilka opcji dostępu do baz danych: Diesel jako w pełni funkcjonalny ORM, SeaORM dla podejścia ActiveRecord oraz SQLx dla surowego SQL ze sprawdzaniem typów. Każde narzędzie służy innemu celowi. Diesel generuje SQL z bezpiecznych typowo konstruktorów zapytań. To podejście działa dobrze dla standardowych operacji CRUD, ale staje się kłopotliwe przy złożonych zapytaniach wymagających specificznych funkcji bazy danych. SQLx przyjmuje inną filozofię - programiści piszą rzeczywiste zapytania SQL, które następnie kompilator weryfikuje pod kątem schematu bazy danych. ```rust use sqlx::postgres::PgPoolOptions; use sqlx::FromRow; #[derive(Debug, FromRow)] struct User { id: i32, email: String, created_at: chrono::DateTime, } async fn fetch_users(pool: &sqlx::PgPool) -> Result, sqlx::Error> { sqlx::query_as::<_, User>( "SELECT id, email, created_at FROM users WHERE active = $1" ) .bind(true) .fetch_all(pool) .await } ``` Makro `query_as` weryfikuje, że kolumny `id`, `email` oraz `created_at` istnieją w tabeli users i że ich typy odpowiadają polom struktury. Jeśli schemat bazy danych ulegnie zmianie, kompilacja nie powiedzie się z jasnymi komunikatami o błędach. ## Konfiguracja Sprawdzania w Czasie Kompilacji Sprawdzanie zapytań w czasie kompilacji wymaga połączenia z bazą danych podczas procesu budowania. SQLx obsługuje to poprzez zmienną środowiskową `DATABASE_URL` lub pliki `.env`. ```bash # .env DATABASE_URL=postgres://user:password@localhost/myapp_dev ``` Dla środowisk CI, gdzie połączenie z bazą danych nie jest dostępne, SQLx zapewnia tryb offline. Polecenie `cargo sqlx prepare` generuje metadane zapytań, które są commitowane do repozytorium. ```bash cargo install sqlx-cli cargo sqlx prepare ``` To polecenie tworzy katalog `.sqlx` zawierający skompilowane informacje o zapytaniach. Gdy zmienna `DATABASE_URL` nie jest ustawiona, SQLx używa tych buforowanych metadanych. ```rust // Wymusza sprawdzanie offline nawet gdy baza danych jest dostępna #[sqlx::test] async fn test_user_query(pool: PgPool) { let users = sqlx::query_as!( User, r#"SELECT id, email, created_at FROM users LIMIT 10"# ) .fetch_all(&pool) .await .unwrap(); assert!(users.len() <= 10); } ``` ## Zaawansowane Wzorce Zapytań Aplikacje produkcyjne wymagają więcej niż prostych operacji CRUD. SQLx obsługuje złożone scenariusze, w tym dynamiczne zapytania, transakcje i operacje wsadowe. ### Zapytania Dynamiczne z Opcjonalnymi Filtrami ```rust use sqlx::QueryBuilder; async fn search_users( pool: &PgPool, email_filter: Option<&str>, min_age: Option, limit: i64, ) -> Result, sqlx::Error> { let mut builder = QueryBuilder::new( "SELECT id, email, created_at FROM users WHERE 1=1" ); if let Some(email) = email_filter { builder.push(" AND email ILIKE "); builder.push_bind(format!("%{}%", email)); } if let Some(age) = min_age { builder.push(" AND age >= "); builder.push_bind(age); } builder.push(" LIMIT "); builder.push_bind(limit); builder .build_query_as::() .fetch_all(pool) .await } ``` ### Transakcje z Obsługą Rollback ```rust async fn transfer_funds( pool: &PgPool, from_account: i32, to_account: i32, amount: rust_decimal::Decimal, ) -> Result<(), sqlx::Error> { let mut tx = pool.begin().await?; // Odejmij z konta źródłowego let result = sqlx::query!( r#"UPDATE accounts SET balance = balance - $1 WHERE id = $2 AND balance >= $1"#, amount, from_account ) .execute(&mut *tx) .await?; if result.rows_affected() == 0 { return Err(sqlx::Error::RowNotFound); } // Dodaj do konta docelowego sqlx::query!( "UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, to_account ) .execute(&mut *tx) .await?; tx.commit().await?; Ok(()) } ``` ### Wstawianie Wsadowe ```rust async fn bulk_insert_users( pool: &PgPool, users: &[(String, String)], ) -> Result<(), sqlx::Error> { let mut builder = QueryBuilder::new( "INSERT INTO users (email, name) " ); builder.push_values(users, |mut b, (email, name)| { b.push_bind(email).push_bind(name); }); builder.build().execute(pool).await?; Ok(()) } ``` ## Migracje i Zarządzanie Schematem SQLx CLI zapewnia wbudowane zarządzanie migracjami, które bezproblemowo integruje się ze sprawdzaniem w czasie kompilacji. ```bash # Utwórz nową migrację sqlx migrate add create_users_table # Uruchom oczekujące migracje sqlx migrate run # Cofnij ostatnią migrację sqlx migrate revert ``` Pliki migracji używają numeracji znaczników czasu dla porządkowania: ```sql -- migrations/20260915120000_create_users_table.sql CREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_users_email ON users(email); ``` Dla złożonych aplikacji migracje mogą zawierać zarówno pliki up jak i down: ```sql -- migrations/20260915120000_create_users_table.up.sql CREATE TABLE users (...); -- migrations/20260915120000_create_users_table.down.sql DROP TABLE users; ``` ## Pula Połączeń i Konfiguracja Produkcyjna Produkcyjne wdrożenia wymagają właściwej konfiguracji puli połączeń, obsługi timeoutów i monitoringu. ```rust use sqlx::postgres::{PgPoolOptions, PgConnectOptions}; use std::time::Duration; async fn create_production_pool() -> Result { let options = PgConnectOptions::new() .host("db.example.com") .database("production") .username("app_user") .password("secure_password") .ssl_mode(sqlx::postgres::PgSslMode::Require); PgPoolOptions::new() .max_connections(20) .min_connections(5) .acquire_timeout(Duration::from_secs(5)) .idle_timeout(Duration::from_secs(600)) .max_lifetime(Duration::from_secs(1800)) .connect_with(options) .await } ``` Rozmiar puli połączeń zależy od kilku czynników: liczby rdzeni CPU, charakterystyki obciążenia bazy danych i limitów zasobów. Typowy punkt wyjścia to 2-3 połączenia na rdzeń CPU z dostosowaniem na podstawie metryk. ## Obsługa Błędów i Odporność Produkcyjne aplikacje bazodanowe muszą elegancko obsługiwać różne scenariusze awarii. ```rust use sqlx::Error as SqlxError; #[derive(Debug, thiserror::Error)] enum DatabaseError { #[error("Record not found")] NotFound, #[error("Duplicate entry: {0}")] Duplicate(String), #[error("Connection failed: {0}")] Connection(String), #[error("Query error: {0}")] Query(String), } impl From for DatabaseError { fn from(err: SqlxError) -> Self { match err { SqlxError::RowNotFound => DatabaseError::NotFound, SqlxError::Database(db_err) => { if let Some(code) = db_err.code() { if code == "23505" { return DatabaseError::Duplicate( db_err.message().to_string() ); } } DatabaseError::Query(db_err.message().to_string()) } SqlxError::PoolTimedOut => DatabaseError::Connection( "Connection pool timeout".to_string() ), _ => DatabaseError::Query(err.to_string()), } } } ``` ## Pytania Rekrutacyjne z SQLx Rozmowy techniczne często eksplorują zrozumienie interakcji z bazami danych, bezpieczeństwa typów i cech specyficznych dla SQLx. **Pytanie: Jak SQLx osiąga weryfikację zapytań w czasie kompilacji bez makr proceduralnych modyfikujących runtime?** SQLx używa makr proceduralnych, które łączą się z bazą danych podczas kompilacji. Makro `query!` parsuje ciąg SQL, wysyła go do bazy danych w celu walidacji i generuje kod Rust, który mapuje typy wyników. Ta generacja kodu następuje całkowicie w czasie kompilacji - runtime wykonuje tylko przygotowane instrukcje. **Pytanie: Jakie są kompromisy między SQLx a Diesel dla produkcyjnych aplikacji?** Diesel zapewnia bardziej ergonomiczne budowanie zapytań i lepsza obsługę kompozycji zapytań poprzez system typów. SQLx oferuje bezpośredni dostęp do SQL i lepszą obsługę funkcji specyficznych dla bazy danych. Diesel wymaga mniej boilerplate dla standardowych operacji CRUD, podczas gdy SQLx zapewnia większą kontrolę dla złożonych zapytań i optymalizacji wydajności. **Pytanie: Jak obsługiwać wartości nullable w SQLx z właściwym bezpieczeństwem typów?** ```rust #[derive(FromRow)] struct UserProfile { id: i32, email: String, bio: Option, // Nullable w bazie danych } // Zapytanie automatycznie obsługuje null let profile = sqlx::query_as!( UserProfile, "SELECT id, email, bio FROM user_profiles WHERE id = $1", user_id ) .fetch_one(pool) .await?; ``` **Pytanie: Wyjaśnij różnicę między `fetch_one`, `fetch_optional` i `fetch_all` w SQLx.** `fetch_one` oczekuje dokładnie jednego wiersza i zwraca błąd jeśli zapytanie zwróci zero lub wiele wierszy. `fetch_optional` zwraca `Option`, obsługując przypadki gdzie wiersz może nie istnieć. `fetch_all` zwraca `Vec` dla zapytań z wieloma wynikami. Wybór właściwej metody fetch zapobiega błędom runtime i wyraża intencje w systemie typów. **Pytanie: Jak zaimplementować paginację z SQLx?** ```rust async fn paginated_users( pool: &PgPool, page: i64, per_page: i64, ) -> Result<(Vec, i64), sqlx::Error> { let offset = (page - 1) * per_page; let users = sqlx::query_as!( User, r#"SELECT id, email, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2"#, per_page, offset ) .fetch_all(pool) .await?; let total: (i64,) = sqlx::query_as( "SELECT COUNT(*) FROM users" ) .fetch_one(pool) .await?; Ok((users, total.0)) } ``` ## Porównanie Wydajności i Benchmarki Wydajność SQLx konkuruje z surowymi sterownikami baz danych, ponieważ nie dodaje narzutu abstrakcji w runtime. System typów i generowanie kodu w czasie kompilacji eliminują koszty refleksji obecne w wielu ORM-ach. Dla aplikacji o intensywnym odczycie pule połączeń i prawidłowe użycie przygotowanych instrukcji mają większy wpływ niż wybór biblioteki. Tryb offline SQLx umożliwia całkowite pominięcie parsowania zapytań w runtime. ```rust // Przygotowana instrukcja - parsowana raz, wykonywana wiele razy let stmt = sqlx::query!("SELECT id, email FROM users WHERE active = $1"); for _ in 0..1000 { let users = stmt.clone().bind(true).fetch_all(pool).await?; } ``` ## Podsumowanie SQLx reprezentuje pragmatyczne podejście do dostępu do baz danych w Rust. Sprawdzanie zapytań w czasie kompilacji wyłapuje błędy wcześnie bez utraty kontroli SQL. Asynchroniczne wsparcie integruje się naturalnie z ekosystemem async Rust, a zestaw narzędzi CLI upraszcza zarządzanie migracjami. Dla zespołów komfortowo pracujących z SQL, którzy chcą bezpieczeństwa typów Rust, SQLx zapewnia najlepsze z obu światów. System typów gwarantuje poprawność podczas kompilacji, podczas gdy surowe SQL zachowuje pełny dostęp do możliwości bazy danych. Zrozumienie wzorców prezentowanych w tym przewodniku przygotowuje programistów zarówno do praktycznego użycia jak i dyskusji technicznych na rozmowach kwalifikacyjnych dotyczących architektury baz danych i specyficznych wyborów dla Rust. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/rust/rust-sqlx-compile-time-queries-tutorial