Horodatage de création et horodatage de la dernière mise à jour avec Hibernate et MySQL


244

Pour une certaine entité Hibernate, nous avons besoin de stocker son heure de création et la dernière fois qu'elle a été mise à jour. Comment concevriez-vous cela?

  • Quels types de données utiliseriez-vous dans la base de données (en supposant MySQL, éventuellement dans un fuseau horaire différent de celui de la JVM)? Les types de données seront-ils sensibles au fuseau horaire?

  • Quels types de données utiliseriez-vous en Java ( Date, Calendar, long, ...)?

  • Qui feriez-vous pour définir les horodatages - la base de données, le cadre ORM (Hibernate) ou le programmeur d'application?

  • Quelles annotations utiliseriez-vous pour le mappage (par exemple @Temporal)?

Je ne cherche pas seulement une solution de travail, mais une solution sûre et bien conçue.

Réponses:


266

Si vous utilisez les annotations JPA, vous pouvez utiliser @PrePersistet les @PreUpdatehooks d'événement le font:

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

ou vous pouvez utiliser l' @EntityListenerannotation sur la classe et placer le code d'événement dans une classe externe.


7
Fonctionne sans problème dans J2SE, car @PrePersist et @PerUpdate sont des annotations JPA.
Kdeveloper

2
@Kumar - Dans le cas où vous utilisez une session Hibernate ordinaire (au lieu de JPA), vous pouvez essayer des écouteurs d'hibernation, bien que ce ne soit pas très élégant et compact par rapport aux annotations JPA.
Shailendra

43
Dans Hibernate actuel avec JPA, on peut utiliser "@CreationTimestamp" et "@UpdateTimestamp"
Florian Loch

@FlorianLoch existe-t-il un équivalent pour Date plutôt que Timestamp? Ou devrais-je créer le mien?
Mike

151

Vous pouvez simplement utiliser @CreationTimestampet @UpdateTimestamp:

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

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

3
merci bro si petite chose besoin de mettre à jour l'horodatage. Je ne savais pas. tu m'as sauvé la journée.
Virendra Sagar

Il y en a TemporalType.DATEdans le premier cas et TemporalType.TIMESTAMPdans le second.
v.ladynev

Voulez-vous dire que cela définit également automatiquement les valeurs? Ce n'est pas mon expérience; il semble même avec @CreationTimestampet @UpdateTimestampon en a besoin @Column(..., columnDefinition = "timestamp default current_timestamp"), ou utilisez @PrePersistet @PreUpdate(ce dernier garantissant également que les clients ne peuvent pas définir une valeur différente).
Arjan

2
Quand je mets à jour l'objet et le persiste, le BD a perdu la date de création ... pourquoi?
Brenno Leal

1
Je retire mon cas nullable=falsedu @Column(name = "create_date" , nullable=false)travail
Shantaram Tupe

113

En prenant les ressources de ce post ainsi que des informations prises à gauche et à droite de différentes sources, je suis venu avec cette solution élégante, créez la classe abstraite suivante

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

et demandez à toutes vos entités de l'étendre, par exemple:

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

