基于mybatis的通用mapper实现


前言

    目前市面上开源的通用mapper有很多,比如mybatis-plus,tkmybatis等等。 可能有人会认为这是在重复造轮子,但是经过自己的摸索探索出来的一套框架还是让自己有所收益。这套框架已经在项目中运行多年,当然功能也在不断改进中。

一、什么是mybatis通用 mapper?

    mybatis通用mapper就是基于mybatis的一个实现框架,可以提供一些常用增、删、改、查的操作,不需要重复写一些常用的sql。简化操作,精简代码,并且达到代码风格统一的目的。

本文我们只讨论使用mybatis做数据库持久层,其他如hibernate,jdbcTemplate等暂不讨论。

我们开发这个通用mapper需要满足一下几点
1、只做增强,不做修改。我们既可以使用通用mapper自带的功能,又要可以自定义写特殊的sql
2、提供简单易用的方法调用,而且无需繁琐的配置文件
3、提供一个代码生成器(我们的实体类,dao甚至service,controller都可以自动生成)

二、为什么我们要用mybatis通用 mapper?

    在我们日常的额开发过程中,基本上对每一张表都会定义一个mapper.xml。在这个xml文件中都会存在常用的sql操作,比如(selectList,selectOne,selectByPrimaryKey,insert,insertSelective,updateByPrimaryKey,updateByPrimaryKeySelective,deleteByPrimaryKey等等),几乎每张表里面都会包含这些sql。我们发现这些sql都是有一些规律的,都是对某一张表的一些增删改查的操作,而且这些sql都是我们开发过程中使用率较高的。当然我们也可以使用mybatis-generator来自动生成这些代码,但是有没有发现,用mybatis-generator自动生成后,我们的mapper.xml文件的内容会非常的庞大,看起很乱,对一些有代码洁癖的人(比如,我就是)来说简直就是灾难。
    这个时候,我们能不能有个工具在运行的时候自动生成一些常用sql呢?很幸运,从mybatis3.0开始,提供了一个XXXProvider注解,可以选择一个类,一个方法,然后返回一个sql来给mybatis执行。好,下面就一步一步的来看看,怎么实现这个通用mapper。

与mybatis-plus对比

  1. 无需修改SqlSessionFactory
  2. 使用mybatis3.0以上版本自带的XXXProvider来动态生成sql,不需要拦截器
  3. 接入简单,无需多余配置

三、通用mapper实现

1.调用流程

mybatis获取SQL方式:

  • 通过XML配置
  • 通过注解(@Select(“select * from table”))
  • 通过XXProvider(指定一个类和一个方法,方法里面返回sql。最终是由 ProviderSqlSource 去返回sql)

处于系统中的位置
在这里插入图片描述

调用流程图:
在这里插入图片描述

1.使用aop拦截器拦截我们的mapper.java里面的所有方法,并且把当前执行DAO对象(包含表名,字段名等信息),方法,参数等信息放入ThreadLocal对象中
2.在XXXProvider指定一个方法去生成sql
3.在这个方法里面可以获得第一步的ThreadLocal对象,然后进行拼接sql

前提:
这里使用了我前面文章提到的读写分离和多数据源自动路由
具体实现,可以见 基于mybatis的读写分离,多数据源自动路由

2.代码实现

    我们以查询列表为例
1、我们的model的类名建议是数据库表名的驼峰结构(也可以是任意的,可以使用@Table注解指定表名)
2、我们的Mapper.java文件需要继承一个BaseMapper<ModelName, PKType>

这里有需要注意的:
1、java的model类名建议使用表名的驼峰结构(也可以在里面使用@Table注解进行指定)
2、类里面的属性就是关联表中所有字段,建议属性名使用表里面字段名的驼峰结构(也可以在属性上使用@Column注解指定字段名称)
3、主键的属性,必须加上@Id注解(主键生成策略依靠应用中生成,或者直接使用表的自增长策略)
4、如果有些字段在自己拼sql的时候需要,但是并不是数据库的字段,那必须在该属性上加上注解@Transient

Mapper.java文件

public interface ISysMenuDAO extends BaseMapper<SysMenu, Integer> {
}

