Registro de data e hora da criação e registro de data e hora da última atualização com Hibernate e MySQL

244

Para uma determinada entidade do Hibernate, temos o requisito de armazenar seu horário de criação e a última vez em que foi atualizado. Como você projetaria isso?

  • Quais tipos de dados você usaria no banco de dados (assumindo o MySQL, possivelmente em um fuso horário diferente da JVM)? Os tipos de dados reconhecem o fuso horário?

  • Quais são os tipos de dados que você usaria em Java ( Date, Calendar, long, ...)?

  • A quem você seria responsável por definir os registros de data e hora - o banco de dados, a estrutura ORM (Hibernate) ou o programador de aplicativos?

  • Quais anotações você usaria para o mapeamento (por exemplo @Temporal)?

Não estou apenas procurando uma solução funcional, mas uma solução segura e bem projetada.

ngn
fonte

Respostas:

266

Se você estiver usando as anotações JPA, poderá usar ganchos de eventos @PrePersiste @PreUpdatefaça isso:

@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();
  }
}

ou você pode usar a @EntityListeneranotação na classe e colocar o código do evento em uma classe externa.

Guðmundur Bjarni
fonte
7
Funciona sem problemas no J2SE, pois @PrePersist e @PerUpdate são anotações JPA.
KDeveloper
2
@Kumar - Caso esteja usando uma sessão simples do Hibernate (em vez do JPA), você pode tentar ouvintes de hibernação, embora isso não seja muito elegante e compacto e as anotações do JPA.
Shailendra
43
Na atual Hibernate com JPA pode-se usar "@CreationTimestamp" e "@UpdateTimestamp"
Florian Loch
@FlorianLoch existe um equivalente para Data em vez de Data e hora? Ou eu teria que criar o meu?
21316 Mike
150

Você pode apenas usar @CreationTimestampe @UpdateTimestamp:

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

@UpdateTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "modify_date")
private Date modifyDate;
idmitriev
fonte
3
obrigado mano uma coisa tão pequena precisa atualizar timestamp. Eu não sabia. você salvou meu dia.
Virendra Sagar
Existe TemporalType.DATEno primeiro caso e TemporalType.TIMESTAMPno segundo.
precisa saber é o seguinte
Você está dizendo que isso também define automaticamente os valores? Essa não é a minha experiência; parece mesmo com @CreationTimestampe @UpdateTimestampum precisa de algum @Column(..., columnDefinition = "timestamp default current_timestamp"), ou usa @PrePersiste @PreUpdate(este último também garante que os clientes não possam definir um valor diferente).
Arjan #
2
Quando atualizo o objeto e o persisto, o bd perdeu o create_date ... por quê?
Brenno Leal
1
Im meu caso a remoção nullable=falsede @Column(name = "create_date" , nullable=false)trabalhado
Shantaram Tupe
113

Tomando os recursos deste post juntamente com as informações tiradas à esquerda e à direita de diferentes fontes, eu vim com esta solução elegante, crie a seguinte classe abstrata

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();
    }
}

e faça com que todas as suas entidades o estendam, por exemplo:

@Entity
@Table(name = "campaign")
public class Campaign extends AbstractTimestampEntity implements Serializable {
...
}
Olivier Refalo
fonte
5
isso é bom até que você queira adicionar diferentes comportamentos exclusivos às suas entidades (e você não pode estender mais de uma classe base). de qualquer maneira, a única maneira de obter o mesmo efeito sem uma classe base é que os ouvintes de aspetos ou eventos vêem @kieren dixon answer
gpilotino
3
Eu faria isso usando um gatilho MySQL para que, mesmo que a entidade completa não seja salva ou seja modificada por qualquer aplicativo externo ou consulta manual, ela ainda atualize esses campos.
Webnet 25/07/2013
3
você pode me dar algum exemplo de trabalho, porque eu estou experimentando exceçãonot-null property references a null or transient value: package.path.ClassName.created
Sumit Ramteke
@rishiAgar, não, eu não tenho. Mas, por enquanto, atribui data à minha propriedade do construtor padrão. Vou deixar você saber uma vez que eu encontrei.
Sumit Ramteke
1
Altere-o para @Column(name = "updated", nullable = false, insertable = false)fazê-lo funcionar. Interessante que esta resposta tem tantos upvotes ..
displayname
20

1. Quais tipos de colunas do banco de dados você deve usar

Sua primeira pergunta foi:

Quais tipos de dados você usaria no banco de dados (assumindo o MySQL, possivelmente em um fuso horário diferente da JVM)? Os tipos de dados reconhecem o fuso horário?

No MySQL, o TIMESTAMPtipo de coluna muda do fuso horário local do driver JDBC para o fuso horário do banco de dados, mas ele pode armazenar apenas carimbos de data / hora até '2038-01-19 03:14:07.999999, portanto, não é a melhor opção para o futuro.

Portanto, use melhor DATETIME, que não tem essa limitação do limite superior. No entanto, DATETIMEnão está ciente do fuso horário. Portanto, por esse motivo, é melhor usar o UTC no lado do banco de dados e usar a hibernate.jdbc.time_zonepropriedade Hibernate.

Para mais detalhes sobre a hibernate.jdbc.time_zoneconfiguração, consulte este artigo .

2. Que tipo de propriedade da entidade você deve usar

Sua segunda pergunta foi:

Quais tipos de dados você usaria em Java (data, calendário, longo, ...)?

No lado do Java, você pode usar o Java 8 LocalDateTime. Você também pode usar o legado Date, mas os tipos Data / Hora do Java 8 são melhores, pois são imutáveis ​​e não fazem um fuso horário mudar para o fuso horário local ao registrá-los.

Para obter mais detalhes sobre os tipos de data / hora do Java 8 suportados pelo Hibernate, consulte este artigo .

Agora, também podemos responder a esta pergunta:

Quais anotações você usaria para o mapeamento (por exemplo @Temporal)?

Se você estiver usando a propriedade LocalDateTimeou java.sql.Timestamppara mapear uma entidade de carimbo de data / hora, não precisará usá-lo, @Temporalpois o HIbernate já sabe que essa propriedade deve ser salva como um carimbo de data / hora do JDBC.

Somente se você estiver usando java.util.Date, precisará especificar a @Temporalanotação, assim:

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

Mas é muito melhor se você mapeá-lo assim:

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

Como gerar os valores da coluna de auditoria

Sua terceira pergunta foi:

A quem você seria responsável por definir os registros de data e hora - o banco de dados, a estrutura ORM (Hibernate) ou o programador de aplicativos?

Quais anotações você usaria para o mapeamento (por exemplo, @Temporal)?

Existem várias maneiras de atingir esse objetivo. Você pode permitir que o banco de dados faça isso.

Para a create_oncoluna, você pode usar uma DEFAULTrestrição de DDL, como:

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

Para a updated_oncoluna, você pode usar um acionador de banco de dados para definir o valor da coluna CURRENT_TIMESTAMP()toda vez que uma determinada linha for modificada.

Ou use JPA ou Hibernate para defini-los.

Vamos supor que você tenha as seguintes tabelas de banco de dados:

Tabelas de banco de dados com colunas de auditoria

E, cada tabela possui colunas como:

  • created_by
  • created_on
  • updated_by
  • updated_on

Usando o Hibernate @CreationTimestampe @UpdateTimestampanotações

O Hibernate oferece as anotações @CreationTimestampe @UpdateTimestampque podem ser usadas para mapear as colunas created_one updated_on.

Você pode usar @MappedSuperclasspara definir uma classe base que será estendida por todas as entidades:

@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
}

E, todas as entidades estenderão o BaseEntity, assim:

@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
}

Para mais detalhes sobre o uso @MappedSuperclass, confira este artigo .

No entanto, mesmo se os createdOne updateOnpropriedades são definidas pelo específicos de hibernação @CreationTimestampe @UpdateTimestampanotações, a createdBye updatedByrequerem registrar um retorno de chamada aplicação, tal como ilustrado pelo seguinte solução APP.

Usando JPA @EntityListeners

Você pode encapsular as propriedades de auditoria em um Incorporável:

@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
}

E, crie um AuditListenerpara definir as propriedades de auditoria:

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());
    }
}

Para registrar AuditListener, você pode usar a @EntityListenersanotação 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
}

Para obter mais detalhes sobre como implementar propriedades de auditoria com a JPA @EntityListener, consulte este artigo .

