Отметка времени создания и отметка времени последнего обновления в Hibernate и MySQL

244

Для определенного объекта Hibernate у нас есть требование хранить время его создания и время последнего обновления. Как бы вы спроектировали это?

  • Какие типы данных вы бы использовали в базе данных (предполагая MySQL, возможно, в другом часовом поясе, чем JVM)? Будут ли типы данных учитывать часовой пояс?

  • Какие типы данных вы будете использовать в Java ( Date, Calendar, long...)?

  • Кого бы вы взяли на себя ответственность за установку меток времени: базы данных, инфраструктуры ORM (Hibernate) или разработчика приложений?

  • Какие аннотации вы бы использовали для отображения (например @Temporal)?

Я ищу не только рабочее решение, но и безопасное и продуманное решение.

СПП
источник

Ответы:

266

Если вы используете аннотации 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аннотацию для класса и поместить код события во внешний класс.

Гудмундур Бьярни
источник
7
Работает без проблем в J2SE, так как @PrePersist и @PerUpdate являются аннотациями JPA.
Kdeveloper
2
@Kumar - Если вы используете обычный Hibernate-сеанс (вместо JPA), вы можете попробовать прослушиватели событий Hibernate, хотя это не очень элегантно и компактно по сравнению с аннотациями JPA.
Шайлендра
43
В текущем Hibernate с JPA можно использовать «@CreationTimestamp» и «@UpdateTimestamp»
Флориан Лох
@FlorianLoch есть ли эквивалент для даты, а не метка времени? Или мне придется создать свой собственный?
Майк
151

Вы можете просто использовать @CreationTimestampи @UpdateTimestamp:

@CreationTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "create_date")
private Date createDate;

@UpdateTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "modify_date")
private Date modifyDate;
idmitriev
источник
3
спасибо братан, такая маленькая вещь, необходимо обновить отметку времени. Я не знал ты спас мой день
Вирендра Сагар
Есть TemporalType.DATEв первом случае и TemporalType.TIMESTAMPво втором.
v.ladynev
Вы говорите, что это также автоматически устанавливает значения? Это не мой опыт; кажется, что даже с @CreationTimestampи @UpdateTimestampнужно либо @Column(..., columnDefinition = "timestamp default current_timestamp"), либо использовать @PrePersistи @PreUpdate(последний также прекрасно гарантирует, что клиенты не могут установить другое значение).
Арджан
2
Когда я обновляю объект и сохраняю его, bd теряет дату создания ... почему?
Бренно Лил
1
Im мой случай удаления nullable=falseот @Column(name = "create_date" , nullable=false)работавшего
Shantaram TUPE
113

Взяв ресурсы из этого поста вместе с информацией, взятой слева и справа из разных источников, я пришел с этим элегантным решением и создал следующий абстрактный класс

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 {
...
}
Оливье Рефало
источник
5
это хорошо до тех пор, пока вы не захотите добавить различные эксклюзивные поведения к вашим сущностям (и вы не можете расширить более одного базового класса). afaik единственный способ получить тот же эффект без базового класса - это использовать aspectj itd или слушатели событий, увидев ответ @kieren dixon
gpilotino
3
Я бы сделал это с помощью триггера MySQL, чтобы даже если полная сущность не была сохранена или изменена каким-либо внешним приложением или ручным запросом, она все равно обновила бы эти поля.
Вебнет
3
not-null property references a null or transient value: package.path.ClassName.created
Можете
@rishiAgar, нет, не знаю. Но сейчас я назначил дату для моего свойства из конструктора по умолчанию. Дам тебе знать, как только я найду.
Суммит Рамтеке
1
Измените это, чтобы @Column(name = "updated", nullable = false, insertable = false)заставить это работать. Интересно, что этот ответ получил так много голосов ..
displayname
20

1. Какие типы столбцов базы данных вы должны использовать

Ваш первый вопрос был:

Какие типы данных вы бы использовали в базе данных (предполагая MySQL, возможно, в другом часовом поясе, чем JVM)? Будут ли типы данных учитывать часовой пояс?

В MySQL TIMESTAMPтип столбца переключается с локального часового пояса драйвера JDBC на часовой пояс базы данных, но он может хранить только временные метки до'2038-01-19 03:14:07.999999 , поэтому это не лучший выбор в будущем.

Таким образом, лучше использовать DATETIMEвместо этого, который не имеет этого ограничения верхней границы. Тем DATETIMEне менее, часовой пояс не знает. Поэтому по этой причине лучше всего использовать UTC на стороне базы данных и использовать hibernate.jdbc.time_zoneсвойство Hibernate.

Для более подробной информации о hibernate.jdbc.time_zoneнастройке, ознакомьтесь с этой статьей .

2. Какой тип свойства объекта следует использовать

Ваш второй вопрос был:

Какие типы данных вы бы использовали в Java (Date, Calendar, long, ...)?

