繁体   English   中英

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

[英]Creation timestamp and last update timestamp with Hibernate and MySQL

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

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

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

  • 你会让谁负责设置时间戳——数据库、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;

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

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 {
...
}
  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因为@Temporal已经知道此属性将被保存为 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 来设置这些。

假设您有以下数据库表:

带有审计列的数据库表

而且,每个表都有如下列:

  • 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属性由休眠特定设置@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
}

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

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

要解决此问题,请将 updatable=false 添加到“created”属性的 @Column 注释中:

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

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

创建一个名为 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 年以来与时区无关的毫秒数,表示为decimal(20)因为 2^64 有 20 位数字且磁盘空间便宜; 让我们直截了当。 此外,我既不会使用DEFAULT CURRENT_TIMESTAMP ,也不会使用触发器。 我不想在数据库中使用魔法。

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

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

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

如果您使用的是会话 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;
    }
}

以下代码对我有用。

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

对于那些想要创建或修改用户详细信息以及使用 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不起作用,您可能会使用较旧的弹簧数据。 在这种情况下,请尝试使用String更改Optional

现在为了启用上面的 Auditior 实现,请使用下面的代码

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

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

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

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

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

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

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

对于设置时间戳,我建议使用 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
    );

这对我们有用。

如果我们在我们的方法使用@Transactional,@CreationTimestamp和@UpdateTimestamp将保存数据库的价值,但使用后保存会返回null(...)。

在这种情况下,使用 saveAndFlush(...) 可以解决问题

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM