encore-go-database

Par encoredev · skills

Requêtes de base de données et migrations avec Encore Go.

npx skills add https://github.com/encoredev/skills --skill encore-go-database

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 Scan pour lire les résultats de requête - les colonnes sont mappées par position
  • Vérifiez sqldb.ErrNoRows quand 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

Skills similaires