使 Mybatis 开发变得更加轻松的增强工具 — Ourbatis

2018-10-22 10:44:42 +08:00
 iamniconico

一、Mybatis 的不足之处

Mybatis 是一款优秀的及其灵活的持久层框架,通过 XML 配置并映射到 Mapper 接口为 Service 层提供基础数据操作入口。

这么优秀的框架竟然还有不足之处?

俗话说人无完人,因为 Mybatis 实在是太灵活了,灵活到每个 Mapper 接口都需要定制对应的 XML,所以就会引发一些问题。

问题一:配置文件繁多

假如一个系统中 DB 中涉及 100 张表,我们就需要写100个 Mapper 接口,还没完,最可怕的是,我们要为这100个 Mapper 接口定制与之对应的100套 XML。而每个 Mapper 都必不可少的需要增删改查功能,我们就要写100遍增删改查,作为高贵的 Java 开发工程师,这是不能容忍的,于是Mybatis Generator诞生了,然而又会引发另一个问题!

问题二:维护困难

我们使用Mybatis Generator解决了问题一,再多的文件生成就是了,简单粗暴,貌似解决了所有的问题,Mybatis 完美了!

不要高兴的太早,在系统刚刚建立起来时,我们使用Mybatis Generator生成了一堆 XML,在开发过程中,产品忽然提了一个新的需求,项目经理根据这个需求在某张表中增加或变动了一个字段,这时,我猜你的操作是这样:

在这个过程中,如果我们在第 2 步时漏复制了一段标签,等整个操作完成之后,又别是一番滋味在心头~

问题三:编写 XML 困难

假如肝不错,问题二也是小 CASE,那么问题又来了,我们如何在繁长的 XML 中去编写和修改我们的 XML 呢。

当我们打开要编辑的 XML,映入眼帘的就是 1000 多行的 XML,其中 900 行都是通用的增删改查操作,要新增一个标签,我们需要拉至文件底部编写新的数据操作,要更新一个标签,我们需要通过Ctrl + F寻找目标标签再进行修改。

如何避免这些问题呢?

如何让 Mybatis 增强通用性又不失灵活呢?

二、使用 Ourbatis 辅助 Mybatis

Ourbatis 是一款 Mybatis 开发增强工具,小巧简洁,项目地址:

特性:

关于 Ourbatis 使用的一个小 Demo

环境:

Spring Boot 2.0.5.RELEASE版本为例,在可以正常使用 Mybatis 的项目中,pom.xml添加如下依赖:

   <dependency>
       	<groupId>com.smallnico</groupId>
       	<artifactId>ourbatis-spring-boot-starter</artifactId>
       	<version>1.0.5</version>
   </dependency>

在配置文件中增加一下配置:

ourbatis.domain-locations=实体类所在包名

接下来,Mapper 接口只需要继承SimpleMapper即可:

import org.nico.ourbatis.domain.User;
public interface UserMapper extends SimpleMapper<User, Integer>{
}

至此,一个使用 Ourbatis 的简单应用已经部署起来了,之后,你就可以使用一些 Ourbatis 默认的通用操作方法:

	public T selectById(K key);
	
	public T selectEntity(T condition);
	
	public List<T> selectList(T condition);
	
	public long selectCount(Object condition);
	
	public List<T> selectPage(Page<Object> page);
	
	default PageResult<T> selectPageResult(Page<Object> page){
		long total = selectCount(page.getEntity());
		List<T> results = null;
		if(total > 0) {
			results = selectPage(page);
		}
		return new PageResult<>(total, results);
	}
	
	public K selectId(T condition);
	
	public List<K> selectIds(T condition);
	
	public int insert(T entity);
	
	public int insertSelective(T entity);
	
	public int insertBatch(List<T> list);
	
	public int update(T entity);
	
	public int updateSelective(T entity);
	
	public int updateBatch(List<T> list);
	
	public int delete(T condition);
	
	public int deleteById(K key);
	
	public int deleteBatch(List<K> list);

Mapper 自定义方法

在很多场景中,我们使用以上的自带的通用方法远远不能满足我们的需求,我们往往需要额外扩展新的 Mapper 方法、XML 标签,我们使用了 Ourbatis 之后该如何实现呢?

