Створення часової мітки та останньої мітки оновлення за допомогою Hibernate та MySQL


244

Для певного сплячого суб'єкта у нас є вимога зберігати час його створення та останній раз, коли він був оновлений. Як би ви це спроектували?

  • Які типи даних ви б використовували в базі даних (припускаючи MySQL, можливо, в іншому часовому поясі, ніж JVM)? Чи будуть відомі типи даних часовим поясом?

  • Які типи даних ви будете використовувати в Java ( Date, Calendar, long...)?

  • Кого б ви відповідали за встановлення часових позначок - база даних, рамка ORM (сплячий режим) або програміст програми?

  • Які анотації ви б використали для картографування (наприклад @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 - Якщо ви використовуєте звичайний сплячий сеанс (замість JPA), ви можете спробувати слухачів сплячої події, хоча це не дуже елегантно та компактно проти анотацій JPA.
Шайлендра

43
У поточному сплячому режимі разом із JPA можна використовувати "@CreationTimestamp" та "@UpdateTimestamp"
Флоріан Лох

@FlorianLoch є еквівалент для Date, а не Timestamp? Або я мав би створити свою власну?
мій

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;

3
спасибі, брат, така маленька річ потрібно оновити часову позначку. Я не знав. ти врятував мій день.
Вірендра Сагар

Є TemporalType.DATEв першому випадку і TemporalType.TIMESTAMPв другому.
v.ladynev

Ви кажете, що це також автоматично встановлює значення? Це не мій досвід; здається, навіть з @CreationTimestampі @UpdateTimestampпотрібно або дещо @Column(..., columnDefinition = "timestamp default current_timestamp"), або використовувати @PrePersistі @PreUpdate(останнє також добре забезпечує клієнтам не змогу встановити інше значення).
Ар’ян

2
Коли я оновлюю об'єкт і зберігаю його, bd втрачає create_date ... чому?
Бренно Ліал

1
Я мій вилучення справи nullable=falseз @Column(name = "create_date" , nullable=false)відпрацьованої
Шантарам Тупе

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 єдиний спосіб отримати той же ефект без базового класу, хоча аспект тощо і слухачі подій див. відповідь @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 (дата, календар, довгий, ...)?

З боку Java ви можете використовувати Java 8 LocalDateTime. Ви також можете використовувати спадщину Date, але типи дати / часу Java 8 є кращими, оскільки вони незмінні, і не здійснюйте переміщення часового поясу на локальний часовий пояс під час реєстрації.

Щоб отримати докладнішу інформацію про типи дати / часу Java 8, підтримуваних Hibernate, перегляньте цю статтю .

Тепер ми також можемо відповісти на це питання:

Які анотації ви б використали для картографування (наприклад @Temporal)?

Якщо ви використовуєте LocalDateTimeабо java.sql.Timestampдля відображення властивості об'єкта часової позначки, вам це не потрібно використовувати, @Temporalоскільки HIbernate вже знає, що це властивість потрібно зберігати як JDBC Timestamp.

Тільки якщо ви користуєтесь java.util.Date, вам потрібно вказати @Temporalпримітку, наприклад:

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_on")
private Date createdOn;

Але, набагато краще, якщо відобразити його так:

@Column(name = "created_on")
private LocalDateTime createdOn;

Як генерувати значення стовпців аудиту

Ваше третє питання:

Кого б ви відповідали за встановлення часових позначок - база даних, рамка ORM (сплячий режим) або програміст програми?

Які анотації ви б використали для картографування (наприклад, @Temporal)?

Є багато способів досягти цієї мети. Ви можете дозволити цьому зробити базу даних ..

Для create_onстовпця ви можете використовувати DEFAULTобмеження DDL, наприклад:

ALTER TABLE post 
ADD CONSTRAINT created_on_default 
DEFAULT CURRENT_TIMESTAMP() FOR created_on;

Для updated_onстовпця ви можете використовувати тригер DB для встановлення значення стовпця CURRENT_TIMESTAMP()щоразу, коли заданий рядок буде змінено.

Або використовуйте JPA або Hibernate, щоб встановити їх.

Припустимо, у вас є такі таблиці баз даних:

Таблиці баз даних зі стовпцями аудиту

І в кожній таблиці є стовпці типу:

  • created_by
  • created_on
  • updated_by
  • updated_on

Використання сплячого режиму @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 окремо і вести розмову в додатку до надання інтерфейсу користувача.
Влад Михальча

Правильно. MySQL timestampзавжди в UTC. MySQL перетворює TIMESTAMPзначення з поточного часового поясу в UTC для зберігання і назад з UTC у поточний часовий пояс для пошуку. Документація на MySQL: типи дати, дати та тиміграму
Ole VV,

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!
olivmir

Тільки для тих, хто страждає з подібною проблемою, як я в цьому контексті: якщо ваша сутність сама не визначає ці додаткові поля (createdAt, ...), але успадковує її від батьківського класу, то цей батьківський клас потрібно анотувати з @MappedSuperclass - в іншому випадку Hibernate не знайде цих полів.
olivmir

17

Завдяки рішенню Олів'є, під час оновлення операторів ви можете стикатися з:

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: стовпець "створено" не може бути нульовим

Щоб вирішити це питання, додайте оновлений = false до анотації "створений" @Column анотацію:

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

1
Ми використовуємо @Version. Коли сутність встановлена ​​двома дзвінками, один - зберегти, а інший - оновити. Я зіткнувся з тим же питанням через це. Одного разу я додав @Column(updatable = false)це вирішив мою проблему.
Ганеш Сатпут

12

Дякую всім, хто допомагав. Провівши кілька досліджень (я хлопець, який задав питання), ось що я знайшов для себе сенс:

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

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

  • Рамка ORM повинна відповідати за автоматичне заповнення даних.

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


Хоча Лонг і Лонг можуть бути непорушними, це не допоможе тобі в описуваній ситуації. Вони все ще можуть сказати foo.setLastUpdate (новий Long (System.currentTimeMillis ());
Ian McLaird

2
Добре. В режимі глибокого сну потрібен сетер все одно (або він спробує отримати доступ до поля безпосередньо через відображення). Я говорив про труднощі переслідування, хто змінює часову позначку з коду нашої програми. Це складно, коли це можна зробити за допомогою геттера.
ngn

Я погоджуюся з вашим твердженням, що рамки ORM повинні відповідати за заповнення дати автоматично, але я б пішов на крок далі і сказав, що дату слід встановлювати з годинника сервера баз даних, а не клієнта. Мені не ясно, чи вдасться досягти цієї мети. У sql я можу це зробити за допомогою функції sysdate, але я не знаю, як це зробити в режимі глибокого сну або будь-якій реалізації JPA.
МігельМуноз

Я не хочу ніякої магії в БД. Я бачу, що ви маєте на увазі, але мені подобається враховувати той факт, що база даних повинна захищати себе від поганих / нових / незрозумілих розробників. Цілісність даних дуже важлива у великій компанії, ви не можете покластися на те, щоб інші вставили хороші дані. Обмеження, параметри за замовчуванням та ФК допоможуть досягти цього.
Icegras

6

Якщо ви використовуєте API сесії, зворотні виклики PrePersist і PreUpdate не працюватимуть відповідно до цієї відповіді .

Я використовую метод персистенту () 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;
    }
}

