Marca de tiempo de creación y marca de tiempo de última actualización con Hibernate y MySQL

244

Para una determinada entidad de Hibernate, tenemos el requisito de almacenar su hora de creación y la última vez que se actualizó. ¿Cómo diseñarías esto?

  • ¿Qué tipos de datos usaría en la base de datos (suponiendo MySQL, posiblemente en una zona horaria diferente a la JVM)? ¿Los tipos de datos serán conscientes de la zona horaria?

  • ¿Qué tipos de datos usaría en Java ( Date, Calendar, long, ...)?

  • ¿A quién responsabilizaría para configurar las marcas de tiempo: la base de datos, el marco ORM (Hibernate) o el programador de aplicaciones?

  • ¿Qué anotaciones usarías para el mapeo (por ejemplo @Temporal)?

No solo busco una solución de trabajo, sino una solución segura y bien diseñada.

ngn
fuente

Respuestas:

266

Si está utilizando las anotaciones JPA, puede usar @PrePersisty los @PreUpdateganchos de eventos hacen esto:

@Entity
@Table(name = "entities")    
public class Entity {
  ...

  private Date created;
  private Date updated;

  @PrePersist
  protected void onCreate() {
    created = new Date();
  }

  @PreUpdate
  protected void onUpdate() {
    updated = new Date();
  }
}

o puede usar la @EntityListeneranotación en la clase y colocar el código del evento en una clase externa.

Guðmundur Bjarni
fuente
77
Funciona sin problemas en J2SE, ya que @PrePersist y @PerUpdate son anotaciones JPA.
Kdeveloper
2
@ Kumar: en caso de que esté utilizando una sesión de Hibernate simple (en lugar de JPA), puede probar los oyentes de eventos de hibernate, aunque eso no es muy elegante y compacto frente a las anotaciones JPA.
Shailendra
43
En la Hibernación actual con JPA, se puede usar "@CreationTimestamp" y "@UpdateTimestamp"
Florian Loch
@FlorianLoch ¿hay un equivalente para Date en lugar de Timestamp? ¿O tendría que crear el mío?
Mike
150

Puedes usar @CreationTimestampy @UpdateTimestamp:

@CreationTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "create_date")
private Date createDate;

@UpdateTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "modify_date")
private Date modifyDate;
idmitriev
fuente
3
gracias hermano, una cosa tan pequeña necesita actualizar la marca de tiempo. No lo sabia Salvaste mi día.
Virendra Sagar
Hay TemporalType.DATEen el primer caso y TemporalType.TIMESTAMPen el segundo.
v.ladynev
¿Estás diciendo que esto también establece automáticamente los valores? Esa no es mi experiencia; parece incluso con @CreationTimestampy @UpdateTimestampuno necesita algo @Column(..., columnDefinition = "timestamp default current_timestamp"), o usa @PrePersisty @PreUpdate(este último también garantiza que los clientes no puedan establecer un valor diferente).
Arjan
2
Cuando actualizo el objeto y lo persisto, el bd perdió el create_date ... ¿por qué?
Brenno Leal
1
Estoy eliminando mi caso nullable=falsedel @Column(name = "create_date" , nullable=false)trabajo
Shantaram Tupe
113

Tomando los recursos en esta publicación junto con la información tomada de izquierda a derecha de diferentes fuentes, llegué con esta elegante solución, crea la siguiente clase abstracta

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@MappedSuperclass
public abstract class AbstractTimestampEntity {

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created", nullable = false)
    private Date created;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "updated", nullable = false)
    private Date updated;

    @PrePersist
    protected void onCreate() {
    updated = created = new Date();
    }

    @PreUpdate
    protected void onUpdate() {
    updated = new Date();
    }
}

y haga que todas sus entidades lo extiendan, por ejemplo:

@Entity
@Table(name = "campaign")
public class Campaign extends AbstractTimestampEntity implements Serializable {
...
}
Olivier Refalo
fuente
55
Esto es bueno hasta que desee agregar diferentes comportamientos exclusivos a sus entidades (y no puede extender más de una clase base). afaik, la única forma de obtener el mismo efecto sin una clase base es a través de los asistentes de aspecto o evento que ven la respuesta @kieren dixon
gpilotino
3
Haría esto usando un activador MySQL para que, incluso si la entidad completa no se guarda o se modifica por cualquier aplicación externa o consulta manual, todavía actualizará estos campos.
Webnet
3
¿Me puede dar algún ejemplo de trabajo porque estoy experimentando una excepciónnot-null property references a null or transient value: package.path.ClassName.created
Sumit Ramteke
@rishiAgar, no, no lo he hecho. Pero por ahora tengo asignar fecha a mi propiedad del constructor predeterminado. Te lo haré saber una vez que lo encuentre.
Sumit Ramteke
1
Cámbielo a @Column(name = "updated", nullable = false, insertable = false)para que funcione. Es interesante que esta respuesta haya recibido tantos votos positivos ..
displayname
20

