MyBatis浅尝笔记
2017-01-20 16:07
375 查看
MyBatis应属于一种轻量级的java持久层技术,它通过简单的SQL xml或注解,将数据库数据映射到接口与POJO。最近项目要用到mybatis,所以学习之后在这里做个总结,文中的示例以xml配置为主,mybatis也支持注解的方式。
对应的实体类
Post
lvyahui
分页工具类PageData
基础Model与注解
其中BaseModel方法,getFieldMap用来获取数据库字段名称->模型属性名称的映射关系,约定数据库中使用"_"分割单词的蛇形字符串,而属性名使用首字母小写的驼峰字符串,例如数据库字段created_at对应属性createdAt。个人认为编程时约定很重要,有了约定很多通用方法才好写。
PostMapper 接口
这里除定义了原字段名到模型属性的映射外,还定义了以"post_"前缀开头的字段名到模型属性的映射,这样做是为了后面做关系查询时要用到,是为了防止其余关系表中存在同名字段时,使用as 别名不冲突。
按主键查询单条记录
按条件查询多条记录,这里按条件查询记录条数、查询记录只需要将count(1)换成*。
按条件查询记录并分页。看网上是有大量的mybatis的分页插件,这里是自己写的分页方法。
当SQL映射需要多个参数时,需要在Mapper对应的方法参数上注解上参数名称,否则只能按mybatis约定的名称或索引来访问变量,比如List会映射到list或者paramter1等等。
指定更新字段更新记录
判断属性值更新非null值字段
注意这里,在foreach中#{post.${value}}基于ongl的语法,由内向外求值,并且,在mybatis中,$与#存在区别,$ 在动态 SQL 解析阶段将会进行变量值string形式替换,# 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符,所以上面xml中的写法是可行。当然还可以在where字句中继续迭代出查询条件。
批量插入,在批量插入时,加了if判断,如果传递的是个空集合,则执行一条select 0语句,insert的返回值为-1,如果执行成功(posts非空),返回值为插入成功的记录条数。
二、关联查询
resultMap中除了result标签指定字段映射外,还支持以association(1)与collection(n)来映射关系模型的查询结果。
主要这里,在association标签中,并没有通过字标签result来映射结果,而是直接通过resultMap属性来映射结果,注意英文UserMapper.BaseResultMap与PostMapper.BaseResultMap并不处在同一个命名空间,所以要写上命名空间。
对应的SQL映射可以是关联查询或者先查询主表记录、再查询副表记录。注意如果字段可以确保不会有歧义,则可以直接写字段名,如果有歧义,则应该分别as一个别名,并且是已经在resultMap中配置好了的别名。
下面在执行sql语句时拦截打印SQL及执行耗时的拦截器代码。
注册插件,xml方式,这里没有单独为mybatis创建配置文件,而是直接在spring配置文件中定义插件,效果是一样的。
Mybatis通过调用SqlSession.getMapper方法,传递mapperInterface(PostMapper.class)为参数,最后以sqlSession,mapperInterface,methodCache为参数构造得到代理对象MapperProxy。最后对mapperInterface(PostMapper)的方法调用,都转发到代理对象执行invoke方法。
调用mapper接口的方法,将调用mapperProxy.invoke方法。在invoke方法中,会封装一个MapperMethod对象,这是被调用的mapper方法的进一步封装。
有四个地方调用了拦截器链的pluginAll方法,pluginAll实际是将待执行对象代理到代理对象上,也就是Plugin对象,demo程序中就是SQLMonitorPlugin。下面列表顺序也代表了被拦截的顺序
org.apache.ibatis.session.Configuration#newExecutor
org.apache.ibatis.session.Configuration#newParameterHandler
org.apache.ibatis.session.Configuration#newResultSetHandler
org.apache.ibatis.session.Configuration#newStatementHandler
执行步骤如下:
sqlSession实际是SQLSessionTemplate类的对象,调用其selectOne方法,最终调用的是代理方法SqlSessionInterceptor#invoke,在该方法中,获取到一个sqlSession(实际是DefaultSqlSession),
调用DefaultSqlSession#selectOne方法进行查询。DefaultSqlSession中封装了所有的对数据库的CRUD操作接口。
在DefaultSqlSession#selectList方法中获取了一个特殊的对象MappedStatement,这个对象是对mapper xml中sql、参数及resultMap的封装。
以MappedStatement、查询参数、分页参数、返回结果处理类(这里是null)为参数调用CachingExecutor#query方法
前面说到,因为Executor已经被代理到SQLMonitorPlugin对象,所以第一个拦截器被执行
在拦截器中,才再次调用CachingExecutor#query方法,在该方法中生成SQL,由SQL及查询参数得到查询缓存的Key
最后再缓存不存在的情况下,会调用到BaseExecutor#queryFromDatabase方法
最后调用SimpleExecutor#doQuery方法得到查询,在该方法中,会调用创建各种Handler(如StatementHandler),如果有对应拦截器,Handler就对被代理到拦截器
最后执行了查询之后,调用DefaultResultSetHandler#handleResultSets按照mappedStatement.getResultMaps()解析查询结果
具体步骤可以以测试代码debug一次
示例代码位置
https://github.com/lvyahui8/java-all/tree/master/mybatis-all
另外可参考阅读笔者之前写的
基于原始JDBC+方式写的通用DAO类
http://www.cnblogs.com/lvyahui/p/4009961.html
与
通用数库查询
http://www.cnblogs.com/lvyahui/p/5626466.html
笔者一直希望能将一些简单基础的CRUD操作一键化,工程化,省去一些简单且重复的劳动。
测试数据
先给出demo所使用的表结构,以典型的用户(1)-文章(n)的关系表做demo数据# # mysql数据库:数据库名 :dblog # DROP TABLE IF EXISTS m_category; CREATE TABLE m_category ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL COMMENT '分类名称', parent_id INT NOT NULL , level INT NOT NULL DEFAULT 0, path VARCHAR(64) NOT NULL COMMENT '栏目路径,rootId-xxId-xxId', PRIMARY KEY (id) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS m_post; CREATE TABLE m_post ( id int(11) NOT NULL AUTO_INCREMENT, category_id INT NOT NULL , user_id INT NOT NULL , title varchar(64) NOT NULL COMMENT '标题', content text COMMENT '正文', created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (id) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS m_user; CREATE TABLE m_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL, password varchar(255) NOT NULL, salt VARCHAR(32) NOT NULL , avatar varchar(64) DEFAULT NULL, type enum('customer','admin','root') NOT NULL DEFAULT 'customer', remember_token varchar(128) DEFAULT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; INSERT INTO m_user(id,username, password, salt,type) VALUE (1,'lvyahui','XXXXXXX','abcs','admin'); DROP TABLE IF EXISTS m_post_comment; CREATE TABLE m_post_comment( id int(11) AUTO_INCREMENT PRIMARY KEY , post_id INT NOT NULL , user_id INT NOT NULL , content VARCHAR(512) NOT NULL DEFAULT '', created_at TIMESTAMP NOT NULL DEFAULT current_timestamp, updated_at TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00' ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
对应的实体类
Post
package org.lyh.java.mybatis.bean; /** * @author lvyahui (lvyahui8@gmail.com,lvyahui8@126.com) * @since 2016/12/12 13:27 */ @SuppressWarnings("unused") public class Condition { private String key; private String opt = "="; private Object value; public Condition(String key, String opt, Object value) { this.key = key; this.opt = opt; this.value = value; } public Condition(String key, Object value){ this(key,"=",value); } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getOpt() { return opt; } public void setOpt(String opt) { this.opt = opt; } public Object getValue() { return value; } public void setValue(Object value) { this.value = value; } }
lvyahui
分页工具类PageData
package org.lyh.java.mybatis.bean; import org.lyh.java.mybatis.model.BaseModel; import java.util.List; /** * * Created by lvyahui on 2015/7/12. */ @SuppressWarnings("unused") public class PageData<T extends BaseModel> { /** * 前端做分页,所以这里limit设置的非常大,相当于不分页 */ public static final int DEFAULT_SIZE = 1000; private List<T> datas; private int currentPage = 1; private int totalPage; private int totalItem; private int maxBtnCount = 10; private int pageSize = DEFAULT_SIZE; private int start = 1; private int end; /** * 总项目数 */ public int getTotalItem() { return totalItem; } public void setTotalItem(int totalItem) { this.totalItem = totalItem; paging(); } private void paging() { totalPage = totalItem / pageSize + 1; if(totalPage > maxBtnCount){ if(currentPage <= (maxBtnCount-1)/2){ // 靠近首页 start = 1; }else if(totalPage-currentPage < (maxBtnCount-1)/2){ // 靠近尾页 start = totalPage - maxBtnCount - 1; }else{ start = currentPage - (maxBtnCount-1)/2; } end = maxBtnCount-1 + start > totalPage ? totalPage : maxBtnCount - 1 + start; }else{ end = totalPage; } // System.out.println("start:"+start+",end:"+end); } /** * 总页数 */ public int getTotalPage() { return totalPage; } /** * 当前页 */ public int getCurrentPage() { return currentPage; } public void setCurrentPage(int currentPage) { this.currentPage = currentPage; } /** * 页面数据 */ public List<T> getDatas() { return datas; } public void setDatas(List<T> datas) { this.datas = datas; } /** * 每页大小,可放多少个项,默认为10 */ public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } /** * @return 最大分页按钮数,默认值为10 */ public int getMaxBtnCount() { return maxBtnCount; } public void setMaxBtnCount(int maxBtnCount) { this.maxBtnCount = maxBtnCount; } /** * @return 第一个按钮的页号 */ public int getStart() { return start; } /** * @return 最后一个按钮上的页号 */ public int getEnd() { return end; } public void setEnd(int end) { this.end = end; } public void setStart(int start) { this.start = start; } private String listUrl; public String getListUrl() { return listUrl; } public void setListUrl(String listUrl) { this.listUrl = listUrl; } @Override public String toString() { return "PageData{" + "datas_size=" + datas.size() + ", currentPage=" + currentPage + ", totalPage=" + totalPage + ", totalItem=" + totalItem + ", maxBtnCount=" + maxBtnCount + ", pageSize=" + pageSize + ", start=" + start + ", end=" + end + '}'; } }
基础Model与注解
package org.lyh.java.mybatis.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author lvyahui (lvyahui8@gmail.com,lvyahui8@126.com) * @since 2017/1/16 10:44 */ @Target(value = { ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface JsonField { String value() default ""; } package org.lyh.java.mybatis.annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * @author lvyahui (lvyahui8@gmail.com,lvyahui8@126.com) * @since 2017/1/15 15:18 */ @Retention(RetentionPolicy.RUNTIME) public @interface NonTableFiled { } package org.lyh.java.mybatis.model; import org.lyh.java.mybatis.annotation.JsonField; import org.lyh.java.mybatis.annotation.NonTableFiled; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author lvyahui (lvyahui8@gmail.com,lvyahui8@126.com) * @since 2017/1/12 22:40 */ @SuppressWarnings("unused") public class BaseModel { public Map<String,Object> jsonValues ; protected Integer id; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public Map<String,String> getFieldMap(){ Map<String,String> fieldMap = new HashMap<String,String>(); Field[] fields = this.getClass().getDeclaredFields(); for (Field field : fields){ if(field.getAnnotation(NonTableFiled.class) == null){ fieldMap.put( // table field -- snake field.getName().replaceAll("([A-Za-z])([A-Z])","$1_$2").toLowerCase(), // bean field -- hump field.getName() ); } } return fieldMap; } public List<Field> getJsonFields(){ Field fields[] = this.getClass().getDeclaredFields(); List<Field> jsonFields = new ArrayList<Field>(); for(Field field : fields){ JsonField jsonField = field.getAnnotation(JsonField.class); if(jsonField == null){ continue; } jsonFields.add(field); } return jsonFields; } public Map<String,Object> getJsonValues(){ if(jsonValues != null){ return jsonValues; } jsonValues = new HashMap<String, Object>(); List<Field> fields = getJsonFields(); for (Field field : fields){ field.setAccessible(true); JsonField jsonField = field.getAnnotation(JsonField.class); try { jsonValues.put(jsonField.value(),field.get(this)); } catch (IllegalAccessException e) { // } finally { field.setAccessible(false); } } return jsonValues; } }
其中BaseModel方法,getFieldMap用来获取数据库字段名称->模型属性名称的映射关系,约定数据库中使用"_"分割单词的蛇形字符串,而属性名使用首字母小写的驼峰字符串,例如数据库字段created_at对应属性createdAt。个人认为编程时约定很重要,有了约定很多通用方法才好写。
一、单表查询
不涉及关系查询的情况还是比较简单的,并且有除去字段名与表名不一致外,有高度的可重用性。笔者在学习mybatis时,试图借助注解、泛型、反射等方法编写出一个通用的DAO类的集合,但因为xml或者注解无法继承包含等原因,一直没有完成一个很好的方案。单表查询示例以m_post表为示例。先来看看基础PostMapper与Xml ResultMapPostMapper 接口
package org.lyh.java.mybatis.mapper; import org.apache.ibatis.annotations.Param; import org.lyh.java.mybatis.bean.Condition; import org.lyh.java.mybatis.model.Post; import java.util.List; /** * @author lvyahui (lvyahui8@gmail.com,lvyahui8@126.com) * @since 2017/1/1 13:59 */ public interface PostMapper { //String table = "m_post"; Post get(Integer id); int insert(Post post); int updateByPrimaryKey(Post post); int updateByPrimaryKeySelective(@Param("post") Post post); int deleteByPrimaryKey(Integer id); int batchInsert(@Param("posts") List<Post> posts); int countSizeWithCondition(@Param("conditions") List<Condition> conditions); List<Post> getPageDataByCondition(@Param("conditions") List<Condition> conditions, @Param("offset") Integer offset, @Param("size") Integer size, @Param("orderProp") String orderProp, @Param("desc") boolean desc); }
<?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="org.lyh.java.mybatis.mapper.PostMapper"> <resultMap id="BaseResultMap" type="org.lyh.java.mybatis.model.Post" > <id column="id" property="id" jdbcType="INTEGER" /> <result column="user_id" property="userId" jdbcType="INTEGER"/> <result column="category_id" property="categoryId" jdbcType="INTEGER"/> <result column="title" property="title" jdbcType="VARCHAR" /> <result column="content" property="content" jdbcType="VARCHAR" /> <result column="created_at" property="createdAt" jdbcType="TIMESTAMP" /> <result column="updated_at" property="updatedAt" jdbcType="TIMESTAMP"/> <result column="post_id" property="id" jdbcType="INTEGER"/> <result column="post_user_id" property="userId" jdbcType="INTEGER"/> <result column="post_category_id" property="categoryId" jdbcType="INTEGER"/> <result column="post_title" property="title" jdbcType="VARCHAR" /> <result column="post_content" property="content" jdbcType="VARCHAR" /> <result column="post_created_at" property="createdAt" jdbcType="TIMESTAMP" /> <result column="post_updated_at" property="updatedAt" jdbcType="TIMESTAMP"/> </resultMap> <resultMap id="BaseResultWithUserMap" type="org.lyh.java.mybatis.model.Post"> <association property="user" column="user_id" javaType="org.lyh.java.mybatis.model.User" resultMap="org.lyh.java.mybatis.mapper.UserMapper.BaseResultMap" /> </resultMap> <!-- SQL配置在下面一一给出 --> </mapper>
这里除定义了原字段名到模型属性的映射外,还定义了以"post_"前缀开头的字段名到模型属性的映射,这样做是为了后面做关系查询时要用到,是为了防止其余关系表中存在同名字段时,使用as 别名不冲突。
查询
查询使用select标签按主键查询单条记录
<select id="get" resultMap="BaseResultMap"> select * from m_post where id = #{id} </select>
按条件查询多条记录,这里按条件查询记录条数、查询记录只需要将count(1)换成*。
<select id="countSizeWithCondition" resultType="int"> SELECT count(1) FROM m_post <if test="conditions != null"> WHERE <foreach item="item" collection="conditions" open="" separator="AND" close=""> ${item.key} ${item.opt} #{item.value} </foreach> </if> </select>
按条件查询记录并分页。看网上是有大量的mybatis的分页插件,这里是自己写的分页方法。
<select id="getPageDataByCondition" resultMap="BaseResultMap"> SELECT * FROM m_post <if test="conditions != null and conditions.size() > 0"> WHERE <foreach item="item" collection="conditions" open="" separator="AND" close=""> ${item.key} ${item.opt} #{item.value} </foreach> </if> <if test="orderProp != null"> ORDER BY ${orderProp} <if test="desc"> DESC </if> </if> LIMIT #{offset},#{size} </select>
当SQL映射需要多个参数时,需要在Mapper对应的方法参数上注解上参数名称,否则只能按mybatis约定的名称或索引来访问变量,比如List会映射到list或者paramter1等等。
更新
更新使用update标签。指定更新字段更新记录
<update id="updateByPrimaryKey" parameterType="org.lyh.java.mybatis.model.Post"> UPDATE m_post SET user_id = #{userId}, category_id = #{categoryId}, title = #{title}, content = #{content}, created_at = #{createdAt}, updated_at = #{updatedAt} WHERE id = #{id} </update>
判断属性值更新非null值字段
<update id="updateByPrimaryKeySelective" parameterType="org.lyh.java.mybatis.model.Post"> UPDATE m_post SET <foreach collection="post.fieldMap" item="value" index="key" separator=","> <if test="post[value] != null"> ${key} = #{post.${value}} </if> </foreach> WHERE id = #{post.id} </update>
注意这里,在foreach中#{post.${value}}基于ongl的语法,由内向外求值,并且,在mybatis中,$与#存在区别,$ 在动态 SQL 解析阶段将会进行变量值string形式替换,# 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符,所以上面xml中的写法是可行。当然还可以在where字句中继续迭代出查询条件。
删除
硬删除<delete id="deleteByPrimaryKey" > DELETE FROM m_post WHERE id = #{id} </delete>
插入与批量插入
单条插入支持返回auto_increament类型的主键id值<insert id="insert" useGeneratedKeys="true" keyProperty="id" keyColumn="id"> INSERT INTO m_post (category_id,user_id,title,content) VALUE (#{categoryId},#{userId},#{title},#{content}) <selectKey keyProperty="id" resultType="int" order="AFTER"> SELECT LAST_INSERT_ID(); </selectKey> </insert>
批量插入,在批量插入时,加了if判断,如果传递的是个空集合,则执行一条select 0语句,insert的返回值为-1,如果执行成功(posts非空),返回值为插入成功的记录条数。
<insert id="batchInsert" parameterType="java.util.List"> <if test="posts.size > 0"> INSERT INTO m_post (category_id,user_id, title,content, created_at,updated_at) VALUES <foreach collection="posts" item="post" index="index" separator=","> (#{post.categoryId},#{post.userId}, #{post.title},#{post.content}, #{post.createdAt},#{post.updatedAt}) </foreach> </if> <if test="posts.size == 0"> select 0; </if> </insert>
二、关联查询
resultMap中除了result标签指定字段映射外,还支持以association(1)与collection(n)来映射关系模型的查询结果。
一对一
双向绑定的话,只需要在两端以association配置映射即可。<?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="org.lyh.java.mybatis.mapper.PostMapper"> <resultMap id="BaseResultMap" type="org.lyh.java.mybatis.model.Post" > <id column="id" property="id" jdbcType="INTEGER" /> <result column="user_id" property="userId" jdbcType="INTEGER"/> <result column="category_id" property="categoryId" jdbcType="INTEGER"/> <result column="title" property="title" jdbcType="VARCHAR" /> <result column="content" property="content" jdbcType="VARCHAR" /> <result column="created_at" property="createdAt" jdbcType="TIMESTAMP" /> <result column="updated_at" property="updatedAt" jdbcType="TIMESTAMP"/> <result column="post_id" property="id" jdbcType="INTEGER"/> <result column="post_user_id" property="userId" jdbcType="INTEGER"/> <result column="post_category_id" property="categoryId" jdbcType="INTEGER"/> <result column="post_title" property="title" jdbcType="VARCHAR" /> <result column="post_content" property="content" jdbcType="VARCHAR" /> <result column="post_created_at" property="createdAt" jdbcType="TIMESTAMP" /> <result column="post_updated_at" property="updatedAt" jdbcType="TIMESTAMP"/> </resultMap> <resultMap id="BaseResultWithUserMap" type="org.lyh.java.mybatis.model.Post"> <association property="user" column="user_id" javaType="org.lyh.java.mybatis.model.User" resultMap="org.lyh.java.mybatis.mapper.UserMapper.BaseResultMap" /> </resultMap> </mapper>
主要这里,在association标签中,并没有通过字标签result来映射结果,而是直接通过resultMap属性来映射结果,注意英文UserMapper.BaseResultMap与PostMapper.BaseResultMap并不处在同一个命名空间,所以要写上命名空间。
一对多
一对多以在1端配置collection映射,并在n端配置association映射实现,其中collection配置如下<?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="org.lyh.java.mybatis.mapper.UserMapper"> <resultMap id="BaseResultMap" type="org.lyh.java.mybatis.model.User" > <!--<id column="id" property="id" jdbcType="INTEGER" />--> <result column="username" property="username" jdbcType="VARCHAR"/> <result column="password" property="password" jdbcType="VARCHAR"/> <result column="salt" property="salt" jdbcType="VARCHAR" /> <result column="avatar" property="avatar" jdbcType="VARCHAR" /> <result column="type" property="type" typeHandler="org.lyh.java.mybatis.type.UserTypeHandler"/> <result column="remember_token" property="rememberToken" jdbcType="VARCHAR"/> <result column="user_id" property="id" jdbcType="INTEGER"/> <result column="user_username" property="username" jdbcType="VARCHAR"/> <result column="user_password" property="password" jdbcType="VARCHAR"/> <result column="user_salt" property="salt" jdbcType="VARCHAR" /> <result column="user_avatar" property="avatar" jdbcType="VARCHAR" /> <result column="user_type" property="type" typeHandler="org.lyh.java.mybatis.type.UserTypeHandler"/> <result column="user_remember_token" property="rememberToken" jdbcType="VARCHAR"/> </resultMap> <resultMap id="BaseResultWithPostsMap" type="org.lyh.java.mybatis.model.User" extends="BaseResultMap"> <collection property="posts" ofType="org.lyh.java.mybatis.model.Post" resultMap="org.lyh.java.mybatis.mapper.PostMapper.BaseResultMap" column="user_id" /> </resultMap> <resultMap id="BaseResultSelectPostsMap" type="org.lyh.java.mybatis.model.User" > <collection property="posts" ofType="org.lyh.java.mybatis.model.Post" select="org.lyh.java.mybatis.mapper.PostMapper.getByUserId" column="user_id" /> </resultMap> </mapper>
对应的SQL映射可以是关联查询或者先查询主表记录、再查询副表记录。注意如果字段可以确保不会有歧义,则可以直接写字段名,如果有歧义,则应该分别as一个别名,并且是已经在resultMap中配置好了的别名。
<select id="getWithPosts" resultMap="BaseResultWithPostsMap"> SELECT user.id AS user_id, username, password, salt, avatar, type, remember_token, post.id AS post_id, category_id, title, content, created_at, updated_at FROM m_user user LEFT OUTER JOIN m_post post ON user.id = post.user_id WHERE user.id = #{id} </select>
拦截器
Mybatis为每次查询维护了一个拦截器链,通过调用InterceptorChain#pluginAll结合Plugin.wrap方法将待拦截对象转成代理对象,当调用待拦截对象的待拦截方法时,被转发到代理对象执行,而这个代理对象就是mybatis定义大插件或者说拦截器。拦截器通过定义在类上注解Signature说明拦截的class与method定义拦截,并通过配置注册插件。下面在执行sql语句时拦截打印SQL及执行耗时的拦截器代码。
package org.lyh.java.mybatis.interceptor; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.apache.ibatis.type.TypeHandlerRegistry; import java.text.DateFormat; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Properties; /** * @author samlv */ @Intercepts({ @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class SQLMonitorPlugin implements Interceptor { public Object intercept(Invocation invocation) throws Throwable { MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; Object parameter = null; if (invocation.getArgs().length > 1) { parameter = invocation.getArgs()[1]; } String sqlId = mappedStatement.getId(); BoundSql boundSql = mappedStatement.getBoundSql(parameter); Configuration configuration = mappedStatement.getConfiguration(); Object returnValue; long start = System.currentTimeMillis(); returnValue = invocation.proceed(); long end = System.currentTimeMillis(); long time = (end - start); if (time > 1) { String sql = getSql(configuration, boundSql, sqlId, time); System.err.println(sql); } return returnValue; } public static String getSql(Configuration configuration, BoundSql boundSql, String sqlId, long time) { String sql = showSql(configuration, boundSql); return sqlId + " : " + sql + " : " + time + "ms"; } private static String getParameterValue(Object obj) { String value; if (obj instanceof String) { value = "'" + obj.toString() + "'"; } else if (obj instanceof Date) { DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA); value = "'" + formatter.format(new Date()) + "'"; } else { if (obj != null) { value = obj.toString(); } else { value = ""; } } return value; } public static String showSql(Configuration configuration, BoundSql boundSql) { Object parameterObject = boundSql.getParameterObject(); List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); String sql = boundSql.getSql().replaceAll("[\\s]+", " "); if (parameterMappings.size() > 0 && parameterObject != null) { TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { sql = sql.replaceFirst("\\?", getParameterValue(parameterObject)); } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); for (ParameterMapping parameterMapping : parameterMappings) { String propertyName = parameterMapping.getProperty(); if (metaObject.hasGetter(propertyName)) { Object obj = metaObject.getValue(propertyName); sql = sql.replaceFirst("\\?", getParameterValue(obj)); } else if (boundSql.hasAdditionalParameter(propertyName)) { Object obj = boundSql.getAdditionalParameter(propertyName); sql = sql.replaceFirst("\\?", getParameterValue(obj)); } } } } return sql; } public Object plugin(Object o) { return Plugin.wrap(o, this); } public void setProperties(Properties properties) { } }
注册插件,xml方式,这里没有单独为mybatis创建配置文件,而是直接在spring配置文件中定义插件,效果是一样的。
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!--<property name="configLocation" value="classpath:mybatis-config.xml"/>--> <property name="plugins"> <array> <bean class="org.lyh.java.mybatis.interceptor.SQLMonitorPlugin"/> </array> </property> <!-- 自动扫描mapping.xml文件 --> <property name="mapperLocations" value="classpath:org/lyh/java/mybatis/mapper/*.xml"/> </bean>
三、源码浅析
Mapper代理对象获取
首先看调用栈Mybatis通过调用SqlSession.getMapper方法,传递mapperInterface(PostMapper.class)为参数,最后以sqlSession,mapperInterface,methodCache为参数构造得到代理对象MapperProxy。最后对mapperInterface(PostMapper)的方法调用,都转发到代理对象执行invoke方法。
调用mapper接口的方法,将调用mapperProxy.invoke方法。在invoke方法中,会封装一个MapperMethod对象,这是被调用的mapper方法的进一步封装。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (Object.class.equals(method.getDeclaringClass())) { try { return method.invoke(this, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
拦截器执行
sqlSession#selectOne调用被代理到org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor#invoke方法上有四个地方调用了拦截器链的pluginAll方法,pluginAll实际是将待执行对象代理到代理对象上,也就是Plugin对象,demo程序中就是SQLMonitorPlugin。下面列表顺序也代表了被拦截的顺序
org.apache.ibatis.session.Configuration#newExecutor
org.apache.ibatis.session.Configuration#newParameterHandler
org.apache.ibatis.session.Configuration#newResultSetHandler
org.apache.ibatis.session.Configuration#newStatementHandler
执行查询
在MapperProxy中调用org.apache.ibatis.binding.MapperMethod#execute方法,可以看到该方法默认时调用selectOne查询方法,在做多表(一对多)连接查询时,要保证主表与副表id不要一致,配置的resultMap不要相同,否则mybatis会认为主表查询结果返回了多条记录,从而抛出org.apache.ibatis.exceptions.TooManyResultsException异常。convertArgsToSqlCommandParam转换Mapper接口被调方法的参数为基础包装类、集合类等等。public Object execute(SqlSession sqlSession, Object[] args) { // ... Object result; switch (command.getType()) { case SELECT: if (method.returnsVoid() && method.hasResultHandler()) { executeWithResultHandler(sqlSession, args); result = null; } else if (method.returnsMany()) { result = executeForMany(sqlSession, args); } else if (method.returnsMap()) { result = executeForMap(sqlSession, args); } else if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; } // ... return result; }
执行步骤如下:
sqlSession实际是SQLSessionTemplate类的对象,调用其selectOne方法,最终调用的是代理方法SqlSessionInterceptor#invoke,在该方法中,获取到一个sqlSession(实际是DefaultSqlSession),
调用DefaultSqlSession#selectOne方法进行查询。DefaultSqlSession中封装了所有的对数据库的CRUD操作接口。
在DefaultSqlSession#selectList方法中获取了一个特殊的对象MappedStatement,这个对象是对mapper xml中sql、参数及resultMap的封装。
以MappedStatement、查询参数、分页参数、返回结果处理类(这里是null)为参数调用CachingExecutor#query方法
前面说到,因为Executor已经被代理到SQLMonitorPlugin对象,所以第一个拦截器被执行
在拦截器中,才再次调用CachingExecutor#query方法,在该方法中生成SQL,由SQL及查询参数得到查询缓存的Key
最后再缓存不存在的情况下,会调用到BaseExecutor#queryFromDatabase方法
最后调用SimpleExecutor#doQuery方法得到查询,在该方法中,会调用创建各种Handler(如StatementHandler),如果有对应拦截器,Handler就对被代理到拦截器
最后执行了查询之后,调用DefaultResultSetHandler#handleResultSets按照mappedStatement.getResultMaps()解析查询结果
具体步骤可以以测试代码debug一次
示例代码位置
https://github.com/lvyahui8/java-all/tree/master/mybatis-all
另外可参考阅读笔者之前写的
基于原始JDBC+方式写的通用DAO类
http://www.cnblogs.com/lvyahui/p/4009961.html
与
通用数库查询
http://www.cnblogs.com/lvyahui/p/5626466.html
笔者一直希望能将一些简单基础的CRUD操作一键化,工程化,省去一些简单且重复的劳动。
相关文章推荐
- [转]asp.net core中的View Component
- ASP.NET MVC编程入门--WEB API 启用PUT方法
- pdf生成库-libharu编译
- iSCSI 2-环境搭建<一>
- List接口
- 全志A10/A20 Bootloader加载过程分析
- pat-bl-1030
- [NOIP2003]加分二叉树
- 微信小程序开发——填坑之旅(1)
- Linux ftp 命令
- VBS错误代码释义
- 如何实现多进程写一个文件
- 设计模式练习(17)——观察者模式
- jsp中去session值,js中替换制定字符串内容
- IDEA 工具tomcat服务器配置
- 杨辉三角
- maven环境配置
- 动态代理(java原生动态代理)
- maven新建web项目
- 产品需求文档(PRD)写作(三) 原型设计(手绘原型,灰模原型,交互原型)