Data / ora della creazione e data / ora dell'ultimo aggiornamento con Hibernate e MySQL


244

Per una certa entità Hibernate abbiamo l'obbligo di memorizzare l'ora di creazione e l'ultima volta che è stato aggiornato. Come lo progetteresti?

  • Quali tipi di dati useresti nel database (supponendo MySQL, possibilmente in un fuso orario diverso da quello della JVM)? I tipi di dati saranno consapevoli del fuso orario?

  • Quali tipi di dati che si utilizza in Java ( Date, Calendar, long, ...)?

  • Di chi saresti responsabile per l'impostazione dei timestamp: il database, il framework ORM (Hibernate) o il programmatore dell'applicazione?

  • Quali annotazioni useresti per la mappatura (ad es. @Temporal)?

Non sto solo cercando una soluzione funzionante, ma una soluzione sicura e ben progettata.

Risposte:


266

Se si utilizzano le annotazioni JPA, è possibile utilizzare @PrePersiste gli @PreUpdatehook di eventi eseguono questa operazione:

@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();
  }
}

oppure puoi usare l' @EntityListenerannotazione sulla classe e posizionare il codice evento in una classe esterna.


7
Funziona senza problemi in J2SE, poiché @PrePersist e @PerUpdate sono annotazioni JPA.
Kdeveloper,

2
@Kumar - Nel caso in cui tu stia utilizzando la semplice sessione Hibernate (anziché JPA) puoi provare gli ascoltatori di eventi in letargo, anche se non è molto elegante e compatto rispetto alle annotazioni JPA.
Shailendra,

43
Nell'attuale Hibernate con JPA si possono usare "@CreationTimestamp" e "@UpdateTimestamp"
Florian Loch

@FlorianLoch esiste un equivalente per Date anziché Timestamp? O dovrei crearne uno mio?
mike

151

Puoi semplicemente usare @CreationTimestampe @UpdateTimestamp:

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

@UpdateTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "modify_date")
private Date modifyDate;

3
grazie fratello una cosa così piccola ha bisogno di aggiornare il timestamp. Non lo sapevo. mi hai salvato la giornata.
Virendra Sagar,

C'è TemporalType.DATEnel primo caso e TemporalType.TIMESTAMPnel secondo.
v.ladynev,

Stai dicendo che anche questo imposta automaticamente i valori? Questa non è la mia esperienza; sembra anche con @CreationTimestampe @UpdateTimestampuno ne ha bisogno @Column(..., columnDefinition = "timestamp default current_timestamp")o ne usa ( @PrePersiste @PreUpdatequest'ultimo assicura anche che i clienti non possano impostare un valore diverso).
Arjan,

2
Quando aggiorno l'oggetto e lo persisto, il bd ha perso create_date ... perché?
Brenno Leal,

1
Im mio caso la rimozione nullable=falsedalla @Column(name = "create_date" , nullable=false)lavorato
Shantaram Tupe

113

Prendendo le risorse in questo post insieme alle informazioni prese a destra e sinistra da diverse fonti, sono arrivato con questa elegante soluzione, creare la seguente classe astratta

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();
    }
}

e estenderlo a tutte le entità, ad esempio:

@Entity
@Table(name = "campaign")
public class Campaign extends AbstractTimestampEntity implements Serializable {
...
}

5
questo va bene fino a quando non si desidera aggiungere diversi comportamenti esclusivi alle entità (e non è possibile estendere più di una classe di base). afaik l'unico modo per ottenere lo stesso effetto senza una classe base è però che gli ascoltatori di aspetti o di eventi vedano la risposta di @kieren dixon
gpilotino,

3
Lo farei usando un trigger MySQL in modo che anche se l'intera entità non viene salvata o modificata da un'applicazione esterna o da una query manuale, aggiornerà comunque questi campi.
Webnet,

3
puoi darmi qualche esempio funzionante perché sto riscontrando un'eccezionenot-null property references a null or transient value: package.path.ClassName.created
Sumit Ramteke,

@rishiAgar, No non l'ho fatto. Ma per ora ho assegnato la data alla mia proprietà dal costruttore predefinito. Ti farò sapere una volta che l'ho trovato.
Sumit Ramteke,

1
Modificalo in @Column(name = "updated", nullable = false, insertable = false)per farlo funzionare. Interessante che questa risposta abbia ricevuto così tanti voti ...
displayname

20

1. Quali tipi di colonne del database dovresti usare

La tua prima domanda è stata:

Quali tipi di dati useresti nel database (supponendo MySQL, possibilmente in un fuso orario diverso da quello della JVM)? I tipi di dati saranno consapevoli del fuso orario?

In MySQL, il TIMESTAMPtipo di colonna si sposta dal fuso orario locale del driver JDBC al fuso orario del database, ma può solo memorizzare i timestamp '2038-01-19 03:14:07.999999, quindi non è la scelta migliore per il futuro.

Quindi, meglio usare DATETIMEinvece, che non ha questo limite superiore. Tuttavia, DATETIMEnon è a conoscenza del fuso orario. Quindi, per questo motivo, è meglio usare UTC sul lato database e usare la hibernate.jdbc.time_zoneproprietà Hibernate.

Per maggiori dettagli hibernate.jdbc.time_zonesull'impostazione, consulta questo articolo .

2. Quale tipo di proprietà dell'entità dovresti usare

La tua seconda domanda era:

Quali tipi di dati useresti in Java (data, calendario, lungo, ...)?

Sul lato Java, è possibile utilizzare Java 8 LocalDateTime. Puoi anche usare l'eredità Date, ma i tipi di data / ora di Java 8 sono migliori poiché sono immutabili e non eseguono il trasferimento del fuso orario al fuso orario locale quando li registri.

Per maggiori dettagli sui tipi di data / ora Java 8 supportati da Hibernate, consulta questo articolo .

Ora possiamo anche rispondere a questa domanda:

Quali annotazioni useresti per la mappatura (ad es. @Temporal)?

Se si utilizza LocalDateTimeo java.sql.Timestampper mappare una proprietà di entità timestamp, non è necessario utilizzarla @Temporalpoiché HIbernate sa già che questa proprietà deve essere salvata come Timestamp JDBC.

Solo se stai utilizzando java.util.Date, devi specificare l' @Temporalannotazione, in questo modo:

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

Ma è molto meglio se lo mappa in questo modo:

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

Come generare i valori della colonna di controllo

La tua terza domanda era:

Di chi saresti responsabile per l'impostazione dei timestamp: il database, il framework ORM (Hibernate) o il programmatore dell'applicazione?

Quali annotazioni useresti per la mappatura (ad es. @Temporal)?

Esistono molti modi per raggiungere questo obiettivo. È possibile consentire al database di farlo.

Per la create_oncolonna, è possibile utilizzare un DEFAULTvincolo DDL, come:

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

Per la updated_oncolonna, è possibile utilizzare un trigger DB per impostare il valore della colonna CURRENT_TIMESTAMP()ogni volta che viene modificata una determinata riga.

Oppure, utilizzare JPA o Hibernate per impostarli.

Supponiamo che tu abbia le seguenti tabelle del database:

Tabelle del database con colonne di controllo

E ogni tabella ha colonne come:

  • created_by
  • created_on
  • updated_by
  • updated_on

Usando l'ibernazione @CreationTimestampe le @UpdateTimestampannotazioni

Hibernate offre le annotazioni @CreationTimestampe @UpdateTimestampche possono essere utilizzate per mappare le colonne created_one updated_on.

È possibile utilizzare @MappedSuperclassper definire una classe di base che verrà estesa da tutte le entità:

@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
}

E, tutte le entità estenderanno BaseEntity, in questo modo:

@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
}

Per maggiori dettagli sull'uso @MappedSuperclass, consulta questo articolo .

Tuttavia, anche se le createdOne updateOnproprietà sono impostate dalla Hibernate-specifico @CreationTimestampe @UpdateTimestampannotazioni, la createdBye updatedByrichiedono la registrazione di un callback domanda, come illustrato dalla seguente soluzione APP.

Utilizzando JPA @EntityListeners

È possibile incapsulare le proprietà di controllo in 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
}

E crea un AuditListenerper impostare le proprietà di controllo:

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());
    }
}

Per registrare il AuditListener, è possibile utilizzare l' @EntityListenersannotazione 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
}

Per ulteriori dettagli sull'implementazione delle proprietà di controllo con l'APP @EntityListener, consulta questo articolo .


Risposta molto approfondita, grazie. Non sono d'accordo su preferendo datetimesopra timestamp. Volete che il vostro database conosca il fuso orario dei vostri timestamp. Ciò impedisce errori di conversione del fuso orario.
Ole VV,

