代码之家  ›  专栏  ›  技术社区  ›  Pace

创建时丢失JPA瞬态信息

jpa
  •  13
  • Pace  · 技术社区  · 14 年前

    我有一个瞬变场的实体。当我想创建一个新的对象实例时,我丢失了我的瞬时信息。下面的示例演示了该问题。为了这个例子,我们假设巴尼斯是一个瞬变场。

    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 回复  |  直到 6 年前
        1
  •  20
  •   ig0774    14 年前

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

    你可以做两件事:

    1. 决定维持瞬变场。如果在将类合并到持久性上下文中之后需要它,那么它看起来并不是短暂的。

    2. 保持持久对象外部的瞬态字段的值。如果这是满足您需要的,那么您可能需要重新考虑域类的结构;如果此字段不是域对象状态的一部分,那么它实际上不应该存在。

    最后一件事:我为域类上的临时字段找到的主要用例是划分派生字段,即可以基于类的持久字段重新计算的字段。

        2
  •  3
  •   Prasad    7 年前

    很晚才加入讨论,但这就是我用 spring aop和jpa提供了@preupdate注释 (添加详细版本)

    用例

    1. 如果对ui进行了更改,我们应该对实体使用spring提供的审计
    2. 如果更改是通过api而不是通过前端服务完成的,我们希望用客户机提供的值覆盖值(@lastModifiedBy和@lastModifiedDate)
    3. 实体具有保存后需要合并的临时值(backendmodifieddate、backendauditor)(不幸的是,spec不能保证这一点)。 这两个字段将保存来自外部服务的审核数据
    4. 在我们的例子中,我们需要一个通用的解决方案来审计所有实体。

    数据库配置

        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 它简化了很多事情,使您可以集中精力于业务逻辑。

    免责声明:我只是杰普斯特的粉丝,与之毫无关系。

        3
  •  2
  •   orirab    6 年前

    基于 @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;
        }
    }