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

您将在数据库中使用什么数据类型(假设MySQL,可能位于与JVM不同的时区)?数据类型是否支持时区? 你会在Java中使用什么数据类型(日期,日历,长,…)? 您会让谁负责设置时间戳——数据库、ORM框架(Hibernate)还是应用程序程序员? 你会为映射使用什么注释(例如@Temporal)?

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


当前回答

应该使用哪些数据库列类型


你的第一个问题是:

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

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

因此,最好使用DATETIME,它没有这个上限限制。然而,DATETIME不支持时区。因此,出于这个原因,最好在数据库端使用UTC,并使用hibernate.jdbc。time_zone休眠属性。

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


你的第二个问题是:

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

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

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

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

如果您正在使用LocalDateTime或java.sql.Timestamp来映射一个时间戳实体属性,那么您不需要使用@Temporal,因为HIbernate已经知道这个属性将被保存为JDBC时间戳。

只有当你使用java.util。日期,你需要指定@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_on和updated_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
}

然而,即使createdOn和updateOn属性是由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,您可以使用@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注释,你可以使用@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注释,并将事件代码放在外部类中。

你可以使用@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,可能位于与JVM不同的时区)?数据类型是否支持时区?

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

因此,最好使用DATETIME,它没有这个上限限制。然而,DATETIME不支持时区。因此,出于这个原因,最好在数据库端使用UTC,并使用hibernate.jdbc。time_zone休眠属性。

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


你的第二个问题是:

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

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

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

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

如果您正在使用LocalDateTime或java.sql.Timestamp来映射一个时间戳实体属性,那么您不需要使用@Temporal,因为HIbernate已经知道这个属性将被保存为JDBC时间戳。

只有当你使用java.util。日期,你需要指定@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_on和updated_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
}

然而,即使createdOn和updateOn属性是由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,您可以使用@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
}

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

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

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

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

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