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:
- Constraints i validacions a BD
- Gestió de transaccions
- Control de concurrència
- Auditoria de canvis
- 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:
BIGINTauto-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ítica | Justificació |
|---|---|---|
dances.owner_id → users | SET NULL | Permetre eliminar user sense perdre dance |
dances.level_id → levels | RESTRICT | No eliminar levels en ús |
ownership_requests.* → users | SET NULL | Preservar historial |
person_roles → persons | CASCADE | Eliminar roles quan s'elimina person |
links → dances | CASCADE | Links 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 negoci | NOT NULL |
| Té valor per defecte | NOT NULL DEFAULT ... |
| És FK requerida | NOT NULL |
| És opcional funcionalment | Nullable (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:
| Escenari | Recomanació |
|---|---|
| Edició normal | Optimistic (default) |
| Transferència d'ownership | Pessimistic |
| Comptadors/saldos | Pessimistic |
| 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
| Entitat | Soft Delete | Justificació |
|---|---|---|
| User | ✅ Sí | Preservar historial, compliment legal |
| Choreographer | ✅ Sí | Relacions amb dances |
| Dance | ✅ Sí | Historial de balls |
| OwnershipRequest | ✅ Sí | Auditoria obligatòria |
| Link | ❌ No | Sense relacions crítiques |
| VerificationCode | ❌ No | Temporal |
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 default | O migració en 2 passos |
| Eliminar columna | ⚠️ Després de deprecació | Marcar deprecated primer |
| Modificar tipus | ❌ No directament | Crear 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
| Data | Versió | Canvis |
|---|---|---|
| 2024-12 | 1.0 | Document inicial |