Spring Hibernate Envers: Capture entity being modified - java

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.

Related

JPA update date timestamp

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

Initializing Enums in Hibernate and Spring

I have one simple class
#Entity
#Table(name="user")
public class User implements Serializable {
#Id
#GeneratedValue
private Integer Id;
#Length(min = 5, message = "Username must be at least 5 characters long.")
#Column(name="username",nullable=false,unique=true)
private String userName;
#ManyToMany(cascade= {CascadeType.PERSIST},fetch=FetchType.EAGER)
#JoinTable(name="user_user_profile")
private Set<UserProfile> userProfile = new HashSet<>();
}
And second class:
#Entity
#Table(name = "user_profile")
public class UserProfile {
#javax.persistence.Id
#GeneratedValue
private int Id;
#Column(name = "type", nullable = false, unique = true)
#Enumerated(EnumType.STRING)
private UserProfileType type = UserProfileType.USER;
}
public enum UserProfileType {
USER("USER"),
ADMIN("ADMIN");
}
I'm using Spring MVC and Spring Secuirty with Hibernate. Is there any way to on start of the app make every possible entry in UserProfile Entity (there is only two)? Do I have to get UserProfile from database (via TypedQuery or EntityManager.find() ) and then add it to the User to not make any exceptions?
The enum items are static in your application, so I wouldn't try to make automatic changes in the database. Adding a new record is trivial, but removing an item that is already referenced may need individual care. These values are essential for your application, so I think they should be included in your SQL scripts.
If you are using DB versioning tools such as Flyway or Liquibase, add/remove records of the user_profile table in the migration scripts. They can be configured to run the migrations before your application (and Hibernate) starts, so the application will always see the correct data.
You can add a application start up event and persist the user profiles. You can delete all the user profiles before the application shut down as well. But I wouldn't recommend this as I assume the UserProfiles wouldn't change frequently. If that is the case, you are better off preloading the user profiles via some sql script as suggested in the other answer. If you really want to do it via app, the safest way would be to delete before the app gets shut down. Following is the sample snippet. I assume you are using spring-data-jpa and provided the snippet.
#Component
public class AppStartedListener implements ApplicationListener<ContextRefreshedEvent> {
#Autowired
private UserProfileRepository repository;
#Override
public void onApplicationEvent(ContextRefreshedEvent event) {
for(UserProfileType userProfileType: UserProfileType.values()) {
UserProfile up = new UserProfile(userProfileType);
repository.save(up);
}
}
}
#Component
public class AppStoppedListener implements ApplicationListener<ContextClosedEvent> {
#Autowired
private UserProfileRepository repository;
#Override
public void onApplicationEvent(ContextRefreshedEvent event) {
repository.deleteAll();
}
}
public interface UserProfileRepository extends CrudRepository<UserProfile, Integer> {
}
So I added method to dao layer:
#Transactional
#EventListener
public void handleContextRefresh(ContextRefreshedEvent event) {
UserProfile user=new UserProfile();
em.persist(user);
UserProfile admin=new UserProfile();
admin.setType(UserProfileType.ADMIN);
em.persist(admin);
}
And now, before adding new User i just use HQL to get persistent UserProfile object that I can add to my User. Altough it works I will probably try to load it from some sort of *.sql file since I had to add method metioned above to the Dao layer interface (because of interface type proxy) and I don't like it to be honest.

spring data jpa composite key duplicate key record insertion resulting in update

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(...).

Logical delete at a common place in hibernate

