使用Hibernate和MySQL创建时间戳和最后更新时间戳


244

对于某些Hibernate实体,我们需要存储其创建时间和上次更新时间。您将如何设计?

  • 您将在数据库中使用哪些数据类型(假设使用MySQL,可能与JVM时区不同)?数据类型是否支持时区?

  • 你会在Java中使用什么数据类型(DateCalendarlong,...)?

  • 您将由谁负责设置时间戳(数据库,ORM框架(休眠)或应用程序程序员)?

  • 您将使用哪些注释进行映射(例如@Temporal)?

我不仅在寻找可行的解决方案,而且还在寻找安全且设计良好的解决方案。

Answers:


266

如果使用的是JPA批注,则可以使用@PrePersist@PreUpdate事件挂钩执行此操作:

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

或者您可以@EntityListener在类上使用注释,并将事件代码放置在外部类中。


7
在J2SE中可以正常工作,因为@PrePersist和@PerUpdate是JPA批注。
Kdeveloper 2010年

2
@Kumar-如果您使用普通的Hibernate会话(而不是JPA),则可以尝试使用hibernate事件侦听器,尽管与JPA批注相比,这不是很优雅和紧凑。
谢朗德拉(Shailendra)

43
在当前使用JPA的Hibernate中,可以使用“ @CreationTimestamp”和“ @UpdateTimestamp”
Florian Loch

@FlorianLoch是否有等效的Date而不是Timestamp?还是我必须创建自己的?
麦克

149

您可以使用@CreationTimestamp@UpdateTimestamp

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

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

3
谢谢兄弟这么小的事情需要更新时间戳。我不知道。你救了我的一天。
Virendra Sagar'1

TemporalType.DATE第一种情况和TemporalType.TIMESTAMP第二种情况都有。
v.ladynev

您是说这还会自动设置值吗?那不是我的经验;似乎即使有了@CreationTimestamp@UpdateTimestamp也可能需要@Column(..., columnDefinition = "timestamp default current_timestamp")或使用@PrePersist@PreUpdate(后者也很好地确保了客户不能设置其他值)。
Arjan

2
当我更新对象并保留它时,BD丢失了create_date ...为什么?
布伦诺·里尔(Brenno Leal)

1
IM我的情况下消除nullable=false@Column(name = "create_date" , nullable=false)工作
项塔兰TUPE

113

利用本文中的资源以及从不同来源获得的左右信息,我带来了这个优雅的解决方案,创建了以下抽象类

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

并让您的所有实体对其进行扩展,例如:

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

5
除非您想向实体添加不同的排他行为(并且您不能扩展多个基类),否则这很好。afaik在没有基类的情况下获得相同效果的唯一方法是通过Aspectj Itd或事件侦听器查看@kieren dixon答案
gpilotino 2012年

3
我将使用MySQL触发器来执行此操作,以便即使未保存完整实体或未通过任何外部应用程序或手动查询修改完整实体,它仍将更新这些字段。
Webnet

3
你可以给我任何工作的例子,因为我遇到了例外not-null property references a null or transient value: package.path.ClassName.created
萨米特Ramteke

@rishiAgar,不,我没有。但是现在我已经从默认构造函数为我的属性分配了日期。一旦找到,便会通知您。
Sumit Ramteke 2014年

1
对其进行更改@Column(name = "updated", nullable = false, insertable = false)以使其起作用。有趣的是,这个答案获得了很多好评..
displayname

20

1.您应该使用哪种数据库列类型

您的第一个问题是:

您将在数据库中使用哪些数据类型(假设使用MySQL,可能与JVM时区不同)?数据类型是否支持时区?

在MySQL中,TIMESTAMP列类型确实从JDBC驱动程序本地时区转换为数据库时区,但是它只能存储时间戳,最大时为'2038-01-19 03:14:07.999999,因此它不是将来的最佳选择。

因此,最好使用DATETIME没有上限的限制。但是,DATETIME不是时区感知的。因此,由于这个原因,最好在数据库端使用UTC并使用hibernate.jdbc.time_zoneHibernate属性。

有关hibernate.jdbc.time_zone设置的更多详细信息,请参阅本文

2.您应该使用哪种实体属性类型

您的第二个问题是:

您将在Java中使用哪些数据类型(日期,日历,长型,...)?

在Java方面,您可以使用Java 8 LocalDateTime。您也可以使用legacy Date,但是Java 8 Date / Time类型更好,因为它们是不可变的,并且在记录它们时不要将时区转换为本地时区。

