对于某个 Hibernate 实体,我们需要存储其创建时间和上次更新时间。你会怎么设计这个?
您将在数据库中使用哪些数据类型(假设 MySQL,可能与 JVM 在不同的时区)?数据类型会感知时区吗?
你会在 Java 中使用哪些数据类型(日期、日历、长......)?
你会让谁负责设置时间戳——数据库、ORM 框架(Hibernate)还是应用程序程序员?
您会为映射使用哪些注释(例如@Temporal)?
我不仅在寻找一个可行的解决方案,而且在寻找一个安全且设计良好的解决方案。
如果您使用 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;
@CreationTimestamp
和 @UpdateTimestamp
似乎也需要一些 @Column(..., columnDefinition = "timestamp default current_timestamp")
,或者使用 @PrePersist
和 @PreUpdate
(后者也很好地确保客户端不能设置不同的值)。
@Column(name = "create_date" , nullable=false)
中删除 nullable=false
的情况有效
使用这篇文章中的资源以及从不同来源获取的信息,我提出了这个优雅的解决方案,创建以下抽象类
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 {
...
}
not-null property references a null or transient value: package.path.ClassName.created
@Column(name = "updated", nullable = false, insertable = false)
以使其工作。有趣的是,这个答案得到了如此多的支持..
您应该使用哪些数据库列类型
你的第一个问题是:
您将在数据库中使用哪些数据类型(假设 MySQL,可能与 JVM 在不同的时区)?数据类型会感知时区吗?
在 MySQL 中,TIMESTAMP
列类型会从 JDBC 驱动程序本地时区转移到数据库时区,但它只能存储最多 2038-01-19 03:14:07.999999
的时间戳,因此它不是未来的最佳选择。
因此,最好改用 DATETIME
,它没有这个上限限制。但是,DATETIME
不支持时区。因此,出于这个原因,最好在数据库端使用 UTC 并使用 hibernate.jdbc.time_zone
Hibernate 属性。
您应该使用什么实体属性类型
你的第二个问题是:
你会在 Java 中使用哪些数据类型(日期、日历、长......)?
在 Java 方面,您可以使用 Java 8 LocalDateTime
。您也可以使用旧版 Date
,但 Java 8 日期/时间类型更好,因为它们是不可变的,并且在记录它们时不会将时区转换为本地时区。
现在,我们也可以回答这个问题:
您会为映射使用哪些注释(例如@Temporal)?
如果您使用 LocalDateTime
或 java.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
列,您可以使用数据库触发器在每次修改给定行时使用 CURRENT_TIMESTAMP()
设置列值。
或者,使用 JPA 或 Hibernate 来设置这些。
假设您有以下数据库表:
https://i.stack.imgur.com/DQ59s.png
而且,每个表都有如下列:
由...制作
创建于
更新人
更新了
使用 Hibernate @CreationTimestamp 和 @UpdateTimestamp 注释
Hibernate 提供了可用于映射 created_on
和 updated_on
列的 @CreationTimestamp
和 @UpdateTimestamp
注释。
您可以使用 @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
}
datetime
而不是 timestamp
。您希望您的数据库知道您的时间戳的时区。这可以防止时区转换错误。
timestsmp
类型不存储时区信息。它只是从应用程序 TZ 到 DB TZ 进行对话。实际上,您希望单独存储客户端 TZ 并在呈现 UI 之前在应用程序中进行对话。
timestamp
始终采用 UTC。 MySQL 将 TIMESTAMP
值从当前时区转换为 UTC 进行存储,然后从 UTC 转换回当前时区进行检索。 MySQL documentation: The DATE, DATETIME, and TIMESTAMP Types
@AttributeOverride
。
使用 Olivier 的解决方案,在更新语句期间,您可能会遇到:
com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException:列“创建”不能为空
为了解决这个问题,将 updatable=false 添加到“created”属性的@Column 注释中:
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created", nullable = false, updatable=false)
private Date created;
@Version
。当一个实体被设置时,会进行两个调用,一个是保存,另一个是更新。因此,我面临同样的问题。一旦我添加了 @Column(updatable = false)
,它就解决了我的问题。
您还可以使用拦截器来设置值
创建一个名为 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;
}
}
并将其注册到会话工厂
感谢所有帮助过的人。在自己做了一些研究之后(我是提出这个问题的人),这是我发现最有意义的:
数据库列类型:自 1970 年以来与时区无关的毫秒数,表示为十进制 (20),因为 2^64 有 20 位并且磁盘空间很便宜;让我们直截了当。另外,我既不会使用 DEFAULT CURRENT_TIMESTAMP,也不会使用触发器。我不想在数据库中使用魔法。
Java 字段类型:long。 Unix 时间戳在各种库中得到很好的支持,long 没有 Y2038 问题,时间戳算法快速简单(主要是 operator < 和 operator +,假设计算中不涉及天/月/年)。而且,最重要的是,与 java.util.Dates 不同,原始 long 和 java.lang.Long 都是不可变的——有效地按值传递;在调试别人的代码时,我会很生气地找到类似 foo.getLastUpdate().setTime(System.currentTimeMillis()) 的东西。
ORM 框架应该负责自动填充数据。
我还没有对此进行测试,但只查看了我认为@Temporal 会完成这项工作的文档;不确定我是否可以为此目的使用@Version。 @PrePersist 和 @PreUpdate 是手动控制它的好选择。将它添加到所有实体的层超类型(公共基类)中,是一个可爱的想法,前提是您确实希望为所有实体添加时间戳。
如果您使用的是 Session API,则 PrePersist 和 PreUpdate 回调将不会根据此 answer 工作。
我在我的代码中使用了 Hibernate Session 的 persist() 方法,所以我可以完成这项工作的唯一方法是使用下面的代码并遵循这个 blog post(也发布在 answer 中)。
@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()
这样的克隆对象,否则其他组件可以操纵内部状态(日期)
对于那些想要创建或修改用户详细信息以及使用 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
不起作用,您可以使用 Java 7 或更早版本。在这种情况下,请尝试将 Optional
更改为 String
。
现在要启用上述 Auditior 实现,请使用以下代码
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class JpaConfig {
@Bean
public AuditorAware<String> auditorAware() {
return new AuditorAwareImpl();
}
}
现在您可以将 BaseDomain
类扩展到您想要创建和修改日期的所有实体类 &时间和用户 ID
现在还有@CreatedDate 和@LastModifiedDate 注释。
(弹簧框架)
以下代码对我有用。
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()
如果我们在方法中使用@Transactional,@CreationTimestamp 和@UpdateTimestamp 会将值保存在DB 中,但在使用save(...) 后将返回null。
在这种情况下,使用 saveAndFlush(...) 就可以了
一个好的方法是为所有实体创建一个通用基类。在这个基类中,如果它在您的所有实体(通用设计)、您的创建和上次更新日期属性中通用命名,您可以拥有您的 id 属性。
对于创建日期,您只需保留一个 java.util.Date 属性。请确保始终使用 new Date() 对其进行初始化。
对于最后一个更新字段,您可以使用 Timestamp 属性,您需要使用 @Version 对其进行映射。使用此注释,Hibernate 将自动更新属性。请注意,Hibernate 也会应用乐观锁定(这是一件好事)。
只是为了强调:java.util.Calender
不适用于时间戳。 java.util.Date
暂时与时区等区域性无关。大多数数据库以这种方式存储东西(即使它们看起来不是;这通常是客户端软件中的时区设置;数据很好)
作为 JAVA 中的数据类型,我强烈推荐使用 java.util.Date。我在使用日历时遇到了非常讨厌的时区问题。请参阅此Thread。
对于设置时间戳,我建议使用 AOP 方法,或者您可以简单地在表上使用触发器(实际上这是我发现使用触发器唯一可以接受的方法)。
您可以考虑将时间存储为 DateTime 和 UTC。我通常使用 DateTime 而不是 Timestamp,因为 MySql 在存储和检索数据时会将日期转换为 UTC 并返回本地时间。我宁愿将任何一种逻辑保留在一个地方(业务层)。我确信在其他情况下使用 Timestamp 更可取。
我们也有类似的情况。我们使用的是 Mysql 5.7。
CREATE TABLE my_table (
...
updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
这对我们有用。
@PrePersist
和 @PrePersist
不包括这种情况。
https://i.stack.imgur.com/DwpZG.png
不定期副业成功案例分享