首先看一下我们的需求,在上述 Demo 中,我们在 UserMapper 中增加一个方法selectNameById

import org.nico.ourbatis.domain.User;
public interface UserMapper extends SimpleMapper<User, Integer>{
    public String selectNameById(Integer userId);
}

和 Mybatis 一样,需要在resources资源目录下新建一个文件夹ourbatis-mappers,然后在其中新建一个 XML 文件,命名规则为:

DomainClassSimpleName + Mapper.xml

其中DomainClassSimpleName就是我们实体类的类名,这里是为User,那么新建的 XML 名为UserMapper.xml

src/main/resources
 - ourbatis-mappers
   - UserMapper.xml

之后,打开UserMapper.xml,开始编写 Mapper 中selectNameById方法对应的标签:

<select id="selectNameById" resultType="java.lang.String">
    select name from user where id = #{userId}
</select>

注意,整个文件中只需要写标签就行了,其他的什么都不需要,这是为什么呢?深入之后你就会明白,这里先不多说!

接下来,就没有接下来了,可以直接使用selectNameById方法了。

深入了解 Ourbatis

当服务启动的时候,Ourbatis 首先会扫描ourbatis.domain-locations配置包下的所有实体类,将之映射为与之对应的表结构数据:

然后通过ourbatis.xml的渲染,生成一个又一个的 XML 文件,最后将之重新 Build 到 Mybatis 容器中!

整个过程分为两个核心点:

我会一一介绍之~

映射实体类为元数据

在映射时,我们要根据自己数据库字段命名的风格去调整映射规则,就需要在第 1 个核心点中去做处理,Ourbatis 使用包装器来完成:

public interface Wrapper<T> {
	public String wrapping(T value);
}

对于需要映射的字段,如表名表字段名,它们都将会经过一个包装器链条的处理之后再投入到ourbatis.xml中做渲染,这样就使得我们可以自定义包装器出更换映射的字段格式,具体方式可以参考官方 Wiki:Wrapper 包装器

使用ourbatis.xml渲染元数据为 XML 文件