Vlad Mihalcea
fonte
Resposta muito completa, obrigado. Eu discordo sobre preferir datetimemais timestamp. Você deseja que seu banco de dados saiba o fuso horário de seus carimbos de data e hora. Isso evita erros de conversão de fuso horário.
Ole VV
O timestsmptipo não armazena informações de fuso horário. Ele apenas conversa do aplicativo TZ para o DB TZ. Na realidade, você deseja armazenar a TZ do cliente separadamente e fazer a conversa no aplicativo antes de renderizar a interface do usuário.
Vlad Mihalcea 23/02
Corrigir. O MySQL timestampestá sempre no UTC. O MySQL converte TIMESTAMPvalores do fuso horário atual em UTC para armazenamento e volta do UTC para o fuso horário atual para recuperação. Documentação do MySQL: Os tipos DATE, DATETIME e TIMESTAMP
Ole VV
17

Você também pode usar um interceptador para definir os valores

Crie uma interface chamada TimeStamped que suas entidades implementam

public interface TimeStamped {
    public Date getCreatedDate();
    public void setCreatedDate(Date createdDate);
    public Date getLastUpdated();
    public void setLastUpdated(Date lastUpdatedDate);
}

Defina o interceptador

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;
    }
}

E registre-o na fábrica de sessões

Kieren Dixon
fonte
1
Funciona, obrigado. Informações adicionais docs.jboss.org/hibernate/core/4.0/manual/en-US/html_single/...
Andrii Nemchenko
Esta é uma solução, se você trabalha com SessionFactory em vez de EntityManager!
Olivmir
Apenas para aqueles que sofrem com um problema semelhante ao meu neste contexto: se sua entidade não definir esses campos extras (createdAt, ...), mas herdá-los de uma classe pai, essa classe pai deverá ser anotada com @MappedSuperclass - caso contrário, o Hibernate não encontra esses campos.
27418 olivmir
17

Com a solução de Olivier, durante as instruções de atualização, você pode encontrar:

com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: a coluna 'criada' não pode ser nula

Para resolver isso, adicione updatable = false à anotação @Column do atributo "created":

@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created", nullable = false, updatable=false)
private Date created;
endriju
fonte
1
Nós estamos usando @Version. Quando uma entidade é definida, duas chamadas são feitas, uma é para salvar e outra para atualizar. Eu estava enfrentando o mesmo problema por causa disso. Depois que adicionei @Column(updatable = false), resolvi meu problema.
Ganesh Satpute
12

Obrigado a todos que ajudaram. Depois de fazer uma pesquisa (eu sou o cara que fez a pergunta), eis o que achei mais interessante:

  • Tipo de coluna do banco de dados: o número independente de fuso horário de milissegundos desde 1970 representado decimal(20)porque 2 ^ 64 tem 20 dígitos e o espaço em disco é barato; sejamos diretos. Além disso, não usarei DEFAULT CURRENT_TIMESTAMPnem gatilhos. Não quero mágica no DB.

  • Java tipo de campo: long. O registro de data e hora do Unix é bem suportado em várias bibliotecas, longnão apresenta problemas no Y2038, a aritmética do registro de data e hora é rápida e fácil (principalmente operador <e operador +, assumindo que não há dias / meses / anos envolvidos nos cálculos). E, mais importante, ambos os primitivos longs e java.lang.Longs são imutáveis -effectively passados por valor, ao contrário de java.util.Dates; Eu ficaria realmente chateado ao encontrar algo parecido foo.getLastUpdate().setTime(System.currentTimeMillis())ao depurar o código de outra pessoa.

  • A estrutura ORM deve ser responsável por preencher os dados automaticamente.

  • Ainda não testei isso, mas apenas olhando os documentos que suponho que @Temporalfarão o trabalho; não tenho certeza se devo usar @Versionpara esse fim. @PrePersiste @PreUpdatesão boas alternativas para controlar isso manualmente. Adicionar isso ao supertipo de camada (classe base comum) para todas as entidades é uma idéia interessante, desde que você realmente queira o carimbo de data e hora para todas as suas entidades.

