ORM, SQL und Driver Crates für Rust

Eine (nicht vollständige) Liste von #Rust #Crates, die auf Persistenzen zugreifen und den Zugriff abstrahieren. Es werden #DB Treiber, #SQL Mapping und #ORM Tools beschrieben.

Einleitung

Dieser Blogartikel ist dafür gedacht, eine fortlaufende Liste an Crates zu beschreiben, die Zugriffe auf Persistenzen erlauben. Die Liste wird über die Zeit aktualisiert.


Treiber

PostgreSQL

Postgres ist eine sehr beliebte RDBMS Datenbank mit objektrelationalen Prinzip (inklusive Table Inheritence und Function overloading). Dazu erlaubt Postgres die Unterstützung von komplexen Datentypen und bietet dafür eine sehr große Anzahl von Erweiterungen. Postgres ist Multi-Prozess-fähig (was aber mehr Speicherverbrauch bedeutet) und hat hohe Performance in Lesend- und Schreibzugriffen, die durch eine (nicht zu verändernde) ACID Storage Engine realisiert wird. Postgres ist sehr gut skalierbar und eignet ich hervorragend auch für den Enterprise Bereich. Der SQL Standard wird ausgezeichnet eingehalten.

SQLite

SQLite ist eine prozessinterne Datenbank (damit ist ein Client/Server Szenario nicht möglich) mit SQL-Unterstützung. Sie gehört inzwischen zu den meistverbreiteten RDBMS, insbesondere wegen ihrer Nutzung im Android Bereich als Standard-Persistenz für Apps. Aber auch in der Entwicklung ist SQLite sehr beliebt, weil sie In-Memory arbeiten kann. Damit ist sie ideal für Testumgebungen und Integrationstests. Sie wird auch immer dann eingesetzt, wenn Applikationen einfach mit einer relationalen DB ausgestattet werden sollen, wo ein Client/Server Szenario nicht benötigt wird.

MySQL

MySQL ist eine sehr beliebte RDBMS Datenbank und in vielen Anwendungen immer noch die erste Wahl für die meisten Anwendungen, die eine SQL-Datenbank verwenden und Client/Server Szenarien benötigen. Zwar geht der Trend deutlich in Richtung von Postgres, aber als Single-Process DB und auch die Möglichkeit, diese In-Memory zu betreiben, macht sie für einige Szenarien attraktiver. MySQL ist stark optimiert auf Read-Access. Wird eine ausgewogene Performance für Read/Write benötigt, sollte man auf Postgres ausweichen. MySQL unterstützt aber eine hohe Anzahl von Storage Engines, die für viele Anwendungsszenarien individuell eingerichtet werden können.

MongoDB

MongoDB ist eine Datenbank, die auf einem nicht relationalem Dokumentmodell basiert und somit unter die schemafreien, dokumentenorientierten No-SQL Datenbanken fällt. Die Dokumente werden als JSON strukturiert.

MS SQL

Der Microsoft SQL Server ist ein relationales Datenbankmanagementsystem von Microsoft.

Oracle

Oracle Database (auch Oracle Database Server, Oracle RDBMS) ist eine Datenbankmanagementsystem-Software des Unternehmens Oracle.

Redis

Redis ist eine In-Memory-Datenbank, die für die Erstellung von Caches, Worker Queues und Microservices verwendet werden kann. Das Redis-Crate bietet sowohl High-Level- als auch Low-Level-APIs. Alle Abfragen sind pipelined, was bedeutet, dass mehrere Abfragen gleichzeitig gesendet werden können.

LevelDB

LevelDB wurde von Google entwickelt und ist ein reiner Single-Process Key-Value-Speicher.

MemCache

Memcached ist ein freies, quelloffenes, hochleistungsfähiges Caching-System für verteilte Speicherobjekte. memcache ist ein in reinem Rust geschriebener Memcached-Client. Er unterstützt mehrere Instanzen von Memcached. Einige Funktionen, einschließlich der automatischen JSON-Serialisierung und Komprimierung, sind noch nicht im Rust-Treiber verfügbar.

Cassandra / ScyllaDB

Cassandra ist eine verteilte, skalierbare NoSQL-Datenbank. crdrs ist ein Datenbanktreiber für Cassandra und ScyllaDB.

ElasticSearch

