【问题标题】:Creation timestamp and last update timestamp with Hibernate and MySQLHibernate 和 MySQL 的创建时间戳和上次更新时间戳
【发布时间】:2010-09-18 07:23:44
【问题描述】:

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

  • 您将在数据库中使用哪些数据类型(假设 MySQL,可能与 JVM 在不同的时区)?数据类型是否可以识别时区?

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

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

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

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

【问题讨论】:

  • 我认为在 Java 实体中使用 LocalDateTime 而不是过时的 Date 更好。此外,db 不应该知道时区,这会导致数据迁移出现许多问题。所以我会使用 datetime SQL 类型。

标签: java hibernate timestamp


【解决方案1】:

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

【讨论】:

  • 在 J2SE 中没有任何问题,因为 @PrePersist 和 @PerUpdate 是 JPA 注释。
  • @Kumar - 如果您使用的是普通的 Hibernate 会话(而不是 JPA),您可以尝试使用 hibernate 事件侦听器,尽管这与 JPA 注释相比不是很优雅和紧凑。
  • 在当前带有 JPA 的 Hibernate 中,可以使用“@CreationTimestamp”和“@UpdateTimestamp”
  • @FlorianLoch 是否有 Date 而不是 Timestamp 的等价物?还是我必须自己创建?
【解决方案2】:

您可以只使用@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(后者也很好地确保客户端不能设置不同的值)。
  • 当我更新对象并持久化它时,bd 丢失了 create_date...为什么?
  • 我的情况是从 @Column(name = "create_date" , nullable=false) 中删除 nullable=false 工作
  • 我发现在使用时间戳时,我总是必须在类中添加以下注释以使其正常运行:@EntityListeners(AuditingEntityListener.class)
【解决方案3】:

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

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

【讨论】:

  • 这很好,直到您想向您的实体添加不同 专有行为(并且您不能扩展多个基类)。 afaik 在没有基类的情况下获得相同效果的唯一方法是通过 aspectj itd 或事件侦听器查看 @kieren dixon 答案
  • 我会使用 MySQL 触发器执行此操作,这样即使未保存完整实体或被任何外部应用程序或手动查询修改,它仍会更新这些字段。
  • 你能给我任何可行的例子吗,因为我遇到了异常not-null property references a null or transient value: package.path.ClassName.created
  • @rishiAgar,不,我没有。但是现在我已经从默认构造函数为我的属性分配了日期。找到后会通知你的。
  • 将其更改为 @Column(name = "updated", nullable = false, insertable = false) 以使其工作。有趣的是,这个答案得到了如此多的支持..
【解决方案4】:
  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 日期/时间类型更好,因为它们是不可变的,并且在记录它们时不要将时区转换为本地时区。

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

您会为映射使用哪些注释(例如@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 列,您可以使用数据库触发器在每次修改给定行时使用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
}

【讨论】:

  • 非常详尽的回答,谢谢。我不同意更喜欢datetime 而不是timestamp。您希望您的数据库知道您的时间戳的时区。这样可以防止时区转换错误。
  • timestsmp 类型不存储时区信息。它只是从应用程序 TZ 到 DB TZ 进行对话。实际上,您希望单独存储客户端 TZ,并在呈现 UI 之前在应用程序中进行对话。
  • 正确。 MySQL timestamp 始终采用 UTC。 MySQL 将 TIMESTAMP 值从当前时区转换为 UTC 进行存储,并从 UTC 转换回当前时区进行检索。 MySQL documentation: The DATE, DATETIME, and TIMESTAMP Types
  • 非常感谢这个详细而清晰的答案!虽然,我想使用 JPA 可嵌入类,但是如果我的表对于 createdBy 和 createdOn 有不同的列名,是否有可能...是否可以在使用可嵌入类的类中指定列名?跨度>
  • 是的,当然,使用@AttributeOverride
【解决方案5】:

使用 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;

【讨论】:

  • 我们正在使用@Version。当一个实体被设置时,会进行两次调用,一个是保存,另一个是更新。因此,我面临同样的问题。一旦我添加了@Column(updatable = false),它就解决了我的问题。
【解决方案6】:

你也可以使用拦截器来设置值

创建一个您的实体实现的名为 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;
    }
}

并向会话工厂注册

【讨论】:

  • 这是一种解决方案,如果您使用 SessionFactory 而不是 EntityManager!
  • 仅适用于那些与我在此上下文中遇到类似问题的人:如果您的实体本身没有定义这些额外字段(createdAt,...)而是从父类继承它,那么这个父类必须用 @MappedSuperclass 注释 - 否则 Hibernate 找不到这些字段。
【解决方案7】:

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

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

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

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

  • 我尚未对此进行测试,但仅查看文档我认为 @Temporal 可以完成这项工作;不确定我是否可以为此目的使用@Version@PrePersist@PreUpdate 是手动控制的不错选择。将其添加到所有实体的层超类型(公共基类)中,这是一个可爱的想法,前提是您确实需要为所有实体添加时间戳。

【讨论】:

  • 虽然 long 和 Long 可能是不可变的,但在您描述的情况下这对您没有帮助。他们仍然可以说 foo.setLastUpdate(new Long(System.currentTimeMillis());
  • 没关系。 Hibernate 无论如何都需要设置器(或者它会尝试通过反射直接访问该字段)。我说的是很难从我们的应用程序代码中追查出谁在修改时间戳。当你可以使用 getter 来做到这一点时,这很棘手。
  • 我同意你的说法,即 ORM 框架应该负责自动填写日期,但我会更进一步说日期应该从数据库服务器的时钟设置,而不是比客户。我不清楚这是否能实现这个目标。在 sql 中,我可以通过使用 sysdate 函数来做到这一点,但我不知道如何在 Hibernate 或任何 JPA 实现中做到这一点。
  • 我不希望数据库中有魔法。 我明白你的意思,但我喜欢考虑这样一个事实,即数据库应该保护自己免受坏/新/无能开发人员的伤害。数据完整性在大公司非常重要,不能依赖别人插入好的数据。约束、默认值和 FK 将有助于实现这一目标。
【解决方案8】:

如果您使用的是 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()这样的克隆对象,否则其他组件可以操纵内部状态(日期)
【解决方案9】:

对于那些想要创建或修改用户详细信息以及使用 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

现在使用下面的代码来启用上面的 Audtior 实现

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

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

【讨论】:

    【解决方案10】:
    【解决方案11】:

    以下代码对我有用。

    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()
    【解决方案12】:

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

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

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

    【讨论】:

    • 使用时间戳列进行乐观锁定是个坏主意。始终使用整数版本列。原因是,2 个 JVM 可能在不同的时间,并且可能没有毫秒精度。如果您改为让休眠使用数据库时间戳,那将意味着从数据库中进行额外的选择。而是只使用版本号。
    【解决方案13】:

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

    【讨论】:

      【解决方案14】:

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

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

      【讨论】:

        【解决方案15】:

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

        【讨论】:

          【解决方案16】:

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

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

          这对我们有用。

          【讨论】:

          • 它也适用于直接在数据库中通过 SQL 查询修改数据的情况。 @PrePersist@PrePersist 不涉及这种情况。
          【解决方案17】:

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

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

          【讨论】:

            【解决方案18】:

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

            【讨论】:

              猜你喜欢
              • 2011-03-30
              • 1970-01-01
              • 2023-03-08
              • 2015-11-19
              • 1970-01-01
              • 2010-12-17
              • 1970-01-01
              相关资源
              最近更新 更多