`
Carsymor
  • 浏览: 11236 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

Spring JPA实体更新时自动补全字段值

阅读更多

问题背景

在spring data jpa的框架设计中,实体类(entity)与数据表一一对应,默认对实体操作时即是对整条数据库记录操作,因此在jpa的保存操作中,保存一个实体即是更新数据库记录的所有字段。基于这种设计,在实际使用中有如下不便利的地方:
1. 在实际业务中,业务数据会有逐步完善的情况,即在不同的阶段,会由不同的人员录入不同的字段信息,最终形成一个完整的业务数据记录。在这种情况下,每个阶段需要补充的信息(即页面中填写的信息)仅为一个信息片段,此时如果不对实体信息进行补全,在保存时即会出现信息丢失的情况。
2. 在实际业务中,核心基础数据会有需要进行拓展的情况,即要增加字段补充信息,而核心数据引用范围一般比较广泛,在jpa的原始设计中,对核心数据的拓展会导致大范围的功能调整。
 

解决方案

基于以上背景,考虑在spring data jpa的基础上构造公用的合并更新类,在更新实体信息前首先取到完整的实体数据,再基于请求接收的参数对实体数据进行合并,最终将合并后的实体进行保存。
其中:
1. 合并中需保留值的字段由controller中接收到的参数决定。
2. 合并操作应在保存的前一步执行,此时实体类中相关信息已填充完毕。
3. 实体类中,与数据库的对应关系由注解定义,因此使用反射方式,可以确定实体类中对应到数据库记录主键的字段,并获取到主键的值,基于这些信息,可获取到任意实体的数据库数据。
 
基于上述实际情况,代码中采用如下设计:
1. 使用拦截器拦截请求并将参数名称存储到请求的ThreadLocal中,此时,当controller/service/dao的代码在同一个线程中处理时,在任一层次的任意方法中,都可以直接获取到拦截器中存储的信息,无需对历史业务代码进行调整。
 
拦截器代码如下:
 
package com.hhh.sgzl.system.interceptors;

import com.hhh.base.model.PageParam;
import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;

public class PageParamInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
    Object handler) throws Exception {
        Enumeration<String> requestParamNames = request.getParameterNames();
	//清理上次线程残留
	PageParam.clear();
        if (requestParamNames != null) {
            String paramName;
            String[] paramNameParts;

            while (requestParamNames.hasMoreElements()) {
                paramName = requestParamNames.nextElement();

                if (paramName.indexOf(".") != -1) { //表单参数
                    paramNameParts = paramName.split(".");
                    PageParam.add(paramNameParts[0], paramNameParts[1]);
                } else { //非表单参数
                    PageParam.add(paramName);
                }
            }
        }
        return true;
    }
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}
 
 
线程数据存储以单独的类处理,此类的代码如下:
 
package com.hhh.base.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 存储页面参数,用于后续的实体部分更新
*/
public final class PageParam {
    private static final ThreadLocal<Map<String, List<String>>> PAGE_PARAM_NAMES = new ThreadLocal<>(); //存储页面参数名
    private static final String DEFALT_KEY = "_head"; //默认key值,用于存储表体外的其他参数名
/**
     * 内容依附于线程,无需实例化
*/
private PageParam() {

    }

    /**
     * 获取参数名的List
     * @param key 两种取值,表头类型参数名key固定为_head,表单类型参数名为表单对应的Bean的类名
* @return 参数名的List
     */
public static List<String> get(String key) {
        Map<String, List<String>> nameMap = PAGE_PARAM_NAMES.get();

        if (nameMap != null) {
            return nameMap.get(key);
        } else {
            return null;
        }
    }

    /**
     * 获取列表外的参数名(单据的表单头)
     * @return 列表外的参数名
*/
public static List<String> get() {
        Map<String, List<String>> nameMap = PAGE_PARAM_NAMES.get();

        if (nameMap != null) {
            return nameMap.get(DEFALT_KEY);
        } else {
            return null;
        }
    }

    /**
     * 记录参数名
* @param key 参数名对应的key
     * @param value 参数名
*/
public static void add(String key, String value) {
        Map<String, List<String>> nameMap = PAGE_PARAM_NAMES.get();

        if (nameMap == null) {
            nameMap = new HashMap<>();
            PAGE_PARAM_NAMES.set(nameMap);
        }

        if (!nameMap.containsKey(key)) {
            nameMap.put(key, new ArrayList<>());
        }
        nameMap.get(key).add(value);
    }

    /**
     * 记录参数名
* @param value 参数名
*/
public static void add(String value) {
        add(DEFALT_KEY, value);
    }
}

/**
* 清理上次线程参数残留
*/
public static void clear() {
    Map<String, List<String>> nameMap = PAGE_PARAM_NAMES.get();
    if (nameMap != null) {
        nameMap.clear();
    }
}
}
 
 
更新前合并数据库数据的公用dao层类代码如下:
 
package com.hhh.sgzl.system.dao;

import com.hhh.base.model.PageParam;
import com.hhh.exceptions.ParticalUpdateException;
import org.apache.commons.beanutils.PropertyUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Repository;
import org.springframework.util.Assert;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * 基于页面字段决定所需要更新字段
*/
@Repository
public class ParticalUpdateDao {
    @PersistenceContext(unitName = "mysqlUnit")
    private EntityManager entityManager;

    /**
     * 根据页面字段设置来部分更新实体
* @param entity 实体对象
* @param <E> 实体类对应的类定义
*/
public <E> void save(E entity) {
        Assert.notNull(entity, "保存的实体不能为null");

        String pkFieldName = this.getPkFieldName(entity);
        Assert.notNull(pkFieldName, "无法获取主键字段");

        try {
            String pkFieldVal = (String) PropertyUtils.getProperty(entity, pkFieldName);

            if (pkFieldVal == null || pkFieldVal.trim().equals("")) { //新增
                PropertyUtils.setSimpleProperty(entity, pkFieldName, null);
                entityManager.persist(entity);
            } else { //修改,需对比同步数据
                List<String> pageFields = this.getPageFields(entity);

                if (pageFields == null || pageFields.isEmpty()) {
                    throw new ParticalUpdateException("实体无法和页面参数建立关联");
                }

                E dataBaseRecord = entityManager.find((Class<E>) entity.getClass(), pkFieldVal);
                String[] ignoreFields = new String[pageFields.size()];
                BeanUtils.copyProperties(dataBaseRecord, entity, pageFields.toArray(ignoreFields));

                entityManager.merge(entity);
            }
        } catch (Exception e) {
            throw new ParticalUpdateException(e.getMessage());
        }
    }

    /**
     * 获取页面上有的实体类字段
* @param entity 实体类对象
* @param <E> 实体类对应的类定义
* @return 页面上有的实体类字段
*/
private <E> List<String> getPageFields(E entity) {
        Class entityClass = entity.getClass();
        List<String> pageParamNames = this.getPageNames(entityClass);

        if (pageParamNames == null) {
            throw new ParticalUpdateException("实体无法和页面参数建立关联");
        }

        Field[] fields = entityClass.getDeclaredFields();
        List<String> fieldNames = new ArrayList<>();
        for (Field field : fields) {
            fieldNames.add(field.getName());
        }

        List<String> pageFields = new ArrayList<>();
        for (String paraName : pageParamNames) {
            if (fieldNames.contains(paraName)) {
                pageFields.add(paraName);
            }
        }

        return pageFields;
    }

    /**
     * 获取页面上关联的参数名
* @param entityClass 实体类对应的类定义
* @return 页面上关联的参数名
*/
private List<String> getPageNames(Class entityClass) {
        String entityClassName = entityClass.getSimpleName();

        List<String> pageParaNames = PageParam.get(entityClassName);

        if (pageParaNames == null) {
            pageParaNames = PageParam.get(entityClassName + "Bean");
        }

        if (pageParaNames == null) {
            pageParaNames = PageParam.get();
        }

        return pageParaNames;
    }

    /**
     * 获取主键字段名
* @param entity 实体对象
* @param <E> 实体类对应的类定义
* @return 主键字段名
*/
private <E> String getPkFieldName(E entity) {
        Class entityClass = entity.getClass();
        Field[] fields = entityClass.getDeclaredFields();

        String pkFieldName = null;
        if (fields != null) {
            for (Field field : fields) {
                if (this.isPkField(field)) {
                    pkFieldName = field.getName();
                    break;
                }
            }
        }

        return pkFieldName;
    }

    /**
     * 判断是否为实体的主键字段
* @param field bean的字段信息
* @return 若为实体的主键字段,则返回true
     */
private boolean isPkField(Field field) {
        Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
        boolean isPkfield = false;
        if (fieldAnnotations != null) {
            for (Annotation fieldAnnotation : fieldAnnotations) {

                if (fieldAnnotation.annotationType().getName().equals("javax.persistence.Id")) {
                    isPkfield = true;
                    break;
                }
            }
        }
        return isPkfield;
    }
}
 
 

不足

1. 公用类中使用了大量的反射,以达到对所有实体类均可生效的效果,效率较正常代码低下。
2. 为避免修改历史业务代码,使用了ThreadLocal传递参数,因此此方法仅可以在单应用服务器的部署场景中使用,不适用于分布式的部署方式。
0
0
分享到:
评论

相关推荐

    解决 Springboot Jpa 自动创建表 和字段乱序问题[凤凰小哥哥]

    产生原因:因为使用 jpa 自动创建表的时候,采用的是TreeMap的,我们要变成LinkedHashMap 的结构。 解决方案:在项目下java文件夹下创建包名为: org.hibernate.cfg ,创建类: PropertyContainer [包名,类名...

    Spring Data JPA中文文档[1.4.3]_springdatajpa_erlang_waitxpf_

    2. **Entity Management**:Spring Data JPA 提供了对实体(Entity)的管理,包括实体的创建、更新、删除等操作。开发者可以通过 `EntityManager` 和 `EntityManagerFactory` 进行这些操作,但 Spring Data JPA 提供...

    Spring Data JPA 笔记

    例如,Spring Data JPA支持自动化的查询生成,只需定义Repository接口,无需编写任何实现代码,就可以执行CRUD(创建、读取、更新、删除)操作。此外,它还支持复杂的查询方法命名,如findByXXX,根据方法名自动构建...

    spring jpa

    在Spring JPA中,你可以通过定义Repository接口来实现对数据库的操作,而Spring会自动帮你完成数据访问的大部分工作。 1. **什么是JPA?** Java Persistence API (JPA) 是Java平台上的一个标准,它定义了如何在Java...

    Spring Data JPA API(Spring Data JPA 开发文档).CHM

    Spring Data JPA API。 Spring Data JPA 开发文档。 官网 Spring Data JPA API。

    spring data jpa 动态更新@DynamicUpdate

    `@DynamicUpdate`是一个实体类级别的注解,当被标记在实体类上时,表示在执行更新操作时,只会更新那些值已经改变的属性,而不是默认的全字段更新。 在描述中提到的博文链接虽然没有给出详细内容,但通常这样的博文...

    JPA实体映射对时间字段的处理

    本文将详细探讨在使用JPA进行实体映射时如何处理时间字段,并提供具体的代码示例和实践建议。 #### 一、时间字段的格式化 在Java中,时间通常由`java.util.Date`类或其子类来表示。但在实际应用中,我们常常需要将...

    spring注解+spring data jpa文档+JPA文档.rar

    只需要在接口上定义方法名,Spring Data JPA就能自动生成对应的SQL语句。例如,`findAll()`会执行SELECT ALL查询,`findByUsername(String username)`将生成一个根据用户名查找的查询。此外,`Pageable`接口可用于...

    maven+springmvc+springjpa+hibernate

    在IT行业中,构建高效、可维护的Web应用是至关重要的,而"Maven+SpringMVC+SpringJPA+Hibernate"的组合就是一个常见的解决方案。这个组合提供了全面的开发工具和技术,帮助开发者快速构建基于Java的Web应用程序。...

    springboot jpa 自动生成实体类的 文件 Generate POJOs.groovy

    springboot jpa 自动生成实体类的 文件 可以拿走直接用 Generate POJOs.groovy

    Spring JPA 配置类包集合 方便大家使用

    Spring JPA通过提供Repository接口、实体管理、事务处理等特性,极大地提升了开发效率。 1. **Repository接口**:Spring JPA的核心在于Repository接口,这是一个接口模板,允许用户自定义数据操作方法。例如,你...

    Spring Data Jpa – 获得实体类部分字段(单表或多表)

    一、单表查询的一个字段、一条数据 @Query(value = select username from user,nativeQuery = true) // 注意返回值用String类型接收,也可以使用Object String findOneUserName(); 二、单表查询的一个字段、多条...

    详解Spring Data Jpa当属性为Null也更新的完美解决方案

    在使用 Spring Data Jpa 进行数据更新时,经常会遇到一个问题,即当属性值为 Null 时,也会被更新,这将导致原本没有更新的属性值全部变为 Null。这个问题的解决方案是使用 @DynamicUpdate 注解,但是这种方法并不好...

    spring jpa操作数据库 级联数据 hibernate

    例如,`@OneToMany`和`@ManyToOne`用于表示一对多或多对一的关系,级联操作如`CascadeType.PERSIST`、`CascadeType.MERGE`等可以确保当一个实体被保存或更新时,与其关联的实体也会相应更新。这在处理关联数据时非常...

    Struts2 Spring Jpa 配置测试

    此外,还需要配置Spring的数据源、事务管理器以及JPA的实体扫描路径。 3. **JPA配置**:JPA的配置主要涉及`persistence.xml`文件,它定义了持久化单元,包括数据源、JPA供应商(如Hibernate)、实体类的包名以及...

    spring mvc spring spring jpa集成

    同时,为实体类提供Repository接口,Spring Data JPA会自动生成对应的方法。 在Spring MVC的Controller中,你可以通过@Autowired注解注入Repository实例,然后调用其方法来执行数据库操作。例如,使用save()方法...

    Spring JPA Example

    在Spring中,我们通常使用Spring Data JPA扩展,它提供了更多的便利功能,如自动查询生成、repository抽象等。要开始使用Spring JPA,我们需要以下步骤: 1. **配置JPA**:在`application.properties`或`...

    spring date jpa

    Spring Data JPA是Spring框架的一个模块,用于简化Java Persistence API(JPA)的使用,它提供了与数据库交互的高级抽象。在Spring应用中整合Spring Data JPA,可以极大地提高开发效率,减少大量的DAO层代码。下面...

    2017 spring data jpa+spring4.2+springmvc+hibernate4.3 maven环境intellij idea增删改查实例

    在本项目中,我们探索的是一个基于2017年技术栈的Java Web应用程序实例,主要涉及Spring Data JPA、Spring 4.2、Spring MVC和Hibernate 4.3。这些技术是Java开发中的核心组件,尤其在企业级应用开发中广泛使用。下面...

Global site tag (gtag.js) - Google Analytics