欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-07-16 21:04:00
...

问题背景

在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传递参数,因此此方法仅可以在单应用服务器的部署场景中使用,不适用于分布式的部署方式。
相关标签: jpa spring