而在于第 2 个核心点中,Ourbatis 通过自定义标签做模板渲染,我们可以先看一下官方默认的ourbatis.xml内部构造:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="@{mapperClassName}">
	<resultMap id="BaseResultMap" type="@{domainClassName}">
		<ourbatis:foreach list="primaryColumns" var="elem">
			<id column="@{elem.jdbcName}" property="@{elem.javaName}" />
		</ourbatis:foreach>
		<ourbatis:foreach list="normalColumns" var="elem">
			<result column="@{elem.jdbcName}" property="@{elem.javaName}" />
		</ourbatis:foreach>
	</resultMap>

	<sql id="Base_Column_List">
		<ourbatis:foreach list="allColumns" var="elem"
			split=",">
			`@{elem.jdbcName}`
		</ourbatis:foreach>
	</sql>

	<select id="selectById" parameterType="java.lang.Object"
		resultMap="BaseResultMap">
		select
		<include refid="Base_Column_List" />
		from @{tableName}
		where 1 = 1
		<ourbatis:foreach list="primaryColumns" var="elem">
			and `@{elem.jdbcName}` = #{@{elem.javaName}}
		</ourbatis:foreach>
	</select>

	<select id="selectEntity" parameterType="@{domainClassName}"
		resultMap="BaseResultMap">
		select
		<include refid="Base_Column_List" />
		from @{tableName}
		where 1 = 1
		<ourbatis:foreach list="allColumns" var="elem">
			<if test="@{elem.javaName} != null">
				and `@{elem.jdbcName}` = #{@{elem.javaName}}
			</if>
		</ourbatis:foreach>
		limit 1
	</select>

	<select id="selectCount" parameterType="@{domainClassName}"
		resultType="long">
		select count(0)
		from @{tableName}
		where 1 = 1
		<ourbatis:foreach list="allColumns" var="elem">
			<if test="@{elem.javaName} != null">
				and `@{elem.jdbcName}` = #{@{elem.javaName}}
			</if>
		</ourbatis:foreach>
		limit 1
	</select>

	<select id="selectPage"
		parameterType="org.nico.ourbatis.entity.Page"
		resultMap="BaseResultMap">
		select
		<include refid="Base_Column_List" />
		from @{tableName}
		where 1 = 1
		<if test="entity != null">
			<ourbatis:foreach list="allColumns" var="elem">
				<if test="entity.@{elem.javaName} != null">
					and `@{elem.jdbcName}` = #{entity.@{elem.javaName}}
				</if>
			</ourbatis:foreach>
		</if>
		<if test="orderBy != null">
			order by ${orderBy}
		</if>
		<if test="start != null and end != null">
			limit ${start},${end}
		</if>
	</select>

	<select id="selectList" parameterType="@{domainClassName}"
		resultMap="BaseResultMap">
		select
		<include refid="Base_Column_List" />
		from @{tableName}
		where 1 = 1
		<ourbatis:foreach list="allColumns" var="elem">
			<if test="@{elem.javaName} != null">
				and `@{elem.jdbcName}` = #{@{elem.javaName}}
			</if>
		</ourbatis:foreach>
	</select>

	<select id="selectId" parameterType="@{domainClassName}"
		resultType="java.lang.Object">
		select
		<ourbatis:foreach list="primaryColumns" var="elem"
			split=",">
			`@{elem.jdbcName}`
		</ourbatis:foreach>
		from @{tableName}
		where 1 = 1
		<ourbatis:foreach list="allColumns" var="elem">
			<if test="@{elem.javaName} != null">
				and `@{elem.jdbcName}` = #{@{elem.javaName}}
			</if>
		</ourbatis:foreach>
		limit 1
	</select>

	<select id="selectIds" parameterType="@{domainClassName}"
		resultType="java.lang.Object">
		select
		<ourbatis:foreach list="primaryColumns" var="elem"
			split=",">
			`@{elem.jdbcName}`
		</ourbatis:foreach>
		from @{tableName}
		where 1 = 1
		<ourbatis:foreach list="normalColumns" var="elem">
			<if test="@{elem.javaName} != null">
				and `@{elem.jdbcName}` = #{@{elem.javaName}}
			</if>
		</ourbatis:foreach>
	</select>

	<delete id="deleteById" parameterType="java.lang.Object">
		delete
		from @{tableName}
		where 1=1
		<ourbatis:foreach list="primaryColumns" var="elem">
			and `@{elem.jdbcName}` = #{@{elem.javaName}}
		</ourbatis:foreach>
	</delete>

	<insert id="insert" keyProperty="@{primaryColumns.0.jdbcName}"
		useGeneratedKeys="true" parameterType="@{domainClassName}">
		insert into @{tableName}
		(
		<include refid="Base_Column_List" />
		)
		values (
		<ourbatis:foreach list="allColumns" var="elem"
			split=",">
			#{@{elem.javaName}}
		</ourbatis:foreach>
		)
	</insert>

	<insert id="insertSelective"
		keyProperty="@{primaryColumns.0.jdbcName}" useGeneratedKeys="true"
		parameterType="@{domainClassName}">
		insert into @{tableName}
		(
		<ourbatis:foreach list="primaryColumns" var="elem"
			split=",">
			`@{elem.jdbcName}`
		</ourbatis:foreach>
		<ourbatis:foreach list="normalColumns" var="elem">
			<if test="@{elem.javaName} != null">
				,`@{elem.jdbcName}`
			</if>
		</ourbatis:foreach>
		)
		values (
		<ourbatis:foreach list="primaryColumns" var="elem">
			#{@{elem.javaName}}
		</ourbatis:foreach>
		<ourbatis:foreach list="normalColumns" var="elem">
			<if test="@{elem.javaName} != null">
				, #{@{elem.javaName}}
			</if>
		</ourbatis:foreach>
		)
	</insert>

	<insert id="insertBatch"
		keyProperty="@{primaryColumns.0.jdbcName}" useGeneratedKeys="true"
		parameterType="java.util.List">
		insert into @{tableName}
		(
		<include refid="Base_Column_List" />
		)
		values
		<foreach collection="list" index="index" item="item"
			separator=",">
			(
			<ourbatis:foreach list="allColumns" var="elem"
				split=",">
				#{item.@{elem.javaName}}
			</ourbatis:foreach>
			)
		</foreach>
	</insert>

	<update id="update" parameterType="@{domainClassName}">
		update @{tableName}
		<set>
			<ourbatis:foreach list="normalColumns" var="elem"
				split=",">
				`@{elem.jdbcName}` = #{@{elem.javaName}}
			</ourbatis:foreach>
		</set>
		where 1=1
		<ourbatis:foreach list="primaryColumns" var="elem">
			and `@{elem.jdbcName}` = #{@{elem.javaName}}
		</ourbatis:foreach>
	</update>

	<update id="updateSelective" parameterType="@{domainClassName}">
		update @{tableName}
		<set>
			<ourbatis:foreach list="primaryColumns" var="elem"
				split=",">
				`@{elem.jdbcName}` = #{@{elem.javaName}}
			</ourbatis:foreach>
			<ourbatis:foreach list="normalColumns" var="elem">
				<if test="@{elem.javaName} != null">
					,`@{elem.jdbcName}` = #{@{elem.javaName}}
				</if>
			</ourbatis:foreach>
		</set>
		where 1=1
		<ourbatis:foreach list="primaryColumns" var="elem">
			and `@{elem.jdbcName}` = #{@{elem.javaName}}
		</ourbatis:foreach>
	</update>

	<update id="updateBatch" parameterType="java.util.List">
		<foreach collection="list" index="index" item="item"
			separator=";">
			update @{tableName}
			<set>
				<ourbatis:foreach list="normalColumns" var="elem"
					split=",">
					`@{elem.jdbcName}` = #{item.@{elem.javaName}}
				</ourbatis:foreach>
			</set>
			where 1=1
			<ourbatis:foreach list="primaryColumns" var="elem">
				and `@{elem.jdbcName}` = #{item.@{elem.javaName}}
			</ourbatis:foreach>
		</foreach>
	</update>

	<delete id="deleteBatch" parameterType="java.util.List">
		delete from @{tableName} where @{primaryColumns.0.jdbcName} in
		<foreach close=")" collection="list" index="index" item="item"
			open="(" separator=",">
			#{item}
		</foreach>
	</delete>

	<delete id="delete" parameterType="@{domainClassName}">
		delete from @{tableName} where 1 = 1
		<ourbatis:foreach list="allColumns" var="elem">
			<if test="@{elem.javaName} != null">
				and `@{elem.jdbcName}` = #{@{elem.javaName}}
			</if>
		</ourbatis:foreach>
	</delete>

	<ourbatis:ref path="classpath:ourbatis-mappers/@{domainSimpleClassName}Mapper.xml" />
