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