ngn
fonte
Embora longs e Longs possam ser imutáveis, isso não o ajudará na situação que você descreve. Eles ainda podem dizer foo.setLastUpdate (new Long (System.currentTimeMillis ());
Ian McLaird
2
Isso é bom. O Hibernate requer o configurador de qualquer maneira (ou tentará acessar o campo diretamente através da reflexão). Eu estava falando sobre dificuldade em caçar quem está modificando o carimbo de data e hora do código do aplicativo. É complicado quando você pode fazer isso usando um getter.
ngn 23/10/08
Concordo com sua afirmação de que a estrutura ORM deve ser responsável por preencher a data automaticamente, mas eu daria um passo adiante e diria que a data deve ser definida no relógio do servidor de banco de dados, e não no cliente. Não estou claro se isso atinge esse objetivo. No sql, eu posso fazer isso usando a função sysdate, mas não sei como fazer isso no Hibernate ou em qualquer implementação JPA.
MiguelMunoz
Não quero mágica no DB. Entendo o que você quer dizer, mas gosto de considerar o fato de que o banco de dados deve se proteger de desenvolvedores ruins / novos / sem noção. A integridade dos dados é muito importante em uma grande empresa; você não pode confiar em outras pessoas para inserir bons dados. Restrições, padrões e FKs ajudarão a conseguir isso.
Icegras
6

Caso você esteja usando a API da sessão, os retornos de chamada PrePersist e PreUpdate não funcionarão de acordo com esta resposta .

Estou usando o método persist () do Hibernate Session no meu código, para que a única maneira de fazer esse trabalho fosse com o código abaixo e seguindo esta postagem do blog (também postada na resposta ).

@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
fonte
Deve retornar objectos clonados como updated.clone()de outro modo outros componentes pode manipular o estado interno (data)
1ambda
3

O código a seguir funcionou para mim.

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
fonte
Oi, por que precisaríamos protected Integer id;como protectedna classe pai em geral, porque eu não poderia usá-lo nos meus casos de teste como.getId()
shareef 18/06
2

Uma boa abordagem é ter uma classe base comum para todas as suas entidades. Nesta classe base, você pode ter sua propriedade id se for comumente nomeada em todas as suas entidades (um design comum), suas propriedades de criação e data da última atualização.

Para a data de criação, você simplesmente mantém uma propriedade java.util.Date . Certifique-se de sempre inicializá-lo com a nova Data () .

Para o último campo de atualização, você pode usar uma propriedade Timestamp, é necessário mapeá-la com @Version. Com esta anotação, a propriedade será atualizada automaticamente pelo Hibernate. Cuidado que o Hibernate também aplicará bloqueio otimista (é uma coisa boa).

Bernardn
fonte
2
usar uma coluna de carimbo de data / hora para bloqueio otimista é uma má ideia. Sempre use uma coluna de versão inteira. Por isso, duas JVMs podem estar em horários diferentes e podem não ter precisão de milissegundos. Se você fizer o hibernate usar o registro de data e hora do banco de dados, isso significaria seleções adicionais no banco de dados. Em vez disso, basta usar o número da versão.
sethu
2

Apenas para reforçar: java.util.Calendernão é para registros de data e hora . java.util.Datepor um momento, agnóstico de coisas regionais, como fusos horários. A maioria dos bancos de dados armazena as coisas dessa maneira (mesmo que pareçam não; isso geralmente é uma configuração de fuso horário no software cliente; os dados são bons)

davetron5000
fonte
1

Como tipo de dados em JAVA, é altamente recomendável usar o java.util.Date. Eu tive problemas bastante desagradáveis ​​de fuso horário ao usar o Calendário. Veja este tópico .

Para definir os registros de data e hora, eu recomendaria usar uma abordagem de AOP ou você poderia simplesmente usar Triggers na tabela (na verdade, essa é a única coisa que considero aceitável o uso de triggers).

huo73
fonte
1

Você pode considerar armazenar a hora como um DateTime e no UTC. Eu normalmente uso DateTime em vez de Timestamp devido ao fato de o MySql converter datas para UTC e voltar para a hora local ao armazenar e recuperar os dados. Prefiro manter esse tipo de lógica em um só lugar (camada de negócios). Tenho certeza de que há outras situações em que o uso do Timestamp é preferível.

mmacaulay
fonte
1

Tivemos uma situação semelhante. Estávamos usando o Mysql 5.7.

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

Isso funcionou para nós.

amdg
fonte
Também funciona em um caso em que os dados são modificados por uma consulta SQL diretamente no banco de dados. @PrePersiste @PrePersistnão cubra esse caso.
pidabrow 20/01
1

Se estivermos usando @Transactional em nossos métodos, @CreationTimestamp e @UpdateTimestamp salvarão o valor no DB, mas retornarão nulos após o uso de save (...).

Nessa situação, o uso de saveAndFlush (...) fez o truque

chomp
fonte
0

Eu acho que é mais limpo não fazer isso no código Java, você pode simplesmente definir o valor padrão da coluna na definição da tabela MySql. insira a descrição da imagem aqui

Wayne Wei
fonte