# 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 ```sql -- 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; ``` ```bash sqlc generate ``` ```go // 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** : ```go // 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)** ```sql -- 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; ``` ```go // 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)** ```sql -- 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** ```sql -- 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 : ```sql -- 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.go` avec wrappers - [ ] Implémenter `Scan/Value` pour 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](../INCONSISTENCIES-ANALYSIS.md#4--orm-sqlc-vs-types-postgis) ## 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 generate` pour valider cohérence SQL/Go **Librairies** : Voir [ADR-020](020-librairies-go.md) pour stack complet (sqlc + golang-migrate + pgx)