問題: 由於公司業務擴大,各個子系統陸續遷移和部署在不同的數據源上,這樣方便擴容,但是因此引出了一些問題。 舉個例子:在查詢"訂單"(位於訂單子系統)列表時,同時需要查詢出所關聯的"用戶"(位於賬戶子系統)的姓名,而這時由於數據存儲在不同的數據源上,沒有辦法通過一條連表的sql獲取到全部的數據,而是 ...
問題:
由於公司業務擴大,各個子系統陸續遷移和部署在不同的數據源上,這樣方便擴容,但是因此引出了一些問題。
舉個例子:在查詢"訂單"(位於訂單子系統)列表時,同時需要查詢出所關聯的"用戶"(位於賬戶子系統)的姓名,而這時由於數據存儲在不同的數據源上,沒有辦法通過一條連表的sql獲取到全部的數據,而是必須進行兩次資料庫查詢,從不同的數據源分別獲取數據,並且在web伺服器中進行關聯映射。在觀察了一段時間後,發現進行關聯映射的代碼大部分都是模板化的,因此產生一個想法,想要把這些模板代碼抽象出來,簡化開發,也增強代碼的可讀性。同時,即使在同一個數據源上,如果能將多表聯查的需求轉化為單表多次查詢,也能夠減少代碼的耦合,同時提高資料庫效率。
設計主要思路:
在關係型資料庫中:
一對一的關係一般表示為:一方的數據表結構中存在一個業務上的外鍵關聯另一張表的主鍵(訂單和用戶是一對一的關係,則訂單表中存在外鍵對應於用戶表的主鍵)。
一對多的關係一般表示為:多方的數據中存在一個業務上的外鍵關聯一方的主鍵(門店和訂單是一對多的關係,則訂單表中存在外鍵對應於門店的主鍵)。
而在非關係型資料庫中:
一對一的關係一般表示為:一方中存在一個屬性,值為關聯的另一方的數據對象(訂單和用戶是一對一的關係,則訂單對象中存在一個用戶屬性)。
一對多的關係一般表示為:一方中存在一個屬性,值為關聯的另一方的數據對象列表(門店和所屬訂單是一對多的關係,則門店對象表存在一個訂單列表(List)屬性)。
可以看出java的對象機制,天然就支持非關係型的數據模型,因此大概的思路就是,將查詢出來的兩個列表進行符合要求的映射即可。
pojo類:
public class OrderForm { /** * 主鍵id * */ private String id; /** * 所屬門店id * */ private String shopID; /** * 關聯的顧客id * */ private String customerID; /** * 關聯的顧客model * */ private Customer customer; } public class Customer { /** * 主鍵id * */ private String id; /** * 姓名 * */ private String userName; } public class Shop { /** * 主鍵id * */ private String id; /** * 門店名 * */ private String shopName; /** * 訂單列表 (一個門店關聯N個訂單 一對多) * */ private List<OrderForm> orderFormList; }
輔助工具函數:
/*** * 將通過keyName獲得對應的bean對象的get方法名稱的字元串 * @param keyName 屬性名 * @return 返回get方法名稱的字元串 */ private static String makeGetMethodName(String keyName){ //:::將第一個字母轉為大寫 String newKeyName = transFirstCharUpperCase(keyName); return "get" + newKeyName; } /*** * 將通過keyName獲得對應的bean對象的set方法名稱的字元串 * @param keyName 屬性名 * @return 返回set方法名稱的字元串 */ private static String makeSetMethodName(String keyName){ //:::將第一個字母轉為大寫 String newKeyName = transFirstCharUpperCase(keyName); return "set" + newKeyName; } /** * 將字元串的第一個字母轉為大寫 * @param str 需要被轉變的字元串 * @return 返迴轉變之後的字元串 */ private static String transFirstCharUpperCase(String str){ return str.replaceFirst(str.substring(0, 1), str.substring(0, 1).toUpperCase()); } /** * 判斷當前的數據是否需要被轉換 * * 兩個列表存在一個為空,則不需要轉換 * @return 不需要轉換返回 false,需要返回 true * */ private static boolean needTrans(List beanList,List dataList){ if(listIsEmpty(beanList) || listIsEmpty(dataList)){ return false; }else{ return true; } } /** * 列表是否為空 * */ private static boolean listIsEmpty(List list){ if(list == null || list.isEmpty()){ return true; }else{ return false; } } /** * 將javaBean組成的list去重 轉為map, key為bean中指定的一個屬性 * * @param beanList list 本身 * @param keyName 生成的map中的key * @return * @throws Exception */ public static Map<String,Object> beanListToMap(List beanList,String keyName) throws Exception{ //:::創建一個map Map<String,Object> map = new HashMap<>(); //:::由keyName獲得對應的get方法字元串 String getMethodName = makeGetMethodName(keyName); //:::遍歷beanList for(Object obj : beanList){ //:::如果當前數據是hashMap類型 if(obj.getClass() == HashMap.class){ Map currentMap = (Map)obj; //:::使用keyName從map中獲得對應的key String result = (String)currentMap.get(keyName); //:::放入map中(如果key一樣,則會被覆蓋去重) map.put(result,currentMap); }else{ //:::否則預設是pojo對象 //:::獲得get方法 Method getMethod = obj.getClass().getMethod(getMethodName); //:::通過get方法從bean對象中得到數據key String result = (String)getMethod.invoke(obj); //:::放入map中(如果key一樣,則會被覆蓋去重) map.put(result,obj); } } //:::返回結果 return map; }
一對一連接介面定義:
/** * 一對一連接 : beanKeyName <---> dataKeyName 作為連接條件 * * @param beanList 需要被存放數據的beanList(主體) * @param beanKeyName beanList中連接欄位key的名字 * @param beanModelName beanList中用來存放匹配到的數據value的屬性 * @param dataList 需要被關聯的data列表 * @param dataKeyName 需要被關聯的data中連接欄位key的名字 * * @throws Exception */ public static void oneToOneLinked(List beanList, String beanKeyName, String beanModelName, List dataList, String dataKeyName) throws Exception { }
如果帶入上述一對一連接的例子,beanList是訂單列表(List<OrderFrom>),beanKeyName是訂單用於關聯用戶的欄位名稱(例如外鍵“OrderForm.customerID”),beanModelName是用於存放用戶類的欄位名稱("例如OrderForm.customer"),dataList是顧客列表(List<Customer>),dataKeyName是被關聯數據的key(例如主鍵"Customer.id")。
一對一連接代碼實現:
/** * 一對一連接 : beanKeyName <---> dataKeyName 作為連接條件 * * @param beanList 需要被存放數據的beanList(主體) * @param beanKeyName beanList中連接欄位key的名字 * @param beanModelName beanList中用來存放匹配到的數據value的屬性 * @param dataList 需要被關聯的data列表 * @param dataKeyName 需要被關聯的data中連接欄位key的名字 * * @throws Exception */ public static void oneToOneLinked(List beanList, String beanKeyName, String beanModelName, List dataList, String dataKeyName) throws Exception { //:::如果不需要轉換,直接返回 if(!needTrans(beanList,dataList)){ return; } //:::將被關聯的數據列表,以需要連接的欄位為key,轉換成map,加快查詢的速度 Map<String,Object> dataMap = beanListToMap(dataList,dataKeyName); //:::進行數據匹配連接 matchedDataToBeanList(beanList,beanKeyName,beanModelName,dataMap); } /** * 將批量查詢出來的數據集合,組裝到對應的beanList之中 * @param beanList 需要被存放數據的beanList(主體) * @param beanKeyName beanList中用來匹配數據的屬性 * @param beanModelName beanList中用來存放匹配到的數據的屬性 * @param dataMap data結果集以某一欄位作為key對應的map * @throws Exception */ private static void matchedDataToBeanList(List beanList, String beanKeyName, String beanModelName, Map<String,Object> dataMap) throws Exception { //:::獲得beanList中存放對象的key的get方法名 String beanGetMethodName = makeGetMethodName(beanKeyName); //:::獲得beanList中存放對象的model的set方法名 String beanSetMethodName = makeSetMethodName(beanModelName); //:::遍歷整個beanList for(Object bean : beanList){ //:::獲得bean中key的method對象 Method beanGetMethod = bean.getClass().getMethod(beanGetMethodName); //:::調用獲得當前的key String currentBeanKey = (String)beanGetMethod.invoke(bean); //:::從被關聯的數據集map中找到匹配的數據 Object matchedData = dataMap.get(currentBeanKey); //:::如果找到了匹配的對象 if(matchedData != null){ //:::獲得bean中對應model的set方法 Class clazz = matchedData.getClass(); //:::如果匹配到的數據是hashMap if(clazz == HashMap.class){ //:::轉為父類map class用來調用set方法 clazz = Map.class; } //:::獲得主體bean用於存放被關聯對象的set方法 Method beanSetMethod = bean.getClass().getMethod(beanSetMethodName,clazz); //:::執行set方法,將匹配到的數據放入主體數據對應的model屬性中 beanSetMethod.invoke(bean,matchedData); } } }
一對多連接介面定義:
/** * 一對多連接 : oneKeyName <---> manyKeyName 作為連接條件 * * @param oneDataList '一方' 數據列表 * @param oneKeyName '一方' 連接欄位key的名字 * @param oneModelName '一方' 用於存放 '多方'數據的列表屬性名 * @param manyDataList '多方' 數據列表 * @param manyKeyName '多方' 連接欄位key的名字 * * 註意: '一方' 存放 '多方'數據的屬性oneModelName類型必須為List * * @throws Exception */ public static void oneToManyLinked(List oneDataList,String oneKeyName,String oneModelName,List manyDataList,String manyKeyName) throws Exception {}
如果帶入上述一對多連接的例子,oneDataList是門店列表(List<Shop>),oneKeyName是門店用於關聯訂單的欄位名稱(例如主鍵“Shop.id”),oneModelName是用於存放訂單列表的欄位名稱(例如"Shop.orderFomrList"),manyDataList是多方列表(List<OrderForm>),manyKeyName是被關聯數據的key(例如外鍵"OrderFrom.shopID")。
一對多連接代碼實現:
/** * 一對多連接 : oneKeyName <---> manyKeyName 作為連接條件 * * @param oneDataList '一方' 數據列表 * @param oneKeyName '一方' 連接欄位key的名字 * @param oneModelName '一方' 用於存放 '多方'數據的列表屬性名 * @param manyDataList '多方' 數據列表 * @param manyKeyName '多方' 連接欄位key的名字 * * 註意: '一方' 存放 '多方'數據的屬性oneModelName類型必須為List * * @throws Exception */ public static void oneToManyLinked(List oneDataList,String oneKeyName,String oneModelName,List manyDataList,String manyKeyName) throws Exception { if(!needTrans(oneDataList,manyDataList)){ return; } //:::將'一方'數據,以連接欄位為key,轉成map,便於查詢 Map<String,Object> oneDataMap = beanListToMap(oneDataList,oneKeyName); //:::獲得'一方'存放 '多方'數據欄位的get方法名 String oneDataModelGetMethodName = makeGetMethodName(oneModelName); //:::獲得'一方'存放 '多方'數據欄位的set方法名 String oneDataModelSetMethodName = makeSetMethodName(oneModelName); //:::獲得'多方'連接欄位的get方法名 String manyDataKeyGetMethodName = makeGetMethodName(manyKeyName); try { //:::遍歷'多方'列表 for (Object manyDataItem : manyDataList) { //:::'多方'對象連接key的值 String manyDataItemKey; //:::判斷當前'多方'對象的類型是否是 hashMap if(manyDataItem.getClass() == HashMap.class){ //:::如果是hashMap類型的,先轉為Map對象 Map manyDataItemMap = (Map)manyDataItem; //:::通過參數key 直接獲取對象key連接欄位的值 manyDataItemKey = (String)manyDataItemMap.get(manyKeyName); }else{ //:::如果是普通的pojo對象,則通過反射獲得get方法來獲取key連接欄位的值 //:::獲得'多方'數據中key的method對象 Method manyDataKeyGetMethod = manyDataItem.getClass().getMethod(manyDataKeyGetMethodName); //:::調用'多方'數據的get方法獲得當前'多方'數據連接欄位key的值 manyDataItemKey = (String) manyDataKeyGetMethod.invoke(manyDataItem); } //:::通過'多方'的連接欄位key從 '一方' map集合中查找出連接key相同的 '一方'數據對象 Object matchedOneData = oneDataMap.get(manyDataItemKey); //:::如果匹配到了數據,才進行操作 if(matchedOneData != null){ //:::將當前迭代的 '多方'數據 放入 '一方' 的對應的列表中 setManyDataToOne(matchedOneData,manyDataItem,oneDataModelGetMethodName,oneDataModelSetMethodName); } } }catch(Exception e){ throw new Exception(e); } } /** * 將 '多方' 數據存入 '一方' 列表中 * @param oneData 匹配到的'一方'數據 * @param manyDataItem 當前迭代的 '多方數據' * @param oneDataModelGetMethodName 一方列表的get方法名 * @param oneDataModelSetMethodName 一方列表的set方法名 * @throws Exception */ private static void setManyDataToOne(Object oneData,Object manyDataItem,String oneDataModelGetMethodName,String oneDataModelSetMethodName) throws Exception { //:::獲得 '一方' 數據中存放'多方'數據屬性的get方法 Method oneDataModelGetMethod = oneData.getClass().getMethod(oneDataModelGetMethodName); //::: '一方' 數據中存放'多方'數據屬性的set方法 Method oneDataModelSetMethod; try { //::: '一方' set方法對象 oneDataModelSetMethod = oneData.getClass().getMethod(oneDataModelSetMethodName,List.class); }catch(NoSuchMethodException e){ throw new Exception("未找到滿足條件的'一方'set方法"); } //:::獲得存放'多方'數據get方法返回值類型 Class modelType = oneDataModelGetMethod.getReturnType(); //::: get方法返回值必須是List if(modelType.equals(List.class)){ //:::調用get方法,獲得數據列表 List modelList = (List)oneDataModelGetMethod.invoke(oneData); //:::如果當前成員變數為null if(modelList == null){ //:::創建一個新的List List newList = new ArrayList<>(); //:::將當前的'多方'數據存入list newList.add(manyDataItem); //:::將這個新創建出的List賦值給 '一方'的對象 oneDataModelSetMethod.invoke(oneData,newList); }else{ //:::如果已經存在了List //:::直接將'多方'數據存入list modelList.add(manyDataItem); } }else{ throw new Exception("一對多連接時,一方指定的model對象必須是list類型"); } }
測試用例在我的github上面 https://github.com/1399852153/linkedQueryUtil。
這是我的第一篇技術博客,無論是排版還是分享的內容上面都還有很多的不足之處,希望大家指出,互相交流,互相進步。