ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

你会在 Java 中使用哪些数据类型(日期、日历、长......)?

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

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

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

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

B
Baldrick

如果您使用 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 是否有日期而不是时间戳的等价物?还是我必须创建自己的?
i
idmitriev

您可以只使用 @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)
A
Alex

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

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) 以使其工作。有趣的是,这个答案得到了如此多的支持..
V
Vlad Mihalcea

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

你的第一个问题是:

您将在数据库中使用哪些数据类型(假设 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)?

如果您使用 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 来设置这些。

假设您有以下数据库表:

https://i.stack.imgur.com/DQ59s.png

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

由...制作

创建于

更新人

更新了

使用 Hibernate @CreationTimestamp 和 @UpdateTimestamp 注释

Hibernate 提供了可用于映射 created_onupdated_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
}

但是,即使 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
e
endriju

使用 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),它就解决了我的问题。
R
Rohit Banga

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

创建一个名为 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 找不到这些字段。
n
ngn

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

数据库列类型:自 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 是手动控制它的好选择。将它添加到所有实体的层超类型(公共基类)中,是一个可爱的想法,前提是您确实希望为所有实体添加时间戳。


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

如果您使用的是 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() 这样的克隆对象,否则其他组件可以操纵内部状态(日期)
M
Mohammed Javad

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


p
prranay

以下代码对我有用。

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()
c
chomp

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

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


这一点,TIL save 不一定会立即将更改保存到数据库中,而是刷新实体与数据库同步。
b
bernardn

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

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

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


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

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


C
Community

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

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


m
mmacaulay

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


a
amdg

我们也有类似的情况。我们使用的是 Mysql 5.7。

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

这对我们有用。


它也适用于直接在数据库中通过 SQL 查询修改数据的情况。 @PrePersist@PrePersist 不包括这种情况。
W
Wayne Wei

https://i.stack.imgur.com/DwpZG.png