1. 概述

在这篇文章中,我们将学习如何使用Hibernate注解@CreationTimestamp@UpdateTimestamp来追踪实体的创建和更新时间。

2. 模型

为了演示这些注解的工作方式,我们从一个简单的Book实体开始:

@Entity
public class Book {
    @Id
    @GeneratedValue
    private Long id;
    private String title;

    public Book() {
    }

    // standard setters and getters
}

我们将使用H2数据库,其基于Book的模式DDL会自动生成。Hibernate要求与@CreationTimestamp@UpdateTimestamp注解字段关联的数据库列必须是基于时间戳的类型,如TimestampDateTime

3. 使用@CreationTimestamp追踪创建日期和时间

我们经常需要持久化实体的创建日期。@CreationTimestamp是一个方便的注解,当实体首次保存时,它会将字段值设置为当前时间戳。让我们在Book中添加一个名为createdOn的字段,带有这个注解。由于该字段需要存储过去的一个确切时刻,我们将声明它为Instant,它代表UTC中的一个特定时刻。在生成的模式中,这个字段将具有Timestamp类型:

@Entity
public class Book {
    //other fields

    @CreationTimestamp
    private Instant createdOn;

    // standard setters and getters

现在,让我们验证Hibernate在保存新书后是否设置了createdOn

@Test
void whenCreatingEntity_ThenCreatedOnIsSet() {
    session = sessionFactory.openSession();
    session.beginTransaction();
    Book book = new Book();

    session.save(book);
    session.getTransaction()
      .commit();
    session.close();

    assertNotNull(book.getCreatedOn());
}

4. 使用@UpdateTimestamp追踪最后更新时间

类似地,我们可能需要记录实体最后一次更新的时间。@UpdateTimestamp是Hibernate提供的另一个注解,它会在每次更新实体时自动设置字段值。让我们添加一个名为lastUpdatedOnInstant字段,这次带有@UpdateTimestamp注解:

@Entity
public class Book {
    //other fields

    @UpdateTimestamp
    private Instant lastUpdatedOn;

    // standard setters and getters

让我们检查Hibernate在实体创建时是否填充了lastUpdatedOn

@Test
void whenCreatingEntity_ThenCreatedOnAndLastUpdatedOnAreBothSet() {
    session = sessionFactory.openSession();
    session.beginTransaction();
    Book book = new Book();

    session.save(book);
    session.getTransaction()
      .commit();
    session.close();

    assertNotNull(book.getCreatedOn());
    assertNotNull(book.getLastUpdatedOn());
}

我们确认Hibernate按预期生成了lastUpdatedOn。接下来,我们也检查lastUpdatedOn在更新book时是否改变,而createdOn保持不变:

@Test
void whenUpdatingEntity_ThenLastUpdatedOnIsUpdatedAndCreatedOnStaysTheSame() {
    session = sessionFactory.openSession();
    session.setHibernateFlushMode(MANUAL);
    session.beginTransaction();

    Book book = new Book();
    session.save(book);
    session.flush();
    Instant createdOnAfterCreation = book.getCreatedOn();
    Instant lastUpdatedOnAfterCreation = book.getLastUpdatedOn();

    String newName = "newName";
    book.setTitle(newName);
    session.save(book);
    session.flush();
    session.getTransaction().commit();
    session.close();
    Instant createdOnAfterUpdate = book.getCreatedOn();
    Instant lastUpdatedOnAfterUpdate = book.getLastUpdatedOn();

    assertEquals(newName, book.getTitle());
    assertNotNull(createdOnAfterUpdate);
    assertNotNull(lastUpdatedOnAfterUpdate);
    assertEquals(createdOnAfterCreation, createdOnAfterUpdate);
    assertNotEquals(lastUpdatedOnAfterCreation, lastUpdatedOnAfterUpdate);
}

在我们为book设置新标题后,只有lastUpdatedOn发生了变化。

5. 当前日期的来源

为了使这些注解发挥作用,我们必须确保时钟正确设置,以确保时间戳的准确性。默认情况下,这两个注解在设置属性值时都使用Java虚拟机的当前日期。

从Hibernate 6.0.0版本开始,我们可以选择性地指定数据库作为日期的来源:

@CreationTimestamp(source = SourceType.DB)
private Instant createdOn;
@UpdateTimestamp(source = SourceType.DB)
private Instant lastUpdatedOn;

在这种情况下,底层数据库指定了如何确定当前日期。例如,这可能是数据库函数current_timestamp()

6. 注意事项

正如我们之前展示的,我们在创建实体时设置了createdOnlastUpdatedOn

@Test
void whenCreatingEntity_ThenCreatedOnAndLastUpdatedOnAreEqual() {
    session = sessionFactory.openSession();
    session.beginTransaction();
    Book book = new Book();

    session.save(book);
    session.getTransaction()
      .commit();
    session.close();

    assertEquals(book.getCreatedOn(), book.getLastUpdatedOn());
}

创建和更新之间的差异可能仅相差毫秒。如果出于某种原因,我们需要在创建后这两个日期完全相同,我们应该使用其他方法设置时间戳。我们可以通过描述的JPA @PrePersist@PreUpdate回调来实现这一点,如使用JPA、Hibernate和Spring Data JPA进行审计

此外,我们必须记住,这些注解只会在我们的Java应用程序创建和修改数据时生成新的时间戳。它们对表的作用范围仅限于此。如果其他应用程序或SQL脚本修改book表,我们必须使用其他方法更新我们的时间戳。

Instant是Java 8中添加的新日期和时间API的一部分,从Hibernate 5.2.3版本开始原生支持。推荐使用这些新类来表示日期,它们位于java.time包中。然而,如果我们需要支持较旧的Hibernate或Java版本,可能需要使用不同的时间戳类型。有关如何使用Hibernate将时间戳映射到Java类字段的更多信息,请阅读Hibernate - 映射日期和时间

7. 总结

在这篇教程中,我们展示了如何使用@CreationTimestamp@UpdateTimestamp自动生成时间戳。使用这些注解是监控实体修改日期的一种简单方法。但是,当我们使用它们时,必须记住Hibernate是以字段为单位生成新时间戳的。这会导致即使是由同一个INSERTUPDATE语句设置的,多个时间戳也可能不同。

另外,我们需要注意,这些注解并没有为表创建全局机制。如果我们的数据库表将被不同的应用程序修改,我们需要确保每个应用程序都能正确设置更新和创建时间戳。

如往常一样,本文的完整代码示例可在GitHub上找到。