1. Qué tipos de columna de base de datos debe usar

Tu primera pregunta fue:

¿Qué tipos de datos usaría en la base de datos (suponiendo MySQL, posiblemente en una zona horaria diferente a la JVM)? ¿Los tipos de datos serán conscientes de la zona horaria?

En MySQL, el TIMESTAMPtipo de columna cambia de la zona horaria local del controlador JDBC a la zona horaria de la base de datos, pero solo puede almacenar marcas de tiempo hasta '2038-01-19 03:14:07.999999, por lo que no es la mejor opción para el futuro.

Entonces, mejor uso DATETIME, que no tiene esta limitación de límite superior. Sin embargo, DATETIMEno es consciente de la zona horaria. Entonces, por esta razón, es mejor usar UTC en el lado de la base de datos y usar la hibernate.jdbc.time_zonepropiedad Hibernate.

Para obtener más detalles sobre la hibernate.jdbc.time_zoneconfiguración, consulte este artículo .

2. Qué tipo de propiedad de entidad debe usar

Su segunda pregunta fue:

¿Qué tipos de datos usarías en Java (Fecha, Calendario, largo, ...)?

En el lado de Java, puede usar Java 8 LocalDateTime. También puede usar el legado Date, pero los tipos de fecha y hora de Java 8 son mejores ya que son inmutables y no hacen un cambio de zona horaria a la zona horaria local al iniciar sesión.

Para obtener más detalles sobre los tipos de fecha / hora de Java 8 compatibles con Hibernate, consulte este artículo .

Ahora, también podemos responder esta pregunta:

¿Qué anotaciones usarías para el mapeo (por ejemplo @Temporal)?

Si está utilizando LocalDateTimeo java.sql.Timestamppara asignar una propiedad de entidad de marca de tiempo, entonces no necesita usarla @Temporalya que HIbernate ya sabe que esta propiedad debe guardarse como una marca de tiempo JDBC.

Solo si está utilizando java.util.Date, debe especificar la @Temporalanotación, así:

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_on")
private Date createdOn;

Pero, es mucho mejor si lo mapea así:

@Column(name = "created_on")
private LocalDateTime createdOn;

Cómo generar los valores de la columna de auditoría

Su tercera pregunta fue:

¿A quién responsabilizaría para configurar las marcas de tiempo: la base de datos, el marco ORM (Hibernate) o el programador de aplicaciones?

¿Qué anotaciones usarías para el mapeo (por ejemplo, @Temporal)?

Hay muchas maneras de lograr este objetivo. Puede permitir que la base de datos haga eso.

Para la create_oncolumna, podría usar una DEFAULTrestricción DDL, como:

ALTER TABLE post 
ADD CONSTRAINT created_on_default 
DEFAULT CURRENT_TIMESTAMP() FOR created_on;

Para la updated_oncolumna, puede usar un desencadenador de base de datos para establecer el valor de la columna CURRENT_TIMESTAMP()cada vez que se modifica una fila determinada.

O use JPA o Hibernate para configurarlos.

Supongamos que tiene las siguientes tablas de base de datos:

Tablas de bases de datos con columnas de auditoría

Y, cada tabla tiene columnas como:

  • created_by
  • created_on
  • updated_by
  • updated_on

Usando Hibernate @CreationTimestampy @UpdateTimestampanotaciones

Hibernate ofrece las anotaciones @CreationTimestampy @UpdateTimestampque se pueden utilizar para asignar las columnas created_ony updated_on.

Puede usar @MappedSuperclasspara definir una clase base que será extendida por todas las entidades:

@MappedSuperclass
public class BaseEntity {

    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "created_on")
    @CreationTimestamp
    private LocalDateTime createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @Column(name = "updated_on")
    @UpdateTimestamp
    private LocalDateTime updatedOn;

    @Column(name = "updated_by")
    private String updatedBy;

    //Getters and setters omitted for brevity
}

Y, todas las entidades extenderán el BaseEntity, así:

@Entity(name = "Post")
@Table(name = "post")
public class Post extend BaseEntity {

    private String title;