发现没有,这个dao只继承一个BaseMapper,而里面没有写一个方法,但是我们却可以拥有很多很多内置的常用方法。
我这里是把增、删、改、查分开,用各自的类去维护,BaseMapper就是把这四个合成一个,方便我们Mappse.java继承。
这里面提供了两个泛型参数:
第一个:是对应的model对象,也就是对应我们数据库表中实体类
第二个:是我们主键的类型(一般为Integer,或Long)

再看下BaseMapper

/**
 * 统一增删改查的父类接口
 * 继承该接口,可以实现最基本的增删改查操作
 * @author huangping 2018年1月26日
 */
public interface BaseMapper<MODEL, PK> 
	extends
	BaseSelectMapper<MODEL, PK>,
	BaseDeleteMapper<MODEL, PK>,
	BaseUpdateMapper<MODEL, PK>,
	BaseInsertMapper<MODEL, PK>
	{
}

3、配置AOP拦截器,拦截DAO里面的所有方法(获取类名,方法名,参数等信息,保存的ThreadLocal中)

<aop:config>
		<aop:pointcut id="daoMethodPoint" expression="${hp.springboot.database.expression:}"/>
		<aop:advisor pointcut-ref="daoMethodPoint" advice-ref="DAOMethodInterceptorHandle" />
</aop:config>

这里面的 ${hp.springboot.database.expression:} 这个参数需要配置我们的dao所在包的路径:
如:execution(* com.hp.springboot.admin.dal.*.*(..))

再看DAOMethodInterceptorHandle这个类,这是拦截我们的dao里面的方法,并且设置一些数据到线程私有变量中

/**
 * 
 * 描述:执行数据库操作之前拦截请求,记录当前线程信息
 * 之所以用抽象类,是因为可以扩展选择持久层框架。可以选择mybatis或jdbcTemplate,又或者hibernate
 * 作者:黄平
 * 时间:2018年4月11日
 */
public abstract class DAOMethodInterceptorHandle implements MethodInterceptor {

	private static Logger log = LoggerFactory.getLogger(DAOMethodInterceptorHandle.class);
	
	/**
	 * 存放当前执行线程的一些信息
	 */
	private static ThreadLocal<DAOInterfaceInfoBean> routeKey = new ThreadLocal<>();
	
	/**
	 * 最大数据库查询时间(超过这个时间,就会打印一个告警日志)
	 */
	@Value("${hp.springboot.database.maxDbDelayTime:150}")
	private long MAX_DB_DELAY_TIME;
	
	/**
	 * 获取dao操作的对象,方法等
	 * @param invocation
	 * @return
	 */
	public abstract DAOInterfaceInfoBean getDAOInterfaceInfoBean(MethodInvocation invocation);
	
	/**
	 * 获取当前线程的数据源路由的key
	 */
	public static DAOInterfaceInfoBean getRouteDAOInfo() {
		return routeKey.get();
	}
	
	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		//获取dao的操作方法,参数等信息,并设置到线程变量里
		this.setRouteDAOInfo(getDAOInterfaceInfoBean(invocation));
		
		//设置进入查询,记录线程执行时长
		entry();
		Object obj = null;
		try {
			//执行实际方法
			obj = invocation.proceed();
			return obj;
		} catch (Exception e) {
			throw  e;
		} finally {
			//退出查询
			exit();
			
			//避免内存溢出,释放当前线程的数据
			this.removeRouteDAOInfo();
		}
	}
	
	/**
	 * 进入查询
	 */
	private void entry() {
		DAOInterfaceInfoBean bean = getRouteDAOInfo();
		//加入到我们的线程调用堆栈里面,可以统计线程调用时间
		ThreadProfile.enter(bean.getMapperNamespace(), bean.getStatementId());
		DBDelayInfo delay = bean.new DBDelayInfo();
		delay.setBeginTime(System.currentTimeMillis());
		bean.setDelay(delay);
	}
	
	/**
	 * 结束查询
	 */
	private void exit() {
		DAOInterfaceInfoBean bean = getRouteDAOInfo();
		DBDelayInfo delay = bean.getDelay();
		delay.setEndTime(System.currentTimeMillis());
		ThreadProfile.exit();
		//输出查询数据库的时间
		if (delay.getEndTime() - delay.getBeginTime() >= MAX_DB_DELAY_TIME) {
			log.warn("execute db expire time. {}", delay);
		}
		
	}
	
	/**
	 * 绑定当前线程数据源路由的key 使用完成后必须调用removeRouteKey()方法删除
	 */
	private void setRouteDAOInfo(DAOInterfaceInfoBean key) {
		routeKey.set(key);
	}

	/**
	 * 删除与当前线程绑定的数据源路由的key
	 */
	private void removeRouteDAOInfo() {
		routeKey.remove();
	}
}

这里面的 getDAOInterfaceInfoBean 这个方法是一个抽象方法,需要具体的实现类去实现获取线程变量的方法。
我这里实现了基于mybatis的实现

public class MyBatisDAOMethodInterceptorHandle extends DAOMethodInterceptorHandle {

	@Override
	public DAOInterfaceInfoBean getDAOInterfaceInfoBean(MethodInvocation invocation) {
		DAOInterfaceInfoBean bean = new DAOInterfaceInfoBean();
		
		// 获取当前类的信息(由于我们使用的mybatis,这里获取到的是spring的代理类信息)
		Class<?> clazz = invocation.getThis().getClass();
		
		// 这里获取的才是我们定义的dao接口对象
		Class<?>[] targetInterfaces = ClassUtils.getAllInterfacesForClass(clazz, clazz.getClassLoader());
		
		// 获取该类的父类(该操作暂时没用使用到)
		Class<?>[] parentClass = targetInterfaces[0].getInterfaces();
		if (ArrayUtils.isNotEmpty(parentClass)) {
			bean.setParentClassName(parentClass[0]);
		}
		
		// 设置类名信息
		bean.setClassName(targetInterfaces[0]);
		
		// 设置方法的类信息
		bean.setMapperNamespace(targetInterfaces[0].getName());
		
		// 设置方法名
		bean.setStatementId(invocation.getMethod().getName());
		
		// 设置方法参数
		bean.setParameters(invocation.getArguments());
		return bean;
	}
}

,好了,这样我们就把该方法的一些信息设置在线路私有变量里,以供后面的程序调用。

4、以查询列表为例看下 BaseSelectMapper 里面的selectList方法

/**
	 * 根据传入的sqlbuild,查询
	 * @param sqlBuilders
	 */
	@SelectProvider(type = BaseSelectProvider.class, method = "selectList")
	public List<MODEL> selectList(@Param(SQLProviderConstant.SQL_BUILDS_ALIAS) SQLBuilders sqlBuilders);

    看到没有,这里有一个注解(SelectProvider),就是通过它来指定一个方法去生成我们所要的sql。我这里使用了SQLBuilders对象,是一个sql构造器,方法sql生成,支持链式调用。
    这里指定了这个sql需要从 BaseSelectProvider.selectList方法中去获取。那我们就看下这个方法

public static String selectList(Map<String, Object> target) {
		//从当前线程变量中,获取需要执行的表,字段等信息
		DynamicEntityBean entity = BaseSQLAOPFactory.getEntity();
		SQLBuilders builders = (SQLBuilders) target.get(SQLProviderConstant.SQL_BUILDS_ALIAS);
		String sql = getSQL(builders, entity);
		log.debug("selectList get sql \r\nsql={} \r\nentity={}", sql, entity);
		return sql;
	}

    这里主要的就是BaseSQLAOPFactory.getEntity();这句话,这里就是获取了我们前面步骤中保存在线程变量中数据,然后通过反射获取表名,字段名等信息。
    然后下面的getSQL方法就是按照条件进行拼接sql的操作。
看下BaseSQLAOPFactory.getEntity();

public static DynamicEntityBean getEntity() {
		//获取线程变量
		DAOInterfaceInfoBean info = DAOMethodInterceptorHandle.getRouteDAOInfo();
		
		//获取泛型对象
		GenericParadigmBean genericParadigmBean = instance.getGenericParadigmByClass(info.getClassName());
		
		//获取表名和所有字段
		DynamicEntityBean entity = instance.getDynamicEntityByClass(genericParadigmBean.getTargetModelClassName());
		return entity;
	}

第一步:DAOMethodInterceptorHandle.getRouteDAOInfo();获取刚才保存的线程变量(包含,类名,方面名,参数等等)。
第二步:通过反射获取当前的Model类,主键等信息
第三步:通过反射获取表名,表里面所有字段,还有各种注解等信息
第二和第三步都使用到了局部缓存和双重校验锁,这样同一个dao对象只要解析一次,后面就全部使用保存的缓存,提升效率。
这里的具体代码,可以打开 我的gitee去查看