</mapper>

可以看出来,ourbatis.xml内容类似于原生的 Mybatis 的 XML,不同的是,有两个陌生的标签:

这是 Ourbatis 中独有的标签,Ourbatis 也提供着对应的入口让我们去自定义标签:

Class: org.nico.ourbatis.Ourbatis
Field: 
public static final Map<String, AssistAdapter> ASSIST_ADAPTERS = new HashMap<String, AssistAdapter>(){
		private static final long serialVersionUID = 1L;
		{
			put("ourbatis:foreach", new ForeachAdapter());
			put("ourbatis:ref", new RefAdapter());
		}
	};

我们可以修改org.nico.ourbatis.Ourbatis类中的静态参数ASSIST_ADAPTERS去删除、更新和添加自定义标签,需要实现一个标签适配器,我们可以看一下最简单的RefAdapter适配器的实现:

public class RefAdapter extends AssistAdapter{
	@Override
	public String adapter(Map<String, Object> datas, NoelRender render, Document document) {
		String path = render.rending(datas, document.getParameter("path"), "domainSimpleClassName");
		String result =  StreamUtils.convertToString(path.replaceAll("classpath:", ""));
		return result == null ? "" : result.trim();
	}
}

Ourbatis 中只定义了上述两个自定义标签已足够满足需求,通过foreach标签,将元数据中的集合遍历渲染,通过ref标签引入外界资源,也就是我们之前所说的对 Mapper 接口中方法的扩展!

<ourbatis:ref path="classpath:ourbatis-mappers/@{domainSimpleClassName}Mapper.xml" />

其中的 path 就是当前项目 classpath 路径的相对路径,而@{domainSimpleClassName}就代表着实体类的类名,更多的系统参数可以参考 Wiki:元数据映射

通过这种模板渲染的机制,Ourbatis 是相当灵活的,我们不仅可以通过引入外部文件进行扩展,当我们需要添加或修改通用方法时,我们可以可以自定义ourbatis.xml的内容,如何做到呢?复制一份将之放在资源目录下就可以了!

看到这里,相信大家已经知道 Ourbatis 的基本原理已经使用方式,我就再次不多说了,更多细节可以去官方 Wiki 中阅读:Ourbtis Wiki

8932 次点击
所在节点    Java
53 条回复
iamniconico
2018-10-22 21:31:30 +08:00
@aristotll 哈哈,gay 还没修好
KingEngine
2018-10-23 00:48:36 +08:00
可以,这很强势
gowk
2018-10-23 08:02:33 +08:00
@bsg1992 除了 Mybatis,还有别的更好的选择吗
godoway
2018-10-23 08:16:27 +08:00
@gowk 不在 xml 里面写 sql 的话,其实还有一个 jooq。
看起来挺美好的,但用起来的感觉也就比原生 jdbc 高级一点。
xuanbg
2018-10-23 08:56:38 +08:00
从来不写 xml 的路过,我们都是用 spring boot 的注解在 mapper 里面直接写 sql。
PhpBestRubbish
2018-10-23 09:12:14 +08:00
可以
gowk
2018-10-23 09:27:16 +08:00
@godoway jooq 就是垃圾,很反感这种把 Sql 拆开硬生生的塞进 Java 里面的做法,把简单问题复杂化了,不止 Java,其它语言里面也有很多这种垃圾,用 jooq 还不如直接 jdbcTemplate 灵活清晰明了。
w10511026
2018-10-23 09:30:28 +08:00
又一个造轮子的
LemonCoo1
2018-10-23 09:42:14 +08:00
我在公司内部全部推的是 tk mybatis 搭配 springboot 开箱即食
iamniconico
2018-10-23 09:43:13 +08:00
@xuanbg 十行以上的 sql 会不会不优雅
iamniconico
2018-10-23 09:47:53 +08:00
@LemonCoo1 tk 很不错,不过现在大多数开源都支持开箱即食
<dependency>
<groupId>com.smallnico</groupId>
<artifactId>ourbatis-spring-boot-starter</artifactId>
<version>1.0.5</version>
</dependency>
rockyou12
2018-10-23 09:50:19 +08:00
所以为啥你们都不用 jpa …… spring data jpa 这种不好嘛……
PythonAnswer
2018-10-23 09:52:28 +08:00
哈哈 还有一个 oursql
sonyxperia
2018-10-23 10:06:39 +08:00
和 mybatis-plus 有什么区别嘛
iamniconico
2018-10-23 10:35:17 +08:00
@sonyxperia 参考 7 楼
popvlovs
2018-10-23 10:55:53 +08:00
@xuanbg +1,XML 太反人类了,写注解看起来舒服很多
xuanbg
2018-10-23 12:22:00 +08:00
@iamniconico 看上去好看有什么用。。。查询效率才是关键。
iamniconico
2018-10-23 12:28:22 +08:00
@xuanbg xml 和注解不都是在服务启动的时候都绑定 mapper 了吗?查询效率有什么差别。。。。我记得官网都推荐 xml,注解功能有限
lihongjie0209
2018-10-23 12:30:17 +08:00
为什么不用 jpa?
iamniconico
2018-10-23 12:47:02 +08:00
@lihongjie0209
首先,对于“为什么不用 jpa ?”这个问题,我要反问一下,我为什么要用 jpa ?

然后再正视这个问题,为什么不用 jpa ?这个问题需要一个前置条件,例如在某某场景下、某某类型的项目中,为什么不用 jpa ?这样才感觉合理,就像为什么吃饭要用筷子一样合理!

JPA 典型的代表是 Hibernate,Mybatis 没有实现 JPA 那一套,JPA 抽象了 api, 为了替代 native sql,增加了学习成本,降低了性能。复杂的查询还是只能用 native sql。

相比 Jpa,不像 Jpa 操作那样方便,但是性能和灵活性提高了上去,它们之前没有比较的必要,就像骑自行车一样和开轿车,从 A 点到 B 点,很可能中间有个胡同可以更快的到达终点,那就选择骑自行车,可能只能走大路,那就选择轿车罢了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/499724

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX