Campos de Auditoría en Spring Boot con Kotlin
Contexto
En aplicaciones Spring Boot con JPA es común rastrear cuándo se crea y actualiza cada entidad. Hacerlo manualmente es propenso a errores y rompe el principio de separación de responsabilidades. Existen mecanismos nativos que delegan esta responsabilidad al framework.
Objetivo
Implementar campos de auditoría automáticos (createdAt, updatedAt) en entidades JPA usando las capacidades nativas
de Spring Data JPA, eliminando la asignación manual de fechas y garantizando consistencia.
Alcance
Incluye
- Implementación con callbacks JPA (
@PrePersist/@PreUpdate). - Configuración de Spring Data JPA Auditing.
- Comparativa de enfoques.
- Consideraciones para testing.
No incluye
- Auditoría de usuario (
createdBy/updatedBy) — mencionado como pendiente. - Frameworks externos de auditoría (Envers, Hibernate Envers avanzado).
- Bases de datos NoSQL.
Detalle
Enfoque 1 — Callbacks JPA: @PrePersist y @PreUpdate
Las fechas se asignan dentro de la propia entidad mediante callbacks del ciclo de vida JPA:
@Entity
@Table(name = "users")
class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(nullable = false, unique = true)
val username: String,
@field:Email(message = "Invalid email format")
@Column(nullable = false, unique = true)
val email: String,
@Column(name = "encrypted_password", nullable = false)
val encryptedPassword: String,
@Column(nullable = false)
val active: Boolean = true,
@Column(name = "created_at", nullable = false, updatable = false)
var createdAt: LocalDateTime? = null,
@Column(name = "updated_at", nullable = true)
var updatedAt: LocalDateTime? = null
) {
@PrePersist
fun prePersist() {
createdAt = LocalDateTime.now()
}
@PreUpdate
fun preUpdate() {
updatedAt = LocalDateTime.now()
}
fun copy(updatedAt: LocalDateTime? = this.updatedAt): User = User(
id = this.id,
username = this.username,
email = this.email,
encryptedPassword = this.encryptedPassword,
active = this.active,
createdAt = this.createdAt,
updatedAt = updatedAt
)
}
Características:
createdAtse asigna al persistir;updatable = falseimpide que cambie.updatedAtse actualiza en cada modificación.- Los callbacks (
@PrePersist,@PreUpdate) se ejecutan antes de las operaciones SQL.
Enfoque 2 — Spring Data JPA Auditing (Recomendado)
Enfoque desacoplado, configuración centralizada y extensible a auditoría de usuario.
Entidad
@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener::class)
class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(nullable = false, unique = true)
val username: String,
@field:Email(message = "Invalid email format")
@Column(nullable = false, unique = true)
val email: String,
@Column(name = "encrypted_password", nullable = false)
val encryptedPassword: String,
@Column(nullable = false)
val active: Boolean = true,
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
var createdAt: LocalDateTime? = null,
@LastModifiedDate
@Column(name = "updated_at", nullable = true)
var updatedAt: LocalDateTime? = null
) {
fun copy(updatedAt: LocalDateTime? = this.updatedAt): User = User(
id = this.id,
username = this.username,
email = this.email,
encryptedPassword = this.encryptedPassword,
active = this.active,
createdAt = this.createdAt,
updatedAt = updatedAt
)
}
Habilitar JPA Auditing
@Configuration
@EnableJpaAuditing
class JpaConfig
Anotaciones Clave
| Anotación | Descripción |
|---|---|
@EntityListeners(AuditingEntityListener::class) | Activa el listener de auditoría en la entidad |
@CreatedDate | Asigna la fecha de creación |
@LastModifiedDate | Actualiza la fecha en cada modificación |
@CreatedBy | Asigna el usuario creador (requiere AuditorAware) |
@LastModifiedBy | Actualiza el usuario modificador |
@EnableJpaAuditing | Habilita la auditoría a nivel de aplicación |
Clase Base Reutilizable
Para evitar duplicar los campos en cada entidad:
@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class Auditable {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
var createdAt: LocalDateTime? = null
protected set
@LastModifiedDate
@Column(name = "updated_at")
var updatedAt: LocalDateTime? = null
protected set
}
Las entidades heredan: class User(...) : Auditable().
Comparativa de Enfoques
| Aspecto | @PrePersist / @PreUpdate | Spring Data JPA Auditing |
|---|---|---|
| Configuración | Mínima (solo entidad) | Requiere @EnableJpaAuditing |
| Acoplamiento | Acoplado a callbacks JPA | Desacoplado, manejado por Spring |
| Extensibilidad | Limitada | Soporta auditoría de usuario (@CreatedBy) |
| Mantenibilidad | Código repetido en cada entidad | Centralizado (vía @MappedSuperclass) |
| Testing | Más complejo (depende del EntityManager) | Facilita mocking con AuditorAware |
Consideraciones para Testing
- Con Auditing, inyectar un
AuditorAwaremockeado en tests de integración. - Verificar que
createdAtsea inmutable trasupdate(no debe cambiar). - Tests con
@DataJpaTestrequieren@EnableJpaAuditingen la configuración de test.
Decisiones
- Spring Data JPA Auditing como enfoque preferido: mayor desacoplamiento y mejor integración con el ecosistema.
updatedAtnullable: distingue registros nunca modificados de modificados.updatable = falseencreatedAt: garantiza inmutabilidad a nivel de base de datos.- Método
copypersonalizado: mantiene la inmutabilidad en Kotlin sobre campos controlados. LocalDateTimesobreDate: API moderna de Java 8+, sin problemas de mutabilidad.- Clase base
Auditable: evita duplicación en múltiples entidades.
Pendientes
- Implementar auditoría de usuario con
@CreatedByy@LastModifiedBy+AuditorAware. - Crear tests de integración que verifiquen los callbacks de auditoría.
- Documentar configuración de zona horaria (UTC vs. local).
- Evaluar uso de
Instanten lugar deLocalDateTimepara consistencia UTC.