【问题标题】:JPA transient information lost on createJPA 瞬态信息在创建时丢失
【发布时间】:2010-04-05 22:54:24
【问题描述】:

我有一个带有瞬态字段的实体。当我想创建对象的新实例时,我会丢失我的瞬态信息。以下示例演示了该问题。举例来说,假设 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)

有什么方法可以保存我的临时信息吗​​?

【问题讨论】:

  • 如果重要的话,Hibernate 和 MySQL 是底层技术。

标签: jpa


【解决方案1】:

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

基本上你可以做两件事:

  1. 决定保留瞬态字段。如果您在将类合并到持久性上下文后需要它,它似乎并不是瞬态的。

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

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

【讨论】:

  • 感谢您的确认。我们已将字段移到对象之外。
【解决方案2】:

加入讨论很晚,但这是我使用 spring AOP 和 JPA 提供的 @PreUpdate 注释 实现的(添加详细版本)

用例

  1. 如果从 UI 进行更改,我们应该使用 Spring 为实体提供的审计
  2. 如果更改是通过 API 而不是通过前端服务完成的,我们希望值(@LastModifiedBy 和 @LastModifiedDate)被客户端提供的我们自己的值覆盖
  3. 实体有临时值(backendModifiedDate、backendAuditor),需要在保存后合并(不幸的是,规范不保证这一点)。 这两个字段将保存来自外部服务的审计数据
  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,它简化了很多事情,让您可以专注于业务逻辑。

免责声明:我只是 Jhipster 的粉丝,与它没有任何关系。

【讨论】:

    【解决方案3】:

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

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

    这些字段将使用以下注释进行注释(此处使用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;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-11-20
      • 2016-05-05
      • 2017-09-15
      • 2019-12-22
      • 2014-03-18
      • 1970-01-01
      • 2017-10-15
      • 2018-06-22
      相关资源
      最近更新 更多