提要 本文是 小小商城-SSM 版的 细节详解系列 之一,项目 github:https://github.com/xenv/S-mall-ssm 本文代码大部分在 github 中 可以找到。
中小型项目使用 Mybatis 如何减少 mapper 的工作量?市面上已经有一款产品 ,叫”通用 mapper”,但是我使用之后有点失望。一个是它侵入型极强,要改掉 mybatis 的 factory,数据库的名字和列名都要改成指定格式,错一个也不行。另外,还不支持关联查询,可谓是有点鸡肋了。于是,就有了我这么一个通用 mapper 的实现,可以让 Mybatis 像 Hibernate 一样使用。只要配置好自定义注解,用 mybatis-generator 生成好代码,就可以自动处理 各种关联查询(一对多、多对一自动插入),还提供接口修改遍历填充关联查询的深度,支持手写的扩展 mappper 接入到填充系统。
那么,这套系统到底是如何工作的呢?我做了一个示意图来说明 Category 表 下面的情况,其他表同理。 (mybatis-generator 隔离手写代码和机器代码请参考我写的文章:SSM 开发 | 合理配置 mybatis-generator,隔离机器生成代码和额外增加代码 )
当服务层调用的时候,通用 mapper 会先查询 服务器层指定的 CategoryMapper,从数据库拿到数据。然后通用 mapper 调用 MapperCore 对 拿到的 Category 进行一对多和多对一对象的填充。最后填充完的 category 对象将会返回给服务层。
一对多和多对一如何配置呢?这需要我们在 extension 类中 用自定义注解配置,然后机器生成的 Category 类 将继承 我们的 extension,因此 我们 最终拿到的 category 就是 有关联对象变量的 对象了。
那么,我们如何知道 一对多 多对一 的对象应该由哪个 mapper 来填充呢,那么我们就需要在 category 类上指定好 对应的 mapper (使用泛型继承)即可,怎么让它自动指定呢?对了,就是用 mybatis-generator 自定义插件。
在看具体实现之前,我假定您已经对 mybatis-generator 、自定义注解、反射有一定的了解。
具体实现 1.创建 实体 extension 配置一对多、多对一信息 创建 extension 参见::SSM 开发 | 合理配置 mybatis-generator,隔离机器生成代码和额外增加代码
配置一对多、多对一信息使用自定义 注解,注解的定义在这里 ORMAnnotation 语法和 hibernate 差不多,临时变量不需要再使用注解,enum 用了 (var=””),取消了 OneToOne,因为本质上和 ManyToOne 是一样的,OneToOne 还容易弄混。
配置好注解之后,效果是这样的: extension
2.定义 mybatis-generator 插件,让自动生成的 mapper 和 实体类 带上 泛型 信息,以供通用 mapper 读取 自定义 mybatis-generator 插件 参见我的文章:SSM 开发 | 开发自定义插件,使 mybatis-generator 支持软删除
插件代码见:MapperExtendsPlugin.java POJOExtendsPlugin.java
之后用 mybatis-generator 生成出来的代码,就会是这样的
1 2 3 undefined public interface CategoryMapper extends BaseMapper <Category , CategoryExample > {}
1 2 public class Category extends CategoryExtension implements POJOMapper <CategoryMapper > {}
这样,我们在填充时,读取这个类的时候,就可以通过反射,拿到 <> 中的内容,比如现在知道了一个 product 对象,里面要我们填充一个 Categorty 类,那么我们先来查 Category 类,知道对应的 Mapper 是 CategoryMapper,对应的 Example 是 CategoryEmaple,那么,我们就可以愉快的调用 CategoryMapper 拿到一个 category ,然后插回 product 即可。
3.开发通用 mapper :静态代理+递归填充 我们先通过拿到 mybatis 自带的 sqlSessionTemplate ,需要在 applicationContext.xml 中 注册一下
1 2 3 <bean id="sqlSessionTemplate" class ="org.mybatis.spring.SqlSessionTemplate" > <constructor-arg index="0" ref="sqlSession" /> </bean>
然后我们开发一个 MapperFactory 工厂类,用来在 Service 层获取我们的通用 mapper,并且从 SqlSessionTemplate 拿到 CateogoryMapper,塞进通用 mapper 里面,最后返回通用 mapper MapperFactory.java
1 2 3 4 5 6 7 8 9 10 11 @Component public class MapperFactory { @Resource private SqlSessionTemplate sqlSessionTemplate; public Mapper getMapper (Class mapperInterface) throws Exception { Mapper mapper = new Mapper(); mapper.setSqlSessionTemplate(sqlSessionTemplate); mapper.setMybatisMapper(mapperInterface); return mapper; } }
通用 mapper 里面其实比较简单,就是直接使用反射做通用具体 mapper 的静态代理,并且调用 mapperCore 中 的递归填充器 Mapper.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class Mapper extends Mapper4ORM { private int defaultTraversalDepth = 2 ; public Object selectByPrimaryKey (Integer id) throws Exception { return selectByPrimaryKey(id, defaultTraversalDepth); } public Object selectByPrimaryKey (Integer id, Integer depth) throws Exception { Object object = mapper.getClass().getMethod("selectByPrimaryKey" , Integer.class ).invoke (mapper , id ) ; fillOnReading(object, depth); return object; } public int insert (Object object) throws Exception { fillOnWriting(object); return (int ) mapper.getClass().getMethod("insert" , object.getClass()).invoke(mapper, object); } public int insertSelective (Object object) throws Exception { fillOnWriting(object); return (int ) mapper.getClass().getMethod("insertSelective" , Object.class ).invoke (mapper , object ) ; } public int updateByPrimaryKeySelective (Object object) throws Exception { fillOnWriting(object); return (int ) mapper.getClass(). getMethod("updateByPrimaryKeySelective" , object.getClass()).invoke(mapper, object); } public int updateByPrimaryKey (Object object) throws Exception { fillOnWriting(object); return (int ) mapper.getClass().getMethod("updateByPrimaryKey" , object.getClass()).invoke(mapper, object); } public List selectByExample (Object example) throws Exception { return selectByExample(example, defaultTraversalDepth); } public List selectByExample (Object example, int depth) throws Exception { List result = (List) mapper.getClass().getMethod("selectByExample" , example.getClass()).invoke(mapper, example); for (int i = 0 ; i < result.size(); i++) { Object item = result.get(i); fillOnReading(item, depth); result.set(i, item); } return result; } }
最后,我们开发我们核心类,就是开发递归填充器,对一对多、多对一进行读取、处理、回填 Mapper4ORM.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 @SuppressWarnings ("unchecked" )public class Mapper4ORM { Object mapper; private Class mapperInterface; private SqlSessionTemplate sqlSessionTemplate; void setSqlSessionTemplate (SqlSessionTemplate sqlSessionTemplate) { this .sqlSessionTemplate = sqlSessionTemplate; } void setMybatisMapper (Class mapperInterface) throws Exception { this .mapperInterface = mapperInterface; mapper = getMapper(mapperInterface); } public Object getMapper (Class mapperInterface) throws Exception { return sqlSessionTemplate.getMapper(mapperInterface); } public BaseExample getExample (Class mapperInterface) throws Exception { ParameterizedType t = (ParameterizedType) mapperInterface.getGenericInterfaces()[0 ]; Class exampleClass = (Class) t.getActualTypeArguments()[1 ]; return (BaseExample) exampleClass.newInstance(); } public Class getMapperInterfaceByPOJO (Class POJOClass) throws Exception { ParameterizedType t = (ParameterizedType) POJOClass.getGenericInterfaces()[0 ]; return (Class) t.getActualTypeArguments()[0 ]; } List<Field> getFieldsEquals (Class objectClass, Class annotationClass) { if (objectClass == null ) { return null ; } List<Field> fields = new ArrayList<>(); for (Class temp = objectClass; temp != Object.class ; temp = temp.getSuperclass()) { fields.addAll(Arrays.asList(temp.getDeclaredFields())); } List<Field> result = new ArrayList<>(); for (Field field : fields) { if (field.getAnnotation(annotationClass) != null ) result.add(field); } return result; } public void fillManyToOneOnReading (Object object, int depth) throws Exception { if (object == null ) { return ; } Class clazz = object.getClass(); List<Field> result = getFieldsEquals(clazz, ManyToOne.class ) ; for (Field field : result) { String joinColumn = field.getAnnotation(JoinColumn.class ).name () ; Class targetMapperClass = getMapperInterfaceByPOJO(field.getType()); Object targetMapper = getMapper(targetMapperClass); Integer joinColumnValue = (Integer) clazz. getMethod("get" + StringUtils.capitalize(joinColumn)).invoke(object); if (joinColumnValue == null ) { continue ; } BaseExample example = getExample(targetMapperClass); Object criteria = example.createCriteria(); criteria.getClass().getMethod("andIdEqualTo" , Integer.class ).invoke (criteria , joinColumnValue ) ; List targetResults = (List) targetMapper.getClass().getMethod("selectByExample" , example.getClass()). invoke(targetMapper, example); if (targetResults.size() > 0 ) { Object targetResult = targetResults.get(0 ); fillOnReading(targetResult, depth - 1 ); clazz.getMethod("set" + StringUtils.capitalize(field.getName()), targetResult.getClass()) .invoke(object, targetResult); } } } public void fillOneToManyOnReading (Object object, int depth) throws Exception { if (object == null ) { return ; } Class clazz = object.getClass(); List<Field> result = getFieldsEquals(clazz, OneToMany.class ) ; for (Field field : result) { String joinColumn = field.getAnnotation(JoinColumn.class ).name () ; Type genericType = field.getGenericType(); ParameterizedType pt = (ParameterizedType) genericType; Class targetClass = (Class) pt.getActualTypeArguments()[0 ]; Class targetMapperClass = getMapperInterfaceByPOJO(targetClass); Object targetMapper = getMapper(targetMapperClass); Integer joinColumnValue = (Integer) clazz. getMethod("getId" ).invoke(object); BaseExample example = getExample(targetMapperClass); Object criteria = example.createCriteria(); criteria.getClass().getMethod("and" + StringUtils.capitalize(joinColumn) + "EqualTo" , Integer.class ).invoke (criteria , joinColumnValue ) ; List targetResults = (List) targetMapper.getClass().getMethod("selectByExample" , example.getClass()). invoke(targetMapper, example); for (int i = 0 ; i < targetResults.size(); i++) { Object item = targetResults.get(i); fillOnReading(item, depth - 1 ); targetResults.set(i, item); } clazz.getMethod("set" + StringUtils.capitalize(field.getName()), List.class ) .invoke (object , targetResults ) ; } } public void fillEnumOnReading (Object object) throws Exception { if (object == null ) { return ; } Class clazz = object.getClass(); List<Field> result = getFieldsEquals(clazz, Enumerated.class ) ; for (Field field : result) { String varName = field.getAnnotation(Enumerated.class ).var () ; String enumString = (String) clazz. getMethod("get" + StringUtils.capitalize(varName)).invoke(object); Enum resultObj = Enum.valueOf((Class<Enum>) field.getType(), enumString); clazz.getMethod("set" + StringUtils.capitalize(field.getName()), resultObj.getClass()) .invoke(object, resultObj); } } public void fillEnumOnWriting (Object object) throws Exception { if (object == null ) { return ; } Class clazz = object.getClass(); List<Field> result = getFieldsEquals(clazz, Enumerated.class ) ; for (Field field : result) { String varName = field.getAnnotation(Enumerated.class ).var () ; Enum enumObj = (Enum) clazz. getMethod("get" + StringUtils.capitalize(field.getName())).invoke(object); String enumString = enumObj.name(); clazz.getMethod("set" + StringUtils.capitalize(varName), String.class ) .invoke (object , enumString ) ; } } public void fillManyToOneOnWriting (Object object) throws Exception { if (object == null ) { return ; } Class clazz = object.getClass(); List<Field> result = getFieldsEquals(clazz, ManyToOne.class ) ; for (Field field : result) { String columnName = field.getAnnotation(JoinColumn.class ).name () ; Object targetObj = clazz .getMethod("get" + StringUtils.capitalize(field.getName())) .invoke(object); if (targetObj == null ) { continue ; } int id = (int ) targetObj.getClass(). getMethod("getId" ).invoke(targetObj); clazz.getMethod("set" + StringUtils.capitalize(columnName), Integer.class ) .invoke (object , id ) ; } } public void fillOnReading (Object object, int depth) throws Exception { if (object == null ) { return ; } if (depth <= 0 ) { return ; } fillManyToOneOnReading(object, depth); fillOneToManyOnReading(object, depth); fillEnumOnReading(object); } public void fillOnWriting (Object object) throws Exception { if (object == null ) { return ; } fillEnumOnWriting(object); fillManyToOneOnWriting(object); } }
4.在 service 层调用 1 2 @Resource private MapperFactory mapperFactory;
拿到 mapperFactory ,如果要对 CategoryMaper 进行填充处理的话,就直接用 mapperFactory.getMapper(mapperInterface);即可拿到对应的通用 mapper ,然后和 原来的 mybatis mapper 使用 方法一样。
后记 毫无疑问,这个通用 mapper 还很不完善,效率也比较低,现在的实现只相当于玩具的级别。而且市面上也已经有了一系列 jpa 系统实现,这个通用 mapper 存在的意义也不是十分大。
但是,我们可以通过这个 mapper 理解到泛型、反射的一系列用法,递归的实操中的使用,还可以对 mybatis-generator 有了更深的理解
这个 mapper 也在我的 小小商城-ssm 版中 完整运用了,为此我可以使用 mybatis 而不用写 一行 sql 代码。
时间仓促,涉及到的知识点也太多,文章非常简略,对新手也不是十分友好,在此表示歉意。如果想详细理解这个 通用 mapper ,可以到项目 github 中 查看全部源代码,或者发邮件、留言和我交流 (邮件地址在 Github 首页)。