Skip to main content

ADR-001: DanceSong Join Entity amb Role i SortOrder

CampValor
Data2026-01-02
EstatAccepted
DecisorsEquip LDP
MigracionsV45__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:

  1. Distingir cançons principals d'alternatives - Un ball normalment té una cançó "oficial" però es pot ballar amb altres.
  2. Ordenar les cançons - L'ordre en què es mostren les cançons a la UI.
  3. 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
)

Enllaços