    @OneToMany(
        mappedBy = "post",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List<PostComment> comments = new ArrayList<>();

    @OneToOne(
        mappedBy = "post",
        cascade = CascadeType.ALL,
        orphanRemoval = true,
        fetch = FetchType.LAZY
    )
    private PostDetails details;

    @ManyToMany
    @JoinTable(
        name = "post_tag",
        joinColumns = @JoinColumn(
            name = "post_id"
        ),
        inverseJoinColumns = @JoinColumn(
            name = "tag_id"
        )
    )
    private List<Tag> tags = new ArrayList<>();

    //Getters and setters omitted for brevity
}

Para obtener más detalles sobre el uso @MappedSuperclass, consulte este artículo .

Sin embargo, incluso si los createdOny updateOnpropiedades se establecen por el Hibernate-específico @CreationTimestampy @UpdateTimestampanotaciones, el createdByy updatedByrequieren el registro de una devolución de llamada de aplicación, como se ilustra por la siguiente solución JPA.

Usando JPA @EntityListeners

Puede encapsular las propiedades de auditoría en un Embeddable:

@Embeddable
public class Audit {

    @Column(name = "created_on")
    private LocalDateTime createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @Column(name = "updated_on")
    private LocalDateTime updatedOn;

    @Column(name = "updated_by")
    private String updatedBy;

    //Getters and setters omitted for brevity
}

Y cree un AuditListenerpara establecer las propiedades de auditoría:

public class AuditListener {

    @PrePersist
    public void setCreatedOn(Auditable auditable) {
        Audit audit = auditable.getAudit();

        if(audit == null) {
            audit = new Audit();
            auditable.setAudit(audit);
        }

        audit.setCreatedOn(LocalDateTime.now());
        audit.setCreatedBy(LoggedUser.get());
    }

    @PreUpdate
    public void setUpdatedOn(Auditable auditable) {
        Audit audit = auditable.getAudit();

        audit.setUpdatedOn(LocalDateTime.now());
        audit.setUpdatedBy(LoggedUser.get());
    }
}

Para registrar el AuditListener, puede usar la @EntityListenersanotación JPA:

@Entity(name = "Post")
@Table(name = "post")
@EntityListeners(AuditListener.class)
public class Post implements Auditable {

    @Id
    private Long id;

    @Embedded
    private Audit audit;

    private String title;

    @OneToMany(
        mappedBy = "post",
        cascade = CascadeType.ALL,
        orphanRemoval = true
    )
    private List<PostComment> comments = new ArrayList<>();

    @OneToOne(
        mappedBy = "post",
        cascade = CascadeType.ALL,
        orphanRemoval = true,
        fetch = FetchType.LAZY
    )
    private PostDetails details;

    @ManyToMany
    @JoinTable(
        name = "post_tag",
        joinColumns = @JoinColumn(
            name = "post_id"
        ),
        inverseJoinColumns = @JoinColumn(
            name = "tag_id"
        )
    )
    private List<Tag> tags = new ArrayList<>();

    //Getters and setters omitted for brevity
}

Para obtener más detalles sobre la implementación de propiedades de auditoría con el JPA @EntityListener, consulte este artículo .

Vlad Mihalcea
fuente
Respuesta muy completa, gracias. No estoy de acuerdo con preferir datetimemás timestamp. Desea que su base de datos conozca la zona horaria de sus marcas de tiempo. Esto evita errores de conversión de zona horaria.
Ole VV
El timestsmptipo no almacena información de zona horaria. Simplemente hace una conversación de la aplicación TZ a DB TZ. En realidad, desea almacenar el TZ del cliente por separado y conversar en la aplicación antes de representar la interfaz de usuario.
Vlad Mihalcea
Correcto. MySQL timestampsiempre está en UTC. MySQL convierte los TIMESTAMPvalores de la zona horaria actual a UTC para el almacenamiento, y vuelve de UTC a la zona horaria actual para la recuperación. Documentación de MySQL: los tipos DATE, DATETIME y TIMESTAMP
Ole VV
17

También puede usar un interceptor para establecer los valores

Cree una interfaz llamada TimeStamped que implementen sus entidades

public interface TimeStamped {
    public Date getCreatedDate();
    public void setCreatedDate(Date createdDate);
    public Date getLastUpdated();
    public void setLastUpdated(Date lastUpdatedDate);
}

Definir el interceptor.

public class TimeStampInterceptor extends EmptyInterceptor {

    public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, 
            Object[] previousState, String[] propertyNames, Type[] types) {
        if (entity instanceof TimeStamped) {
            int indexOf = ArrayUtils.indexOf(propertyNames, "lastUpdated");
            currentState[indexOf] = new Date();
            return true;
        }
        return false;
    }

