java 分库关联查询工具类
问题:
由于公司业务扩大,各个子系统陆续迁移和部署在不同的数据源上,这样方便扩容,但是因此引出了一些问题。
举个例子:在查询"订单"(位于订单子系统)列表时,同时需要查询出所关联的"用户"(位于账户子系统)的姓名,而这时由于数据存储在不同的数据源上,没有办法通过一条连表的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。
这是我的第一篇技术博客,无论是排版还是分享的内容上面都还有很多的不足之处,希望大家指出,互相交流,互相进步。