Skip to main content

Integritat de Base de Dades

Tipus: Document d'Arquitectura
Versió: 1.0
Actualitzat: 2024-12
Estat: Normatiu


Resum

Aquest document defineix les estratègies d'integritat de dades a nivell de base de dades per a LDP. Cobreix:

  1. Constraints i validacions a BD
  2. Gestió de transaccions
  3. Control de concurrència
  4. Auditoria de canvis
  5. Estratègies de soft delete

1. Principis d'Integritat

1.1. Defense at Database Level

La base de dades és l'última línia de defensa. Tot i que l'aplicació valida, la BD ha de garantir:

  • Integritat referencial: FK sempre vàlides
  • Integritat de domini: valors dins de rangs acceptables
  • Integritat d'entitat: PKs úniques, no nul·les
  • Consistència: transaccions ACID

1.2. Principi de Fail-Safe

Si l'aplicació falla en validar, la BD ha de rebutjar dades incorrectes.


2. Constraints Obligatòries

2.1. Primary Keys

Totes les taules han de tenir:

id BIGSERIAL PRIMARY KEY
  • Tipus: BIGINT auto-increment
  • Nom: sempre id
  • Mai reutilitzar IDs eliminats

2.2. Foreign Keys

Regla: Tota relació ha de tenir FK explícita.

-- Exemple: Dance -> Level
ALTER TABLE dances
ADD CONSTRAINT fk_dances_level
FOREIGN KEY (level_id) REFERENCES levels(id);

-- Exemple: amb ON DELETE
ALTER TABLE ownership_requests
ADD CONSTRAINT fk_ownership_requests_user
FOREIGN KEY (requested_by_id) REFERENCES users(id) ON DELETE SET NULL;

Polítiques ON DELETE:

RelacióPolíticaJustificació
dances.owner_id → usersSET NULLPermetre eliminar user sense perdre dance
dances.level_id → levelsRESTRICTNo eliminar levels en ús
ownership_requests.* → usersSET NULLPreservar historial
person_roles → personsCASCADEEliminar roles quan s'elimina person
links → dancesCASCADELinks no tenen sentit sense dance

2.3. Not Null Constraints

Regla: Camps obligatoris han de ser NOT NULL a BD, no només a Java.

-- Camps sempre obligatoris
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,

-- Camps de negoci obligatoris
name VARCHAR(255) NOT NULL,

Checklist per camp:

Si el camp...Llavors
És identificador de negociNOT NULL
Té valor per defecteNOT NULL DEFAULT ...
És FK requeridaNOT NULL
És opcional funcionalmentNullable (sense constraint)

2.4. Unique Constraints

-- Email únic (case-insensitive via índex)
CREATE UNIQUE INDEX idx_users_email_lower ON users (LOWER(email));

-- Combinació única
ALTER TABLE person_roles
ADD CONSTRAINT uq_person_role UNIQUE (person_id, role_type);

-- Nom únic dins de context
ALTER TABLE event_occurrences
ADD CONSTRAINT uq_event_occurrence UNIQUE (event_id, venue_id, start_at);

2.5. Check Constraints

Per validar dominis a BD:

-- Valors positius
ALTER TABLE songs ADD CONSTRAINT chk_songs_bpm_positive CHECK (bpm > 0);
ALTER TABLE dances ADD CONSTRAINT chk_dances_counts_positive CHECK (counts > 0);

-- Enums simulats
ALTER TABLE users ADD CONSTRAINT chk_users_role
CHECK (role IN ('USER', 'TEACHER', 'ADMIN'));

-- Rangs
ALTER TABLE event_occurrences ADD CONSTRAINT chk_occurrence_dates
CHECK (end_at > start_at);

3. Gestió de Transaccions

3.1. Principis

  • Transacció per operació de negoci: una acció d'usuari = una transacció
  • Rollback automàtic: qualsevol excepció no gestionada fa rollback
  • Evitar transaccions llargues: màxim uns segons

3.2. Configuració Spring

// Default: readonly per queries, transactional per mutations
@Service
@Transactional(readOnly = true) // Default lectura
public class DanceService {

@Transactional // Override per escriptura
public Dance save(Dance dance) {
return repo.save(dance);
}
}

3.3. Patrons de Transaccions

Operacions Simples

@Transactional
public DanceDto create(CreateDanceRequest request) {
Dance dance = mapper.toEntity(request);
dance.setOwner(authUtil.getCurrentUser());
return mapper.toDto(repo.save(dance));
}

Operacions Compostes

@Transactional
public void transferOwnership(Long choreographerId, Long newOwnerId) {
Choreographer choreo = choreographerRepo.findById(choreographerId)
.orElseThrow(() -> new NotFoundException("Choreographer not found"));

User newOwner = userRepo.findById(newOwnerId)
.orElseThrow(() -> new NotFoundException("User not found"));

// Tot dins la mateixa transacció
choreo.setOwner(newOwner);
choreo.setListingStatus(ListingStatus.CLAIMED);
choreographerRepo.save(choreo);

// Actualitzar request relacionada
ownershipRequestRepo.updateStatus(choreographerId, Status.APPROVED);
}

Anti-patró: Transaccions Niades

// ❌ EVITAR: transaccions niades poden causar problemes
@Transactional
public void methodA() {
methodB(); // Si methodB té @Transactional, comportament complex
}

// ✅ MILLOR: un únic punt de transacció
@Transactional
public void orchestrator() {
// Tota la lògica aquí
stepA(); // Mètodes privats sense @Transactional
stepB();
stepC();
}

4. Control de Concurrència

4.1. Optimistic Locking

Per a entitats amb modificacions concurrents (recomanat per defecte):