Il timestsmptipo non memorizza le informazioni sul fuso orario. Fa solo una conversazione dall'app TZ al DB TZ. In realtà, si desidera archiviare il client TZ separatamente ed eseguire la conversazione nell'applicazione prima di eseguire il rendering dell'interfaccia utente.
Vlad Mihalcea,

Corretta. MySQL timestampè sempre in UTC. MySQL converte i TIMESTAMPvalori dal fuso orario corrente a UTC per l'archiviazione e viceversa da UTC al fuso orario corrente per il recupero. Documentazione MySQL: i tipi DATE, DATETIME e TIMESTAMP
Ole VV,

17

È inoltre possibile utilizzare un intercettore per impostare i valori

Crea un'interfaccia chiamata TimeStamped che le tue entità implementano

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

Definire l'intercettore

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;
    }
}

E registralo con la factory di sessione



Questa è una soluzione, se lavori con SessionFactory anziché EntityManager!
olivmir,

Solo per coloro che soffrono di un problema simile a quello che ho fatto in questo contesto: se la tua entità non definisce esso stesso questi campi extra (createAt, ...) ma lo eredita da una classe genitore, allora questa classe genitore deve essere annotata con @MappedSuperclass - altrimenti Hibernate non trova questi campi.
olivmir,

17

Con la soluzione di Olivier, durante le dichiarazioni di aggiornamento potresti imbatterti in:

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: la colonna 'creata' non può essere nulla

Per risolvere questo, aggiungi updatable = false all'annotazione @Column dell'attributo "creato":

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

1
Stiamo usando @Version. Quando un'entità è impostata, vengono effettuate due chiamate una da salvare e un'altra da aggiornare. Stavo affrontando lo stesso problema a causa di questo. Una volta aggiunto, ho @Column(updatable = false)risolto il mio problema.
Ganesh Satpute,

12

Grazie a tutti coloro che hanno aiutato. Dopo aver fatto delle ricerche da solo (sono il ragazzo che ha posto la domanda), ecco cosa ho trovato più sensato:

  • Tipo di colonna del database: il numero di millisecondi del fuso orario fuso dal 1970 rappresentato come decimal(20)perché 2 ^ 64 ha 20 cifre e lo spazio su disco è economico; siamo semplici. Inoltre, non userò DEFAULT CURRENT_TIMESTAMPné trigger. Non voglio magia nel DB.

  • Java tipo di campo: long. Il timestamp di Unix è ben supportato in varie librerie, longnon ha problemi con Y2038, l'aritmetica del timestamp è semplice e veloce (principalmente operatore <e operatore +, supponendo che non siano coinvolti giorni / mesi / anni nei calcoli). E, soprattutto, sia le longs primitive java.lang.Longsono immutabili - efficacemente passate per valore - a differenza di java.util.Dates; Sarei davvero incazzato per trovare qualcosa di simile foo.getLastUpdate().setTime(System.currentTimeMillis())durante il debug del codice di qualcun altro.

  • Il framework ORM dovrebbe essere responsabile della compilazione automatica dei dati.

  • Non l'ho ancora testato, ma solo guardando i documenti presumo che @Temporalfaranno il lavoro; non sono sicuro se potrei usare @Versionper questo scopo. @PrePersiste @PreUpdatesono buone alternative per controllarlo manualmente. Aggiungerlo al supertipo di livello (classe base comune) per tutte le entità, è un'idea carina a condizione che tu voglia davvero il timestamp per tutte le tue entità.


