Whenever I insert or update the entity with JPA, I want to set the update date column on database systimestamp value. Note I want to use the time of the database node, not the time of the application server. Is this possible with JPA or EclipseLink annotations?
Such a feature is supported by some JPA providers, sadly Eclipselink is not one of them.
Fortunately, the custom EclipseLink attribute converter mechanism allows you to access the Session during conversion, so the following workaround works:
#Converter(name = "database-timestamp", converterClass = DatabaseTimestampFieldConverter.class)
#Entity
public class AuditedEntity {
#Id
#GeneratedValue
private Long id;
private String name;
...
#Convert("database-timestamp")
private Timestamp updatedDate;
#PreUpdate
protected void preUpdate() {
updatedDate = null; // needed to trigger the conversion; if you don't want the extra method here, use #EntityListeners instead
}
}
where DatabaseTimestampFieldConverter is defined as:
public class DatabaseTimestampFieldConverter implements Converter {
#Override
public Object convertObjectValueToDataValue(Object objectValue, Session session) {
return session.executeQuery(new ValueReadQuery("SELECT CURRENT_TIMESTAMP"));
}
#Override
public Object convertDataValueToObjectValue(Object dataValue, Session session) {
return dataValue;
}
#Override
public boolean isMutable() {
return true;
}
#Override
public void initialize(DatabaseMapping mapping, Session session) {
}
}
Alternatively, you could try building on top of the auditing example from the docs. It uses hardcoded database column names rather than field-level annotations, though.
Of course, using the mechanisms your database provides (e.g. triggers) would likely be a more performant solution.
You can use CURRENT_DATE | CURRENT_TIME | CURRENT_TIMESTAMP to get Database Time
Related
I have an TimelineEntity entity, that uses HoTimelineType enum with custom integer value. That custom integer value is stored in the database. Implemented via Using #PostLoad and #PrePersist Annotations
Sprint JPA Repository is used to save and get entities.
Here is the issue:
#Entity
#Table(name = TABLE_NAME)
#IdClass(TimelineKey.class)
public class TimelineEntity {
public interface Persistence {
String TABLE_NAME = "timelines";
}
#Id
#Column(name = "node_id")
private Long nodeId;
#Id
#Column(name = "timeline_id")
private Long timelineId;
#Column(name = "ho_timeline_type")
private Integer hoTimelineTypeValue;
#Transient
private HoTimelineType hoTimelineType;
public Long getNodeId() {
return nodeId;
}
public void setNodeId(Long nodeId) {
this.nodeId = nodeId;
}
public Long getTimelineId() {
return timelineId;
}
public void setTimelineId(Long timelineId) {
this.timelineId = timelineId;
}
public HoTimelineType getHoTimelineType() {
return hoTimelineType;
}
public void setHoTimelineType(HoTimelineType hoTimelineType) {
this.hoTimelineType = hoTimelineType;
}
public Integer getHoTimelineTypeValue() {
return hoTimelineTypeValue;
}
public void setHoTimelineTypeValue(Integer hoTimelineTypeValue) {
this.hoTimelineTypeValue = hoTimelineTypeValue;
}
#PostLoad
private void postLoad() {
this.hoTimelineType = HoTimelineType.of(hoTimelineTypeValue);
}
#PrePersist
private void prePersist() {
this.hoTimelineTypeValue = hoTimelineType.getValue();
}
}
#Eager
public interface TimelineEntityRepository extends JpaRepository<TimelineEntity, TimelineKey> {
List<TimelineEntity> findByNodeId(Long nodeId);
}
#Autowired
private TimelineEntityRepository timelineEntityRepository;
...
TimelineEntity newTE = new TimelineEntity();
newTE.setNodeId(10L);
newTE.setTimelineId(22L);
newTE.setHoTimelineType(HoTimelineType.TYPE_1);
newTE = timelineEntityRepository.save(newTE);
When the newTE entity is saved, prePersist is invoked, and inside this method, the hoTimelineType is null and I get NPE. nodeId and timelineId are not nulls. If I stay with a debugger on the last line, outside of prePersist, I see that hoTimelineType has the value, I set before.
When I load entities, inserted with test data, everything works fine and both hoTimelineType and hoTimelineTypeValue have not nullable values.
I skipped the code of TimelineKey and HoTimelineType to simplify the example. Can add it, if needed.
What could reset hoTimelineType? What do I miss?
It seems there is no way to control the saving behaviour of spring jpa repository proxy.
Possible solutions for issue:
Via javax.persistence.Converter. It is pretty clear, the structe of an entity is simple. Can confirm it works fine with Spring Jpa Repository generation.
Explicitely set hoTimelineTypeValue before you save an entity. Error-prone solution. Everytime you save an entity you must think about the difference between the hoTimelineTypeValue and hoTimelineType.
You could enrich setters and getters of the entity class, to explicitely control the consistency between the fields. It makes implementation of entity classes not so obvious. You get more compicated solution for nothing. As a result error-prone solution. Do not recommend it as well.
Cause of disadvantages of #2 and #3 I do not provide examples. It makes no sense.
Example of the solution #1 can be found here: Using JPA 2.1 #Converter Annotation
Is it posible to capture the entity (Book) that is being modified inside a CustomEntityTrackingListener or a CustomRevisionListener ?
Im trying to get all the information that is being passed through the apis /saveBook or /update/{id}/{pages}, not just the revision information.
When auditing an Entity in envers, it creates automatically a _AUD table for each entity and a revision table to connect the entity and its _AUD table
Using a custom revision listener I can get only the info about the revision, but I would like to reach the entity itself is being modified and saved.
...
#PostMapping("/saveBook")
public Book saveBook(#RequestBody Book book) {
return repository.save(book);
}
#PutMapping("/update/{id}/{pages}")
public Book updateBook(#PathVariable int id, #PathVariable int pages) {
Book book = repository.findById(id).get();
book.setPages(pages);
return repository.save(book);
}
...
#Entity
#AllArgsConstructor
#NoArgsConstructor
#Data
#Audited
public class Book {
#Id
#GeneratedValue
private int id;
private String name;
private int pages;
}
#Entity
//#RevisionEntity(ExampleListener.class)
#RevisionEntity(CustomEntityTrackingRevisionListener.class)
public class ExampleRevEntity extends DefaultRevisionEntity {
private String username;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
#OneToMany(mappedBy="revision", cascade={CascadeType.PERSIST, CascadeType.REMOVE})
private Set<ModifiedEntityTypeEntity> modifiedEntityTypes =
new HashSet<ModifiedEntityTypeEntity>();
}
public class ExampleListener implements RevisionListener {
#Override
public void newRevision(Object revisionEntity) {
ExampleRevEntity exampleRevEntity = (ExampleRevEntity) revisionEntity;
//Identity identity = (Identity) Component.getInstance("org.jboss.seam.security.identity");
exampleRevEntity.setUsername("Joaquin");
}
}
public class CustomEntityTrackingRevisionListener implements EntityTrackingRevisionListener {
#Override
public void entityChanged(Class entityClass, String entityName,
Serializable entityId, RevisionType revisionType,
Object revisionEntity) {
String type = entityClass.getName();
//((CustomTrackingRevisionEntity)revisionEntity).addModifiedEntityType(type);
((ExampleRevEntity)revisionEntity).addModifiedEntityType(type);
}
#Override
public void newRevision(Object revisionEntity) {
}
}
EntityTrackingRevisionListener.entityChanged() is executed after the object persistence, so you can get it from persistence context via find() method of your EntityManager using the identifier and entity class provided.
There are a couple ways you can accomplish this.
Introduce your own event listeners
Use a CDC (change data capture) technology like Debezium.
In the first approach, you would likely want to follow the suggestions in the Envers documentation about how you would do conditional auditing and introduce custom event listeners that extend the Envers listeners in order to deduce the changes and perform whatever tasks you need.
This can be a very daunting and tedious step because you have to understand both how Hibernate emits its data in the events, how to resolve differences, etc.
I believe the easier approach here would be to use a tool such as Debezium that enables you to setup a job that monitors a configured number of tables, in your use case the specific _AUD tables of interest. Every time Envers inserts into those tables, Debezium would react to the insert by generating an event that you can then react against asynchronously.
Debezium has several ways of being used including being embedded into an application which might be suitable for your use case or in a Kafka Connect instance that is separate from the application and provides redundancy and fault tolerance for event capture and dispatch.
I have a simple Persistable Class:
public class Profile implements Persistable<String>{
#Id
private String username;
#CreatedDate
public Date createdDate;
public Profile(String username) {
this.username = username;
}
#Override
public String getId() {
return username;
}
#Override
public boolean isNew() {
return username == null;
}
}
And a simple repository:
public interface ProfileRepository extends MongoRepository<Profile, String> {
}
My Spring Boot Application class is also annotated with #EnableMongoAuditing. But i still can't get the annotation #CreatedDate work.
ProfileRepository.save(new Profile("user1")) writes the entity without the field createdDate. What do i do wrong?
EDIT: This is my Application class (without #EnableMongoRepositories, but it works since the repositories are in the sub-packages i guess)
#SpringBootApplication
#EnableMongoAuditing
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
EDIT: Also adding the annotation EnableMongoRepositories did not change anything.
simply just add #Version field to you #Document class and leave #EnableMongoAuditing i.e.
#Document
public class Profile implements Persistable<String>{
#Version
private Long version;
#Id
private String username;
#CreatedDate
public Date createdDate;
public Profile(String username) {
this.username = username;
}
#Override
public String getId() {
return username;
}
#Override
public boolean isNew() {
return username == null;
}
}
Here is a related issue: https://jira.spring.io/browse/DATAMONGO-946
I just ran into this problem myself, it happens because you are creating the id yourself.
public Profile(String username) {
this.username = username;
}
By doing this, mongo thinks it is not a new object and doesn't use the #CreatedDate annotation. You could also use the #Document annotation instead of implementing the Persistable Class, like this:
#Document
public class Profile{}
As described in Spring Data MongoDB issue DATAMONGO-946, the created date functionality uses the isNew() method to determine whether the created date should be set since the entity is new. In your case, your isNew method is always returning false, since the username is always set.
The comments in the issue presents two possible solutions to this problem.
Persistable solution
The first option is to fix the isNew strategy so that it correctly registers new objects. One way suggested in the comments would be to change the implementation to check the createdDate field itself, since it should only be set on non-new objects.
#Override
public boolean isNew() {
return createdDate == null;
}
Persistent entity solution
The second option is to change from implementing Persistable to instead use a persistent entity and use the #Version annotation to inject a version property in the persisted MongoDB entity. Note that this will change how the data is persisted, as it adds an auto-incrementing version field to the data.
import org.springframework.data.mongodb.core.mapping.Document;
#Document
public class Profile {
#Id
private String username;
#CreatedDate
public Date createdDate;
#Version
public Integer version;
public Profile(String username) {
this.username = username;
}
}
If that's your real class (Profile) then there's nothing you can do with standard tools.
Your isNew method will always return false, because you set the username by yourself and when Profile is about to be saved by Spring it will check the isNew and you probably have username already set. #LastModifiedDate would work in your case, but #CreatedDate won't.
If you haven't got other fields that you can use in isNew method then you have to set the value of createdDate manually (or maybe there's some kind of interceptor that may wrap all mongo template method, but I wouldn't go that way).
For example, check if profile with given username already exists in DB, if so just get it's createdDate(you can use projection here) and set to the profile that you're about to save. Otherwise set createdDate to new date.
For me, i just do it as:
#CreatedDate
#Field("created_date")
#JsonIgnore
private Instant createdDate = Instant.now();
And make sure the Get/Set is available.
Hope this help
I have a similar situation where I needed to set the id manually sometimes and there really is no way to know ahead of time if the id will be a new one or an update without first searching the database to be sure. I also know that over the lifetime of the entity, I will be updating my entity many times, but creating it only once. So to avoid the extra database operations, I went with this solution:
Profile savedProfile = profileRepo.save(profile);
if (savedProfile.getCreatedDate() == null ){
savedProfile.setCreatedDate(savedProfile.getLastModifiedDate());
savedProfile = profileRepo.save(profile);
}
This is taking advantage of the fact that I also have a #LastModifiedDate field on Profile as well that is always updated by spring data when an entity is saved.
I have a simple Persistable Class:
public class Profile implements Persistable<String>{
#Id
private String username;
#CreatedDate
public Date createdDate;
public Profile(String username) {
this.username = username;
}
#Override
public String getId() {
return username;
}
#Override
public boolean isNew() {
return username == null;
}
}
And a simple repository:
public interface ProfileRepository extends MongoRepository<Profile, String> {
}
My Spring Boot Application class is also annotated with #EnableMongoAuditing. But i still can't get the annotation #CreatedDate work.
ProfileRepository.save(new Profile("user1")) writes the entity without the field createdDate. What do i do wrong?
EDIT: This is my Application class (without #EnableMongoRepositories, but it works since the repositories are in the sub-packages i guess)
#SpringBootApplication
#EnableMongoAuditing
public class Application {
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
EDIT: Also adding the annotation EnableMongoRepositories did not change anything.
simply just add #Version field to you #Document class and leave #EnableMongoAuditing i.e.
#Document
public class Profile implements Persistable<String>{
#Version
private Long version;
#Id
private String username;
#CreatedDate
public Date createdDate;
public Profile(String username) {
this.username = username;
}
#Override
public String getId() {
return username;
}
#Override
public boolean isNew() {
return username == null;
}
}
Here is a related issue: https://jira.spring.io/browse/DATAMONGO-946
I just ran into this problem myself, it happens because you are creating the id yourself.
public Profile(String username) {
this.username = username;
}
By doing this, mongo thinks it is not a new object and doesn't use the #CreatedDate annotation. You could also use the #Document annotation instead of implementing the Persistable Class, like this:
#Document
public class Profile{}
As described in Spring Data MongoDB issue DATAMONGO-946, the created date functionality uses the isNew() method to determine whether the created date should be set since the entity is new. In your case, your isNew method is always returning false, since the username is always set.
The comments in the issue presents two possible solutions to this problem.
Persistable solution
The first option is to fix the isNew strategy so that it correctly registers new objects. One way suggested in the comments would be to change the implementation to check the createdDate field itself, since it should only be set on non-new objects.
#Override
public boolean isNew() {
return createdDate == null;
}
Persistent entity solution
The second option is to change from implementing Persistable to instead use a persistent entity and use the #Version annotation to inject a version property in the persisted MongoDB entity. Note that this will change how the data is persisted, as it adds an auto-incrementing version field to the data.
import org.springframework.data.mongodb.core.mapping.Document;
#Document
public class Profile {
#Id
private String username;
#CreatedDate
public Date createdDate;
#Version
public Integer version;
public Profile(String username) {
this.username = username;
}
}
If that's your real class (Profile) then there's nothing you can do with standard tools.
Your isNew method will always return false, because you set the username by yourself and when Profile is about to be saved by Spring it will check the isNew and you probably have username already set. #LastModifiedDate would work in your case, but #CreatedDate won't.
If you haven't got other fields that you can use in isNew method then you have to set the value of createdDate manually (or maybe there's some kind of interceptor that may wrap all mongo template method, but I wouldn't go that way).
For example, check if profile with given username already exists in DB, if so just get it's createdDate(you can use projection here) and set to the profile that you're about to save. Otherwise set createdDate to new date.
For me, i just do it as:
#CreatedDate
#Field("created_date")
#JsonIgnore
private Instant createdDate = Instant.now();
And make sure the Get/Set is available.
Hope this help
I have a similar situation where I needed to set the id manually sometimes and there really is no way to know ahead of time if the id will be a new one or an update without first searching the database to be sure. I also know that over the lifetime of the entity, I will be updating my entity many times, but creating it only once. So to avoid the extra database operations, I went with this solution:
Profile savedProfile = profileRepo.save(profile);
if (savedProfile.getCreatedDate() == null ){
savedProfile.setCreatedDate(savedProfile.getLastModifiedDate());
savedProfile = profileRepo.save(profile);
}
This is taking advantage of the fact that I also have a #LastModifiedDate field on Profile as well that is always updated by spring data when an entity is saved.
I have one entity having composite key and I am trying to persist it by using spring data jpa repository to mysql databse as given below:
#Embeddable
public class MobileVerificationKey implements Serializable{
private static final long serialVersionUID = 1L;
#Column(name="CUSTOMERID")
private Long customerId;
#Column(name="CUSTOMERTYPE")
private Integer customerType;
#Column(name="MOBILE")
private Long mobile;
#Embeddable
public class MobileVerificationKey implements Serializable{
private static final long serialVersionUID = 1L;
#Column(name="CUSTOMERID")
private Long customerId;
#Column(name="CUSTOMERTYPE")
private Integer customerType;
#Column(name="MOBILE")
private Long mobile;
//getter and setters
}
And Entity as
#Entity
#Table(name="mobileverificationdetails")
public class MobileVerificationDetails {
#EmbeddedId
private MobileVerificationKey key;
#Column(name="MOBILETYPE")
private String mobileType;
#Column(name="MOBILEPIN")
private Integer mobilePin;
//getters and setters
}
My spring data jpa repository look like this:
public interface MobileVerificationDetailsRepository extends
CrudRepository<MobileVerificationDetails, MobileVerificationKey> {
#Override
MobileVerificationDetails save(MobileVerificationDetails mobileVerificationDetails);
#Override
MobileVerificationDetails findOne(MobileVerificationKey id);
}
Now if I am trying to add duplicate record with same key for original record and different values for other fields .when i try to insert second record it results in update of existing record with new values instead of throwing exception for violating primary key constraint...can any one please explain me this behavior.
The easiest (and least invasive) way to work around this is probably by making sure the id only gets set right before the persist. This can be achieved in a #PrePersist callback:
abstract class MobileVerificationDetails {
#EmbeddedId
private MobileVerificationKey id;
#PrePersist
void initIdentifier() {
if (id == null) {
this.id = … // Create ID instance here.
}
}
}
Alternatively to that you can enforce persist(…) being used by implementing Persistable and implementing isNew() accordingly. Make sure this method returns true on first insert. We usually see people holding a transient boolean flag that is updated in an #PostPersist/#PostLoad annotated method.
abstract class AbstractEntity<ID extends Serializable> implements Persistable<ID> {
private #Transient boolean isNew = true;
#Override
public boolean isNew() {
return isNew;
}
#PostPersist
#PostLoad
void markNotNew() {
this.isNew = false;
}
}
Spring Data Jpa Repository functionality is implemented via the SimpleJpaRepository class containing following save(..) method:
#Transactional
public <S extends T> S save(S entity) {
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
Thus the Spring Jpa Data Repository save(...) method merges an already existing entity.
Opposed to that the naked EntityManager#persist() throws an exception if invoked with already existing entity.
The problem might be solved by adding custom behavior to Spring Data Repository/ies. The custom behavior might be added using one of the approaches as described in 1.3.1 Adding custom behavior to single repositories with example here or in 1.3.2 Adding custom behavior to all repositories with example here. In both cases the custom behavior would include a new persist() method delegating to EntityManager#persist(). Note that in approach 1.3.2. you already have a EntityManager instance, in the approach 1.3.1 you are able to inject EntityManager instance using the #PersistenceContext.
Opposed to my comment I would recommend adding new method to the repository and not overwriting the existing save(...).