5
c'est bon jusqu'à ce que vous vouliez ajouter différents comportements exclusifs à vos entités (et vous ne pouvez pas étendre plus d'une classe de base). afaik le seul moyen d'obtenir le même effet sans une classe de base est que les auditeurs aspectj itd ou événement voient @kieren dixon réponse
gpilotino

3
Je le ferais en utilisant un déclencheur MySQL de sorte que même si l'entité complète n'est pas enregistrée ou modifiée par une application externe ou une requête manuelle, elle mettra toujours à jour ces champs.
Webnet

3
pouvez-vous me donner un exemple de travail parce que je vis une exceptionnot-null property references a null or transient value: package.path.ClassName.created
Sumit Ramteke

@rishiAgar, non, je ne l'ai pas fait. Mais pour l'instant, j'ai attribué une date à ma propriété à partir du constructeur par défaut. Je vous ferai savoir une fois que j'ai trouvé.
Sumit Ramteke

1
Changez-le pour @Column(name = "updated", nullable = false, insertable = false)le faire fonctionner. Intéressant que cette réponse ait eu autant de votes positifs ..
displayname

20

1. Quels types de colonnes de base de données vous devez utiliser

Votre première question était:

Quels types de données utiliseriez-vous dans la base de données (en supposant MySQL, éventuellement dans un fuseau horaire différent de celui de la JVM)? Les types de données seront-ils sensibles au fuseau horaire?

Dans MySQL, le TIMESTAMPtype de colonne passe du fuseau horaire local du pilote JDBC au fuseau horaire de la base de données, mais il ne peut stocker que des horodatages jusqu'à '2038-01-19 03:14:07.999999, ce n'est donc pas le meilleur choix pour l'avenir.

Donc, mieux vaut utiliser à la DATETIMEplace, qui n'a pas cette limite supérieure. Cependant, DATETIMEn'est pas au courant du fuseau horaire. Donc, pour cette raison, il est préférable d'utiliser UTC côté base de données et d'utiliser la hibernate.jdbc.time_zonepropriété Hibernate.

Pour plus de détails sur le hibernate.jdbc.time_zoneparamètre, consultez cet article .

2. Quel type de propriété d'entité utiliser

Votre deuxième question était:

Quels types de données utiliseriez-vous en Java (date, calendrier, long, ...)?

Côté Java, vous pouvez utiliser Java 8 LocalDateTime. Vous pouvez également utiliser l'héritage Date, mais les types de date / heure Java 8 sont meilleurs car ils sont immuables et ne font pas passer un fuseau horaire au fuseau horaire local lors de leur journalisation.

Pour plus de détails sur les types de date / heure Java 8 pris en charge par Hibernate, consultez cet article .

Maintenant, nous pouvons également répondre à cette question:

Quelles annotations utiliseriez-vous pour le mappage (par exemple @Temporal)?

Si vous utilisez LocalDateTimeou java.sql.Timestamppour mapper une propriété d'entité d'horodatage, vous n'avez pas besoin de l'utiliser, @Temporalcar HIbernate sait déjà que cette propriété doit être enregistrée en tant qu'horodatage JDBC.

Seulement si vous utilisez java.util.Date, vous devez spécifier l' @Temporalannotation, comme ceci:

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

Mais, c'est beaucoup mieux si vous le mappez comme ceci:

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

Comment générer les valeurs de colonne d'audit

Votre troisième question était:

Qui feriez-vous pour définir les horodatages - la base de données, le cadre ORM (Hibernate) ou le programmeur d'application?

Quelles annotations utiliseriez-vous pour le mappage (par exemple @Temporal)?

Il existe de nombreuses façons d'atteindre cet objectif. Vous pouvez autoriser la base de données à le faire.

Pour la create_oncolonne, vous pouvez utiliser une DEFAULTcontrainte DDL, comme:

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

Pour la updated_oncolonne, vous pouvez utiliser un déclencheur DB pour définir la valeur de la colonne à CURRENT_TIMESTAMP()chaque fois qu'une ligne donnée est modifiée.

Ou, utilisez JPA ou Hibernate pour les définir.

Supposons que vous disposez des tables de base de données suivantes:

Tables de base de données avec colonnes d'audit

Et, chaque table a des colonnes comme:

  • created_by
  • created_on
  • updated_by
  • updated_on

Utilisation d'Hibernate @CreationTimestampet d' @UpdateTimestampannotations

Hibernate propose les annotations @CreationTimestampet @UpdateTimestampqui peuvent être utilisées pour mapper les colonnes created_onet updated_on.

Vous pouvez utiliser @MappedSuperclasspour définir une classe de base qui sera étendue par toutes les entités:

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

Et, toutes les entités étendront le BaseEntity, comme ceci:

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

Pour plus de détails sur l'utilisation @MappedSuperclass, consultez cet article .

Cependant, même si les createdOnet updateOnpropriétés sont définies par la mise en veille prolongée spécifique @CreationTimestampet @UpdateTimestampannotations, la createdByet updatedByexigent l' enregistrement d' un rappel d'application, comme illustré par la solution JPA suivante.

Utilisation de JPA @EntityListeners

Vous pouvez encapsuler les propriétés d'audit dans 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
}

Et créez un AuditListenerpour définir les propriétés d'audit:

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

Pour enregistrer le AuditListener, vous pouvez utiliser l' @EntityListenersannotation 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
}

Pour plus de détails sur l'implémentation des propriétés d'audit avec le JPA @EntityListener, consultez cet article .


Réponse très complète, merci. Je suis en désaccord au sujet préférant datetimeplus timestamp. Vous voulez que votre base de données connaisse le fuseau horaire de vos horodatages. Cela empêche les erreurs de conversion de fuseau horaire.
Ole VV

Le timestsmptype ne stocke pas les informations de fuseau horaire. Il fait juste une conversation de l'application TZ à DB TZ. En réalité, vous souhaitez stocker le client TZ séparément et faire la conversation dans l'application avant de rendre l'interface utilisateur.
Vlad Mihalcea

Correct. MySQL timestampest toujours en UTC. MySQL convertit les TIMESTAMPvaleurs du fuseau horaire actuel en UTC pour le stockage, et en retour de l'UTC vers le fuseau horaire actuel pour la récupération. Documentation MySQL: Les types DATE, DATETIME et TIMESTAMP
Ole VV

17

Vous pouvez également utiliser un intercepteur pour définir les valeurs

Créez une interface appelée TimeStamped que vos entités implémentent

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

Définir l'intercepteur

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

Et enregistrez-le auprès de la fabrique de sessions


1
Fonctionne, merci. Informations supplémentaires docs.jboss.org/hibernate/core/4.0/manual/en-US/html_single/…
Andrii Nemchenko

C'est une solution, si vous travaillez avec SessionFactory au lieu d'EntityManager!
olivmir

Juste pour ceux qui souffrent d'un problème similaire à celui que j'ai rencontré dans ce contexte: si votre entité ne définit pas elle-même ces champs supplémentaires (createdAt, ...) mais l'hérite d'une classe parent, alors cette classe parent doit être annotée avec @MappedSuperclass - sinon Hibernate ne trouve pas ces champs.
olivmir

17

Avec la solution d'Olivier, lors des déclarations de mise à jour, vous pouvez rencontrer:

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: la colonne «créée» ne peut pas être nulle

Pour résoudre ce problème, ajoutez updatable = false à l'annotation @Column de l'attribut "créé":

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

1
Nous utilisons @Version. Lorsqu'une entité est définie, deux appels sont effectués, l'un pour enregistrer et l'autre pour mettre à jour. J'étais confronté au même problème à cause de cela. Une fois que j'ai ajouté, @Column(updatable = false)cela a résolu mon problème.
Ganesh Satpute

12