Якщо повертати клоновані об'єкти, як 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;
}

Привіт, навіщо нам це потрібно, protected Integer id;як protectedу батьківському класі взагалі, тому що я не міг використовувати його у своїх тестових випадках як.getId()
shareef

2

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

Для дати створення ви просто зберігаєте властивість java.util.Date . Не забудьте завжди ініціалізувати його з новою датою () .

Для останнього поля оновлення ви можете використовувати властивість Timestamp, вам потрібно зіставити його за допомогою @Version. За допомогою цієї Анотації властивість автоматично оновлюватиметься в сплячому режимі. Остерігайтеся, що сплячий режим також застосує оптимістичне блокування (це добре).


2
використання стовпчика часової позначки для оптимістичного блокування - погана ідея. Завжди використовуйте цілий стовпець версії. Причина: 2 СВМ можуть бути в різний час і не мати точності в мілісекундах. Якщо ви замість цього перебуваєте у сплячому режимі, використовуйте часову позначку БД, це означатиме додаткові вибори з БД. Замість цього просто використовуйте номер версії.
sethu

2

Тільки для підкріплення: java.util.Calenderне для Timestamps . java.util.Dateє на мить часом агностиком таких регіональних речей, як часові пояси. Більшість баз даних зберігають речі таким чином (навіть якщо вони здаються не такими; це зазвичай налаштування часового поясу в програмному забезпеченні клієнта; дані хороші)


1

Як тип даних у JAVA, я настійно рекомендую використовувати java.util.Date. У мене виникли досить неприємні проблеми з часовим поясом при використанні календаря. Дивіться цю нитку .

Для встановлення часових позначок я рекомендую використовувати або AOP-підхід, або ви можете просто використовувати тригери на столі (насправді це єдине, що я вважаю можливим використання тригерів).


1

Ви можете розглянути можливість зберігання часу як DateTime, так і в UTC. Зазвичай я використовую DateTime замість Timestamp через те, що MySql перетворює дати в UTC та назад у місцевий час під час зберігання та отримання даних. Я вважаю за краще зберігати будь-яку подібну логіку в одному місці (Бізнес-рівень). Я впевнений, що є й інші ситуації, коли використання Timestamp є кращим.


1

У нас була схожа ситуація. Ми використовували Mysql 5.7.

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

Це працювало на нас.


Він також працює у випадку, коли дані модифікуються за допомогою SQL-запиту безпосередньо в базі даних. @PrePersistі @PrePersistне висвітлювати такий випадок.
pidabrow

1

Якщо ми використовуємо @Transactional в наших методах, @CreationTimestamp та @UpdateTimestamp збережуть значення в БД, але повернуть нульове значення після використання save (...).

У цій ситуації, використовуючи saveAndFlush (...) зробив свою справу


0

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.