@Entity
public class Choreographer {
@Id
private Long id;

@Version
private Long version; // Auto-gestionat per JPA

// ...
}
// En actualització concurrent, llança OptimisticLockException
@Transactional
public void update(Long id, UpdateRequest request) {
Choreographer entity = repo.findById(id).orElseThrow();
// Si un altre procés ha modificat, version no coincidirà
entity.setName(request.name());
repo.save(entity); // Pot llançar OptimisticLockException
}

Gestió d'errors:

@ExceptionHandler(OptimisticLockException.class)
public ResponseEntity<ErrorResponse> handleOptimisticLock(OptimisticLockException ex) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(new ErrorResponse("CONFLICT", "El recurs ha estat modificat. Recarrega i torna-ho a provar."));
}

4.2. Pessimistic Locking (Casos Especials)

Només per operacions crítiques on no es pot tolerar conflicte:

// Bloqueig exclusiu durant la transacció
@Query("SELECT c FROM Choreographer c WHERE c.id = :id")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Choreographer> findByIdForUpdate(@Param("id") Long id);

Quan usar:

EscenariRecomanació
Edició normalOptimistic (default)
Transferència d'ownershipPessimistic
Comptadors/saldosPessimistic
Lectura sense modificacióSense lock

4.3. Unique Constraints com a Lock

Per evitar duplicats en condicions de carrera:

-- La constraint evita dos claims simultanis
CREATE UNIQUE INDEX idx_ownership_pending
ON ownership_requests (resource_type, resource_id)
WHERE status = 'PENDING';
// Si dos intenten simultàniament, un fallarà per constraint
@Transactional
public OwnershipRequest createClaimRequest(...) {
// Validacions de negoci...
return repo.save(request); // DataIntegrityViolationException si duplicat
}

5. Auditoria de Canvis

5.1. Camps d'Auditoria Bàsics

Totes les entitats principals han de tenir:

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class AuditableEntity {

@CreatedDate
@Column(nullable = false, updatable = false)
private Instant createdAt;

@LastModifiedDate
@Column(nullable = false)
private Instant updatedAt;

@CreatedBy
@Column(updatable = false)
private Long createdById;

@LastModifiedBy
private Long updatedById;
}

5.2. Configuració Spring Data Auditing

@Configuration
@EnableJpaAuditing
public class AuditingConfig {

@Bean
public AuditorAware<Long> auditorProvider(AuthUtil authUtil) {
return () -> {
try {
return Optional.of(authUtil.getCurrentUser().getId());
} catch (Exception e) {
return Optional.empty(); // Sistema o anònim
}
};
}
}

5.3. Historial de Canvis (Roadmap)

Per a entitats crítiques, considerar taula d'historial:

CREATE TABLE choreographer_history (
id BIGSERIAL PRIMARY KEY,
choreographer_id BIGINT NOT NULL,
changed_at TIMESTAMP NOT NULL DEFAULT NOW(),
changed_by_id BIGINT,
change_type VARCHAR(20) NOT NULL, -- CREATE, UPDATE, DELETE
old_values JSONB,
new_values JSONB
);

6. Soft Delete

6.1. Quan Aplicar

EntitatSoft DeleteJustificació
User✅ SíPreservar historial, compliment legal
Choreographer✅ SíRelacions amb dances
Dance✅ SíHistorial de balls
OwnershipRequest✅ SíAuditoria obligatòria
Link❌ NoSense relacions crítiques
VerificationCode❌ NoTemporal

6.2. Implementació

@Entity
public class User {
// ...

@Column(name = "deleted_at")
private Instant deletedAt;

public boolean isDeleted() {
return deletedAt != null;
}
}
// Filtrar automàticament entitats eliminades
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

// Per defecte, excloure eliminats
@Override
@Query("SELECT u FROM User u WHERE u.deletedAt IS NULL")
List<User> findAll();

// Mètode explícit per incloure eliminats (admin)
@Query("SELECT u FROM User u")
List<User> findAllIncludingDeleted();
}

6.3. Alternativa: @Where (Hibernate)

@Entity
@Where(clause = "deleted_at IS NULL") // Filtre automàtic
public class User {
// ...
}

7. Migracions i Esquema

7.1. Flyway (Recomanat)

Totes les modificacions d'esquema via migracions versionades:

src/main/resources/db/migration/
├── V1__initial_schema.sql
├── V2__add_ownership_requests.sql
├── V3__add_person_roles.sql
└── V4__add_audit_columns.sql

7.2. Regles de Migració

AccióPermès en produccióNotes
Afegir columna nullable✅ SíSense downtime
Afegir columna NOT NULL⚠️ Amb defaultO migració en 2 passos
Eliminar columna⚠️ Després de deprecacióMarcar deprecated primer
Modificar tipus❌ No directamentCrear nova + migrar + eliminar
Afegir FK✅ SíVerificar dades existents
Afegir índex✅ Sí (CONCURRENTLY)Pot ser lent en taules grans

8. Checklist d'Integritat

Per Nova Entitat

- [ ] PK BIGSERIAL
- [ ] FKs amb ON DELETE adequat
- [ ] NOT NULL en camps obligatoris
- [ ] UNIQUE on cal
- [ ] CHECK per validacions de domini
- [ ] Camps d'auditoria (created_at, updated_at)
- [ ] Índexs per queries freqüents
- [ ] @Version si cal concurrència
- [ ] Migració Flyway creada

Per Modificació d'Entitat

- [ ] Migració Flyway (no modificar existents)
- [ ] Backward compatible si possible
- [ ] Tests d'integració actualitzats
- [ ] Documentació actualitzada

9. Referències


Historial de Canvis

DataVersióCanvis
2024-121.0Document inicial