refactor(docs): réorganiser ADR et règles métier pour clarté
**Changements majeurs** : 1. **Suppression ADR-010 (Commandes volant et likes)** : - Contenu consolidé dans Règle 05 (section 5.3) - Raison : ADR-010 était du métier déguisé en architecture - Section "Implémentation Technique" ajoutée à Règle 05 - Pattern correct (addition) vs incorrect (multiplication) 2. **Déplacement ADR-011 → Compliance** : - `docs/adr/011-conformite-stores.md` → `docs/compliance/stores-submission.md` - Raison : Nature opérationnelle/légale, pas architecture technique - Nouveau dossier `/docs/compliance/` créé 3. **Renumérotation ADR (010-022)** : - Combler les trous de numérotation (010 et 011) - ADR-012→010, ADR-013→011, ..., ADR-024→022 - 22 ADR numérotés en continu (001-022) - Historique Git préservé (git mv) 4. **Mise à jour références** : - Règle 03 : ADR-010 → Règle 05 (section 5.3) - Règle 09 : ADR-010 → Règle 05 (section 5.3) - INCONSISTENCIES-ANALYSIS.md : toutes références mises à jour - Incohérence #15 annulée (faux problème : modes séparés) **Résultat** : - ✅ Séparation claire ADR (technique) vs Règles métier (fonctionnel) - ✅ Documentation compliance séparée - ✅ Numérotation ADR continue sans trous - ✅ Single Source of Truth (pas de redondance) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
211
docs/adr/011-orm-acces-donnees.md
Normal file
211
docs/adr/011-orm-acces-donnees.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# ADR-011 : 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-018](018-librairies-go.md) pour stack complet (sqlc + golang-migrate + pgx)
|
||||
Reference in New Issue
Block a user