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

对于某个Hibernate实体,我们需要存储它的创建时间和最后一次更新时间。你会怎么设计呢?

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

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

  • 你会让谁来负责设置时间戳:数据库、ORM框架(Hibernate),还是应用程序程序员?

  • 你会为映射使用什么注释(例如@Temporal)?

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

368949 次浏览

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

对于创建日期,只需保留java.util.Date属性。请确保始终使用新的日期()初始化它。

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

作为JAVA中的数据类型,我强烈建议使用JAVA .util. date。在使用Calendar时,我遇到了非常糟糕的时区问题。请看这个线程

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

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

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

您可以考虑将时间存储为DateTime,并且使用UTC。我通常使用DateTime而不是Timestamp,因为MySql在存储和检索数据时将日期转换为UTC和本地时间。我宁愿把所有这些逻辑都放在一个地方(业务层)。我相信在其他情况下使用时间戳更可取。

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

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

  • Java字段类型:long。Unix时间戳在各种库中都得到了很好的支持,long没有Y2038问题,时间戳算法快速简单(主要是操作符<+,假设计算中不涉及天/月/年)。而且,最重要的是,基本的__abc0和__abc5都是不可变—有效地通过value传递,不像java.util.Dates;如果在调试别人的代码时发现类似foo.getLastUpdate().setTime(System.currentTimeMillis())的东西,我会非常生气。

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

  • 我还没有测试这个,但只看文档,我假设@Temporal将完成这项工作;我不确定是否可以使用@Version来实现这个目的。@PrePersist@PreUpdate是手动控制的良好替代方案。将它添加到所有实体的层超类型(公共基类),是一个可爱的想法,如果你真的想要为all的实体加盖时间戳。

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

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

您还可以使用拦截器来设置这些值

创建一个名为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;
}
}

并将其注册到会话工厂

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

com.mysql.jdbc.exceptions.jdbc4。MySQLIntegrityConstraintViolationException:列“created”不能为空

要解决这个问题,在"created"属性的@Column注释中添加updatable=false:

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

如果你正在使用会话API,根据这个回答, PrePersist和PreUpdate回调将不起作用。

我在我的代码中使用Hibernate会话的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;
}
}

你可以只使用@CreationTimestamp@UpdateTimestamp:

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


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

我们也遇到过类似的情况。我们使用的是Mysql 5.7。

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

这对我们很管用。

下面的代码对我有用。

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

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

在这种情况下,使用saveAndFlush(…)达到了目的

我认为在Java代码中不这样做更整洁,你可以简单地在MySql表定义中设置列默认值。 enter image description here < / p >
  1. 应该使用哪些数据库列类型

你的第一个问题是:

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

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

因此,最好使用DATETIME代替,它没有这个上限限制。然而,DATETIME不是时区感知的。因此,出于这个原因,最好在数据库端使用UTC并使用hibernate.jdbc.time_zone Hibernate属性。

  1. 你应该使用什么实体属性类型

你的第二个问题是:

你会在Java中使用什么数据类型(日期,日历,长,…)?

在Java端,您可以使用Java 8 LocalDateTime。你也可以使用遗留的Date,但是Java 8 Date/Time类型更好,因为它们是不可变的,并且在记录它们时不做时区转移到本地时区。

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

你会为映射使用什么注释(例如@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框架(Hibernate)还是应用程序程序员?

你会为映射使用什么注释(例如@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来设置这些。

让我们假设你有以下数据库表:

带审计列的数据库表

并且,每个表都有这样的列:

  • created_by
  • created_on
  • updated_by
  • updated_on

使用Hibernate @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
}

然而,即使createdOnupdateOn属性是由特定于hibernate的@CreationTimestamp@UpdateTimestamp注释设置的,createdByupdatedBy也需要注册一个应用程序回调,如下面的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,你可以使用@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
}

对于那些想要使用JPA和Spring Data创建或修改用户详细信息以及时间的人,可以遵循这一点。你可以在base域中添加@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不能工作,你可以使用Java 7或更老的版本。在这种情况下,尝试用String改变Optional

现在使用下面的代码来启用上述审计器实现

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

现在你可以将BaseDomain类扩展到你想要创建和修改日期的所有实体类&时间以及用户Id