Type-Safe SQL in Go mit sqlc
Min-jun Kim
Dev Intern · Leapcell

Einführung
Die Schnittstellen, die von der database/sql
Standardbibliothek in der Go-Sprache bereitgestellt werden, sind relativ Low-Level. Dies erfordert, dass wir eine große Menge an repetitivem Code schreiben. Diese beträchtliche Menge an Boilerplate-Code ist nicht nur umständlich zu schreiben, sondern auch fehleranfällig. Manchmal, wenn Sie den Feldtyp ändern, müssen Sie möglicherweise Änderungen an vielen Stellen vornehmen; wenn Sie ein neues Feld hinzufügen, müssen Sie auch die Stellen ändern, an denen zuvor die select *
Abfrageanweisung verwendet wurde. Wenn es Auslassungen gibt, kann dies zu einer Panik während der Laufzeit führen. Selbst wenn Sie eine ORM-Bibliothek verwenden, können diese Probleme nicht vollständig gelöst werden! Hier kommt sqlc ins Spiel! sqlc kann typsicheren und idiomatischen Go-Interface-Code basierend auf den von uns geschriebenen SQL-Anweisungen generieren, und wir müssen nur diese Methoden aufrufen.
Schnellstart
Installation
Installieren Sie zuerst sqlc:
$ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
Natürlich benötigen Sie auch den entsprechenden Datenbanktreiber:
$ go get github.com/lib/pq $ go get github.com/go-sql-driver/mysql
SQL-Anweisungen schreiben
Schreiben Sie die Tabellenerstellungsanweisung. Schreiben Sie den folgenden Inhalt in die Datei schema.sql
:
CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, bio TEXT );
Schreiben Sie die Abfrageanweisungen. Schreiben Sie den folgenden Inhalt in die Datei query.sql
:
-- name: GetUser :one SELECT * FROM users WHERE id = $1 LIMIT 1; -- name: ListUsers :many SELECT * FROM users ORDER BY name; -- name: CreateUser :exec INSERT INTO users ( name, bio ) VALUES ( $1, $2 ) RETURNING *; -- name: DeleteUser :exec DELETE FROM users WHERE id = $1;
sqlc unterstützt PostgreSQL. sqlc benötigt lediglich eine kleine Konfigurationsdatei sqlc.yaml
:
version: "1" packages: - name: "db" path: "./db" queries: "./query.sql" schema: "./schema.sql"
Konfigurationserklärung:
version
: Version.packages
:name
: Der generierte Paketname.path
: Der Pfad der generierten Dateien.queries
: Die Abfrage-SQL-Datei.schema
: Die Tabellenerstellungs-SQL-Datei.
Go-Code generieren
Führen Sie den folgenden Befehl aus, um den entsprechenden Go-Code zu generieren:
sqlc generate
sqlc generiert den Datenbankoperationscode im selben Verzeichnis. Die Verzeichnisstruktur ist wie folgt:
db
├── db.go
├── models.go
└── query.sql.go
sqlc generiert die Modellobjektstruktur gemäß schema.sql
und query.sql
:
// models.go type User struct { ID int64 Name string Bio sql.NullString }
Und die Operation-Interfaces:
// query.sql.go func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) func (q *Queries) DeleteUser(ctx context.Context, id int64) error func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) func (q *Queries) ListUsers(ctx context.Context) ([]User, error)
Wobei Queries
eine von sqlc gekapselte Struktur ist.
Anwendungsbeispiel
package main import ( "database/sql" "fmt" "log" _ "github.com/lib/pq" "golang.org/x/net/context" "github.com/leapcell/examples/sqlc" ) func main() { pq, err := sql.Open("postgres", "dbname=sqlc sslmode=disable") if err != nil { log.Fatal(err) } queries := db.New(pq) users, err := queries.ListUsers(context.Background()) if err != nil { log.Fatal("ListUsers error:", err) } fmt.Println(users) insertedUser, err := queries.CreateUser(context.Background(), db.CreateUserParams{ Name: "Rob Pike", Bio: sql.NullString{String: "Co-author of The Go Programming Language", Valid: true}, }) if err != nil { log.Fatal("CreateUser error:", err) } fmt.Println(insertedUser) fetchedUser, err := queries.GetUser(context.Background(), insertedUser.ID) if err != nil { log.Fatal("GetUser error:", err) } fmt.Println(fetchedUser) err = queries.DeleteUser(context.Background(), insertedUser.ID) if err != nil { log.Fatal("DeleteUser error:", err) } }
Der generierte Code befindet sich unter dem Paket db
(angegeben durch die Option packages.name
). Rufen Sie zuerst db.New()
auf und übergeben Sie den Rückgabewert sql.DB
von sql.Open()
als Parameter, um das Queries
-Objekt zu erhalten. Alle unsere Operationen an der Tabelle users
müssen über die Methoden dieses Objekts abgeschlossen werden.
PostgreSQL starten und die Datenbank und Tabellen erstellen
Damit das obige Programm ausgeführt werden kann, müssen Sie auch PostgreSQL starten und die Datenbank und Tabellen erstellen:
$ createdb sqlc $ psql -f schema.sql -d sqlc
Der erste Befehl erstellt eine Datenbank namens sqlc
, und der zweite Befehl führt die Anweisungen in der Datei schema.sql
in der Datenbank sqlc
aus, d. h. erstellt die Tabelle.
Das Programm ausführen
$ go run .
Beispiel für das Laufergebnis:
[]
{1 Rob Pike {Co-author of The Go Programming Language true}}
Code-Generierung
Zusätzlich zu den SQL-Anweisungen selbst benötigt sqlc, dass wir einige grundlegende Informationen für das generierte Programm in Form von Kommentaren beim Schreiben von SQL-Anweisungen bereitstellen. Die Syntax lautet wie folgt:
-- name: <name> <cmd>
name
ist der Name der generierten Methode, wie z. B. CreateUser
, ListUsers
, GetUser
, DeleteUser
usw. oben. cmd
kann die folgenden Werte haben:
:one
: Gibt an, dass die SQL-Anweisung ein Objekt zurückgibt, und der Rückgabewert der generierten Methode ist(Objekttyp, Fehler)
, und der Objekttyp kann vom Tabellennamen abgeleitet werden.:many
: Gibt an, dass die SQL-Anweisung mehrere Objekte zurückgibt, und der Rückgabewert der generierten Methode ist([]Objekttyp, Fehler)
.:exec
: Gibt an, dass die SQL-Anweisung kein Objekt zurückgibt und nur einenFehler
zurückgibt.:execrows
: Gibt an, dass die SQL-Anweisung die Anzahl der betroffenen Zeilen zurückgeben muss.
:one
Beispiel
-- name: GetUser :one SELECT id, name, bio FROM users WHERE id = $1 LIMIT 1
Das --name
im Kommentar weist an, die Methode GetUser
zu generieren. Abgeleitet vom Tabellennamen ist der Basistyp des Rückgabewerts User
. :one
gibt an, dass nur ein Objekt zurückgegeben wird. Daher ist der endgültige Rückgabewert (User, error)
:
// db/query.sql.go const getUser = `-- name: GetUser :one SELECT id, name, bio FROM users WHERE id = $1 LIMIT 1 ` func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) { row := q.db.QueryRowContext(ctx, getUser, id) var i User err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err }
:many
Beispiel
-- name: ListUsers :many SELECT * FROM users ORDER BY name;
Das --name
im Kommentar weist an, die Methode ListUsers
zu generieren. Abgeleitet vom Tabellennamen users
ist der Basistyp des Rückgabewerts User
. :many
gibt an, dass ein Slice von Objekten zurückgegeben wird. Daher ist der endgültige Rückgabewert ([]User, error)
:
// db/query.sql.go const listUsers = `-- name: ListUsers :many SELECT id, name, bio FROM users ORDER BY name ` func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { rows, err := q.db.QueryContext(ctx, listUsers) if err != nil { return nil, err } defer rows.Close() var items []User for rows.Next() { var i User if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil }
Hier ist ein Detail zu beachten. Selbst wenn wir select *
verwenden, wird die SQL-Anweisung im generierten Code in bestimmte Felder umgeschrieben:
SELECT id, name, bio FROM users ORDER BY name
Auf diese Weise kann diese SQL-Anweisung und die Methode ListUsers()
konsistent gehalten werden, was sehr praktisch ist, solange wir später die Felder hinzufügen oder löschen und den Befehl sqlc
ausführen!
:exec
Beispiel
-- name: DeleteUser :exec DELETE FROM users WHERE id = $1
Das --name
im Kommentar weist an, die Methode DeleteUser
zu generieren. Abgeleitet vom Tabellennamen users
ist der Basistyp des Rückgabewerts User
. :exec
gibt an, dass kein Objekt zurückgegeben wird. Daher ist der endgültige Rückgabewert error
:
// db/query.sql.go const deleteUser = `-- name: DeleteUser :exec DELETE FROM users WHERE id = $1 ` func (q *Queries) DeleteUser(ctx context.Context, id int64) error { _, err := q.db.ExecContext(ctx, deleteUser, id) return err }
:execrows
Beispiel
-- name: DeleteUserN :execrows DELETE FROM users WHERE id = $1
Das --name
im Kommentar weist an, die Methode DeleteUserN
zu generieren. Abgeleitet vom Tabellennamen users
ist der Basistyp des Rückgabewerts User
. :exec
gibt an, dass die Anzahl der betroffenen Zeilen (d. h. wie viele Zeilen gelöscht wurden) zurückgegeben wird. Daher ist der endgültige Rückgabewert (int64, error)
:
// db/query.sql.go const deleteUserN = `-- name: DeleteUserN :execrows DELETE FROM users WHERE id = $1 ` func (q *Queries) DeleteUserN(ctx context.Context, id int64) (int64, error) { result, err := q.db.ExecContext(ctx, deleteUserN, id) if err != nil { return 0, err } return result.RowsAffected() }
Egal wie komplex das geschriebene SQL ist, es folgt den obigen Regeln. Wir müssen nur eine zusätzliche Kommentarzeile hinzufügen, wenn wir SQL-Anweisungen schreiben, und sqlc kann idiomatische SQL-Operationsmethoden für uns generieren. Der generierte Code unterscheidet sich nicht von dem, was wir von Hand schreiben, die Fehlerbehandlung ist auch sehr vollständig, und es vermeidet auch die Mühe und die Fehler des Schreibens von Hand.
Modellobjekte
sqlc generiert die entsprechende Modellstruktur für alle Tabellenerstellungsanweisungen. Der Strukturname ist die Singularform des Tabellennamens, und der erste Buchstabe wird großgeschrieben. Zum Beispiel:
CREATE TABLE users ( id SERIAL PRIMARY KEY, name text NOT NULL );
Es wird die entsprechende Struktur generiert:
type User struct { ID int Name string }
Darüber hinaus kann sqlc die ALTER TABLE
Anweisung analysieren, und es wird die Struktur des Modellobjekts gemäß der endgültigen Tabellenstruktur generiert. Zum Beispiel:
CREATE TABLE users ( id SERIAL PRIMARY KEY, birth_year int NOT NULL ); ALTER TABLE users ADD COLUMN bio text NOT NULL; ALTER TABLE users DROP COLUMN birth_year; ALTER TABLE users RENAME TO writers;
In den obigen SQL-Anweisungen gibt es zwei Spalten id
und birth_year
, wenn die Tabelle erstellt wird. Die erste ALTER TABLE
Anweisung fügt eine Spalte bio
hinzu, die zweite löscht die Spalte birth_year
, und die dritte ändert den Tabellennamen von users
in writers
. sqlc generiert den Code gemäß dem endgültigen Tabellennamen writers
und den Spalten id
und bio
in der Tabelle:
package db type Writer struct { ID int Bio string }
Konfigurationsfelder
Weitere Konfigurationsfelder können auch in der Datei sqlc.yaml
festgelegt werden.
emit_json_tags
Der Standardwert ist false
. Wenn Sie dieses Feld auf true
setzen, können Sie den generierten Modellobjektstrukturen JSON-Tags hinzufügen. Zum Beispiel:
CREATE TABLE users ( id SERIAL PRIMARY KEY, created_at timestamp NOT NULL );
Es wird generiert:
package db import ( "time" ) type User struct { ID int `json:"id"` CreatedAt time.Time `json:"created_at"` }
emit_prepared_queries
Der Standardwert ist false
. Wenn Sie dieses Feld auf true
setzen, wird die entsprechende vorbereitete Anweisung für das SQL generiert. Wenn Sie beispielsweise diese Option im Schnellstartbeispiel festlegen, fügt die endgültig generierte Struktur Queries
alle vorbereiteten Anweisungsobjekte hinzu, die dem SQL entsprechen:
type Queries struct { db DBTX tx *sql.Tx createUserStmt *sql.Stmt deleteUserStmt *sql.Stmt getUserStmt *sql.Stmt listUsersStmt *sql.Stmt }
Und eine Prepare()
Methode:
func Prepare(ctx context.Context, db DBTX) (*Queries, error) { q := Queries{db: db} var err error if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil { return nil, fmt.Errorf("error preparing query CreateUser: %w", err) } if q.deleteUserStmt, err = db.PrepareContext(ctx, deleteUser); err != nil { return nil, fmt.Errorf("error preparing query DeleteUser: %w", err) } if q.getUserStmt, err = db.PrepareContext(ctx, getUser); err != nil { return nil, fmt.Errorf("error preparing query GetUser: %w", err) } if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { return nil, fmt.Errorf("error preparing query ListUsers: %w", err) } return &q, nil }
Die anderen generierten Methoden verwenden alle diese Objekte anstelle von direkten SQL-Anweisungen:
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { row := q.queryRow(ctx, q.createUserStmt, createUser, arg.Name, arg.Bio) var i User err := row.Scan(&i.ID, &i.Name, &i.Bio) return i, err }
Wir müssen diese Prepare()
Methode während der Programminitialisierung aufrufen.
emit_interface
Der Standardwert ist false
. Wenn Sie dieses Feld auf true
setzen, wird eine Schnittstelle für die Abfragestruktur generiert. Wenn Sie beispielsweise diese Option im Schnellstartbeispiel festlegen, weist der endgültig generierte Code eine zusätzliche Datei querier.go
auf:
// db/querier.go type Querier interface { CreateUser(ctx context.Context, arg CreateUserParams) (User, error) DeleteUser(ctx context.Context, id int64) error DeleteUserN(ctx context.Context, id int64) (int64, error) GetUser(ctx context.Context, id int64) (User, error) ListUsers(ctx context.Context) ([]User, error) }
Fazit
Obwohl sqlc noch einige nicht perfekte Dinge hat, kann es die Komplexität des Schreibens von Datenbankcode in Go wirklich erheblich vereinfachen, unsere Codiereffizienz verbessern und die Wahrscheinlichkeit von Fehlern verringern. Für diejenigen, die PostgreSQL verwenden, wird dringend empfohlen, es auszuprobieren!
Referenzen
Leapcell: Die Serverlose Plattform der nächsten Generation für Golang-Hosting
Schließlich empfehle ich eine Plattform, die sich am besten für die Bereitstellung von Go-Diensten eignet: Leapcell
1. Multi-Sprachen Unterstützung
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Unbegrenzte Projekte kostenlos bereitstellen
- zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Entdecken Sie mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