MyBatis

一. 简介

  1. mybatis的HelloWorld程序

    • 创建全局配置文件 mybatis-config.xml ,添加数据源相关信息
<!--将写好的sql文件注册到全局配置文件中-->
<mappers>
    <mapper resource="EmployeeMapper.xml"/>
</mappers>
```
+ sql的映射文件 EmployeeMapper.xml

```xml
```
+ 代码测试

```java
//1.根据XML配置文件(全局配置文件)创建一个SqlSessionFactory对象
//2.sql映射文件,配置了每一个 sql,以及sql的封装规则
//3.将sql映射文件注册在全局配置配置文件中
//4.编写测试代码
//     1)根据全局配置文件得到sqlSessionFactory
//     2)使用sqlSessionFactory得到SqlSession对象,来执行增删改查。一个SqlSession就代表一次会话。用完关闭
//     3)使用sql的唯一标识来告诉MyBatis执行那个sql。sql都保存在sql映射文件中。
@Test
public void test() throws Exception {
    String resource = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory =
            new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession session = sqlSessionFactory.openSession();
    try{
        //@param statement Unique identifier matching the statement to use. SQL唯一标识 namespace+id
        //@param parameter A parameter object to pass to the statement. 参数
        Employee employee = session.selectOne(
                "org.mybatis.example.EmployeeMapper.selectEmployee", 1);
        System.out.println(employee);
    } finally {
        session.close();
    }
}
 
2. 接口式编程
    + 首先写mapper 接口
    
    ```java
public interface EmployeeMapper {
    public Employee getEmployeeById(Integer id);
}
+ 接口与SQL映射文件绑定(mybatis动态生成接口实现类)

```xml
```
> SqlSession 代表和数据库的一次会话;用完必须关闭
> SqlSession 和 connection 一样都是非线程安全的。每次使用都应该去获取新的对象。
> mapper 接口没有实现类,但是mybatis会为这个接口生成一个代理对象。(将接口和xml绑定)
> 两个重要配置文件:mybatis全局配置文件:包含数据库连接池信息,事务管理等系统环境;sql映射文件:

二. MyBatis configuration 全局配置文件

  1. properties:用来引入外部的数据源配置

<properties resource="jdbc.properties"></properties>

2. settings

    ```xml
    <!--settings包含很多重要的设置-->
    <settings> 
        <!--开启驼峰转下滑线-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
  1. typeHandlers:将java类型和数据库类型进行适配。

  2. plugins:插件
    MyBatis allows you to intercept calls to at certain points within the execution of a mapped statement.
    By default, MyBatis allows plug-ins to intercept method calls of:

    • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 执行器
    • ParameterHandler (getParameterObject, setParameters) 参数处理器
    • ResultSetHandler (handleResultSets, handleOutputParameters) 结果集处理器
    • StatementHandler (prepare, parameterize, batch, update, query) SQL语句处理器
  3. environments:MyBatis can be configured with multiple environments.
    environment:配置一个具体的环境信息,必须有两个标签,id代表当前环境的唯一标识。

    <!--通过default切换环境-->
    <environments default="development">
        <environment id="development">
            <!--配置事务管理器;type 事务管理器的类型 了解-->
            <transactionManager type="JDBC"/>
            <!--dataSource数据源;type数据源类型:UNPOOLED、POOLED、JNDI-->
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    
  
6. databaseIdProvider:MyBatis is able to execute different statements depending on your database vendor. 

