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传递参数,因此此方法仅可以在单应用服务器的部署场景中使用,不适用于分布式的部署方式。
下一篇: 一次通过漏洞挖掘成功渗透某网站的过程