I am using Spring and Hibernate for my application.
I am only allowing logical delete in my application where I need to set the field isActive=false. Instead of repeating the same field in all the entities, I created a Base Class with the property and getter-setter for 'isActive'.
So, during delete, I invoke the update() method and set the isActive to false.
I am not able to get this working. If any one has any idea, please let me know.
Base Entity
public abstract class BaseEntity<TId extends Serializable> implements IEntity<TId> {
#Basic
#Column(name = "IsActive")
protected boolean isActive;
public Boolean getIsActive() {
return isActive;
}
public void setIsActive(Boolean isActive) {
isActive= isActive;
}
}
Child Entity
#Entity(name="Role")
#Table(schema = "dbo")
public class MyEntity extends BaseEntity {
//remaining entities
}
Hibernate Util Class
public void remove(TEntity entity) {
//Note: Enterprise data should be never removed.
entity.setIsActive(false);
sessionFactory.getCurrentSession().update(entity);
}
Try to replace the code in setIsActive method with:
public void setIsActive(Boolean isActive) {
this.isActive = isActive;
}
in your code the use of variable name without this could be ambiguos...
I think you should also add #MappedSuperclass annotation to your abstract class to achieve field inheritance.
The issue with the proposed solution (which you allude to in your comment to that answer) is that does not handle cascading delete.
An alternative (Hibernate specific, non-JPA) solution might be to use Hibernate's #SQLDelete annotation:
http://docs.jboss.org/hibernate/orm/3.6/reference/en-US/html/querysql.html#querysql-cud
I seem to recall however that this Annotation cannot be defined on the Superclass and must be defined on each Entity class.
The problem with Logical delete in general however is that you then have to remember to filter every single query and every single collection mapping to exclude these records.
In my opinion an even better solution is to forget about logical delete altogether. Use Hibernate Envers as an auditing mechanism. You can then recover any deleted records as required.
http://envers.jboss.org/
You can use the SQLDelete annotation...
#org.hibernate.annotations.SQLDelete;
//Package name...
//Imports...
#Entity
#Table(name = "CUSTOMER")
//Override the default Hibernation delete and set the deleted flag rather than deleting the record from the db.
#SQLDelete(sql="UPDATE customer SET deleted = '1' WHERE id = ?")
//Filter added to retrieve only records that have not been soft deleted.
#Where(clause="deleted <> '1'")
public class Customer implements java.io.Serializable {
private long id;
...
private char deleted;
Source: http://featurenotbug.com/2009/07/soft-deletes-using-hibernate-annotations/

hibernate jpa entitymanager commit not writing objects to the database

I'm using hibernate JPA (without Spring) and it's working well, but I have come across a problem which has stumped me for the last 3 days.
I have written some generic DAO classes and am using them to persist my objects. They all work fine, except for one class of object which is not being persisted. No exceptions are thrown. I've tried debugging inside the hibernate code and found that the reason the entity is not being persisted is that in the org.hibernate.event.def.DefaultFlushListener onFlush() method, source.getPersistenceContext().getEntityEntries().size() == 0 so no flushing is performed. But I can't work out why that would be the case.
The classes in question look like this:
#Entity
#Table(name="er_batch_runs")
public class BatchRun implements Serializable, Comparable<BatchRun>, BatchBean {
private Long runId;
private String hostname;
.... more field here
#Override
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="runseq")
#SequenceGenerator(name="runseq", sequenceName="er_batch_runs_seq", allocationSize=1 /*, initialValue = 10*/)
#Column(name="batch_run_id")
public Long getId() {
return runId;
}
public void setId(long runId) {
this.runId = runId;
}
#Column(name="hostname")
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
pretty straightforward hibernate JPA stuff.
Here's another class:
#Entity
#Table(name="er_batch_txns")
public class BatchTxn implements Serializable, Comparable<BatchTxn>, BatchBean {
private long id;
.......... more fields
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="batchtxnseq")
#SequenceGenerator(name="batchtxnseq", sequenceName="ER_BATCH_TXNS_SEQ", allocationSize=1/*00, initialValue = 10*/)
#Override
#Id
#Column(name="BATCH_TXN_ID")
public Long getId() {
return id;
}
the BatchBean interface is what allows me to use generic DAOs like this:
public Long create(BatchBean newInstance) {
getOpenEntityManager().persist(newInstance);
logger.debug("hopefully created {} with id {}",newInstance.getTypeName(),newInstance.getId());
return newInstance.getId();
}
Transactions are being handled manually. I've set the flush type to COMMIT (ie flush on commit) and when I've completed the persist, I do a commit. After the persist, then BatchTxn object has been assigned a primary key from the sequence. When I debug hibernate I can see that getPersistenceContext().getEntityEntries() returns an empty Map.
so the question is why the BatchTxn is not being persisted by the commit, when the BatchRuns, and 5 other classes which implement BatchBean, are?
I'm using hibernate 3.6.0 Final
The only thing I saw that is suspected in your code is this in the BatchTxn class:
private long id;
This will be set automatically to zero. Maybe you should use Long (with a capital letter)?

Categories