5、这一步就是按照具体传入的参数,就行拼接sql
具体代码,可以打开 我的gitee去查看

OK,大功告成
看看,我们在项目中怎么使用

List<SysMenu> menuList = sysMenuDAO.selectList(SQLBuilders.create()
				.withSelect("id, name") //指定需要查询的字段,如果不指定,则查询所有字段
				.withWhere(SQLWhere.builder() //指定查询条件,这里提供了很多筛选方法(=,like,in,大于,小于等等)
						.eq("name", "abc")//等于
						.not_eq("name", "abc")//不等于
						.gt("name", 1)//大于
						.gte("name", 1)//大于等于
						.lt("name", 1)//小于
						.lte("name", 1)//小于等于
						.like("name", "abc")//模糊匹配
						.prefix("name", "abc")//前缀匹配
						.suffix("name", "abc")//后缀匹配
						.in("id", "1,2,3,4")//in
						.not_in("id", "1,2,3,4")//not in
						.build())
				.withOrder("id", "DESC")//指定排序
				.withPage(1, 10)//指定分页参数
				);

是不是很惊喜,我们的查询居然可以这么简单。
上面调用大家可以看到,我们不用再去拼接sql了,而发现查询是被我们设计出来的,通过一步步的链式调用设计我们的查询条件。以前那种枯燥无味的写sql过程不见了,取而代之的是一步一步的设计过程,是不是很有成就感!
Mapper.java里面只要继承一个父类,我们一大堆操作都有了

总结

    其实也不是很复杂,就是把原来我们写在xml文件里面的sql,通过一个方法去获取。
    而这个方法又是通过之前拦截器获取的数据进行反射得到所需要的表名,字段名等信息。然后通过泛型变量的方法可以做成一个通用mapper。

    使用该框架的步骤:
1、maven依赖

<dependency>
	<groupId>com.hp.springboot</groupId>
	<artifactId>hp-springboot-mybatis</artifactId>
	<version>1.0.0-SNAPSHOT</version>
</dependency>

2、创建实体类(建议表名字段名使用表的驼峰结果)
3、创建DAO,并且继承BaseMapper,指定泛型
4、指定expression表达式,指向我们的Mapper.java所在的package
5、直接调用内置方法实现功能

这里还提供了一个代码自动生成器,可以自动生成实体类,dao,service,controller

curl -H "Content-Type: application/json" -X POST  \
 -d '{ 
    "mainPathDir" : "D:/workspaces/test", 
    "tableNameList" : [ 
        "test_table" 
    ], 
    "projectPackage" : "com.test", 
    "serviceMavenModule" : "test-mvc", 
    "controllerMavenModule" : "test-mvc", 
    "webMavenModuleName" : "test-start", 
    "dalMavenModule" : "test-dal", 
    "commonMavenModule" : "test-common", 
    "modelMavenModule" : "test-model", 
    "servicePackageName" : "mvc", 
    "controllerPackageName" : "mvc", 
    "createService" : true, 
    "createController" : true, 
    "createFtl" : true 
 }'  \
 "http://192.168.136.1:8080/demo/AutoCreateRest/create"
字段名描述默认值备注
mainPathDir项目的主目录./建议写绝对目录
tableNameList所需要自动生成代码的表名数组形式
projectPackage项目的主包目录com.yoho.none一般在多模块工程中指定主包的路径
serviceMavenModuleservice子项目的项目名service
controllerMavenModulecontroller子项目的项目名controller
webMavenModuleName启动子项目的项目名start
dalMavenModuledao子项目的项目名dal
commonMavenModulecommon子项目的项目名common
modelMavenModulemodel子项目的项目名model
servicePackageNameservice所在的包名service
controllerPackageNamecontroller所在的包名mvc
createService是否生成servicefalse
createController是否生成controllerfalse
createFtl是否生成页面false页面是使用freemarker生成的,基于layui页面

我的项目目录如下:

    以上就是我对通用mapper的一个实现。其实我这里DAOMethodInterceptorHandle这个是提供了一个抽象类,我们项目中实现了基于mybatis的实现,其实也可以提供一个基于jdbcTemplate的实现,那样的话,就可以在jdbcTemplate中使用通用mapper了。
    完整的代码在我的gitee中 我的gitee ,欢迎大家留言讨论!!

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