ElasticSearch ist im Prinzip keine Datenbank, sondern ein invertierter Suchindex, basierend auf JSON Dokumenten.

OpenSearch

OpenSearch ist ein Fork von ElasticSearch und ist damit auch keine Datenbank, sondern ein invertierter Suchindex, basierend auf JSON Dokumenten.

ODBC

Die ODBC-Schnittstelle (Microsoft Open Database Connectivity) ist eine C-Programmiersprachenschnittstelle, mit der Anwendungen auf Daten aus einer Vielzahl von Datenbankverwaltungssystemen (Database Management Systems, DBMS) zugreifen können. ODBC ist eine Low-Level-Schnittstelle, die speziell für RDBMS entwickelt wurde.

ODBC Treiber greifen also nicht auf eine spezifische Datenbank zu, sondern bieten eine API für ODBC-fähige Datenbanken.


SQL Abstraktionen

Cornucopia (cornucopia)

Cornucopia ist ein kleines CLI-Programm, das auf tokio-postgres aufbaut, um PostgreSQL-Workflows in Rust zu erleichtern.

Cornucopia wandelt PostgreSQL-Abfragen in Rust Code um. Jede Abfrage wird gegen das Schema vorbereitet, um sicherzustellen, dass die Statements gültiges SQL sind. Diese Prepared Statements werden dann verwendet, um typ-geprüften Rust-Code für die Abfragen zu erzeugen.

Das Framework ist kein OR-Mapper, sondern ein Sourcecode Generator für SQL Statements.

Hat man folgendes Schema:

CREATE TABLE Author (
    Id SERIAL NOT NULL,
    Name VARCHAR(70) NOT NULL,
    Country VARCHAR(100) NOT NULL,
    PRIMARY KEY(Id)
);

Und diese annotierte SQL Anweisung:

--! authors()*
SELECT * FROM Author;

generiert sich folgende Rust Funktion:

pub async fn authors<T: GenericClient>(client: &T) -> Result<Vec<(i32, String, String)>, Error> {
    let stmt = client
        .prepare(
"SELECT
*
FROM
Author;
",
        )
        .await?;
    let res = client
        .query_raw(&stmt, std::iter::empty::<i32>())
        .await?
        .map(|res| {
            res.map(|res| {
                let return_value_0: i32 = res.get(0);
                let return_value_1: String = res.get(1);
                let return_value_2: String = res.get(2);
                (return_value_0, return_value_1, return_value_2)
            })
        })
        .try_collect()
        .await?;
    Ok(res)
}

Damit wird eine Menge Boilerplate Implementierung an den Code-Generator ausgelagert und in der Entwicklung muss man sich nicht mehr damit rumschlagen.

Bewertung

Cornucopia fokussiert sich auf die Codegenerierung durch annotierte SQL Statements und macht dies ausschließlich für tokio-postgres. Vorteil ist, dass man keine Makros hat und der generierte Code exakt zum DB Schema passt, Async Tokio von Haus aus beherrscht und Connection Pooling via deadpool-postgress einbindet. Damit ist es nur für ganz bestimmte Lösungen geeignet, erfreut sich aber einer begeisterten Anhängerschaft.

Fähigkeiten

  • Treiber
    • Tokio PostgreSQL (Rust safe)
  • Async
  • TLS/SSL
    • Über den Tokio PostgreSQL Treiber

Ressourcen

SQLx

SQLx ist eine SQL Abstraktions-Bibliothek, um sichere SQL Abfragen, unabhängig der darunter liegenden Datenbank zu schreiben. Typischerweise sind die Abfragen laufzeit-dynamisch, da alle Bezeichner als Strings deklariert werden. SQLx überprüft nicht die Syntax der generierten SQL Statements. D.h. es ist durchaus möglich, fehlerhaften SQL mit SQLx zu generieren. Applikationen mit SQLx benötigen deswegen eine optimale Testabdeckung. Zusätzlich hat SQLx eine Statement Verifikation zur Compiler-Zeit.

Die Abstraktion ist sehr Low-Level, wie das folgende Beispiel zeigt:

#[derive(sqlx::FromRow)]
struct User { name: String, id: i64 }

let mut stream = sqlx::query_as::<_, User>(
        "SELECT * FROM users WHERE email = ? OR name = ?")
    .bind(user_email)
    .bind(user_name)
    .fetch(&mut conn);

Bewertung

SQLx ist ein ausgezeichnetes Framework zur flexiblen Erzeugung von SQL Statements, das kaum einschränkt und dazu eine große Treiberunterstützung (sogar mit zwei Rust Treibern) bietet sowie Async und TLS nutzen kann. Jenseits der Verifikation während des Compile-Vorgangs ist man aber auf sich und die eigene Testabdeckung angewiesen. Benötigt man eine zusätzliche Abstraktion, solle man sich SeaQuery oder gar SeaORM anschauen, die beide auf SQLx basieren.

Fähigkeiten

  • Treiber
    • SQlite
    • PostgreSQL (Rust safe)
    • MySQL (Rust safe)
    • MS SQL
  • Async
  • Transport Layer Security
    • Für Postgres und MySQL

Ressourcen

SeaQL / SeaQuery

SeaQuery ist ein dynamischer Query Builder, der SQLx nutzt. Damit bietet SeaQuery eine sichere Typisierung, als es SQLx bietet. Die Bezeichner der SQL Elemente werden über Enums mit Traits implementiert, indem die Enums zu den String-Bezeichnern mit pattern matching zugewiesen werden (was über ein derive automatisiert werden kann). Der QueryBuilder arbeitet dann nicht mehr mit Strings, sondern nur mit den Enums, die Anwendern schon zur Compile-Zeit eine Fehlerprüfung auf korrekte Bezeichner bietet.

Eine Abfrage sieht dann so aus:

assert_eq!(
    Query::select()
        .column(Glyph::Id)
        .from(Glyph::Table)
        .cond_where(
            Cond::any()
                .add(
                    Cond::all()
                        .add(Expr::col(Glyph::Aspect).is_null())
                        .add(Expr::col(Glyph::Image).is_null())
                )
                .add(
                    Cond::all()
                        .add(Expr::col(Glyph::Aspect).is_in(vec![3, 4]))
                        .add(Expr::col(Glyph::Image).like("A%"))
                )
        )
        .to_string(PostgresQueryBuilder),
    [
        r#"SELECT "id" FROM "glyph""#,
        r#"WHERE"#,
        r#"("aspect" IS NULL AND "image" IS NULL)"#,
        r#"OR"#,
        r#"("aspect" IN (3, 4) AND "image" LIKE 'A%')"#,
    ]
    .join(" ")
);

Bewertung

Wenn man sich nicht mit Schreibfehlern an zig verschiedenen Stellen im Sourecode rumschlagen will, ist SeaQuery eine sehr gute Abstraktion für SQLx. Damit wird die Deklaration der Bezeichner an einem zentralen Punkt der Schema-Definition festgelegt und man umgeht die fehlerträchtigen String Konstanten. Mit dem Iden-derive-Makro wird das Mapping der Bezeichner sogar automatisiert. Ansonsten kann man mit SeaQuery alles machen, was man von SQLx gewohnt ist. Die zusätzliche Indirektion über die Enums ist zwar etwas aufwendiger, aber der Gewinn an korrekten Code ist damit aufgewogen. Aber auch für SeaQuery gilt, dass keinerlei Prüfung gegen echte DB Schemata gemacht wird. D.h., was als Deklaration festgelegt wird, muss nichts mit dem DB-Schema zur Laufzeit zu tun haben. Es erlaubt eine hohe Flexibilität, fordert aber auch weiterhin eine umfangreiche Testabdeckung.

Fähigkeiten

  • Treiber
    • sqlx-mysql (Rust safe)
    • sqlx-postgres (Rust safe)
    • sqlx-sqlite
    • postgres
    • postgres-*
    • rusqlite

Ressourcen


Object Relational Modeling (ORM)

Es gibt nicht viele Multi-DB ORM Crates, aber die wenigen lassen sich gut produktiv einsetzen und sind auch keine Konkurrenz zueinander, sondern bilden ganz eigene Paradigmen ab.

Ansonsten gibt es Projekte, wie Sand am Meer, die für spezifische Datenbanken ORM-Funktionalitäten anbieten. Diese habe ich hier aber nicht betrachtet.

Diesel

Diesel ist ein statisches ORM Builder System, das darauf abzielt, schon zur Compiler-Zeit ein vollständiges OR-Mapping durchzuführen. Die Makros sind auf typische Spaltenzahlen für Tabellen konfigurierbar. Wenn man z.B. mehr als 32 Spalten benötigt, muss man die Konfiguration anpassen, was aber auch die Compiler-Zeit verlangsamt.