    public boolean onSave(Object entity, Serializable id, Object[] state, 
            String[] propertyNames, Type[] types) {
            if (entity instanceof TimeStamped) {
                int indexOf = ArrayUtils.indexOf(propertyNames, "createdDate");
                state[indexOf] = new Date();
                return true;
            }
            return false;
    }
}

Y regístralo en la fábrica de sesiones

Kieren Dixon
fuente
1
Funciona, gracias. Información adicional docs.jboss.org/hibernate/core/4.0/manual/en-US/html_single/…
Andrii Nemchenko
¡Esta es una solución si trabaja con SessionFactory en lugar de EntityManager!
olivmir
Solo para aquellos que sufren un problema similar al que yo tuve en este contexto: si su entidad no define estos campos adicionales (createdAt, ...) sino que los hereda de una clase padre, entonces esta clase padre debe ser anotada con @MappedSuperclass; de lo contrario, Hibernate no encuentra estos campos.
olivmir
17

Con la solución de Olivier, durante las declaraciones de actualización puede toparse con:

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: la columna 'creada' no puede ser nula

Para resolver esto, agregue Updatable = false a la anotación @Column del atributo "creado":

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created", nullable = false, updatable=false)
private Date created;
endriju
fuente
1
Estamos utilizando @Version. Cuando se establece una entidad, se realizan dos llamadas, una para guardar y otra para actualizar. Estaba enfrentando el mismo problema debido a esto. Una vez que lo agregué @Column(updatable = false)resolvió mi problema.
Ganesh Satpute
12

Gracias a todos los que ayudaron. Después de investigar un poco (soy el tipo que hizo la pregunta), esto es lo que encontré que tiene más sentido:

  • Tipo de columna de la base de datos: el número de milisegundos agnósticos de zona horaria desde 1970 representado como decimal(20)porque 2 ^ 64 tiene 20 dígitos y el espacio en disco es económico; seamos directos. Además, no usaré DEFAULT CURRENT_TIMESTAMPni disparadores. No quiero magia en el DB.

  • Java tipo de campo: long. La marca de tiempo de Unix es compatible con varias bibliotecas, longno tiene problemas con Y2038, la aritmética de marca de tiempo es rápida y fácil (principalmente operador <y operador +, suponiendo que no haya días / meses / años involucrados en los cálculos). Y, lo que es más importante, tanto las primitivas longs como las java.lang.Longs son inmutables, efectivamente pasadas por valor, a diferencia de java.util.Dates; Realmente me enojaría encontrar algo como foo.getLastUpdate().setTime(System.currentTimeMillis())cuando depuro el código de otra persona.

  • El marco ORM debe ser responsable de completar los datos automáticamente.

  • Todavía no he probado esto, pero solo mirando los documentos supongo que @Temporalhará el trabajo; No estoy seguro de si podría usar @Versionpara este propósito. @PrePersisty @PreUpdateson buenas alternativas para controlar eso manualmente. Agregar eso al supertipo de capa (clase base común) para todas las entidades, es una buena idea, siempre y cuando realmente desee la marca de tiempo para todas sus entidades.

ngn
fuente
Si bien longs y longs pueden ser inmutables, eso no lo ayudará en la situación que describa. Todavía pueden decir foo.setLastUpdate (nuevo Long (System.currentTimeMillis ());
Ian McLaird
2
Esta bien. Hibernate requiere el setter de todos modos (o intentará acceder al campo directamente a través de la reflexión). Estaba hablando de la dificultad para perseguir quién está modificando la marca de tiempo de nuestro código de aplicación. Es complicado cuando puedes hacerlo usando un getter.
ngn
Estoy de acuerdo con su afirmación de que el marco ORM debería ser responsable de completar la fecha automáticamente, pero iría un paso más allá y diría que la fecha debe establecerse desde el reloj del servidor de la base de datos, en lugar del cliente. No estoy claro si esto logra este objetivo. En sql, puedo hacer esto usando la función sysdate, pero no sé cómo hacer esto en Hibernate o cualquier implementación de JPA.
MiguelMunoz
No quiero magia en el DB. Entiendo lo que quieres decir, pero me gusta considerar el hecho de que la base de datos debería protegerse de los desarrolladores malos / nuevos / despistados. La integridad de los datos es muy importante en una gran empresa, no puede confiar en que otros inserten buenos datos. Las restricciones, los valores predeterminados y los FK ayudarán a lograr eso.
Icegras
6

En caso de que esté utilizando la API de sesión, las devoluciones de llamada PrePersist y PreUpdate no funcionarán de acuerdo con esta respuesta .

Estoy usando el método persist () de Hibernate Session en mi código, por lo que la única forma de hacer que esto funcione es con el código a continuación y siguiendo esta publicación de blog (también publicada en la respuesta ).

@MappedSuperclass
public abstract class AbstractTimestampEntity {

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created")
    private Date created=new Date();

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "updated")
    @Version
    private Date updated;

    public Date getCreated() {
        return created;
    }

    public void setCreated(Date created) {
        this.created = created;
    }

    public Date getUpdated() {
        return updated;
    }

    public void setUpdated(Date updated) {
        this.updated = updated;
    }
}
vicch
fuente
Debería devolver objetos clonados como updated.clone()si no, otros componentes pueden manipular el estado interno (fecha)
1ambda
3