На стороне Java вы можете использовать Java 8 LocalDateTime. Вы также можете использовать устаревшие версии Date, но типы даты / времени в Java 8 лучше, поскольку они неизменны и не выполняют смещение часового пояса к локальному часовому поясу при их регистрации.

Чтобы узнать больше о типах даты и времени Java 8, поддерживаемых Hibernate, ознакомьтесь с этой статьей .

Теперь мы также можем ответить на этот вопрос:

Какие аннотации вы бы использовали для отображения (например @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, чтобы установить их.

Предположим, у вас есть следующие таблицы базы данных:

Таблицы базы данных со столбцами аудита

И каждая таблица имеет столбцы, такие как:

  • created_by
  • created_on
  • updated_by
  • updated_on

Использование Hibernate @CreationTimestampи @UpdateTimestampаннотаций

Hibernate предлагает @CreationTimestampи @UpdateTimestampаннотации , которые могут быть использованы для сопоставления created_onи updated_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
}

Для более подробной информации об использовании @MappedSuperclass, ознакомьтесь с этой статьей .

Тем не менее, даже если 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
}

Чтобы узнать больше о реализации свойств аудита с помощью JPA @EntityListener, ознакомьтесь с этой статьей .

Влад Михалча
источник
Очень подробный ответ, спасибо. Я не согласен с тем, что предпочитаюdatetime более timestamp. Вы хотите, чтобы ваша база данных знала часовой пояс ваших временных меток. Это предотвращает ошибки преобразования часового пояса.
Оле В.В.
timestsmpТип не хранит данные часового пояса. Он просто делает разговор из приложения TZ в DB TZ. В действительности вы хотите хранить клиентскую TZ отдельно и вести диалог в приложении до визуализации пользовательского интерфейса.
Влад Михалча
Верный. MySQLtimestamp всегда в UTC. MySQL преобразует TIMESTAMPзначения из текущего часового пояса в UTC для хранения и обратно из UTC в текущий часовой пояс для извлечения. Документация по MySQL: типы DATE, DATETIME и TIMESTAMP
Оле В.В.
17

Вы также можете использовать перехватчик для установки значений

Создайте интерфейс с именем 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;
    }
}

И зарегистрируйте его в фабрике сессий

Кирен Диксон
источник
1
Работает, спасибо. Дополнительная информация docs.jboss.org/hibernate/core/4.0/manual/en-US/html_single/…
Андрей Немченко
Это одно решение, если вы работаете с SessionFactory вместо EntityManager!
Оливмир
Просто для тех, кто страдает с подобной проблемой, как я сделал в этом контексте: если ваша сущность сама не определяет эти дополнительные поля (созданный, ...), но наследует его от родительского класса, тогда этот родительский класс должен быть аннотирован с @MappedSuperclass - иначе Hibernate не найдет эти поля.
Оливмир
17

С помощью решения Оливье во время обновлений вы можете столкнуться с:

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: столбец «создан» не может быть пустым

Чтобы решить эту проблему, добавьте updatable = false к аннотации @Column атрибута «create»:

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created", nullable = false, updatable=false)
private Date created;
endriju
источник
1
Мы используем @Version. Когда для объекта задано два вызова, один должен сохранить, а другой - обновить. Я столкнулся с той же проблемой из-за этого. Как только я добавил, @Column(updatable = false)это решило мою проблему.
Ганеш Сатпут
12

Спасибо всем, кто помог. Проведя некоторые исследования сам (я парень, который задал вопрос), вот что я нашел наиболее значимым:

  • Тип столбца базы данных: независимое от часового пояса число миллисекунд с 1970 года, представленное как decimal(20)2 ^ 64, имеет 20 цифр, а дисковое пространство дешево; давайте будем простыми. Также я не буду использовать ни DEFAULT CURRENT_TIMESTAMPтриггеры, ни триггеры. Я не хочу магии в БД.

  • Java тип поля: long. Отметка времени Unix хорошо поддерживается различными библиотеками, longне имеет проблем с Y2038, арифметика отметки времени является быстрой и простой (в основном оператор <и оператор +, при условии, что в расчетах не участвуют дни / месяцы / годы). И, самое главное, и примитивы longs, и java.lang.Longs неизменны - эффективно передаются по значению - в отличие от java.util.Dates; Я бы очень разозлился, foo.getLastUpdate().setTime(System.currentTimeMillis())когда нашел что-то подобное при отладке чужого кода.

  • Платформа ORM должна отвечать за автоматическое заполнение данных.

  • Я еще не проверял это, но только смотрю на документы, которые, как я предполагаю, @Temporalсработают; не уверен, могу ли я использовать @Versionдля этой цели. @PrePersistи @PreUpdateявляются хорошими альтернативами, чтобы контролировать это вручную. Добавление этого в супертип слоя (общий базовый класс) для всех сущностей - это милая идея, при условии, что вам действительно нужна временная метка для всех ваших сущностей.

