JPA瞬态信息在创建时丢失

时间:2010-04-05 22:54:24

标签: jpa

我有一个具有瞬态字段的实体。当我想创建对象的新实例时,我会丢失我的瞬态信息。以下示例演示了此问题。为了举例,我们可以说barness是一个瞬态场。

FooEntity fooEntity = new FooEntity();
fooEntity.setFoobosity(5);
fooEntity.setBarness(2);
fooEntity = fooEntityManager.merge(fooEntity);
System.out.println(fooEntity.getFoobosity()); //5
System.out.println(fooEntity.getBarness()); //0 (or whatever default barness is)

有没有办法维护我的瞬态信息?

3 个答案:

答案 0 :(得分:21)

这或多或少都按设计工作。瞬态的语义恰恰是the data is not persisted。事实上,从entityManager.merge(obj)返回的实体是一个全新的实体,它维护传递给合并的对象的状态(状态,在此上下文中,是不属于持久对象的任何东西)。这在the JPA spec中有详细说明。注意:可能有JPA实现在合并对象后维护瞬态字段(仅仅因为它们返回相同的对象),但规范不保证这种行为。

你可以做两件事:

  1. 决定保持瞬态场。如果在将类合并到持久化上下文后需要它,它似乎并不是暂时的。

  2. 维持持久对象之外的瞬态字段的值。如果这符合您的需求,您可能需要重新考虑域类的结构;如果这个字段不是域对象状态的一部分,它实际上不应该存在。

  3. 最后一件事:我在域类的瞬态字段中发现的主要用例是划分派生字段,即可以根据类的持久字段重新计算的字段。

答案 1 :(得分:3)

迟到加入讨论,但这就是我使用 spring AOP和JPA提供@PreUpdate注释(添加详细版本)

的方法

使用案例

  1. 如果从UI进行更改,我们应该使用Spring提供的审核实体
  2. 如果通过API而不是通过前端服务进行更改,我们希望使用客户提供的自己的值覆盖值(@ LastModifiedBy和@LastModifiedDate)
  3. 实体具有瞬态值(backendModifiedDate,backendAuditor),需要在保存后合并(遗憾的是Spec不保证这一点)。 这两个字段将保存来自外部服务的审计数据
  4. 在我们的案例中,我们需要一个通用的解决方案来审核所有实体。
  5. Db配置

        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

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

    抽象实体与JPA @PreUpdate
    这实际上会设置@ LastModifiedBy和@LastModifiedDate字段的值

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

    合并后合并保留数据的方面
    这将拦截对象(实体)并重置字段

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

    <强>用法
    如果实体具有backendAuditor和/或backendModifiedDate设置,那么将使用此值,否则将采用Spring Audit提供的值。

    最后归功于Jhipster,它简化了很多事情,使您可以专注于业务逻辑。

    免责声明:我只是Jhipster的粉丝,无论如何都与它无关。

答案 2 :(得分:2)

基于@Prassed惊人的answer,我创建了一个更通用的代码:

我需要在实体上允许一些临时字段(我的意思是我们不保留在数据库中的字段,但是我们允许用户用我们发送给服务器的数据填充它们[使用@JsonSerialize / @JsonDeserialize]并上传到文件存储。

这些字段将带有以下注释(RetentionPolicy。此处使用RUNTIME,因此我可以在运行时对这些字段使用反射):

@Retention(RetentionPolicy.RUNTIME)
public @interface PreservePostMerge { }

然后,我使用apache的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;
    }
}