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,在开发过程中,产品忽然提了一个新的需求,项目经理根据这个需求在某张表中增加或变动了一个字段,这时,我猜你的操作是这样:
Mybatis Generator重新生成该表的 XML在这个过程中,如果我们在第 2 步时漏复制了一段标签,等整个操作完成之后,又别是一番滋味在心头~
假如肝不错,问题二也是小 CASE,那么问题又来了,我们如何在繁长的 XML 中去编写和修改我们的 XML 呢。
当我们打开要编辑的 XML,映入眼帘的就是 1000 多行的 XML,其中 900 行都是通用的增删改查操作,要新增一个标签,我们需要拉至文件底部编写新的数据操作,要更新一个标签,我们需要通过Ctrl + F寻找目标标签再进行修改。
如何避免这些问题呢?
如何让 Mybatis 增强通用性又不失灵活呢?
Ourbatis 是一款 Mybatis 开发增强工具,小巧简洁,项目地址:
特性:
环境:
以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 方法、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.domain-locations配置包下的所有实体类,将之映射为与之对应的表结构数据:
然后通过ourbatis.xml的渲染,生成一个又一个的 XML 文件,最后将之重新 Build 到 Mybatis 容器中!
整个过程分为两个核心点:
ourbatis.xml渲染元数据为 XML 文件我会一一介绍之~
在映射时,我们要根据自己数据库字段命名的风格去调整映射规则,就需要在第 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
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.