284

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

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

  • 你会在 Java 中使用哪些数据类型(Date, Calendar, long, ...)?

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

  • 您会为映射使用哪些注释(例如@Temporal)?

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

4

18 回答 18

290

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

于 2008-10-21T13:14:29.233 回答
202

您可以使用@CreationTimestampand @UpdateTimestamp

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

@UpdateTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "modify_date")
private Date modifyDate;
于 2016-09-10T16:01:22.407 回答
122

使用这篇文章中的资源以及从不同来源获取的信息,我提出了这个优雅的解决方案,创建以下抽象类

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 {
...
}
于 2010-10-27T22:49:30.073 回答
76
  1. 您应该使用哪些数据库列类型

你的第一个问题是:

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

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

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

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

你的第二个问题是:

你会在 Java 中使用哪些数据类型(日期、日历、长......)?

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

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

您会为映射使用哪些注释(例如@Temporal)?

如果您使用LocalDateTimeorjava.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列,您可以使用DEFAULTDDL 约束,例如:

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注释设置的,createdBy和 也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
}
于 2020-02-23T08:00:57.463 回答
19

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

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException:列“创建”不能为空

为了解决这个问题,将 updatable=false 添加到“created”属性的@Column 注释中:

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created", nullable = false, updatable=false)
private Date created;
于 2014-04-25T05:25:55.630 回答
18

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

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

并将其注册到会话工厂

于 2011-10-21T01:32:29.253 回答
12

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

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

  • Java 字段类型:long. Unix 时间戳在各种库中得到很好的支持,long没有 Y2038 问题,时间戳算法快速简单(主要是 operator<和 operator +,假设计算中不涉及天/月/年)。而且,最重要的是,与 s 不同,原始longs 和java.lang.Longs 都是不可变的——有效地按值传递java.util.Datefoo.getLastUpdate().setTime(System.currentTimeMillis())在调试别人的代码时,我真的很生气。

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

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

于 2008-10-23T14:22:36.520 回答
6

如果您使用的是 Session 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;
    }
}
于 2015-10-14T16:20:57.873 回答
5

对于那些想要创建或修改用户详细信息以及使用 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 和 overridegetCurrentAuditor()方法的实现。而在里面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 或更早版本。在这种情况下,请尝试Optional使用String.

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

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

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

于 2020-12-11T12:00:50.713 回答
3

现在还有@CreatedDate 和@LastModifiedDate 注释。

=> https://programmingmitra.blogspot.fr/2017/02/automatic-spring-data-jpa-auditing-saving-CreatedBy-createddate-lastmodifiedby-lastmodifieddate-automatically.html

(弹簧框架)

于 2018-04-22T22:27:54.290 回答
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;
}
于 2019-04-06T06:42:12.040 回答
2

一个好的方法是为所有实体创建一个通用基类。在这个基类中,如果它在您的所有实体(通用设计)、您的创建和上次更新日期属性中通用命名,您可以拥有您的 id 属性。

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

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

于 2008-10-21T12:27:44.943 回答
2

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

于 2008-10-21T13:02:30.800 回答
1

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

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

于 2008-10-21T12:30:52.660 回答
1

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

于 2008-10-21T13:16:32.287 回答
1

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

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

这对我们有用。

于 2017-06-24T07:35:57.977 回答
1

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

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

于 2019-09-23T13:29:11.703 回答
0

我认为在 Java 代码中不这样做会更整洁,您可以简单地在 MySql 表定义中设置列默认值。 在此处输入图像描述

于 2019-09-27T09:29:14.683 回答