您的位置:首页 > 其它

MyBatis浅尝笔记

2017-01-20 16:07 375 查看
MyBatis应属于一种轻量级的java持久层技术,它通过简单的SQL xml或注解,将数据库数据映射到接口与POJO。最近项目要用到mybatis,所以学习之后在这里做个总结,文中的示例以xml配置为主,mybatis也支持注解的方式。

测试数据

先给出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 ResultMap

PostMapper 接口

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操作一键化,工程化,省去一些简单且重复的劳动。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: