SQLC vs GORM – Zwei Ansätze zur Datenbankinteraktion in Go
Wenhao Wang
Dev Intern · Leapcell

Einleitung
Die Interaktion mit Datenbanken ist das Fundament fast jeder ernsthaften Anwendung, und Go hat sich mit seiner starken Typisierung und Leistungsmerkmalen zu einer beliebten Wahl für den Aufbau solcher Systeme entwickelt. Bei der Arbeit mit relationalen Datenbanken in Go entscheiden sich Entwickler oft zwischen zwei breiten Kategorien von Werkzeugen: denen, die rohe SQL-Abfragen und Code-Generierung betonen, und denen, die eine objektrelationale Abbildungsschicht (ORM) anbieten. Dieser Artikel befasst sich mit zwei prominenten Beispielen aus diesen Kategorien: SQLC und GORM. Wir werden ihre unterschiedlichen Philosophien, praktischen Implementierungen und geeigneten Anwendungsfälle untersuchen, um ein klares Verständnis dafür zu vermitteln, wie jeder die Herausforderung der Datenbankinteraktion in Go angeht.
Verständnis der Philosophien: SQLC und GORM
Bevor wir auf die Einzelheiten eingehen, wollen wir ein gemeinsames Verständnis der zugrunde liegenden Kernkonzepte schaffen.
SQLC (SQL Code Generation): SQLC ist ein Werkzeug, das Go-Code aus rohen SQL-Abfragen generiert. Seine Philosophie basiert auf der Überzeugung, dass die deklarative Natur von SQL mächtig ist und direkt genutzt werden sollte. Durch das Schreiben von SQL-Abfragen generiert SQLC dann automatisch typsicheren Go-Code für die Ausführung dieser Abfragen und das Scannen der Ergebnisse in Go-Structs. Dieser Ansatz priorisiert explizites SQL, Typsicherheit zur Kompilierzeit und minimale Abstraktion über Datenbankoperationen.
GORM (Go ORM - Object-Relational Mapping): GORM ist dagegen eine vollwertige ORM-Bibliothek für Go. Seine Philosophie konzentriert sich auf die Abbildung von Datenbanktabellen auf Go-Structs und bietet eine High-Level-API für die Interaktion mit der Datenbank unter Verwendung von Go-Idiomen anstelle von rohem SQL. GORM zielt darauf ab, das zugrunde liegende SQL zu abstrahieren, sodass Entwickler Daten als Go-Objekte manipulieren können. Dieser Ansatz priorisiert Entwicklerfreundlichkeit, schnelle Entwicklung und eine objektorientierte Sicht auf die Datenbank.
Lassen Sie uns diese Unterschiede anhand praktischer Beispiele veranschaulichen.
SQLC: Explizites SQL und Code-Generierung nutzen
Der Workflow von SQLC beinhaltet das Schreiben Ihres SQL-Schemas und Ihrer Abfragen in .sql-Dateien. SQLC verarbeitet diese Dateien dann zur Generierung von Go-Code.
Beispiel: Definieren eines Schemas und einer Abfrage mit SQLC
Definieren Sie zuerst Ihr SQL-Schema (z. B. schema.sql):
CREATE TABLE authors ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, bio TEXT );
Definieren Sie als Nächstes Ihre SQL-Abfragen (z. B. query.sql):
-- name: GetAuthor :one SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1; -- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name; -- name: CreateAuthor :one INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id, name, bio; -- name: UpdateAuthor :one UPDATE authors SET name = $1, bio = $2 WHERE id = $3 RETURNING id, name, bio; -- name: DeleteAuthor :exec DELETE FROM authors WHERE id = $1;
Nachdem Sie sqlc generate ausgeführt haben, erstellt SQLC Go-Dateien (z. B. db.go, models.go, query.sql.go). Hier ist ein Ausschnitt des generierten query.sql.go:
// Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.25.0 // source: query.sql package db import ( "context" ) const createAuthor = `-- name: CreateAuthor :one INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id, name, bio ` type CreateAuthorParams struct { Name string `json:"name"` Bio *string `json:"bio"` } func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) var i Author err := row.Scan( &i.ID, &i.Name, &i.Bio, ) return i, err } const getAuthor = `-- name: GetAuthor :one SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1 ` func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { row := q.db.QueryRowContext(ctx, getAuthor, id) var i Author err := row.Scan( &i.ID, &i.Name, &i.Bio, ) return i, err } // ... andere generierte Funktionen
Und so würden Sie es in Ihrer Anwendung verwenden:
package main import ( "context" "database/sql" "fmt" _ "github.com/lib/pq" // PostgreSQL-Treiber "your-project/db" // Angenommen, das db-Paket ist dort, wo sqlc Code generiert ) func main() { connStr := "user=postgres password=password dbname=sqlc_example sslmode=disable" conn, err := sql.Open("postgres", connStr) if err != nil { panic(err) } defer conn.Close() queries := db.New(conn) ctx := context.Background() // Einen Autor erstellen newAuthor, err := queries.CreateAuthor(ctx, db.CreateAuthorParams{ Name: "Jane Doe", Bio: sql.NullString{String: "Eine talentierte Schreiberin", Valid: true}, }) if err != nil { panic(err) } fmt.Printf("Erstellt Autor: %+v\n", newAuthor) // Einen Autor abrufen author, err := queries.GetAuthor(ctx, newAuthor.ID) if err != nil { panic(err) } fmt.Printf("Abgerufener Autor: %+v\n", author) // Autoren auflisten authors, err := queries.ListAuthors(ctx) if err != nil { panic(err) } fmt.Printf("Alle Autoren: %+v\n", authors) }
Die Hauptvorteile von SQLC sind:
- Typsicherheit zur Kompilierzeit: SQL-Fehler werden zur Kompilierungszeit und nicht zur Laufzeit erkannt.
- Keine Laufzeitreflexion: Der generierte Code ist reines Go, was zu einer hervorragenden Leistung führt.
- Direkte SQL-Kontrolle: Die volle Leistung von SQL ist verfügbar, einschließlich komplexer Joins, Fensterfunktionen usw.
- Weniger Boilerplate-Code: Das Scannen von Ergebnissen in Structs wird automatisch behandelt.
Er erfordert jedoch einen zusätzlichen generate-Schritt und kann sich für einfache CRUD-Operationen umständlicher anfühlen.
GORM: Objektrelationale Abbildung für Go
GORM verfolgt einen anderen Ansatz und ermöglicht es Ihnen, Go-Structs zu definieren, die direkt auf zugeordnete Datenbanktabellen abgebildet werden.
Beispiel: Definieren eines Modells und von Operationen mit GORM
Definieren Sie zuerst Ihr Go-Struct (Modell):
package main import ( "gorm.io/gorm" ) type Author struct { gorm.Model // Stellt ID, CreatedAt, UpdatedAt, DeletedAt bereit Name string `gorm:"not null"` Bio *string }
Verwenden Sie dann die API von GORM, um mit der Datenbank zu interagieren:
package main import ( "fmt" "gorm.io/driver/postgres" gorm.io/gorm" gorm.io/gorm/logger" // Importieren Sie logger für eine bessere Ausgabe "log" "os" "time" ) func main() { dsn := "host=localhost user=postgres password=password dbname=gorm_example port=5432 sslmode=disable TimeZone=Asia/Shanghai" // GORM-Logger konfigurieren newLogger := logger.New( log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer logger.Config{ SlowThreshold: time.Second, // Langsame SQL-Schwellenwerte LogLevel: logger.Info, // Log-Level IgnoreRecordNotFoundError: false, // ErrRecordNotFound Fehler für den Logger ignorieren Colorful: true, // Farbe aktivieren }, ) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: newLogger, // Logger anwenden }) if err != nil { panic("Verbindung zur Datenbank fehlgeschlagen") } // Das Schema migrieren (erstellt die Autoren-Tabelle, falls sie nicht existiert) db.AutoMigrate(&Author{}) // Einen Autor erstellen bio := "Eine talentierte Schreiberin" author := Author{Name: "John Doe", Bio: &bio} result := db.Create(&author) // Zeiger auf Daten an Create übergeben if result.Error != nil { panic(result.Error) } fmt.Printf("Erstellter Autor: %+v\n", author) // Einen Autor anhand seines Primärschlüssels abrufen var fetchedAuthor Author db.First(&fetchedAuthor, author.ID) // Autor mit ID 1 finden fmt.Printf("Abgerufener Autor: %+v\n", fetchedAuthor) // Einen Autor aktualisieren db.Model(&fetchedAuthor).Update("Name", "Jonathan Doe") fmt.Printf("Aktualisierter Autor: %+v\n", fetchedAuthor) // Einen Autor löschen (standardmäßig weiches Löschen mit gorm.Model) // db.Delete(&fetchedAuthor, fetchedAuthor.ID) // Dies würde ein weiches Löschen durchführen }
GORM bietet:
- Komfort und Schnelligkeit: Die Abstraktion von SQL reduziert den Boilerplate-Code für gängige Operationen.
- Objektorientierte Interaktion: Arbeiten Sie direkt mit Go-Structs, was den Code für Go-Entwickler idiomatischer macht.
- Erweiterte Funktionen: Integrierte Hooks, Assoziationen, Eager Loading und Transaktionsmanagement.
- Datenbankabstraktion: Wechseln Sie mit minimalen Codeänderungen einfach zwischen verschiedenen SQL-Datenbanken.
Allerdings stützt sich GORM auf Laufzeitreflexion, was zu Leistungseinbußen führen kann (die jedoch für typische Anwendungen oft zu vernachlässigen sind). Es abstrahiert auch SQL, was das Debuggen komplexer Abfragen manchmal erschweren und den Zugriff auf datenbankspezifische Funktionen einschränken kann.
Wann welche wählen?
Wählen Sie SQLC, wenn:
- Sie Wert auf volle Kontrolle über SQL, Leistung und Kompilierungszeitgarantien legen.
- Ihr Projekt komplexe, handoptimierte SQL-Abfragen beinhaltet.
- Sie Laufzeitreflexion und deren potenzielle Leistungseinbußen vermeiden möchten.
- Sie einen "SQL-First"-Entwicklungsansatz bevorzugen.
- Sie sich direkt in vorhandene Datenbankschemata integrieren müssen.
Wählen Sie GORM, wenn:
- Sie schnelle Entwicklung, Komfort und eine höhere Abstraktionsebene bevorzugen.
- Ihr Projekt hauptsächlich Standard-CRUD-Operationen beinhaltet.
- Sie lieber mit Go-Structs als mit rohem SQL arbeiten.
- Sie integrierte Funktionen wie Assoziationen, Hooks und Migrationen benötigen.
- Sie möglicherweise zwischen verschiedenen SQL-Datenbanken wechseln müssen.
Fazit
SQLC und GORM stellen zwei unterschiedliche, aber gleichermaßen gültige Philosophien für die Interaktion mit Datenbanken in Go dar. SQLC setzt sich für explizites SQL und Typsicherheit durch Code-Generierung ein und bietet unübertroffene Kontrolle und Leistung für Anwendungen, die stark auf handoptimierte Abfragen angewiesen sind. GORM hingegen nutzt den Komfort und die Abstraktion des Objektrelationalen Mappings und beschleunigt die Entwicklung mit seiner idiomatischen Go-API und seinem robusten Funktionsumfang. Die Wahl zwischen ihnen hängt letztendlich von den Projektanforderungen, den Teampräferenzen und dem gewünschten Gleichgewicht zwischen SQL-Explizitheit und Go-Abstraktion ab. Beide Werkzeuge sind in ihren jeweiligen Bereichen ausgezeichnet und bieten leistungsstarke Möglichkeiten zur Verwaltung von Daten in Ihren Go-Anwendungen.