有关Hibernate支持的Java 8日期/时间类型的更多详细信息,请参阅本文

现在,我们也可以回答这个问题:

您将使用哪些注释进行映射(例如@Temporal)?

如果使用LocalDateTimejava.sql.Timestamp映射时间戳实体属性,则不需要使用,@Temporal因为HIbernate已经知道该属性将另存为JDBC时间戳。

仅当使用时java.util.Date,才需要指定@Temporal注释,如下所示:

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

但是,如果这样映射它会更好:

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

如何生成审核列值

您的第三个问题是:

您将由谁负责设置时间戳(数据库,ORM框架(休眠)或应用程序程序员)?

您将使用哪些注释进行映射(例如@Temporal)?

您可以通过多种方式实现此目标。您可以允许数据库执行此操作。

对于该create_on列,您可以使用DEFAULTDDL约束,例如:

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

对于updated_on列,您可以在CURRENT_TIMESTAMP()每次修改给定行时使用DB触发器来设置列值。

或者,使用JPA或Hibernate进行设置。

假设您具有以下数据库表:

具有审计列的数据库表

而且,每个表都有类似以下的列:

  • created_by
  • created_on
  • updated_by
  • updated_on

使用休眠@CreationTimestamp@UpdateTimestamp注释

Hibernate提供了@CreationTimestamp@UpdateTimestamp注释,可用于映射created_onupdated_on列。

您可以@MappedSuperclass用来定义将由所有实体扩展的基类:

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

并且,所有实体都将扩展BaseEntity,如下所示:

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

有关使用的更多详细信息@MappedSuperclass,请查看本文

但是,即使createdOnand updateOn属性是由特定于Hibernate的@CreationTimestampand @UpdateTimestamp注释设置的,createdByand也updatedBy需要注册一个应用程序回调,如以下JPA解决方案所示。

使用JPA @EntityListeners

您可以将审核属性封装在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
}

并且,创建一个AuditListener来设置审核属性:

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

要注册AuditListener,您可以使用@EntityListenersJPA批注:

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

有关使用JPA实施审核属性的更多详细信息@EntityListener,请参阅本文


非常详尽的答案,谢谢。我不同意宁愿datetimetimestamp。您希望数据库知道时间戳的时区。这样可以防止时区转换错误。
Ole VV

timestsmp类型不存储时区信息。它只是进行从应用程序TZ到DB TZ的对话。实际上,您要单独存储客户端TZ,并在呈现UI之前在应用程序中进行对话。
Vlad Mihalcea

正确。MySQL timestamp始终采用UTC。MySQL将TIMESTAMP值从当前时区转换为UTC以进行存储,然后从UTC转换为当前时区以进行检索。 MySQL文档:DATE,DATETIME和TIMESTAMP类型
Ole VV

17

您也可以使用拦截器来设置值

创建一个称为TimeStamped的接口,您的实体可以实现该接口

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

定义拦截器

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

并在会话工厂中注册



如果您使用SessionFactory而不是EntityManager,这是一种解决方案!
olivmir

只针对那些遭受与我在此情况下相似的问题的人:如果您的实体本身未定义这些额外的字段(createdAt,...),而是从父类继承而来,则必须对该父类进行注释使用@MappedSuperclass-否则Hibernate找不到这些字段。
olivmir

17

使用奥利维尔的解决方案,在更新语句期间,您可能会遇到:

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException:列'created'不能为null

要解决此问题,请将updatable = false添加到“创建的”属性的@Column注释中:

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

1
我们正在使用@Version。设置实体后,将进行两次调用,一次调用保存,另一次更新。因此,我面临着同样的问题。一旦添加,@Column(updatable = false)就解决了我的问题。
加内什

12

谢谢大家的帮助。自己做了一些研究之后(我是问这个问题的人),这是我发现最有意义的:

  • 数据库列类型:自1970年以来与时区无关的毫秒数,表示为decimal(20)2 ^ 64有20位数字,并且磁盘空间便宜;让我们简单明了。另外,我将既不使用DEFAULT CURRENT_TIMESTAMP,也不使用触发器。我不希望数据库中有任何魔术。

  • Java字段类型:long。Unix时间戳在各个库中得到了很好的支持,long没有Y2038问题,时间戳算法快速简便(主要是operator <和operator +,假定计算中不涉及日/月/年)。而且,最重要的是,longjava.lang.Longs 不同,原始s和s都是不可变的-有效地通过值传递java.util.Datefoo.getLastUpdate().setTime(System.currentTimeMillis())当调试其他人的代码时,我会很生气地发现类似的东西。

  • ORM框架应负责自动填充数据。

  • 我还没有测试过,但是仅查看我认为@Temporal可以完成此工作的文档。不知道我是否可以@Version用于此目的。 @PrePersist并且@PreUpdate是手动控制的好选择。如果您确实想为所有实体加盖时间戳,那么将其添加到所有实体的层超类型(公共基类)中是一个不错的主意。


尽管多头和多头可能是一成不变的,但这对您描述的情况没有帮助。他们仍然可以说foo.setLastUpdate(new Long(System.currentTimeMillis());
Ian McLaird

2
没关系。Hibernate无论如何都需要setter(否则它将尝试通过反射直接访问该字段)。我说的是很难追逐谁在从我们的应用程序代码中修改时间戳。当您可以使用吸气剂做到这一点时,这很棘手。
ngn

我同意您的说法,即ORM框架应负责自动填充日期,但是我要走得更远,说日期应该从数据库服务器而不是客户端的时钟中设置。我尚不清楚这是否可以实现此目标。在sql中,我可以使用sysdate函数执行此操作,但是我不知道如何在Hibernate或任何JPA实现中执行此操作。
MiguelMunoz

我不希望数据库中有任何魔术。我明白您的意思,但是我想考虑一个事实,即数据库应该保护自己免受不良/新手/无知的开发人员的侵害。在大型公司中,数据完整性非常重要,您不能依靠其他人来插入良好的数据。约束,默认值和FK将有助于实现这一目标。
Icegras

6

如果您正在使用会话API,则根据此答案,PrePersist和PreUpdate回调将不起作用。

我在我的代码中使用了Hibernate Session的persist()方法,因此,我可以执行此工作的唯一方法是使用下面的代码以及此博客文章之后的代码(也发布在答案中)。

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

应该像updated.clone()其他对象一样返回克隆的对象,否则其他组件可以操纵内部状态(日期)
1ambda


3

以下代码为我工作。

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

嗨,为什么我们需要protected Integer id;protected在一般的父类,因为我不能在我的测试案例使用它.getId()
谢里夫

2

一个好的方法是为所有实体都使用一个通用的基类。在此基类中,如果您的id属性在所有实体中都被通用命名(一个共同的设计),您的创建和最后更新日期属性,则可以具有它的id属性。

对于创建日期,您只需保留一个java.util.Date属性。确保始终使用new Date()对其进行初始化。

对于最后一个更新字段,您可以使用Timestamp属性,您需要使用@Version对其进行映射。使用此注释,该属性将由Hibernate自动更新。注意,Hibernate也将应用乐观锁定(这是一件好事)。


2
使用时间戳列进行乐观锁定不是一个好主意。始终使用整数版本列。原因是,两个JVM可能处于不同的时间,并且可能没有毫秒精度。如果改为让休眠使用数据库时间戳,则意味着要从数据库中进行其他选择。而是只使用版本号。
sethu

2

只是为了加强:java.util.Calender不适用于Timestampsjava.util.Date在一段时间内,与时区等区域性事物无关。大多数数据库都以这种方式存储事物(即使它们看起来不那么存储;这通常是客户端软件中的时区设置;数据很好)


1

作为JAVA中的数据类型,我强烈建议使用java.util.Date。使用日历时,我遇到了非常讨厌的时区问题。看到这个线程

对于设置时间戳,我建议您使用AOP方法,或者您可以简单地在表上使用触发器(实际上,这是我发现使用触发器的唯一方法)。


1

您可能考虑将时间存储为DateTime和UTC。我通常使用DateTime代替Timestamp,因为在存储和检索数据时MySql会将日期转换为UTC并转换回本地时间。我宁愿将任何一种逻辑都放在一个地方(业务层)。我敢肯定,在其他情况下,最好使用时间戳记。


1

我们有类似的情况。我们正在使用Mysql 5.7。

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

这对我们有用。


在直接通过数据库中的SQL查询修改数据的情况下,此方法也适用。@PrePersist而且@PrePersist不要涵盖这种情况。
pidabrow

1

如果我们在方法中使用@Transactional,则@CreationTimestamp和@UpdateTimestamp会将值保存在DB中,但在使用save(...)之后将返回null。

在这种情况下,使用saveAndFlush(...)就可以了


0

我认为在Java代码中不执行此操作比较整洁,您只需在MySql表定义中设置列默认值即可。 在此处输入图片说明

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.