Diesel und SQLx: Ein tiefer Einblick in Rust ORMs
Lukas Schneider
DevOps Engineer · Leapcell

Einleitung
In der Welt der Webentwicklung und datengesteuerten Anwendungen spielen Object-Relational Mapper (ORMs) eine entscheidende Rolle bei der Überbrückung der Kluft zwischen objektorientierten Programmiersprachen und relationalen Datenbanken. Sie abstrahieren die Komplexität von rohen SQL-Abfragen und ermöglichen es Entwicklern, mit ihren Datenbanken über gewohnte Sprachkonstrukte zu interagieren. Rust, mit seinem starken Fokus auf Sicherheit, Leistung und Nebenläufigkeit, hat einen Aufschwung an hochentwickelten ORM-Lösungen erlebt. Unter diesen stechen Diesel und SQLx als herausragende Optionen hervor, die jeweils einen eigenständigen Ansatz zur Gewährleistung der Datenintegrität und Entwicklerproduktivität bieten. Dieser Artikel taucht tief in diese beiden leistungsstarken Rust-ORMs ein, untersucht ihre Kernphilosophien, Implementierungsdetails und praktischen Auswirkungen und bietet so ein umfassendes Verständnis ihrer jeweiligen Stärken und Anwendungsfälle.
Kernkonzepte
Bevor wir uns den Besonderheiten von Diesel und SQLx widmen, wollen wir einige grundlegende Begriffe festlegen, die für das Verständnis ihrer Funktionsweise entscheidend sind:
- ORM (Object-Relational Mapper): Ein Programmierwerkzeug, das ein Datenbankschema auf ein objektorientiertes Paradigma abbildet und es Entwicklern ermöglicht, Datenbankeinträge als Objekte in ihrer Programmiersprache zu manipulieren.
- Query Builder: Eine Bibliothek oder eine Komponente, die beim programmatischen Erstellen von SQL-Abfragen hilft und oft eine API bereitstellt, die der Struktur von SQL ähnelt.
- Schema Migration: Der Prozess der Entwicklung eines Datenbankschemas im Laufe der Zeit, um Änderungen am Datenmodell einer Anwendung zu berücksichtigen.
- Compile-Time Checks (Kompilierzeitprüfungen): Überprüfungen, die vom Compiler während des Kompilierungsprozesses durchgeführt werden und die Korrektheit und Sicherheit des Codes vor der Ausführung gewährleisten. Dies ist ein Kernprinzip von Rust.
- Makros: Code-Generierungsmechanismen in Rust, die es Entwicklern ermöglichen, Code zu schreiben, der anderen Code schreibt. Sie können prozedural (wie
proc-macros) oder deklarativ sein und werden oft für Metaprogrammierungsaufgaben verwendet.
Diesel: Compile-Time-Garantien durch das Typsystem
Diesel ist ein leistungsstarker und sehr meinungsstarker ORM, der das robuste Typsystem von Rust nutzt, um Compile-Time-Garantien für die Korrektheit Ihrer SQL-Abfragen zu bieten. Ziel ist es, häufige Datenbankfehler, wie Tippfehler in Spaltennamen oder Typenkonflikte, zu vermeiden, bevor Ihre Anwendung überhaupt ausgeführt wird.
Wie Diesel funktioniert
Diesel erreicht seine Compile-Time-Prüfungen hauptsächlich durch seinen Query Builder und sein Schema-Management. Sie definieren Ihr Datenbankschema in Rust, typischerweise über eine schema.rs-Datei, die mit dem Befehl diesel print-schema generiert wird. Diese Schemadatei enthält Rust-Typen, die Ihre Datenbanktabellen und Spalten widerspiegeln. Wenn Sie Abfragen mit der API von Diesel erstellen, stellt der Rust-Compiler sicher, dass Ihre Operationen mit diesem definierten Schema übereinstimmen.
Beispiel: Schema definieren und mit Diesel abfragen
Nehmen wir zuerst an, wir haben eine posts-Tabelle in unserer Datenbank:
CREATE TABLE posts ( id SERIAL PRIMARY KEY, title VARCHAR NOT NULL, body TEXT NOT NULL, published BOOLEAN NOT NULL DEFAULT FALSE );
Mit diesel print-schema erhalten wir etwas Ähnliches in src/schema.rs:
// @generated automatically by Diesel CLI. diesel::table! { posts (id) { id -> Int4, title -> Varchar, body -> Text, published -> Bool, } }
Nun schreiben wir einige Diesel-Code, um diese Tabelle abzufragen:
use diesel::prelude::*; use diesel::PgConnection; // Oder welche Datenbank Sie auch immer verwenden #[derive(Queryable, Selectable)] #[diesel(table_name = crate::schema::posts)] pub struct Post { pub id: i32, pub title: String, pub body: String, pub published: bool, } pub fn establish_connection() -> PgConnection { let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); PgConnection::establish(&database_url) .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)) } pub fn get_posts() -> Vec<Post> { use crate::schema::posts::dsl::*; let mut connection = establish_connection(); posts .filter(published.eq(true)) .limit(5) .select(Post::as_select()) .load::<Post>(&mut connection) .expect("Error loading posts") } fn main() { let published_posts = get_posts(); for post in published_posts { println!("Title: {}", post.title); } }
In diesem Beispiel:
- Das Makro
#[derive(Queryable, Selectable)]hilft bei der Abbildung von Datenbankzeilen auf Rust-Strukturen. posts.filter(published.eq(true))wird zur Kompilierzeit typgeprüft. Wenn Sie versuchen würdenposts.filter(non_existent_column.eq(true)), würde der Compiler sofort einen Fehler anzeigen, danon_existent_columnnicht Teil derposts-Tabellendefinition inschema.rsist.select(Post::as_select())stellt sicher, dass die ausgewählten Spalten mit den Feldern in derPost-Struktur übereinstimmen.
Anwendungsfälle für Diesel
Diesel eignet sich hervorragend für Anwendungen, bei denen:
- Starke Compile-Time-Garantien oberste Priorität haben: Das frühzeitige Erkennen von datenbankbezogenen Fehlern im Entwicklungszyklus ist entscheidend.
- Komplexe Abfragen üblich sind: Der typsichere Query Builder hilft bei der Verwaltung komplexer SQL-Logik.
- Das Datenbankschema relativ stabil ist: Häufige Schemaänderungen können umständlich sein, da
schema.rsneu generiert werden muss. - Leistung ein wichtiger Faktor ist: Diesel generiert effizientes SQL, das oft mit handgeschriebenen Abfragen vergleichbar ist.
SQLx: Compile-Time-Makros für rohes SQL
SQLx verfolgt einen anderen, aber ebenso leistungsstarken Ansatz zur Compile-Time-Sicherheit. Anstatt sich auf ein generiertes Schema zu verlassen, verwendet es prozedurale Makros, um sich während der Kompilierung mit einer live Datenbank zu verbinden und Ihre rohen SQL-Abfragen zu validieren. Das bedeutet, Sie schreiben reines SQL, aber SQLx stellt dessen Korrektheit sicher.
Wie SQLx funktioniert
SQLx erreicht seine Magie durch das sql!-Makro. Wenn Sie dieses Makro verwenden, verbindet sich SQLx mit der von Ihrer DATABASE_URL angegebenen Datenbank (die zur Kompilierzeit verfügbar sein muss), führt die SQL-Abfrage im "Dry Run"-Stil aus und leitet die Eingabeparameter und Ausgabetypen ab. Wenn ein Syntaxfehler in Ihrem SQL, ein Abgleichfehler bei erwarteten Spalten oder ein falscher Parametertyp vorliegt, meldet der Compiler dies.
Beispiel: Abfragen mit SQLx
Verwenden wir dasselbe Beispiel der posts-Tabelle.
use sqlx::{PgPool, FromRow, postgres::PgPoolOptions}; use dotenvy::dotenv; #[derive(Debug, FromRow)] pub struct Post { pub id: i32, pub title: String, pub body: String, pub published: bool, } pub async fn establish_connection() -> PgPool { dotenv().ok(); let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); PgPoolOptions::new() .max_connections(5) .connect(&database_url) .await .expect("Failed to connect to Postgres.") } pub async fn get_posts_sqlx() -> Result<Vec<Post>, sqlx::Error> { let pool = establish_connection().await; // Hier passiert die Magie! let posts = sqlx::query_as!( Post, "SELECT id, title, body, published FROM posts WHERE published = $1 LIMIT $2", true, // $1 5_i64 // $2, Typ ist wichtig für sqlx ) .fetch_all(&pool) .await?; Ok(posts) } #[tokio::main] async fn main() { match get_posts_sqlx().await { Ok(posts) => { for post in posts { println!("Title: {}", post.title); } } Err(e) => { eprintln!("Error fetching posts: {:?}", e); } } }
In diesem SQLx-Beispiel:
- Das
sqlx::query_as!-Makro nimmt rohes SQL als erstes Argument. - Während der Kompilierung verbindet sich SQLx mit Ihrer Datenbank, validiert die
SELECT-Anweisung und überprüft, ob die Spaltennamen und -typen mit denen in derPost-Struktur übereinstimmen. - Wenn Sie eine nicht existierende Spalte einfügen, z. B.
SELECT non_existent_column FROM posts, gibt der Compiler einen Fehler wie "column "non_existent_column" does not exist" aus. - Es prüft auch die Parametertypen. Wenn Sie einen
Stringübergeben, wo eini64für$2erwartet wird, fängt der Compiler dies ab. - SQLx behandelt optional nullable Spalten implizit mit
Option<T>in Ihrer Rust-Struktur.
Anwendungsfälle für SQLx
SQLx glänzt in Szenarien, in denen:
- Entwickler bevorzugen das Schreiben von rohem SQL: Volle Kontrolle über die Abfragenoptimierung und komplexe SQL-Funktionen (z. B. CTEs, Window Functions) gewünscht wird.
- Bestehende SQL-Abfragen integriert werden müssen: Einfacheres Portieren bestehender SQL-Codebasen.
- Asynchrone Operationen eine erstklassige Bürger sind: SQLx ist mit
async/awaitim Hinterkopf erstellt und passt daher gut zu nebenläufigen Anwendungen. - Schemaänderungen häufig oder dynamisch sind: Keine Notwendigkeit, Schemadateien neu zu generieren, da SQLx direkt gegen die live Datenbank validiert.
- Minimale ORM-Abstraktion bevorzugt wird: SQLx fungiert eher als typsichere Query Builder mit Compile-Time-Validierung als ein Full-Fledged ORM, der versucht, SQL zu verbergen.
Fazit
Sowohl Diesel als auch SQLx bieten überzeugende Lösungen für die Datenbankinteraktion in Rust, die jeweils leicht unterschiedliche Präferenzen und Projektanforderungen erfüllen. Diesel priorisiert mit seinen Compile-Time-Prüfungen über ein generiertes Schema die Typsicherheit und eine hochgradig idiomatische Rust-API für den Abfrageaufbau, was es ideal für robuste Anwendungen macht, bei denen Schema-Stabilität und solide Abstraktionen geschätzt werden. SQLx hingegen umarmt rohes SQL und nutzt Compile-Time-Makros, um ein ebenso starkes Sicherheitsnetz zu bieten, das unübertroffene Flexibilität und Kontrolle über Datenbankabfragen bietet und sich besonders gut für Async-Anwendungen und reine SQL-Enthusiasten eignet. Die Wahl zwischen ihnen beruht oft auf einem Kompromiss zwischen dem gewünschten Grad der ORM-Abstraktion und der bevorzugten Methode zur Gewährleistung der Abfragekorrektheit, aber beide heben zweifellos den Standard der Datenbankprogrammierung in Rust an.