El siguiente código funcionó para mí.

package com.my.backend.models;

import java.util.Date;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

import com.fasterxml.jackson.annotation.JsonIgnore;

import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import lombok.Getter;
import lombok.Setter;

@MappedSuperclass
@Getter @Setter
public class BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Integer id;

    @CreationTimestamp
    @ColumnDefault("CURRENT_TIMESTAMP")
    protected Date createdAt;

    @UpdateTimestamp
    @ColumnDefault("CURRENT_TIMESTAMP")
    protected Date updatedAt;
}
prranay
fuente
Hola, ¿por qué lo necesitaríamos protected Integer id;como protecteden la clase para padres en general, porque no podría usarlo en mis casos de prueba como.getId()
shareef
2

Un buen enfoque es tener una clase base común para todas sus entidades. En esta clase base, puede tener su propiedad de identificación si se nombra comúnmente en todas sus entidades (un diseño común), sus propiedades de creación y fecha de última actualización.

Para la fecha de creación, simplemente mantenga una propiedad java.util.Date . Asegúrese de inicializarlo siempre con una nueva fecha () .

Para el último campo de actualización, puede usar una propiedad de marca de tiempo, debe asignarla con @Version. Con esta anotación, Hibernate actualizará automáticamente la propiedad. Tenga en cuenta que Hibernate también aplicará un bloqueo optimista (es algo bueno).

bernardn
fuente
2
Usar una columna de marca de tiempo para un bloqueo optimista es una mala idea. Utilice siempre una columna de versión entera. La razón es que 2 JVM pueden estar en diferentes momentos y pueden no tener una precisión de milisegundos. Si, en cambio, hace que hibernar use la marca de tiempo de la base de datos, eso significaría selecciones adicionales de la base de datos. En su lugar, solo use el número de versión.
Sethu
2

Solo para reforzar: java.util.Calenderno es para marcas de tiempo . java.util.Datees, por un momento, agnóstico de cosas regionales como las zonas horarias. La mayoría de las bases de datos almacenan cosas de esta manera (incluso si parecen no serlo; esto generalmente es una configuración de zona horaria en el software del cliente; los datos son buenos)

davetron5000
fuente
1

Como tipo de datos en JAVA, recomiendo utilizar java.util.Date. Me encontré con problemas de zona horaria bastante desagradables al usar Calendar. Ver este hilo .

Para configurar las marcas de tiempo, recomendaría usar un enfoque AOP o simplemente podría usar Triggers en la tabla (en realidad, esto es lo único que considero aceptable el uso de disparadores).

huo73
fuente
1

Puede considerar almacenar la hora como DateTime y en UTC. Por lo general, uso DateTime en lugar de Timestamp debido al hecho de que MySql convierte las fechas a UTC y de nuevo a la hora local al almacenar y recuperar los datos. Prefiero mantener cualquiera de ese tipo de lógica en un solo lugar (capa empresarial). Sin embargo, estoy seguro de que hay otras situaciones en las que es preferible usar Timestamp.

mmacaulay
fuente
1

Tuvimos una situación similar. Estábamos usando Mysql 5.7.

CREATE TABLE my_table (
        ...
      updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    );

Esto funcionó para nosotros.

amdg
fuente
También funciona en un caso cuando los datos son modificados por una consulta SQL directamente en la base de datos. @PrePersisty @PrePersistno cubras tal caso.
pidabrow
1

Si estamos usando @Transactional en nuestros métodos, @CreationTimestamp y @UpdateTimestamp guardarán el valor en DB pero volverán nulos después de usar save (...).

En esta situación, usar saveAndFlush (...) hizo el truco

chomp
fuente
0

Creo que es mejor no hacer esto en el código Java, simplemente puede establecer el valor predeterminado de la columna en la definición de la tabla MySql. ingrese la descripción de la imagen aquí

Wayne Wei
fuente