7. mappers:使用 class:直接注册接口,要能绑定成功,**接口和SQL映射文件同名且同一目录**

    ```xml
    <!--将写好的sql文件注册到全局配置文件中-->
    <mappers>
        <!--
        mapper:注册一个sql映射;
        resource:引入类路径下的资源
        url:网络路径,磁盘路径
        class:直接注册接口,要能绑定成功,接口和SQL映射文件同名且同一目录
        -->
        <!--<mapper resource="EmployeeMapper.xml"/>-->
        <mapper class="cn.guet.mapper.EmployeeMapper" ></mapper>
        <!--批量注册:接口与SQL映射文件同包名-->
        <!--<package name="cn.guet.mapper.EmployeeMapper"/>-->
    </mappers>
  1. 注:在maven项目中如果将 mapper.xml 放在 mapper 接口的包下面,需在pom文件中配置编译xml的指令

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
    </build>
    

### 三. Mapper XML Files 

1. select、insert 、update 、delete 
    
    ```xml
    <select id="getEmpById" resultType="cn.guet.bean.Employee">
		select * from tb_employee where id = #{id}
	</select>

    <!--parameterType可以省略-->
    <insert id="addEmp" parameterType="cn.guet.bean.Employee">
		insert  into tb_employee(last_name,email,gender) values(#{lastName},#{email},#{gender})
	</insert>

    <update id="updateEmp">
		update tb_employee set last_name = #{lastName},email=#{email},gender = #{gender} where id = #{id}
	</update>

    <delete id="deleteEmpById">
		delete from tb_employee where id = #{id}
	</delete>
  1. 单个参数处理:mybatis不会做特殊处理;#{参数名}:取出参数

  2. 多个参数处理
    如果直接写多个形参,会报如下错误 org.apache.ibatis.binding.BindingException:

    多个参数mybatis会做特殊处理,多个参数会被封装成一个map。key:param1.....paramN,或者索引;value:传入的参数值
    通常使用 @Param("") 注解来指定参数名。如果多个参数是业务逻辑模型,可传入pojo,#{属性名} 直接取出。如果没有对应的pojo,不经常使用,可传入map。经常使用的话,可以编写TO数据传输对象。

    public Employee getEmpByIdAndLastName(@Param("id") Integer id,@Param("lastname") String lastname);
    
    > 注意:如果是Collection(List、Set)类型或者是数组,也会做特殊处理,也是把传入的list或者数组封装在map中。key:Collection(collection)、如果是List 还可以使用 key(list),数组(array)

4. 参数处理源码分析
    mybatis使用ParamNameResolver 解析封装参数

    ```java
//构造方法
    public ParamNameResolver(Configuration config, Method method) {
        //获取参数列表中每个参数的类型
        final Class<?>[] paramTypes = method.getParameterTypes();
        //获取参数列表上的注解
        final Annotation[][] paramAnnotations = method.getParameterAnnotations();
        //该集合用于记录参数索引与参数名称的对应关系
        final SortedMap<Integer, String> map = new TreeMap<>();
        //注解的个数
        int paramCount = paramAnnotations.length;
        // get names from @Param annotations
        for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) { //遍历方法所有参数
            if (isSpecialParameter(paramTypes[paramIndex])) {
                //如果参数是RowBounds类型或ResultHandler类型,跳过对该参数的分析
                // skip special parameters
                continue;
            }
            String name = null;
            //遍历该参数对应的注解集合
            for (Annotation annotation : paramAnnotations[paramIndex]) {
                if (annotation instanceof Param) {
                    //@Param 注解出现过一次,就将hasParamAnnotation初始化为true
                    hasParamAnnotation = true;
                    //获取Param注解指定的参数名称
                    name = ((Param) annotation).value();
                    break;
                }
            }
            //这个if代码解释了上面的实例中names集合的valu为什么是0和1
            if (name == null) {
                // @Param was not specified.
                //该参数没有对应的@Param注解,则根据配置决定是否使用参数实际名作为其名称
                if (config.isUseActualParamName()) {
                    name = getActualParamName(method, paramIndex);
                }
                if (name == null) { //使用参数的索引作为名称
                    // use the parameter index as the name ("0", "1", ...)
                    // gcode issue #71
                    name = String.valueOf(map.size()); 
                }
            }
            map.put(paramIndex, name); //记录保存到map
        }
        names = Collections.unmodifiableSortedMap(map);
    }
```java
    //将实参与其对应名称进行关联
public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) { //无参数,返回null
        return null;
    } else if (!hasParamAnnotation && paramCount == 1) { //未使用@param注解,只有一个参数
        return args[names.firstKey()];
    } else {  //处理使用了@Param注解指定了参数名称或有多个参数的情况
        //param这个map中记录了参数名称与实参之间的对应关系
        final Map<String, Object> param = new ParamMap<>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            //将参数名与实参对应关系记录到param中
            param.put(entry.getValue(), args[entry.getKey()]);
            // add generic param names (param1, param2, ...)
            //为参数创建“param+索引”格式默认参数名称,如:param1,param2 等,并添加到param集合中
            final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
            // ensure not to overwrite parameter named with @Param
            if (!names.containsValue(genericParamName)) {
                param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
        }
        return param;
    }
}
5. ${ xx }  和 #{ xx  } 的区别:
    #{ xx } :是以预编译的形式,将参数设置到sql语句中。
    ${ xx } :取出的值直接拼装在sql语句中。会有安全问题,不能防止sql注入。
    大多数情况都应该使用 #{ xx } 取参数。原生JDBC不支持占位符的地方,可以使用${ xx } 取值。 

    ```sql
select * from ${year }_salary where xxx;
  1. Select 记录封装成 Map

    //多条记录封装成map,键是这条记录的主键,值是封装后的javabean
    //告诉mybatis封装map的时候,用哪个属性作为map的key
    @MapKey("id")
    Map<Integer, Employee> getEmpByLastNameReturnMap(String lastName);
    
    ```
	<select id="getEmpByLastNameReturnMap" resultType="cn.guet.bean.Employee">
		select * from tb_employee where last_name like #{lastName}
	</select>
  1. resultMap:自定义结果集映射规则

    <!-- resultMap:自定义结果集映射规则;
    id:唯一id,方便引用
    type:自定义规则的Java类型-->
    <resultMap id="myEmp" type="cn.guet.bean.Employee">
        <!--主键用id定义,column:指定那一列,property指定对应的JavaBean属性-->
        <id column="id" property="id"></id>
        <result column="last_name" property="lastName"></result>
        <!--其他不指定的,自动封装。我们只要写resultMap,全部映射规则都写上-->
        <result column="email" property="email"></result>
        <result column="gender" property="gender"></result>
    </resultMap>
    
    <select id="getEmpById" resultMap="myEmp">
    	select * from tb_employee  where id = #{id}
    </select>
    