СПП
источник
Хотя longs и Longs могут быть неизменными, это не поможет вам в ситуации, которую вы описываете. Они все еще могут сказать foo.setLastUpdate (new Long (System.currentTimeMillis ());
Ян Маклаирд,
2
Это хорошо. В любом случае Hibernate требует установщика (или попытается получить доступ к полю напрямую через отражение). Я говорил о трудностях с поиском тех, кто изменяет метку времени из кода нашего приложения. Это сложно, когда вы можете сделать это с помощью геттера.
NGN
Я согласен с вашим утверждением, что платформа ORM должна отвечать за автоматическое заполнение даты, но я бы пошел еще дальше и сказал, что дата должна быть установлена ​​с часов сервера базы данных, а не с клиента. Мне не ясно, достигает ли это этой цели. В SQL я могу сделать это с помощью функции sysdate, но я не знаю, как это сделать в Hibernate или любой реализации JPA.
MiguelMunoz
Я не хочу магии в БД. Я понимаю, что вы имеете в виду, но мне нравится учитывать тот факт, что база данных должна защищать себя от плохих / новых / невежественных разработчиков. Целостность данных очень важна в большой компании, вы не можете полагаться на других, чтобы вставить хорошие данные. Ограничения, значения по умолчанию и FKs помогут достичь этого.
Айсгра
6

Если вы используете Session API, обратные вызовы PrePersist и PreUpdate не будут работать согласно этому ответу .

Я использую метод persist () Hibernate Session в своем коде, поэтому единственный способ выполнить эту работу - использовать приведенный ниже код и следовать этому сообщению в блоге (также опубликованному в ответе ).

@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;
    }
}
vicch
источник
Должен возвращать клонированные объекты, как в updated.clone()противном случае другие компоненты могут манипулировать внутренним состоянием (дата)
1ambda
3

Следующий код работал для меня.

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;
}
prranay
источник
Привет, зачем нам вообще нужно, protected Integer id;как protectedв родительском классе, потому что я не мог использовать его в своих тестах, как.getId()
shareef
2

Хороший подход - иметь общий базовый класс для всех ваших сущностей. В этом базовом классе вы можете иметь свойство id, если оно обычно именуется во всех ваших сущностях (общий дизайн), ваших свойствах создания и даты последнего обновления.

На дату создания вы просто сохраняете свойство java.util.Date . Будьте уверены, чтобы всегда инициализировать его с новым Date () .

Для последнего поля обновления вы можете использовать свойство Timestamp, вам нужно сопоставить его с @Version. С этой аннотацией свойство будет автоматически обновляться Hibernate. Помните, что Hibernate также применяет оптимистическую блокировку (это хорошо).

bernardn
источник
2
использование столбца метки времени для оптимистической блокировки - плохая идея. Всегда используйте столбец целочисленной версии. Причина в том, что 2 JVM могут быть в разное время и могут не обладать точностью до миллисекунды. Если вместо этого вы используете hibernate, используйте метку времени БД, это будет означать дополнительные выборки из БД. Вместо этого просто используйте номер версии.
Сету
2

Просто чтобы усилить: java.util.Calenderэто не для отметок времени . java.util.Dateявляется на мгновение агностиком региональных вещей, таких как часовые пояса. Большинство баз данных хранят вещи таким образом (даже если кажется, что это не так; обычно это настройка часового пояса в клиентском программном обеспечении; данные хороши)

davetron5000
источник
1

В качестве типа данных в JAVA я настоятельно рекомендую использовать java.util.Date. Я столкнулся с довольно неприятными проблемами с часовым поясом при использовании Календаря. Смотрите эту тему .

Для установки меток времени я бы порекомендовал использовать либо подход AOP, либо вы можете просто использовать триггеры на столе (на самом деле это единственное, что я нахожу когда-либо приемлемым для использования триггеров).

huo73
источник
1

Вы можете сохранить время как в формате DateTime и в формате UTC. Я обычно использую DateTime вместо Timestamp из-за того, что MySql преобразует даты в UTC и обратно в местное время при хранении и получении данных. Я бы предпочел хранить любую такую ​​логику в одном месте (бизнес-уровень). Я уверен, что есть и другие ситуации, когда использование Timestamp предпочтительнее.

mmacaulay
источник
1

У нас была похожая ситуация. Мы использовали Mysql 5.7.

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

Это сработало для нас.

amdg
источник
Это также работает в случае, когда данные изменяются с помощью SQL-запроса непосредственно в базе данных. @PrePersistи @PrePersistне покрывайте такой случай.
pidabrow
1

Если мы используем @Transactional в наших методах, @CreationTimestamp и @UpdateTimestamp сохранят значение в БД, но вернут значение null после использования save (...).

В этой ситуации, используя saveAndFlush (...), добились цели

чавкать
источник
0

Я думаю, что лучше не делать это в коде Java, вы можете просто установить значение столбца по умолчанию в определении таблицы MySql. введите описание изображения здесь

Уэйн Вэй
источник