如何解決CRUD操作中與業務無關的欄位賦值,crud賦值
提高效率一直是個永恒的話題,編程中有一項也是可以提到效率的,那就是專註做一件事情,讓其它沒有強緊密聯絡的與之分開。這裡分享下我們做CRUD時遇到的常見資料處理情境:
- 資料庫表欄位全部設計為非空,即使這個欄位在業務上是可以為空白的,之所以將資料庫表欄位全部設計為非空,這裡有優點也有缺點,我們認為優點大於缺點,所以選擇了它
優點:
缺點:
- 系統欄位的賦值,比如建立人,建立人id,建立時間,編輯人,編輯人id,編輯時間等,這些都需要在實際插入資料庫前賦值給Model。這些系統欄位與具體的業務一般沒有太大的關聯關係,只是起到標註資料被什麼人在什麼時間處理的,當這些非業務相關的代碼充斥在代碼中時,就顯得有些多餘,而且這類代碼多了也會顯示冗餘,最後帶來的結果就是非關鍵代碼比例大。
上面關於預設值與null語義問題不需要解決,因為我們認為具有預設值帶來的優點遠大於可空欄位帶來的煩惱,我們來看預設值與系統欄位一般情況下如何處理:
- 在操作ORM時,將模型所有可空的欄位都手動賦值成預設值,int的賦值為0等。
- 在設計資料庫時,將非空欄位加上預設值,讓資料庫來處理這些未插入值的欄位,如果使用mybatis的話,mapper中提到的插入操作有兩個:insert,insertSelective,後面這個insertSelective就是處理非空欄位的,即插入的模型對於不需要賦值的欄位就保持null值,資料庫在插入時產生的sql語句也不會包含這些欄位,這樣就可以利用上資料庫的預設值了。如果正巧資料庫的結構當初設計時沒有設計預設值,又不能改的情況就比較糟糕了,情況回到上面手動賦值,可能會出現類似如下的代碼:編寫一個函數通過反射來解析每個欄位,如果為null就修改為預設值:
public static <T> void emptyNullValue(final T model) { Class<?> tClass = model.getClass(); List<Field> fields = Arrays.asList(tClass.getDeclaredFields()); for (Field field : fields) { Type t = field.getType(); field.setAccessible(true); try { if (t == String.class && field.get(model) == null) { field.set(model, ""); } else if (t == BigDecimal.class && field.get(model) == null) { field.set(model, new BigDecimal(0)); } else if (t == Long.class && field.get(model) == null) { field.set(model, new Long(0)); } else if (t == Integer.class && field.get(model) == null) { field.set(model, new Integer(0)); } else if (t == Date.class && field.get(model) == null) { field.set(model, TimeHelper.LocalDateTimeToDate(java.time.LocalDateTime.of(1990, 1, 1, 0, 0, 0, 0))); } } catch (IllegalAccessException e) { e.printStackTrace(); } } }
然後在代碼調用insert前調用函數來解決:
ModelHelper.emptyNullValue(request);
如何處理系統欄位呢,在建立編輯資料時,需要擷取目前使用者,然後根據邏輯分別更新建立人資訊以及編輯人資訊,我們專門編寫一個反射機制的函數來處理系統欄位:
註:下面的系統欄位的識別,是靠系統約定實現的,比如creator約定為建立人等,可根據不同的情況做資料相容,如果系統設計的好,一般在一個系統下所有表的風格應該是相同的。
public static <T> void buildCreateAndModify(T model,ModifyModel modifyModel,boolean isCreate){ Class<?> tClass = model.getClass(); List<Field> fields = Arrays.asList(tClass.getDeclaredFields()); for (Field field : fields) { Type t = field.getType(); field.setAccessible(true); try { if(isCreate){ if (field.getName().equals(modifyModel.getcId())) { field.set(model, modifyModel.getUserId()); } if (field.getName().equals(modifyModel.getcName())) { field.set(model, modifyModel.getUserName()); } if (field.getName().equals(modifyModel.getcTime())) { field.set(model, new Date()); } } if (field.getName().equals(modifyModel.getmId())) { field.set(model, modifyModel.getUserId()); } if (field.getName().equals(modifyModel.getmName())) { field.set(model, modifyModel.getUserName()); } if (field.getName().equals(modifyModel.getmTime())) { field.set(model, new Date()); } } catch (IllegalAccessException e) { e.printStackTrace(); } } }
最後在資料處理前,根據建立或者編輯去調用函數來給系統欄位賦值,這類代碼都混雜在業務代碼中。
ModifyModel modifyModel = new ModifyModel(); modifyModel.setUserId(getCurrentEmployee().getId()); modifyModel.setUserName(getCurrentEmployee().getName()); if (request.getId() == 0) { ModelHelper.buildCreateAndModify(request, modifyModel, true); deptService.insert(request); } else { ModelHelper.buildCreateAndModify(request, modifyModel, false); deptService.updateByPrimaryKey(request); }
我們可以利用參數注入來解決。參數注入的理念就是在spring mvc接收到前台請求的參數後,進一步對接收到的參數做處理以達到預期的效果。我們來建立ManageModelConfigMethodArgumentResolver,它需要實現HandlerMethodArgumentResolver,這個介面看起來比較簡單,包含兩個核心方法:
- 判斷是否是需要注入的參數,一般通過判斷參數上是否有特殊的註解來實現,也可以增加一個其它的參數判斷,可根據具體的業務做調整,我這裡只以是否有特殊注釋來判定是否需要參數注入。
@Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(ManageModelConfig.class); }
- 參數注入,它提供了一個擴充入口,讓我們有機會對接收到的參數做進一步的處理。
@Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { Object manageModel =getRequestResponseBodyMethodProcessor().resolveArgument(parameter, mavContainer, webRequest, binderFactory); ServletRequest servletRequest = webRequest.getNativeRequest(ServletRequest.class); Employee currentUser = (Employee) servletRequest.getAttribute(DEFAULT_ATTRIBUTE_GET_USER_FROM_REQUEST); if (null == currentUser) { return manageModel; } ManageModelConfig parameterAnnotation = parameter.getParameterAnnotation(ManageModelConfig.class); ModelHelper.setDefaultAndSystemFieldsValue(manageModel, currentUser,parameterAnnotation.isSetDefaultFieldsValue()); return manageModel; }
這段函數有幾處核心邏輯:
- 取得參數對象,因為我們處理的是ajax請求的參數,最簡單的注入方法就是得到實際參數通過反射去處理預設欄位以及系統的值。ajax請求與form表單post提交的資料繫結略有不同,可參考之前文章分享的列表頁動態搜尋的參數注入(列表頁的動態條件搜尋)。擷取當前請求參數對象,我們可以藉助如下兩個對象配合來完成:
- RequestMappingHandlerAdapter
- RequestResponseBodyMethodProcessor
private RequestMappingHandlerAdapter requestMappingHandlerAdapter=null; private RequestResponseBodyMethodProcessor requestResponseBodyMethodProcessor = null; private RequestResponseBodyMethodProcessor getRequestResponseBodyMethodProcessor() { if(null==requestMappingHandlerAdapter) { requestMappingHandlerAdapter=new RequestMappingHandlerAdapter(); } if (null==requestResponseBodyMethodProcessor) { List<HttpMessageConverter<?>> messageConverters = requestMappingHandlerAdapter.getMessageConverters(); messageConverters.add(new MappingJackson2HttpMessageConverter()); requestResponseBodyMethodProcessor = new RequestResponseBodyMethodProcessor(messageConverters); } return requestResponseBodyMethodProcessor; }
通過如下代碼就可以取到參數對象了,其實就是讓spring mvc重新解析了一遍參數。
Object manageModel =getRequestResponseBodyMethodProcessor().resolveArgument(parameter, mavContainer, webRequest, binderFactory);
- 如何擷取目前使用者,我們在成功登入系統後,將目前使用者的資訊儲存在request中,然後就可以在函數中擷取目前使用者,也可以採用其它方案,比如ThreadLocal,緩衝等等。
ServletRequest servletRequest = webRequest.getNativeRequest(ServletRequest.class); Employee currentUser = (Employee) servletRequest.getAttribute(DEFAULT_ATTRIBUTE_GET_USER_FROM_REQUEST);
- 調用處理函數解決預設欄位以及系統的賦值,可以根據配置來決定是否處理欄位預設值。
ManageModelConfig parameterAnnotation = parameter.getParameterAnnotation(ManageModelConfig.class); ModelHelper.setDefaultAndSystemFieldsValue(manageModel, currentUser,parameterAnnotation.isSetDefaultFieldsValue());
最後將我們的參數注入邏輯啟動起來,這裡選擇在xml中配置:
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"> <mvc:argument-resolvers> <bean class="cn.wanmei.party.management.common.mvc.method.annotation.ManageModelConfigMethodArgumentResolver"/> </mvc:argument-resolvers> </mvc:annotation-driven>
再看action中的調用:只需要在參數前面增加註解@ManageModelConfig,如果需要處理預設值,則將啟用預設值的選項設定成true即可,下面的實現部分完全看不到任何與業務無關的代碼。
@RequestMapping(value = "/addOrUpdateUser") @ResponseBody public Map<String, Object> addOrUpdateUser(@ManageModelConfig(isSetDefaultFieldsValue=true) EmployeeDto request) { Map<String, Object> ret = new HashMap<>(); ValidateUtil.ValidateResult result= new ValidateUtil().ValidateModel(request); boolean isCreate=request.getId() == 0; try { if (isCreate) { employeeService.insert(request); } else { employeeService.updateByPrimaryKey(request); } ret.put("data", "ok"); }catch (Exception e){ ret.put("err", e.getMessage()); } return ret; }
通過自訂實現HandlerMethodArgumentResolver,來捕獲ajax請求的參數,利用反射機制動態將系統欄位以及需要處理預設值的欄位自動賦值,避免人工幹預,起到了代碼精簡,邏輯乾淨,問題統一處理的目的。需要注意的是這些實現都是結合當前系統設計的,比如我們認為id欄位>0就代表是更新操作,為空白或者等於小於0就代表是建立,系統欄位也是約定名稱的等等。