### 四. Dynamic SQL

1. if

    ```xml
    <select id="findActiveBlogLike"
            resultType="Blog">
        SELECT * FROM BLOG WHERE state = ‘ACTIVE’
        <if test="title != null">
            AND title like #{title}
        </if>
        <if test="author != null and author.name != null">
            AND author_name like #{author.name}
        </if>
    </select>
  1. choose (when, otherwise)

    <select id="findActiveBlogLike"
            resultType="Blog">
        SELECT * FROM BLOG WHERE state = ‘ACTIVE’
        <choose>
            <when test="title != null">
                AND title like #{title}
            </when>
            <when test="author != null and author.name != null">
                AND author_name like #{author.name}
            </when>
            <otherwise>
                AND featured = 1
            </otherwise>
        </choose>
    </select>
    
3. trim (where, set)

    ```xml
    <select id="findActiveBlogLike"
            resultType="Blog">
        SELECT * FROM BLOG
        <where>
            <if test="state != null">
                state = #{state}
            </if>
            <if test="title != null">
                AND title like #{title}
            </if>
            <if test="author != null and author.name != null">
                AND author_name like #{author.name}
            </if>
        </where>
    </select>
  1. foreach

      <!--
        SELECT * FROM POST P WHERE ID in (1,2,3)
        collection:指定要遍历的集合
        list类型的参数会封装在map中,key就是list
        item:将遍历出来的值赋给指定变量
    -->
    <select id="selectPostIn" resultType="domain.blog.Post">
        SELECT *
        FROM POST P
        WHERE ID in
        <foreach item="item" index="index" collection="list"
                 open="(" separator="," close=")">
            #{item}
        </foreach>
    </select>
    

### 五. 缓存机制

1. MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。缓存可以极大的提升查询效率。

2. MyBatis系统中默认定义了两级缓存。一级缓存和二级缓存。
    + 默认情况下,只有一级缓存( SqlSession级别的缓存,也称为本地缓存)开启。
    + 二级缓存需要手动开启和配置,他是基于namespace级别的缓存。
    + 为了提高扩展性。 MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存

3. 一级缓存
    + 一级缓存(local cache), 即本地缓存, 作用域默认为sqlSession。当 Session flush 或 close 后, 该Session 中的所有 Cache 将被清空。
    + 本地缓存不能被关闭, 但可以调用 clearCache()来清空本地缓存, 或者改变缓存的作用域.。
    + 在mybatis3.1之后, 可以配置本地缓存的作用域.,在 mybatis.xml 中配置。
    + 同一次会话期间只要查询过的数据都会保存在当前SqlSession的一个Map中。key:hashCode+查询的SqlId+编写的sql查询语句+参数

