Opérations de base de données Encore Go
Instructions
Configuration de la base de données
package user
import "encore.dev/storage/sqldb"
var db = sqldb.NewDatabase("userdb", sqldb.DatabaseConfig{
Migrations: "./migrations",
})
Méthodes de requête
L'API de base de données d'Encore reprend le package standard database/sql de Go. Utilisez .Scan() pour lire les résultats de requête dans des variables.
Query - Plusieurs lignes
type User struct {
ID string
Email string
Name string
}
func listActiveUsers(ctx context.Context) ([]*User, error) {
rows, err := db.Query(ctx, `
SELECT id, email, name FROM users WHERE active = true
`)
if err != nil {
return nil, err
}
defer rows.Close()
var users []*User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Email, &u.Name); err != nil {
return nil, err
}
users = append(users, &u)
}
return users, rows.Err()
}
QueryRow - Une seule ligne
func getUser(ctx context.Context, id string) (*User, error) {
var u User
err := db.QueryRow(ctx, `
SELECT id, email, name FROM users WHERE id = $1
`, id).Scan(&u.ID, &u.Email, &u.Name)
if errors.Is(err, sqldb.ErrNoRows) {
return nil, &errs.Error{
Code: errs.NotFound,
Message: "user not found",
}
}
if err != nil {
return nil, err
}
return &u, nil
}
Exec - Pas de valeur de retour
Pour les opérations INSERT, UPDATE, DELETE :
func createUser(ctx context.Context, email, name string) error {
_, err := db.Exec(ctx, `
INSERT INTO users (id, email, name)
VALUES ($1, $2, $3)
`, generateID(), email, name)
return err
}
func updateUser(ctx context.Context, id, name string) error {
_, err := db.Exec(ctx, `
UPDATE users SET name = $1 WHERE id = $2
`, name, id)
return err
}
func deleteUser(ctx context.Context, id string) error {
_, err := db.Exec(ctx, `
DELETE FROM users WHERE id = $1
`, id)
return err
}
Migrations
Structure des fichiers
user/
└── migrations/
├── 1_create_users.up.sql
├── 2_add_posts.up.sql
└── 3_add_indexes.up.sql
Convention de nommage
- Commencez par un numéro (1, 2, etc.)
- Suivi d'un tiret bas et d'une description
- Terminez par
.up.sql - Les numéros doivent être séquentiels
Exemple de migration
-- migrations/1_create_users.up.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
Transactions
func transferFunds(ctx context.Context, fromID, toID string, amount int) error {
tx, err := db.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback() // No-op if committed
_, err = tx.Exec(ctx, `
UPDATE accounts SET balance = balance - $1 WHERE id = $2
`, amount, fromID)
if err != nil {
return err
}
_, err = tx.Exec(ctx, `
UPDATE accounts SET balance = balance + $1 WHERE id = $2
`, amount, toID)
if err != nil {
return err
}
return tx.Commit()
}
Utiliser Scan
La méthode Scan lit les colonnes des résultats de requête dans des variables. Les colonnes sont mappées par position, pas par nom - l'ordre des arguments de Scan doit correspondre à l'ordre des colonnes dans votre instruction SELECT.
type User struct {
ID string
Email string
Name string
CreatedAt time.Time
}
// Une seule ligne avec QueryRow
func getUser(ctx context.Context, id string) (*User, error) {
var u User
err := db.QueryRow(ctx, `
SELECT id, email, name, created_at FROM users WHERE id = $1
`, id).Scan(&u.ID, &u.Email, &u.Name, &u.CreatedAt)
if err != nil {
return nil, err
}
return &u, nil
}
// Vous pouvez aussi scanner dans une struct inline
func getItem(ctx context.Context, id int64) error {
var item struct {
ID int64
Title string
Done bool
}
err := db.QueryRow(ctx, `
SELECT id, title, done FROM items WHERE id = $1
`, id).Scan(&item.ID, &item.Title, &item.Done)
return err
}
Protection contre les injections SQL
Utilisez toujours des requêtes paramétrées :
// SAFE - les valeurs sont paramétrées
var u User
err := db.QueryRow(ctx, `
SELECT id, email, name FROM users WHERE email = $1
`, email).Scan(&u.ID, &u.Email, &u.Name)
// WRONG - risque d'injection SQL
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
Gestion des erreurs
import (
"errors"
"encore.dev/storage/sqldb"
"encore.dev/beta/errs"
)
func getUser(ctx context.Context, id string) (*User, error) {
var u User
err := db.QueryRow(ctx, `
SELECT id, email, name FROM users WHERE id = $1
`, id).Scan(&u.ID, &u.Email, &u.Name)
if errors.Is(err, sqldb.ErrNoRows) {
return nil, &errs.Error{
Code: errs.NotFound,
Message: "user not found",
}
}
if err != nil {
return nil, err
}
return &u, nil
}
Recommandations
- Utilisez toujours des requêtes paramétrées (
$1,$2, etc.) - Utilisez
Scanpour lire les résultats de requête - les colonnes sont mappées par position - Vérifiez
sqldb.ErrNoRowsquand vous attendez une seule ligne - Les migrations sont appliquées automatiquement au démarrage
- Les noms de bases de données doivent être en minuscules et descriptifs
- Chaque service a généralement sa propre base de données
- Utilisez les transactions pour les opérations qui doivent être atomiques