Saltar al contenido principal

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:

  • createdAt se asigna al persistir; updatable = false impide que cambie.
  • updatedAt se 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ónDescripción
@EntityListeners(AuditingEntityListener::class)Activa el listener de auditoría en la entidad
@CreatedDateAsigna la fecha de creación
@LastModifiedDateActualiza la fecha en cada modificación
@CreatedByAsigna el usuario creador (requiere AuditorAware)
@LastModifiedByActualiza el usuario modificador
@EnableJpaAuditingHabilita 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 / @PreUpdateSpring Data JPA Auditing
ConfiguraciónMínima (solo entidad)Requiere @EnableJpaAuditing
AcoplamientoAcoplado a callbacks JPADesacoplado, manejado por Spring
ExtensibilidadLimitadaSoporta auditoría de usuario (@CreatedBy)
MantenibilidadCódigo repetido en cada entidadCentralizado (vía @MappedSuperclass)
TestingMás complejo (depende del EntityManager)Facilita mocking con AuditorAware

Consideraciones para Testing​

  • Con Auditing, inyectar un AuditorAware mockeado en tests de integración.
  • Verificar que createdAt sea inmutable tras update (no debe cambiar).
  • Tests con @DataJpaTest requieren @EnableJpaAuditing en la configuración de test.

Decisiones​

  • Spring Data JPA Auditing como enfoque preferido: mayor desacoplamiento y mejor integración con el ecosistema.
  • updatedAt nullable: distingue registros nunca modificados de modificados.
  • updatable = false en createdAt: garantiza inmutabilidad a nivel de base de datos.
  • Método copy personalizado: mantiene la inmutabilidad en Kotlin sobre campos controlados.
  • LocalDateTime sobre Date: 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 @CreatedBy y @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 Instant en lugar de LocalDateTime para consistencia UTC.

Referencias​