MyException - 我的异常网
当前位置:我的异常网» 软件架构设计 » Spring JPA实业更新时自动补全字段值

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

www.MyException.Cn  网友分享于:2013-11-16  浏览:0次
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传递参数,因此此方法仅可以在单应用服务器的部署场景中使用,不适用于分布式的部署方式。

文章评论

为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
程序员必看的十大电影
程序员必看的十大电影
程序员应该关注的一些事儿
程序员应该关注的一些事儿
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
 程序员的样子
程序员的样子
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
总结2014中国互联网十大段子
总结2014中国互联网十大段子
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
Web开发者需具备的8个好习惯
Web开发者需具备的8个好习惯
一个程序员的时间管理
一个程序员的时间管理
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
老程序员的下场
老程序员的下场
我的丈夫是个程序员
我的丈夫是个程序员
为什么程序员都是夜猫子
为什么程序员都是夜猫子
旅行,写作,编程
旅行,写作,编程
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
中美印日四国程序员比较
中美印日四国程序员比较
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
程序员都该阅读的书
程序员都该阅读的书
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
代码女神横空出世
代码女神横空出世
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
Java程序员必看电影
Java程序员必看电影
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
我是如何打败拖延症的
我是如何打败拖延症的
编程语言是女人
编程语言是女人
漫画:程序员的工作
漫画:程序员的工作
鲜为人知的编程真相
鲜为人知的编程真相
那些争议最大的编程观点
那些争议最大的编程观点
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
程序员和编码员之间的区别
程序员和编码员之间的区别
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有