5.8 KiB
ADR-013 : ORM et Accès Données
Statut : Accepté Date : 2025-01-20
Contexte
RoadWave nécessite des requêtes SQL complexes (PostGIS géospatiales) avec performance optimale et type safety. Le choix entre ORM, query builder ou SQL brut impacte maintenabilité et performance.
Décision
sqlc pour génération de code Go type-safe depuis SQL.
Alternatives considérées
| Solution | Performance | Type Safety | Contrôle SQL | Courbe apprentissage |
|---|---|---|---|---|
| sqlc | Excellente | Très haute | Total | Faible |
| GORM | Moyenne | Moyenne | Limité | Faible |
| pgx + SQL brut | Excellente | Faible | Total | Moyenne |
| sqlx | Bonne | Faible | Total | Faible |
Justification
- Performance : Génération compile-time, zero overhead runtime
- Type safety : Structs Go générées automatiquement, erreurs détectées à la compilation
- Contrôle SQL : Requêtes PostGIS complexes écrites en pur SQL (pas de limitations ORM)
- Maintenabilité : Modifications SQL →
sqlc generate→ code mis à jour - Simplicité : Pas de magic, code généré lisible et debuggable
Workflow
-- queries/content.sql
-- name: GetContentNearby :many
SELECT id, title, ST_Distance(location, $1::geography) as distance
FROM contents
WHERE ST_DWithin(location, $1::geography, $2)
ORDER BY distance
LIMIT $3;
sqlc generate
// Code Go type-safe généré automatiquement
contents, err := q.GetContentNearby(ctx, location, radius, limit)
Gestion des Types PostGIS
Problème : sqlc génère du code depuis SQL, mais les types PostGIS (geography, geometry) ne mappent pas naturellement en Go → types opaques ([]byte, interface{}), perte de type safety.
Solution : Wrappers Typés + Fonctions de Conversion
Architecture :
SQL (PostGIS)
↓ ST_AsGeoJSON() / ST_AsText() / pgtype.Point
↓
Code Go (wrapper types)
↓
Business Logic (strongly-typed)
Implémentation :
// backend/internal/geo/types.go
package geo
import (
"database/sql/driver"
"encoding/json"
"github.com/jackc/pgx/v5/pgtype"
)
// GeoJSON représente un point géographique en format JSON
type GeoJSON struct {
Type string `json:"type"` // "Point"
Coordinates [2]float64 `json:"coordinates"` // [lon, lat]
}
// Value() et Scan() implémentent sql.Valuer et sql.Scanner
// pour conversion automatique avec sqlc
func (g GeoJSON) Value() (driver.Value, error) {
return json.Marshal(g)
}
func (g *GeoJSON) Scan(value interface{}) error {
bytes, _ := value.([]byte)
return json.Unmarshal(bytes, g)
}
// Distance calcule la distance entre 2 points (Haversine)
func (g GeoJSON) Distance(other GeoJSON) float64 {
// Implémentation Haversine formula
// ...
}
// WKT représente un point en Well-Known Text
type WKT string // "POINT(2.3522 48.8566)"
func (w WKT) Scan(value interface{}) error {
if str, ok := value.(string); ok {
*w = WKT(str)
}
return nil
}
Patterns SQL Recommandés
Pattern 1 : GeoJSON (recommandé pour frontend)
-- queries/poi.sql
-- name: GetPOIsNearby :many
SELECT
id,
name,
ST_AsGeoJSON(location)::jsonb as location, -- ← Conversion en JSON
ST_Distance(location, $1::geography) as distance_meters
FROM points_of_interest
WHERE ST_DWithin(location, $1::geography, $2)
ORDER BY distance_meters
LIMIT $3;
// Code généré par sqlc
type GetPOIsNearbyRow struct {
ID int64
Name string
Location json.RawMessage // ← Peut être parsé en GeoJSON
DistanceMeters float64
}
// Utilisation
rows, err := q.GetPOIsNearby(ctx, userLocation, radius, limit)
for _, row := range rows {
var poi geo.GeoJSON
json.Unmarshal(row.Location, &poi)
// poi est maintenant strongly-typed
}
Pattern 2 : WKT (pour debug/logging)
-- name: GetPOILocation :one
SELECT
id,
ST_AsText(location) as location_wkt -- ← "POINT(2.3522 48.8566)"
FROM points_of_interest
WHERE id = $1;
Pattern 3 : Utiliser pgtype pour types natifs
-- name: GetDistanceBetweenPOIs :one
SELECT
ST_Distance(
(SELECT location FROM points_of_interest WHERE id = $1),
(SELECT location FROM points_of_interest WHERE id = $2)
)::float8 as distance_meters; -- ← Force conversion en float64
Index PostGIS pour Performance
Créer un index GIST pour optimiser les requêtes ST_DWithin :
-- migrations/002_add_postgis_indexes.up.sql
CREATE INDEX idx_poi_location_gist
ON points_of_interest USING GIST(location);
CREATE INDEX idx_user_last_position_gist
ON user_locations USING GIST(last_position);
Checklist d'Implémentation
- Créer package
backend/internal/geo/types.goavec wrappers - Implémenter
Scan/Valuepour conversion automatique - Écrire requêtes SQL avec
ST_AsGeoJSON()/ST_AsText() - Ajouter index GIST sur colonnes géographiques
- Documenter patterns dans
backend/README.md - Tests d'intégration avec Testcontainers (PostGIS réel)
Impact
- ✅ Type safety retrouvée : Plus de
interface{}opaques - ✅ Performance : Index GIST + conversion optimisée
- ✅ Maintenabilité : Patterns clairs, réutilisables
- ❌ Complexité : Une couche de plus, mais justifiée
Référence : Résout incohérence #4 dans INCONSISTENCIES-ANALYSIS.md
Conséquences
- Dépendance :
github.com/sqlc-dev/sqlc - Fichier
sqlc.yamlà la racine pour configuration - Migrations gérées séparément avec
golang-migrate - CI doit exécuter
sqlc generatepour valider cohérence SQL/Go
Librairies : Voir ADR-020 pour stack complet (sqlc + golang-migrate + pgx)