JPA transient information lost on create

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

  1. If changes made from the UI, we should use Spring provided Audit for entities
  2. If changes are done via an API and not via Front end services, We wanted the values (@LastModifiedBy and @LastModifiedDate) to be overwritten with our own value provided by the client
  3. Entity has transient values (backendModifiedDate, backendAuditor) which needed to be merged after save (unfortunately Spec do not guarantee this). These two fields would save the audit data from external services
  4. In our case we wanted a generic solution for auditing all entities.

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.


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

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:

  1. 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.

  2. 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.

Tags:

Jpa