4. 一级缓存失效的四种情况
    + 不同的SqlSession对应不同的一级缓存
    + 同一个SqlSession但是查询条件不同
    + 同一个SqlSession两次查询期间执行了任何一次增删改操作
    + 同一个SqlSession两次查询期间手动清空了缓存

5. 二级缓存:全局缓存,基于namespace级别的缓存,一个namespace对应一个二级缓存。
    
6. 二级缓存 工作机制:
    + 一个会话,查询一条数据,这个数据机会被放在当前会话的一级缓存中;
    + 如果会话关闭,一级缓存中的数据会被保存到二级缓存;新的会话查询信息,就可以参照二级缓存。
    + 不同namespace查出的数据会放在自己的缓存中(map)。

7. 二级缓存
    + 二级缓存(second level cache),全局作用域缓存
    + 二级缓存默认不开启,需要手动配置
    + MyBatis提供二级缓存的接口以及实现,缓存实现要求POJO实现Serializable接口
    + 二级缓存在 SqlSession 关闭或提交之后才会生效

8. 二级缓存的使用
    + 开启全局二级缓存的配置
    
    ```xml
	<settings>
		<setting name="cacheEnabled" value="true"/>
	</settings>
+ 去mapper.xml中配置使用二级缓存

```xml
<!--
    eviction:缓存的回收策略
    flushInterval:缓存刷新间隔,缓存多长时间清空一次,设置一个毫秒值
    readOnly:是否只读。true:只读,mybatis默认只读,不会修改。非只读:不给引用,用反序列化技术克隆给你
    size:缓存中保存多少个
    type:指定自定义缓存的全类名
-->
<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1"></cache>

    + POJO实现反序列化接口

9. 缓存原理图
    ![](https://img2018.cnblogs.com/blog/1320041/201909/1320041-20190923201602319-364466595.png)

### 六. mybatis-spring

略

### 七. mybatis工作原理

1. 获取sqlSessionFactory对象:
    解析文件的每一个信息保存在Configuration中,返回包含Configuration的DefaultSqlSession;
    注意:【MappedStatement】:代表一个增删改查的详细信息
    ![](https://img2018.cnblogs.com/blog/1320041/201909/1320041-20190924194817506-971604217.png)
    ![](https://img2018.cnblogs.com/blog/1320041/201909/1320041-20190924194901850-1908106780.png)
    ![](https://img2018.cnblogs.com/blog/1320041/201909/1320041-20190924194930914-1336251386.png)
    ![](https://img2018.cnblogs.com/blog/1320041/201909/1320041-20190924195013173-557725219.png)
    ![](https://img2018.cnblogs.com/blog/1320041/201909/1320041-20190924195038990-1598787763.png)


2. 获取sqlSession对象
    返回一个DefaultSQlSession对象,包含Executor和Configuration;
    这一步会创建Executor对象;
    ![](https://img2018.cnblogs.com/blog/1320041/201909/1320041-20190924200556025-389941342.png)


3. 获取接口的代理对象(MapperProxy)
    getMapper,使用MapperProxyFactory创建一个MapperProxy的代理对象
    代理对象里面包含了,DefaultSqlSession(Executor)
    ![](https://img2018.cnblogs.com/blog/1320041/201909/1320041-20190924212119475-1136847373.png)


4. 执行增删改查方法
    ![](https://img2018.cnblogs.com/blog/1320041/201910/1320041-20191002143414937-1343414254.png)

    ![](https://img2018.cnblogs.com/blog/1320041/201910/1320041-20191002143919107-1675785992.png)

5. 总结:
    + 根据配置文件(全局,sql映射)初始化出Configuration对象
    + 创建一个DefaultSqlSession对象,
        他里面包含Configuration以及
        Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor)
    + DefaultSqlSession.getMapper():拿到Mapper接口对应的MapperProxy;
    + MapperProxy里面有(DefaultSqlSession);
    + 执行增删改查方法:
        调用DefaultSqlSession的增删改查(Executor);
        会创建一个StatementHandler对象。(同时也会创建出ParameterHandler和ResultSetHandler)
        调用StatementHandler预编译参数以及设置参数值;。使用ParameterHandler来给sql设置参数
        调用StatementHandler的增删改查方法;
    + ResultSetHandler封装结果

    > 四大对象每个创建的时候都有一个interceptorChain.pluginAll(parameterHandler);
posted @ 2019-09-17 10:09  HappyFish101  阅读(193)  评论(0编辑  收藏  举报