I have an entity with a transient field. When I want to create a new instance of the object I lose my transient information. The following example demonstrates the issue.
This is, more or less, working as designed. The semantics of transient are precisely that the data is not persisted. The entity returned from entityManager.merge(obj)
is, in fact, an entirely new entity that maintains the state of the object passed into merge (state, in this context, being anything that is not part of the persistent object). This is detailed in the JPA spec. Note: There may be JPA implementations that do maintain the transient field after the object is merged (simply because they return the same object), but this behavior is not guaranteed by the spec.
There are essentially two things you can do:
Decide to persist the transient field. It doesn't really seem to be transient if you need it after merging the class into the persistence context.
Maintain the value of the transient field outside of the persistent object. If this is what meets your needs, you may want to rethink the structure of your domain class; if this field is not part of the state of the domain object it really shouldn't be there.
One final thing: the main use case I've found for transient fields on domain classes is to demarcate derived fields, i.e., fields that can be recalculated based on the persistent fields of the class.
Based on @Prassed Amazing answer I've created a more generic code:
I need to allow some transient fields on the entity (I mean fields that we do not keep on the DB, but we allow the user to fill them with data that we send to the server [with @JsonSerialize/@JsonDeserialize] and upload to file storage).
These fields will be annotated with the below annotation (RetentionPolicy.RUNTIME is used here so I can use reflection on those fields at runtime):
@Retention(RetentionPolicy.RUNTIME)
public @interface PreservePostMerge { }
Then, I traverse those fields using apache's FieldUtil:
@Aspect
@Component
public class PreservePostMergeData {
private final Logger log = LoggerFactory.getLogger(PreservePostMergeData.class);
@Around("execution(public !void javax.persistence.EntityManager.merge(..))")
private Object preserveTransientDataPostMerge(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
Object afterMerge = joinPoint.proceed();
if (args.length > 0) {
Object beforeMerge = args[0];
Field[] annotatedFieldsToPreserve = FieldUtils.getFieldsWithAnnotation(beforeMerge.getClass(), PreservePostMerge.class);
Arrays.stream(annotatedFieldsToPreserve).forEach(field -> {
try {
FieldUtils.writeField(field, afterMerge, FieldUtils.readField(field, beforeMerge, true), true);
} catch (IllegalAccessException exception) {
log.warn("Illegal accesss to field: {}, of entity: {}. Data was not preserved.", field.getName(), beforeMerge.getClass());
}
});
}
return afterMerge;
}
}
Late to join the discussion but this is how I achieved it using spring AOP and JPA provided @PreUpdate annotation (Adding detailed version)
Use Case
Db configuration
package config;
import io.github.jhipster.config.JHipsterConstants;
import io.github.jhipster.config.liquibase.AsyncSpringLiquibase;
import liquibase.integration.spring.SpringLiquibase;
import org.h2.tools.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.core.task.TaskExecutor;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.sql.SQLException;
@Configuration
@EnableJpaRepositories("repository")
@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware")
@EnableTransactionManagement
public class DatabaseConfiguration {
private final Logger log = LoggerFactory.getLogger(DatabaseConfiguration.class);
private final Environment env;
public DatabaseConfiguration(Environment env) {
this.env = env;
}
/* Other code */
}
SpringSecurityAuditorAware for injecting the Username
package security;
import config.Constants;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
/**
* Implementation of AuditorAware based on Spring Security.
*/
@Component
public class SpringSecurityAuditorAware implements AuditorAware<String> {
@Override
public String getCurrentAuditor() {
String userName = SecurityUtils.getCurrentUserLogin();
return userName != null ? userName : Constants.SYSTEM_ACCOUNT;
}
}
abstract entity with JPA @PreUpdate
This will actually set the value for the @LastModifiedBy and @LastModifiedDate fields
package domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.hibernate.envers.Audited;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import javax.persistence.PreUpdate;
import java.io.Serializable;
import java.time.Instant;
/**
* Base abstract class for entities which will hold definitions for created, last modified by and created,
* last modified by date.
*/
@MappedSuperclass
@Audited
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractAuditingEntity implements Serializable {
private static final long serialVersionUID = 1L;
@CreatedBy
@Column(name = "created_by", nullable = false, length = 50, updatable = false)
@JsonIgnore
private String createdBy;
@CreatedDate
@Column(name = "created_date", nullable = false)
@JsonIgnore
private Instant createdDate = Instant.now();
@LastModifiedBy
@Column(name = "last_modified_by", length = 50)
@JsonIgnore
private String lastModifiedBy;
@LastModifiedDate
@Column(name = "last_modified_date")
@JsonIgnore
private Instant lastModifiedDate = Instant.now();
private transient String backendAuditor;
private transient Instant backendModifiedDate;
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public Instant getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Instant createdDate) {
this.createdDate = createdDate;
}
public String getLastModifiedBy() {
return lastModifiedBy;
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public Instant getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(Instant lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
public String getBackendAuditor() {
return backendAuditor;
}
public void setBackendAuditor(String backendAuditor) {
this.backendAuditor = backendAuditor;
}
public Instant getBackendModifiedDate() {
return backendModifiedDate;
}
public void setBackendModifiedDate(Instant backendModifiedDate) {
this.backendModifiedDate = backendModifiedDate;
}
@PreUpdate
public void preUpdate(){
if (null != this.backendAuditor) {
this.lastModifiedBy = this.backendAuditor;
}
if (null != this.backendModifiedDate) {
this.lastModifiedDate = this.backendModifiedDate;
}
}
}
Aspect for merging the data for retention after merge
This would intercept the object (Entity) and reset the fields
package aop.security.audit;
import domain.AbstractAuditingEntity;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.Instant;
@Aspect
@Component
public class ExternalDataInflowAudit {
private final Logger log = LoggerFactory.getLogger(ExternalDataInflowAudit.class);
// As per our requirements, we need to override @LastModifiedBy and @LastModifiedDate
// https://stackoverflow.com/questions/2581665/jpa-transient-information-lost-on-create?answertab=active#tab-top
@Around("execution(public !void javax.persistence.EntityManager.merge(..))")
private Object resetAuditFromExternal(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
AbstractAuditingEntity abstractAuditingEntity;
Instant lastModifiedDate = null;
String lastModifiedBy = null;
if (args.length > 0 && args[0] instanceof AbstractAuditingEntity) {
abstractAuditingEntity = (AbstractAuditingEntity) args[0];
lastModifiedBy = abstractAuditingEntity.getBackendAuditor();
lastModifiedDate = abstractAuditingEntity.getBackendModifiedDate();
}
Object proceed = joinPoint.proceed();
if (proceed instanceof AbstractAuditingEntity) {
abstractAuditingEntity = (AbstractAuditingEntity) proceed;
if (null != lastModifiedBy) {
abstractAuditingEntity.setLastModifiedBy(lastModifiedBy);
abstractAuditingEntity.setBackendAuditor(lastModifiedBy);
log.debug("Setting the Modified auditor from [{}] to [{}] for Entity [{}]",
abstractAuditingEntity.getLastModifiedBy(), lastModifiedBy, abstractAuditingEntity);
}
if (null != lastModifiedDate) {
abstractAuditingEntity.setLastModifiedDate(lastModifiedDate);
abstractAuditingEntity.setBackendModifiedDate(lastModifiedDate);
log.debug("Setting the Modified date from [{}] to [{}] for Entity [{}]",
abstractAuditingEntity.getLastModifiedDate(), lastModifiedDate, abstractAuditingEntity);
}
}
return proceed;
}
}
Usage
if the entity has backendAuditor and or backendModifiedDate set then this value would be used else Spring Audit provided values would be taken.
At the end thanks to Jhipster which simplifies a lot of things so that you can concentrate on the business logic.
Disclaimer: I am just a fan of Jhipster and nowhere related to it in any way.