Active Record und Data Mapper in Rust ORMs
Emily Parker
Product Engineer · Leapcell

Verständnis von ORM-Architekturen: Sea-ORM vs. Diesel
Die Programmiersprache Rust, die für ihre Leistung und Speichersicherheit gelobt wird, hat sich in der Backend-Entwicklung rasant durchgesetzt. Mit zunehmender Komplexität von Anwendungen wird die effektive Interaktion mit Datenbanken unerlässlich. Object-Relational Mapper (ORMs) schließen die Lücke zwischen objektorientierten Programmierparadigmen und relationalen Datenbanken und bieten eine ergonomischere Möglichkeit, Daten zu verwalten. Allerdings sind nicht alle ORMs gleich, und ihre zugrunde liegenden architektonischen Philosophien können erheblich beeinflussen, wie Entwickler mit ihnen interagieren. Dieser Artikel befasst sich mit zwei prominenten Rust-ORMs – Sea-ORM und Diesel – und vergleicht ihre Ansätze anhand der Muster Active Record und Data Mapper.
Die Wahl zwischen einem Active Record- und einem Data Mapper-ORM ist mehr als nur eine syntaktische Präferenz; es ist eine Entscheidung, die die Anwendungsstruktur, Testbarkeit und Wartbarkeit beeinflusst. Das Verständnis der Kernprinzipien jedes Ansatzes kann Entwicklern helfen, das am besten geeignete Werkzeug für ihre spezifischen Projektanforderungen auszuwählen. Diese Diskussion zielt darauf ab, diese Architekturmuster im Rust-Ökosystem zu entmystifizieren und praktische Einblicke und Codebeispiele zu liefern, um ihre Unterschiede und Stärken zu veranschaulichen.
Erläuterung der Architekturen
Bevor wir Sea-ORM und Diesel analysieren, wollen wir ein klares Verständnis der beiden grundlegenden ORM-Architekturmuster entwickeln: Active Record und Data Mapper.
Active Record
Das Active Record-Muster, wie von Martin Fowler beschrieben, kapselt sowohl Daten als auch Verhalten in einem einzigen Objekt. Jedes Active Record-Objekt entspricht direkt einer Zeile in einer Datenbanktabelle. Das bedeutet, dass Methoden für die Persistenz (Speichern, Aktualisieren, Löschen) und das Abrufen normalerweise direkt auf dem Entitätsmodell selbst verfügbar sind. Die "Domänenlogik" verbleibt oft direkt innerhalb dieser Modellobjekte.
Schlüsselmerkmale von Active Record:
- Direkte Zuordnung: Eine starke, oft 1:1-Entsprechung zwischen einer Modellklasse und einer Datenbanktabelle.
- Autarke Entitäten: Modellobjekte sind für ihre eigene Persistenz verantwortlich.
- Einfachheit bei CRUD-Operationen: Führt oft zu weniger Boilerplate-Code für grundlegende Datenoperationen.
- Potenzial für Kopplung: Geschäftslogik und Datenzugriffslogik können innerhalb derselben Klasse eng miteinander verknüpft werden.
Data Mapper
Im Gegensatz dazu führt das Data Mapper-Muster eine Abstraktionsebene zwischen dem In-Memory-Objekt und der Datenbank ein. Dieser Mapper, oft eine separate Klasse oder eine Menge von Funktionen, ist für die Übertragung von Daten zwischen dem Objekt und der Datenbank bzw. umgekehrt verantwortlich. Die Domänenobjekte (Entitäten) sind somit davon befreit, etwas über das Datenbankschema oder die Art und Weise, wie sie gespeichert werden, zu wissen.
Schlüsselmerkmale von Data Mapper:
- Trennung der Belange: Klare Unterscheidung zwischen Domänenobjekten und Datenzugriffslogik.
- Persistenzunabhängigkeit: Domänenobjekte enthalten keinen datenbankspezifischen Code.
- Flexibilität: Einfachere Zuordnung komplexer Datenbankschemata zu Objektmodellen und das Austauschen von Persistenzmechanismen.
- Erhöhte Komplexität: Kann mehr Boilerplate-Code erfordern, insbesondere für einfache Anwendungen, aufgrund der zusätzlichen Mappingschicht.
Sea-ORM: Ein Active Record-Ansatz
Sea-ORM (entwickelt vom SeaQL-Team, demselben Team hinter SeaQuery und SeaSchema) verkörpert das Active Record-Muster in Rust. Es bietet eine flüssige API zum Erstellen von Abfragen und zur Interaktion mit der Datenbank, mit einem starken Fokus auf die Ableitung des Datenbankschemas aus Rust-Structs.
Lassen Sie uns dies mit einem einfachen Post-Beispiel veranschaulichen.
// entities/src/post.rs use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "posts")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, pub title: String, pub content: String, pub created_at: DateTimeUtc, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {}
In Sea-ORM repräsentiert die Model-Struct die Daten selbst, während ActiveModel die veränderliche Version ist, die zum Erstellen und Aktualisieren von Datensätzen verwendet wird. Das DeriveEntityModel-Makro generiert viel des Boilerplate-Codes, der für Active Record-Operationen benötigt wird.
Persistenz mit Sea-ORM:
use sea_orm::{ActiveModelTrait, DatabaseConnection, Set}; use super::entities::post; // Angenommen entities/src/post.rs async fn create_and_save_post(db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> { let new_post = post::ActiveModel { title: Set("Mein erster Beitrag".to_owned()), content: Set("Dies ist der Inhalt meines ersten Beitrags.".to_owned()), created_at: Set(chrono::Utc::now()), ..Default::default() // Füllt Standardwerte für andere Felder aus }; let post_result = new_post.insert(db).await?; println!("Erstellter Beitrag: {:?}", post_result); Ok(()) } async fn find_and_update_post(db: &DatabaseConnection, post_id: i32) -> Result<(), sea_orm::DbErr> { let mut post: post::ActiveModel = post::Entity::find_by_id(post_id) .one(db) .await?; .ok_or(sea_orm::DbErr::RecordNotFound("Beitrag nicht gefunden".to_string()))? // Korrigierte Fehlerbehandlung .into_active_model(); post.title = Set("Aktualisierter Titel".to_owned()); post.update(db).await?; println!("Aktualisierter Beitrag mit ID {}: {:?}", post_id, post); Ok(()) }
Beachten Sie, wie die Methoden insert und update direkt auf den ActiveModel-Instanzen aufgerufen werden, was das Active Record-Prinzip demonstriert, bei dem das Objekt selbst über seine Persistenz informiert ist. Sea-ORM bietet eine äußerst ergonomische Möglichkeit, diese Operationen durchzuführen, oft mit weniger Einrichtungsaufwand für einfache CRUD-Fälle.
Anwendungsfälle für Sea-ORM:
- Anwendungen mit unkomplizierten Datenbankschemata und direkter Zuordnung zu Domänenmodellen.
- Prototypen und Anwendungen, bei denen die Entwicklungsgeschwindigkeit für grundlegende Operationen eine hohe Priorität hat.
- Szenarien, in denen eine enge Kopplung zwischen Daten und Verhalten in Objektmodellen akzeptabel oder wünschenswert ist.
Diesel: Ein Data Mapper-Ansatz
Diesel, ein seit langem etabliertes und weit verbreitetes ORM in der Rust-Community, verfolgt das Data Mapper-Muster. Es trennt Ihre Rust-Structs (die Ihre Domänenmodelle repräsentieren) von der Logik für die Interaktion mit der Datenbank. Diesel erreicht dies durch ein leistungsstarkes Makrosystem, das schemabewusste Abfrage-Builder generiert, und ein starkes Typsystem, das die Korrektheit von Abfragen zur Kompilierzeit sicherstellt.
Betrachten wir dasselbe Post-Beispiel in Diesel. Zuerst definieren wir unser Datenbankschema mit dem table!-Makro von Diesel oder über seine Code-Generierungs-Tools (diesel print-schema).
// src/schema.rs (generiert von diesel print-schema) diesel::table! { posts (id) { id -> Int4, title -> Varchar, content -> Text, created_at -> Timestamptz, } }
Als nächstes definieren wir unsere Rust-Struct, die die Post-Entität repräsentiert. Diese Struct ist "persistenzunabhängig".
// src/models.rs use diesel::{Queryable, Insertable}; use chrono::NaiveDateTime; use super::schema::posts; #[derive(Queryable, Debug, PartialEq, Eq)] pub struct Post { pub id: i32, pub title: String, pub content: String, pub created_at: NaiveDateTime, } #[derive(Insertable)] #[diesel(table_name = posts)] pub struct NewPost { pub title: String, pub content: String, pub created_at: NaiveDateTime, }
Beachten Sie, dass die Post- und NewPost-Structs keine Methoden zum Speichern oder Aktualisieren ihrer selbst enthalten. Diese Operationen werden von den Abfrage-Buildern von Diesel gehandhabt.
Persistenz mit Diesel:
use diesel::prelude::*; use diesel::PgConnection; // Oder Ihre Wahl der Datenbank use crate::models::{Post, NewPost}; use crate::schema::posts::dsl::*; use chrono::Utc; fn create_and_save_post(conn: &mut PgConnection) -> Result<Post, diesel::result::Error> { let new_post = NewPost { title: "Mein erster Beitrag".to_owned(), content: "Dies ist der Inhalt meines ersten Beitrags.".to_owned(), created_at: Utc::now().naive_utc(), }; diesel::insert_into(posts) .values(&new_post) .get_result(conn) // Führt die Abfrage aus und gibt das eingefügte Objekt zurück } fn find_and_update_post(conn: &mut PgConnection, post_id: i32) -> Result<Post, diesel::result::Error> { let target_post = posts.filter(id.eq(post_id)); let updated_post = diesel::update(target_post) .set(title.eq("Aktualisierter Titel")) .get_result(conn)?; Ok(updated_post) }
In Diesel sind insert_into und update Funktionen, die eine Datenbankverbindung entgegennehmen und eine Abfrage erstellen. Die Post- und NewPost-Structs repräsentieren strikt die Daten; die Funktionen diesel::insert_into, diesel::update und filter sind die Mapper, die zwischen Ihren Objekten und der Datenbank vermitteln. Diese explizite Trennung bietet mehr Kontrolle und ermöglicht komplexe Abfragen und Mappings.
Anwendungsfälle für Diesel:
- Anwendungen, die eine strikte Trennung der Belange zwischen Domänenlogik und Datenpersistenz erfordern.
- Projekte, bei denen komplexe Abfragen, benutzerdefinierte SQL-Abfragen oder hochoptimierte Datenbankinteraktionen häufig vorkommen.
- Anwendungen, die robuste Kompilierungszeitgarantien für die Korrektheit von Abfragen und Typsicherheit benötigen.
- Beim Erstellen einer großen, wartbaren Codebasis, bei der Testbarkeit und Modularität entscheidend sind.
Kontrastierende Architekturen
| Merkmal | Sea-ORM (Active Record) | Diesel (Data Mapper) |
|---|---|---|
| Philosophie | Objekt weiß, wie es sich selbst persistent macht. | Separater Mapper kümmert sich um Objekt-Datenbank-Übersetzung. |
| Entitätsdesign | Model und ActiveModel für Zustand und Verhalten. | Reine Structs für Daten (persistenzunabhängig). |
| API-Stil | Flüssig, Methodenkette auf Entitätsinstanzen (.insert()). | Abfrage-Builder arbeiten mit Table DSL (diesel::insert_into()). |
| Kopplung | Höhere Kopplung zwischen Objekt und Persistenz. | Geringe Kopplung; Domänenobjekte sind unabhängig von der Persistenz. |
| Boilerplate | Weniger für grundlegendes CRUD durch Makroableitung auf Entität. | Mehr für grundlegendes CRUD, erlaubt aber feingranulare Kontrolle. |
| Testen | Kann schwieriger sein, Domänenlogik isoliert zu testen. | Domänenlogik einfacher unabhängig von der DB zu testen. |
| Abfrageflexibilität | Gut für gängige Abfragen, kann rohes SQL verwenden. | Hochgradig flexibel, robuster Abfrage-Builder, unterstützt benutzerdefiniertes SQL. |
| Schemadefinition | Abgeleitet von Rust-Structs. | Definiert durch table!-Makro oder print-schema (Datenbank-first). |
| Kompilierungszeit-Prüfungen | Fokus auf Entitätsgültigkeit. | Starke Kompilierungszeit-Prüfungen für Abfragekorrektheit und Typen. |
Fazit
Sowohl Sea-ORM als auch Diesel bieten überzeugende Lösungen für die Datenbankinteraktion in Rust, die jeweils für unterschiedliche Vorlieben und Projektanforderungen optimiert sind. Sea-ORM mit seinem Active Record-Muster vereinfacht grundlegende CRUD-Operationen, indem es Persistenzlogik direkt in das Modell einbettet, was es zu einer ausgezeichneten Wahl für die schnelle Entwicklung und Anwendungen mit unkomplizierten Datenmodellen macht. Diesel verfolgt durch die Übernahme des Data Mapper-Musters einen robusten, hochgradig typsicheren und entkoppelten Ansatz, der sich ideal für komplexe Anwendungen eignet, die feingranulare Kontrolle über Datenbankinteraktionen, umfangreiche benutzerdefinierte Abfragen und eine strenge Trennung der Belange erfordern.
Die endgültige Wahl hängt von der Skalierung, Komplexität Ihres Projekts und den architektonischen Präferenzen Ihres Teams ab. Ob Sie die inhärente Einfachheit von Active Record-Entitäten oder die leistungsstarke Abstraktion von Data Mappern suchen, das ORM-Ökosystem von Rust bietet ausgereifte und fähige Optionen, um leistungsstarke und zuverlässige Anwendungen aufzubauen.

