使用Hibernate和MySQL创建时间戳和上次更新时间戳

时间:2008-10-21 12:06:50

标签: java hibernate timestamp

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

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

  • 您将在Java中使用哪些数据类型(DateCalendarlong,...)?

  • 您将负责设置时间戳 - 数据库,ORM框架(Hibernate)或应用程序员吗?

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

我不仅要寻找一个有效的解决方案,还要寻找一个安全且设计良好的解决方案。

18 个答案:

答案 0 :(得分:249)

如果您使用的是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注释,并将事件代码放在外部类中。

答案 1 :(得分:113)

您可以使用@CreationTimestamp@UpdateTimestamp

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

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

答案 2 :(得分:105)

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

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 {
...
}

答案 3 :(得分:44)

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

您的第一个问题是:

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

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

因此,最好改用DATETIME,因为它没有此上限限制。但是,DATETIME不支持时区。因此,因此,最好在数据库端使用UTC并使用hibernate.jdbc.time_zone Hibernate属性。

有关hibernate.jdbc.time_zone设置的更多详细信息,请查看this article

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

您的第二个问题是:

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

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

有关Hibernate支持的Java 8日期/时间类型的更多详细信息,请查看this article

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

您将使用哪些注释进行映射(例如@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列,您可以使用DEFAULT DDL约束,例如:

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

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

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

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

Database tables with audit columns

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

  • 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的更多详细信息,请查看this article

但是,即使createdOnupdateOn属性是由特定于Hibernate的@CreationTimestamp@UpdateTimestamp注释设置的,createdBy和{{1 }}需要注册一个应用程序回调,如以下JPA解决方案所示。

使用JPA updatedBy

您可以将审核属性封装在Embeddable中:

@EntityListeners

然后,创建一个@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 JPA批注:

@EntityListeners

有关使用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 } 实施审核属性的更多详细信息,请查看this article

答案 4 :(得分: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;
    }
}

并将其注册到会话工厂

答案 5 :(得分:13)

使用Olivier的解决方案,在更新语句期间,您可能遇到:

  

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException:Column&#39; created&#39;不能为空

要解决此问题,请将updatable = false添加到&#34;创建&#34;的@Column注释中。属性:

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

答案 6 :(得分:12)

感谢所有帮助过的人。在自己做了一些研究之后(我就是问过这个问题的人),这是我发现最有意义的事情:

  • 数据库列类型:自1970年以来的时区不可知的毫秒数,表示为 decimal(20) ,因为2 ^ 64有20位数,磁盘空间便宜;让我们直截了当。另外,我既不使用DEFAULT CURRENT_TIMESTAMP也不使用触发器。我想在DB中没有魔法。

  • Java字段类型: long 。 Unix时间戳在各种库中得到很好的支持,long没有Y2038问题,时间戳算法快速而简单(主要是运算符<和运算符+,假设没有天/月/年参与计算)。而且,最重要的是,原始longjava.lang.Long s 不可变 - 通过值有效传递 - 与java.util.Date s不同;在调试其他人的代码时,我真的很生气,找到像foo.getLastUpdate().setTime(System.currentTimeMillis())这样的东西。

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

  • 我还没有对此进行测试,但只考虑了我认为 @Temporal 可以完成工作的文档;不确定我是否可以将@Version用于此目的。 @PrePersist @PreUpdate 是手动控制的好方法。将其添加到所有实体的图层超类型(公共基类),这是一个可爱的想法,前提是您确实需要为实体的所有添加时间戳。

答案 7 :(得分:6)

如果您使用的是Session API,则根据此answer,PrePersist和PreUpdate回调将无效。

我在我的代码中使用Hibernate Session的persist()方法,所以我能够完成这项工作的唯一方法是使用下面的代码并遵循此blog post(也发布在answer中)。< / p>

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

答案 8 :(得分:3)

答案 9 :(得分:2)

一个好方法是为所有实体建立一个公共基类。在此基类中,如果通常在所有实体(通用设计),创建和上次更新日期属性中命名,则可以拥有id属性。

对于创建日期,您只需保留 java.util.Date 属性。请务必使用 new Date()来初始化它。

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

答案 10 :(得分:2)

只是强调: java.util.Calender不适用于时间戳java.util.Date有一段时间,不知道像时区这样的区域性事物。大多数数据库以这种方式存储东西(即使它们看起来不是这样;这通常是客户端软件中的时区设置;数据很好)

答案 11 :(得分:1)

作为JAVA中的数据类型,我强烈建议使用java.util.Date。使用Calendar时,我遇到了非常讨厌的时区问题。请参阅此Thread

为了设置时间戳,我建议使用AOP方法,或者你可以简单地在表上使用触发器(实际上这是我发现触发器使用的唯一可接受的东西)。

答案 12 :(得分:1)

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

在这种情况下,使用saveAndFlush(...)可以达到目的

答案 13 :(得分:1)

以下代码对我有用。

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

答案 14 :(得分:1)

您可以考虑将时间存储为DateTime,并以UTC格式存储。我通常使用DateTime而不是Timestamp,因为MySql在保存和检索数据时将日期转换为UTC并返回本地时间。我宁愿在一个地方保留任何这种逻辑(业务层)。我确信还有其他情况下使用Timestamp是可取的。

答案 15 :(得分:0)

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

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

这对我们有用。

答案 16 :(得分:0)

我认为在Java代码中不这样做会更整洁,您只需在MySql表定义中设置列默认值即可。 enter image description here

答案 17 :(得分:0)

对于那些想要创建或修改用户详细信息以及使用JPA和Spring Data的时间的用户,可以遵循此规则。您可以在基本域中添加@CreatedDate@LastModifiedDate@CreatedBy@LastModifiedBy。如下所示,用@MappedSuperclass@EntityListeners(AuditingEntityListener.class)标记基本域:

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseDomain implements Serializable {

    @CreatedDate
    private Date createdOn;

    @LastModifiedDate
    private Date modifiedOn;

    @CreatedBy
    private String createdBy;

    @LastModifiedBy
    private String modifiedBy;

}

由于我们用AuditingEntityListener标记了基础域,所以我们可以告知JPA当前登录的用户。因此,我们需要提供AuditorAware的实现并覆盖getCurrentAuditor()方法。在getCurrentAuditor()内部,我们需要返回当前授权的用户ID。

public class AuditorAwareImpl implements AuditorAware<String> {
    @Override
    public Optional<String> getCurrentAuditor() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return authentication == null ? Optional.empty() : Optional.ofNullable(authentication.getName());
    }
}

在上面的代码中,如果Optional不起作用,则可以使用较旧的spring数据。在这种情况下,请尝试将Optional更改为String

现在要启用上述Audtior实现,请使用以下代码

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class JpaConfig {
    @Bean
    public AuditorAware<String> auditorAware() {
        return new AuditorAwareImpl();
    }
}

现在,您可以将BaseDomain类扩展到要与用户ID一起创建和修改日期和时间的所有实体类