Mentre longs e Longs possono essere immutabili, ciò non ti aiuterà nella situazione che descrivi. Possono ancora dire foo.setLastUpdate (nuovo Long (System.currentTimeMillis ());
Ian McLaird,

2
Va bene. L'ibernazione richiede comunque il setter (o proverà ad accedere direttamente al campo tramite la riflessione). Stavo parlando della difficoltà di inseguire chi sta modificando il timestamp dal nostro codice dell'applicazione. È difficile quando puoi farlo usando un getter.
ngn,

Concordo con l'affermazione secondo cui il framework ORM dovrebbe essere responsabile del riempimento automatico della data, ma farei un ulteriore passo avanti e direi che la data dovrebbe essere impostata dall'orologio del server di database, piuttosto che dal client. Non sono chiaro se questo raggiunge questo obiettivo. In sql, posso farlo usando la funzione sysdate, ma non so come farlo in Hibernate o in qualsiasi implementazione JPA.
MiguelMunoz,

Non voglio magia nel DB. Capisco cosa intendi, ma mi piace considerare il fatto che il database dovrebbe proteggersi da sviluppatori cattivi / nuovi / senza conoscenza. L'integrità dei dati è molto importante in una grande azienda, non è possibile fare affidamento su altri per inserire dati validi. Vincoli, valori predefiniti e FK aiuteranno a raggiungere questo obiettivo.
Icegras,

6

Nel caso in cui si utilizzi l'API Session, i callback PrePersist e PreUpdate non funzioneranno in base a questa risposta .

Sto usando il metodo persist () di Hibernate Session nel mio codice, quindi l'unico modo per farlo funzionare era con il codice qui sotto e seguendo questo post del blog (pubblicato anche nella risposta ).

@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;
    }
}

Dovrebbe restituire oggetti clonati come updated.clone()altrimenti, altri componenti possono manipolare lo stato interno (data)
1ambda


3

Il seguente codice ha funzionato per me.

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;
}

Ciao, perché dovremmo aver bisogno protected Integer id;come protectednella classe genitore in generale, perché non potevo usarlo nei miei casi di test come.getId()
Shareef

2

Un buon approccio consiste nell'avere una classe base comune per tutte le entità. In questa classe di base, puoi avere la tua proprietà id se è comunemente denominata in tutte le entità (un design comune), le proprietà della data di creazione e dell'ultimo aggiornamento.

Per la data di creazione, è sufficiente mantenere una proprietà java.util.Date . Assicurati di inizializzarlo sempre con la nuova Data () .

Per l'ultimo campo di aggiornamento, è possibile utilizzare una proprietà Timestamp, è necessario mapparla con @Version. Con questa annotazione la proprietà verrà aggiornata automaticamente da Hibernate. Attenzione che Hibernate applicherà anche il blocco ottimistico (è una buona cosa).


2
usare una colonna timestamp per un blocco ottimistico è una cattiva idea. Utilizzare sempre una colonna di versione intera. A ragione, 2 JVM potrebbero trovarsi in tempi diversi e potrebbero non avere una precisione di millisecondi. Se invece fai ibernare usa il timestamp DB, ciò significherebbe ulteriori selezioni dal DB. Invece usa solo il numero di versione.
sethu,

2

Solo per rafforzare: java.util.Calendernon è per i timestamp . java.util.Dateè per un momento nel tempo, agnostico di cose regionali come i fusi orari. La maggior parte dei database memorizza le cose in questo modo (anche se sembrano non farlo; di solito si tratta di un'impostazione del fuso orario nel software client; i dati sono buoni)


1

Come tipo di dati in JAVA, consiglio vivamente di usare java.util.Date. Ho riscontrato problemi di fuso orario piuttosto brutti durante l'utilizzo di Calendar. Vedi questa discussione .

Per impostare i timestamp, ti consiglio di utilizzare un approccio AOP o potresti semplicemente usare Trigger sul tavolo (in realtà questa è l'unica cosa che trovo mai accettabile l'uso di trigger).


1

È possibile considerare la memorizzazione dell'ora come DateTime e in UTC. In genere uso DateTime invece di Timestamp a causa del fatto che MySql converte le date in UTC e torna all'ora locale durante la memorizzazione e il recupero dei dati. Preferirei mantenere qualsiasi di quel tipo di logica in un unico posto (livello aziendale). Sono sicuro che ci sono altre situazioni in cui è preferibile utilizzare Timestamp.


1

Abbiamo avuto una situazione simile. Stavamo usando Mysql 5.7.

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

Questo ha funzionato per noi.


Funziona anche nel caso in cui i dati vengano modificati da una query SQL direttamente nel database. @PrePersiste @PrePersistnon coprire un caso del genere.
pidabrow

1

Se stiamo usando @Transactional nei nostri metodi, @CreationTimestamp e @UpdateTimestamp salveranno il valore nel DB ma restituiranno null dopo aver usato save (...).

In questa situazione, usare saveAndFlush (...) ha funzionato


0

Penso che sia più ordinario non farlo nel codice Java, puoi semplicemente impostare il valore predefinito della colonna nella definizione della tabella MySql. inserisci qui la descrizione dell'immagine

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.