Merci à tous ceux qui ont aidé. Après avoir fait moi-même des recherches (je suis le gars qui a posé la question), voici ce que j'ai trouvé le plus logique:

  • Type de colonne de base de données: le nombre de millisecondes indépendant du fuseau horaire depuis 1970 représenté comme étant decimal(20)parce que 2 ^ 64 a 20 chiffres et que l'espace disque est bon marché; soyons francs. De plus, je n'utiliserai DEFAULT CURRENT_TIMESTAMPni déclencheurs. Je ne veux pas de magie dans la DB.

  • Java type de champ: long. L'horodatage Unix est bien pris en charge sur diverses bibliothèques, longn'a aucun problème Y2038, l'arithmétique d'horodatage est rapide et facile (principalement opérateur <et opérateur +, en supposant qu'aucun jour / mois / année ne soit impliqué dans les calculs). Et, plus important encore, les longs et les java.lang.Longs primitifs sont immuables - effectivement transmis par la valeur - contrairement à java.util.Dates; Je serais vraiment énervé de trouver quelque chose comme foo.getLastUpdate().setTime(System.currentTimeMillis())lors du débogage du code de quelqu'un d'autre.

  • Le cadre ORM devrait être chargé de remplir automatiquement les données.

  • Je n'ai pas encore testé cela, mais en regardant uniquement les documents, je suppose que @Temporalcela fera l'affaire; ne sais pas si je pourrais utiliser @Versionà cette fin. @PrePersistet @PreUpdatesont de bonnes alternatives pour contrôler cela manuellement. Ajouter cela au super-type de couche (classe de base commune) pour toutes les entités, est une idée mignonne à condition que vous souhaitiez vraiment un horodatage pour toutes vos entités.


Bien que les longs et les longs puissent être immuables, cela ne vous aidera pas dans la situation que vous décrivez. Ils peuvent toujours dire foo.setLastUpdate (nouveau Long (System.currentTimeMillis ());
Ian McLaird

2
C'est très bien. Hibernate nécessite de toute façon le passeur (ou il essaiera d'accéder directement au champ par réflexion). Je parlais de la difficulté à traquer qui modifie l'horodatage de notre code d'application. Il est difficile de le faire à l'aide d'un getter.
ngn

Je suis d'accord avec votre affirmation selon laquelle le cadre ORM devrait être responsable du remplissage automatique de la date, mais j'irais plus loin et dirais que la date devrait être définie à partir de l'horloge du serveur de base de données, plutôt que du client. Je ne sais pas si cela atteint cet objectif. En sql, je peux le faire en utilisant la fonction sysdate, mais je ne sais pas comment faire cela dans Hibernate ou n'importe quelle implémentation JPA.
MiguelMunoz

Je ne veux pas de magie dans la DB. Je vois ce que vous voulez dire, mais j'aime considérer le fait que la base de données devrait se protéger des mauvais / nouveaux / désemparés développeurs. L'intégrité des données est très importante dans une grande entreprise, vous ne pouvez pas compter sur les autres pour insérer de bonnes données. Les contraintes, les valeurs par défaut et les FK aideront à y parvenir.
Icegras

6

Si vous utilisez l'API de session, les rappels PrePersist et PreUpdate ne fonctionneront pas selon cette réponse .

J'utilise la méthode persist () de Hibernate Session dans mon code, donc la seule façon dont je pouvais faire ce travail était avec le code ci-dessous et après cet article de blog (également publié dans la réponse ).

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

Devrait retourner des objets clonés comme updated.clone()autrement, d'autres composants peuvent manipuler l'état interne (date)
1ambda


3

Le code suivant a fonctionné pour moi.

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

Salut, pourquoi aurions-nous besoin protected Integer id;de protectedla classe parent en général, parce que je ne pouvais pas l'utiliser dans mes cas de test comme.getId()
shareef

2

Une bonne approche consiste à avoir une classe de base commune pour toutes vos entités. Dans cette classe de base, vous pouvez avoir votre propriété id si elle est communément nommée dans toutes vos entités (une conception commune), vos propriétés de date de création et de dernière mise à jour.

Pour la date de création, vous conservez simplement une propriété java.util.Date . Assurez-vous de toujours l'initialiser avec new Date () .

Pour le dernier champ de mise à jour, vous pouvez utiliser une propriété Timestamp, vous devez la mapper avec @Version. Avec cette annotation, la propriété sera mise à jour automatiquement par Hibernate. Attention, Hibernate appliquera également un verrouillage optimiste (c'est une bonne chose).


2
l'utilisation d'une colonne d'horodatage pour un verrouillage optimiste est une mauvaise idée. Utilisez toujours une colonne de version entière. La raison en est que 2 JVM peuvent être à des moments différents et peuvent ne pas avoir une précision en millisecondes. Si vous faites plutôt hibernation utiliser l'horodatage de la base de données, cela signifierait des sélections supplémentaires dans la base de données. Utilisez plutôt le numéro de version.
sethu

2

Juste pour renforcer: ce java.util.Calendern'est pas pour les horodatages . java.util.Dateest pour un moment dans le temps, agnostique des choses régionales comme les fuseaux horaires. La plupart des bases de données stockent les choses de cette façon (même si elles ne le semblent pas; il s'agit généralement d'un paramètre de fuseau horaire dans le logiciel client; les données sont bonnes)


1

En tant que type de données dans JAVA, je recommande fortement d'utiliser java.util.Date. J'ai rencontré des problèmes de fuseau horaire assez désagréables lors de l'utilisation de Calendar. Voir ce fil .

Pour définir les horodatages, je recommanderais d'utiliser une approche AOP ou vous pourriez simplement utiliser des déclencheurs sur la table (en fait, c'est la seule chose que je trouve jamais l'utilisation de déclencheurs acceptable).


1

Vous pouvez envisager de stocker l'heure en tant que DateTime et en UTC. J'utilise généralement DateTime au lieu de Timestamp en raison du fait que MySql convertit les dates en UTC et en heure locale lors du stockage et de la récupération des données. Je préfère garder tout ce genre de logique en un seul endroit (couche métier). Je suis sûr qu'il existe d'autres situations où l'utilisation de Timestamp est préférable.


1

Nous avons eu une situation similaire. Nous utilisions Mysql 5.7.

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

Cela a fonctionné pour nous.


Cela fonctionne également dans le cas où les données sont modifiées par une requête SQL directement dans la base de données. @PrePersistet @PrePersistne couvre pas un tel cas.
pidabrow

1

Si nous utilisons @Transactional dans nos méthodes, @CreationTimestamp et @UpdateTimestamp enregistreront la valeur dans DB mais retourneront null après avoir utilisé save (...).

Dans cette situation, l'utilisation de saveAndFlush (...) a fait l'affaire


0

Je pense qu'il est plus simple de ne pas le faire dans le code Java, vous pouvez simplement définir la valeur par défaut de la colonne dans la définition de la table MySql. entrez la description de l'image ici

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.