ADR-001: DanceSong Join Entity amb Role i SortOrder
| Camp | Valor |
|---|---|
| Data | 2026-01-02 |
| Estat | Accepted |
| Decisors | Equip LDP |
| Migracions | V45__create_dance_song_join_table.sql |
Context
El model original tenia una relació @ManyToMany directa entre Dance i Song mitjançant una taula dance_songs amb només les claus forànies (dance_id, song_id). Aquesta estructura era suficient per l'MVP però no permetia:
- Distingir cançons principals d'alternatives - Un ball normalment té una cançó "oficial" però es pot ballar amb altres.
- Ordenar les cançons - L'ordre en què es mostren les cançons a la UI.
- Afegir notes - Comentaris sobre per què una cançó és alternativa (ex: "versió més lenta per a principiants").
Decisió
Convertim la relació ManyToMany en una join entity DanceSong amb camps addicionals:
CREATE TABLE dance_songs (
id BIGSERIAL PRIMARY KEY,
dance_id BIGINT NOT NULL REFERENCES dances(id) ON DELETE CASCADE,
song_id BIGINT NOT NULL REFERENCES songs(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL DEFAULT 'PRIMARY', -- PRIMARY | ALTERNATIVE
sort_order INT NOT NULL DEFAULT 0,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(dance_id, song_id)
);
Regla de negoci: "Exactament 1 PRIMARY si hi ha songs"
Si un ball té cançons associades, exactament una ha de tenir role = PRIMARY. Això es valida a nivell de servei (DanceService) i no a la BD perquè:
- Permet crear un ball sense cançons inicialment.
- Evita triggers complexos que dificulten el testing.
- Fa la lògica explícita i testejable al codi Java.
Enums
public enum DanceSongRole {
PRIMARY, // Cançó principal/oficial
ALTERNATIVE // Altres cançons compatibles
}
Alternatives considerades
1. Camp is_primary boolean
- Pro: Més simple.
- Contra: No extensible si volem afegir més rols (DEMO, PRACTICE, etc.).
2. Validació a la BD amb CHECK constraint
- Pro: Garantia absoluta.
- Contra: Complexitat amb triggers per validar "exactament 1 PRIMARY".
3. Mantenir @ManyToMany i afegir taula separada per metadata
- Pro: Menys canvis al model existent.
- Contra: Inconsistència i joins addicionals.
Conseqüències
Positives
- ✅ Model més ric i expressiu.
- ✅ Preparats per futures extensions (més rols, timestamps d'assignació, etc.).
- ✅ API clara amb DTOs que inclouen
role,sortOrder,notes. - ✅ UI pot mostrar "Cançó principal" destacada i alternatives com a llista.
Negatives
- ⚠️ Migració V45 requereix transformar dades existents (totes es posen com PRIMARY temporalment).
- ⚠️ Més complexitat en la validació de creació/actualització de danses.
DTO Shape
// Al DanceDto
record DanceSongDto(
Long id, // DanceSong.id
Long songId,
String songTitle,
String songArtist,
DanceSongRole role,
Integer sortOrder,
String notes
)
// Al CreateDanceRequest / UpdateDanceRequest
record DanceSongRequest(
Long songId,
DanceSongRole role,
Integer sortOrder,
String notes
)