问题:
由于公司业务扩大, 各个子系统陆续迁移和部署在不同的数据源上, 这样方便扩容, 但是因此引出了一些问题.
举个例子: 在查询 "订单"(位于订单子系统)列表时, 同时需要查询出所关联的 "用户"(位于账户子系统)的姓名, 而这时由于数据存储在不同的数据源上, 没有办法通过一条连表的 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),beanKeyName 是订单用于关联用户的字段名称(例如外键 "OrderForm.customerID"),beanModelName 是用于存放用户类的字段名称("例如 OrderForm.customer"),dataList 是顾客列表(List),dataKeyName 是被关联数据的 key(例如主键 "http://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),oneKeyName 是门店用于关联订单的字段名称(例如主键 "http://Shop.id"),oneModelName 是用于存放订单列表的字段名称(例如 "Shop.orderFomrList"),manyDataList 是多方列表(List),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://juejin.im/post/5bfea5af5188252e8966a9f7