Spalten werden als Structs modelliert und erhalten Traits, um die Predicates zu deklarieren.

Abfragen in Diesel bieten eine gute Abstraktion:

let versions = Version::belonging_to(krate)
  .select(id)
  .order(num.desc())
  .limit(5);
let downloads = version_downloads
  .filter(date.gt(now - 90.days()))
  .filter(version_id.eq(any(versions)))
  .order(date)
  .load::<Download>(&conn)?;

Bewertung

Man findet wohl Diesel in fast jedem Softwareprojekt, dass Persistierung in den drei meistgenutzten RDBMS Systemen durchführt. Damit gibt es eine Menge Beispielcode, eine sehr stabile Codebasis und das statische OR Mapping ist zur Laufzeit unschlagbar schnell. Das erkauft man sich mit einigen fehlenden Features (wie beispielsweise Async) und ggf. eine lange Compiler-Zeit.

Diesel eignet sich besonders für überschaubare Datenmodelle, die primär mit CRUD Aktionen auskommen und es einfache, normalisierte Relationen gibt.

Fähigkeiten

  • Treiber
    • PostgreSQL
    • MySQL
    • SQLite
  • Async
    • Nein (Experimentell via https://github.com/weiznich/diesel_async - mit tokio postgres)
  • Transport Layer Security (TLS)
    • Nein

Ressourcen

SeaORM

SeaORM ist jünger als Diesel und basiert auf SeaQuery und SQLx. Damit ist SeaORM ein dynamisches ORM System, dass zur Laufzeit Objekt-Relationen und das Mapping generiert. Wenn man mehr Flexibilität benötigt, kann man SeaQuery benutzen, welches eine SQL Abstraktion bietet, die nicht an ein OR Mapping typisiert ist.

SeaORM nutzt Enums, um Spalten zu modellieren.

SeaORM und SeaQuery können gemeinsam genutzt werden.

Bewertung

Man kann SeaORM nicht wirklich mit Diesel vergleichen, da sie unterschiedliche Ansätze verfolgen. Wenn aber einen schnellen Compilerlauf für sehr große Datenmodelle benötigt und man zudem (eben aufgrund des großen Datenmodells) eine hohe Flexibilität an Abfragemöglichkeiten benötigt, wenn man nicht mit einem statischen Datenmodell arbeiten kann, dann ist SeaORM unschlagbar. Das erkauft man sich mit potenziellen Laufzeitfehlern, die man gut mit Tests abdecken sollte. Auch da bietet SeaORM eine gute Unterstützung. Überdies bietet SeaORM von Haus aus eine hervorragende Dokumentation.

SeaORM ist sinnvoll für große Datenbank-Schemata, mit sehr vielen Tabellen und Attributen, die mit komplexen Abfragen verknüpft werden müssen. Ist das ORM noch zu statisch, kann mit SeaQuery jedes erdenkliche SQL Statement gebaut werden. SeaORM ist alternativlos, wenn man mit der gleichen Applikation auf unterschiedlichen Schemata arbeiten will.

Fähigkeiten

  • Treiber
    • SQlite (via sqlx-sqlite)
    • PostgreSQL (via sqlx-postgres, Rust safe)
    • MySQL (via sqlx-mysql, Rust safe)
    • MS SQL - Unbekannt, SQLx unterstützt es, in der SeaORM Doku wird es nicht erwähnt
  • Async
  • Transport Layer Security (TLS)
    • Für Postgres und MySQL (über SQLx)

Ressourcen

rustorm

Das Framework hat sich auf das Data Access Object Mapping von SQL Ergebnissen spezialisiert und unterstützt damit die Übertragung der SQL Resultsets in Structs. Binding von Abfrage-Parameter besteht wohl, aber es gibt keine Beispiele.

Bewertung

Die SQL Abfragen sind sehr Low-Level. Ein Guide oder umfangreiche Dokumentation gibt es nicht, aber die Möglichkeiten sind sehr fokussiert, sodass man sich schnell einarbeiten kann. Transaktionen werden wohl nicht unterstützt. Für größere Projekte ist rustorm eher nicht zu empfehlen.

Fähigkeiten

  • Treiber
    • SQlite
    • PostgreSQL
    • MySQL

Ressourcen