Mybatis 框架

1,JDBC

全称:java database connectivity,简称jdbc, 翻译就是 Java 数据库连接,就是 java 用来操作数据库的。

JDBC 使用

  1. 导入 jar 包
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.33</version>
</dependency>
  1. 测试类
//1, 注册驱动
Class.forName("com.mysql.jdbc.Driver");

//2, 获取连接
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "1234");

//3, 定义 sql 语句
String sql = "select * from test";

//4,获取执行 sql 对象
Statement statement = con.createStatement();

//5,执行 sql
ResultSet resultSet = statement.executeQuery(sql);

while( resultSet.next() ){
    System.out.print("id:" + resultSet.getInt("id"));
    System.out.print("name:" + resultSet.getString("name"));
    System.out.print("type:" + resultSet.getString("type"));
    System.out.println();
}
  1. 预编译版
//1, 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");

//2, 获取连接
Connection con = DriverManager.getConnection(
        "jdbc:mysql://localhost:3307/auth?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowMultiQueries=true&allowPublicKeyRetrieval=true",
        "root",
        "1234");

//3, 定义 sql 语句
String sql = "select * from pro1 where e_id = ?";

//4,获取执行 sql 对象
PreparedStatement preparedStatement = con.prepareStatement(sql);
preparedStatement.setInt(1, 1);

//5,执行 sql
preparedStatement.execute();
ResultSet resultSet = preparedStatement.getResultSet();

//6,解析结果
while( resultSet.next() ){
    System.out.print("id:" + resultSet.getString("name"));
    System.out.print("name:" + resultSet.getString("value"));
    System.out.print("type:" + resultSet.getString("e_id"));
    System.out.println();
}

2,mybatis 使用

  1. 导入 jar 包
<!-- jdbc -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.33</version>
</dependency>

<!-- mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.6</version>
</dependency>
  1. 配置文件

jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=1234

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!--
        mybatis配置文件中一定要按照顺序来进行配置
        properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,
        objectWrapperFactory?,reflectorFactory?,plugins?,environments?,
        databaseIdProvider?,mappers?)".
    -->

    <!--引入配置文件-->
    <properties resource="jdbc.properties"/>

    <settings>
        <!--打印日志-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>

        <!--二级缓存-->
        <setting name="cacheEnabled" value="true"/>

        <!---->
        <!--<setting name="localCacheScope" value="STATEMENT"/>-->
    </settings>

    <typeAliases>
        <!--进行书写别名
            类型别名他不进行区分大小写 ,如果alias不进行设置就会拥有一个默认的别名,默认的就是当前的类名
            typealias: 设置某个类型的别名
            属性:
                type:  设置需要设置别名的类型
                alias: 设置某个类型的别名,若不设置该属性,就会拥有一个默认的别名,默认的别名就是当前的类名。
        -->
        <!--<typeAlias type="com.fangshaolei.mybatis.pojo.User"  alias="user"/>-->
        <package name="com.demo.mybatis.entity"/> <!--通过以包为单位来进行设置-->
    </typeAliases>


    <plugins>
        <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
    </plugins>

    <!--
        environments: 配置多个连接数据的环境
        属性:
            default: 设置默认使用的环境id
    -->
    <environments default="development">
        <!--开发环境
            environment: 用来配置某个具体的环境
            属性:
                id: 表示连接数据库环境的唯一标识,不能重复
                属性:
                    type: = "JDBC | MANAGED"
                    JDBC: 表示当前环境中,执行SQL时,使用的是JDBC中原生的事务管理方式, 事务的提交和回滚需要手动来进行处理
                    MANAGED: 被管理,例如spring
        -->
        <environment id="development">
            <!--事务管理器通过jdbc来进行管理-->
            <transactionManager type="JDBC"/>
            <!--数据源,从数据连接池中进行取用
                datasource: 配置数据源
                属性:
                    type: 设置数据源类型
                    type = "POOLED | UNPOOLED | JNDI"
                    POOLED: 表示使用数据库连接池缓存数据库连接
                    UNPOOLED: 表示不使用数据库中的连接词
                    JNDI: 表示使用上下文中的数据源连接池
            -->
            <dataSource type="POOLED">
                <!--设施连接数据库的驱动-->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <!--设置连接地址-->
                <property name="url" value="jdbc:mysql://localhost:3306/test"/>
                <!--用户名-->
                <property name="username" value="root"/>
                <!--密码-->
                <property name="password" value="1234"/>
            </dataSource>
        </environment>
        <!--测试-->
        <environment id="test">
            <!--事务管理器通过jdbc来进行管理-->
            <transactionManager type="JDBC"/>
            <!--数据源,从数据连接池中进行取用 -->
            <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>
    <!--映入配置文件-->
    <mappers>
         <mapper resource="mapper/TestMapper.xml"/>
        <!--
            以包围单位引入映射文件
            要求:
            1.mapper接口所在的包要和映射文件所在的包一致
            2. mapper接口要和映射文件的名字是一致的
        -->
<!--        <package name="mapper"/>-->
    </mappers>
</configuration>

  1. mapper 接口
public interface TestMapper {
    List<Test> findList();
}
  1. mapper.xml 配置文件(非必要)

文件名要与 mapper 接口名保持一致。namespace = 接口全限定名。
当然也可以用注解在 mapper 头上写 sql。

<?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="com.demo.mybatis.mapper.TestMapper">
    <cache type="org.mybatis.caches.redis.RedisCache"/>

    <select id="findList" resultType="com.demo.mybatis.entity.Test" useCache="true">
        select * from test
    </select>
</mapper>
  1. 测试类
public static void baseMybatisTest() throws IOException {
    //1,读取 mybatis-config.xml 文件
    InputStream stream = Resources.getResourceAsStream("mybatis-config.xml");

    //2,构件 sqlSessionFactory
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(stream);

    //3,打开 sqlSession
    SqlSession sqlSession = factory.openSession(false);

    //4,获取 Mapper 接口对象
    TestMapper mapper = sqlSession.getMapper(TestMapper.class);

    //5,获取 mapper 接口对象的方法操作数据库
    List<Test> list = mapper.findList();

    System.out.println(list);
}

3,mybatis 缓存

3.1,一级缓存

获取数据顺序:二级缓存 > 一级缓存 > 数据库

1,一级缓存简介

一级缓存又称本地缓存,作用域是 sqlSession,同一个 sqlSession 执行相同 sql,第一次会查询数据库并且写到缓存中,第二次从一级缓存中获取。一级缓存是基于 PerpetualCache 的 HashMap 本地缓存,默认打开一级缓存

2,清空一级缓存

当 sqlSession 执行 commit 操作(执行增删改),就会清空一级缓存。

3,一级缓存实例

public static void firstCache() throws IOException {
    //1,读取 mybatis-config.xml 文件
    InputStream stream = Resources.getResourceAsStream("mybatis-config.xml");

    //2,构件 sqlSessionFactory
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(stream);

    //3,打开 sqlSession
    SqlSession sqlSession = factory.openSession();

    //4,获取 Mapper 接口对象
    TestMapper mapper = sqlSession.getMapper(TestMapper.class);
    TestMapper mapper1 = sqlSession.getMapper(TestMapper.class);

    //5,获取 mapper 接口对象的方法操作数据库
    List<Test> list = mapper.findList();
    List<Test> list1 = mapper1.findList();
}

只会执行一次 sql 语句。

4,关闭或者让一级缓存不起作用

  1. 在 mapper 的 select 标签中设置 statementType=STATEMENT

  2. 在 mapper 的 select 标签中设置 flushCache=“true”,直接刷新 cache

  3. 全局设置 localCacheScope = STATEMENT

  //SESSION: 当前会话数据保存在会话缓存中,使用一级缓存。
  //STATEMENT:可以禁用一级缓存。
  <setting name="localCacheScope" value="STATEMENT"/>

3.2,二级缓存

Mybatis 二级缓存是mapper级别的 需要手动开启,他的作用范围更广也就是mapper文件的一个命名空间(namespace)中。

1,二级缓存需要手动开启

  1. 全局配置文件中开启
<setting name="cacheEnabled" value="true"/>
  1. 在 mapper.xml 文件中加入下边配置 或者 使用注解SQL开发的话使用 @CacheNamespace 来开启
<cache/>
或者
<cache-ref namespace="com.xxx.xxx.mapper.UserMapper"/> 
  1. 查询返回结果集实体类需要实现序列化接口

  2. 当第一次查询事务提交后,才会真正存取二级缓存中。

  3. 如果某个 sql 不想走缓存的话 需要在像如下操作 useCache=false

<select id="findLogs" resultType="map" useCache="false">  

2,清除二级缓存

  1. 执行增删改后会清除二级缓存。

  2. 二级缓存设置的时间过期

  3. mapper 的 select 标签下,useCache = “false” 不走缓存。

3.3,三级缓存

一二级缓存都是本地缓存,在分布式系统上不好使。因此我们需要三级缓存,即分布式缓存。自定义缓存对象,需要实现 org.apache.ibatis.cache.Cache 接口。这里以 redis 为例。

1,三级缓存使用

  1. 导入 mybatis 集成 redis jar 包。
<dependency>
            <groupId>org.mybatis.caches</groupId>
            <artifactId>mybatis-redis</artifactId>
            <version>1.0.0-beta2</version>
        </dependency>

这个包中有一个实现类 org.mybatis.caches.redis.RedisCache 实现了 Cache 接口。当然也可以自定义实现 Cache 接口。

2,redis 配置文件。

redis.properties

redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0

3,在 mapper.xml 文件中使用自定义配置。

<cache type="org.mybatis.caches.redis.RedisCache"/>

其实是二级缓存的升级,将本地缓存放到单独的 redis 服务中。

4. mybatis 分页插件

  1. 导入 jar 包
<dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper</artifactId>
            <version>5.1.11</version>
        </dependency>
  1. 测试类
/**
 * 分页插件
 * */
public static void pageMybatisTest() throws IOException {
    //1,读取 mybatis-config.xml 文件
    InputStream stream = Resources.getResourceAsStream("mybatis-config.xml");

    //2,构件 sqlSessionFactory
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(stream);

    //3,打开 sqlSession
    SqlSession sqlSession = factory.openSession();

    //4,获取 Mapper 接口对象
    TestMapper mapper = sqlSession.getMapper(TestMapper.class);

    //5,设置分页
    PageHelper.startPage(2,2);

    //6,获取 mapper 接口对象的方法操作数据库
    List<Test> list = mapper.findList();

    //7,封装到 pageInfo 中
    PageInfo<Test> pageInfo = new PageInfo<Test>(list);


    System.out.println(list);
}

5,#{} 和 ${} 的区别

  “#{}”:占位符号,变量的替换是在 DBMS 中,预编译。
  “${}”:sql 拼接,变量的替换阶段是在动态 SQL 解析阶段,表名或者字段名这种必须使用 $符号。

sql 预编译

  指的是数据库驱动在发送 sql 语句和参数给 DBMS 之前对 sql 语句进行编译,这样 DBMS 执行 sql 时,就不需要重新编译。JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。
  预编译阶段可以优化 sql 的执行。预编译之后的 sql 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的sql,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。预编译语句对象可以重复利用。把一个 sql 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个sql,可以直接使用这个缓存的 PreparedState 对象。mybatis 默认情况下,将对所有的 sql 进行预编译。
  防范SQL注入的原理是在SQL参数未注入之前,提前对SQL语句进行预编译,而其后注入的参数将不会再进行SQL编译。也就是说其后注入进来的参数系统将不会认为它会是一条SQL语句,而默认其是一条一个参数。

6,标签和注解

标签
注解

7,模糊匹配

(1)like "%{param}%"

(2)like concat('%', #{param}, '%') (mysql,其他 sql 自己的方法)

(3)bind 标签
name 定义一个 bind 标签的名字,value 定义参数值

<select id="like$Test" resultType="com.demo.mybatis.entity.Test">
    <bind name="typePartten" value="'%' + type + '%'"></bind>
    <bind name="typePartten1" value="'%' + test.type + '%'"></bind>

        select * from test
        where
            type like '%${type}%'
            and type like "%${type}%"
            and type like concat('%',#{type},'%')
            and type like #{typePartten}
            and type like #{typePartten1}
</select>

8,Mybatis 的 dao 是如何工作的?

  Dao 接口即 Mapper 接口。接口的全限名,就是映射文件中的 namespace 的值;接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;接口方法内的参数,就是传递给 sql 的参数。
  Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个 <select><insert><update><delete>标签,都会被解析为一个MapperStatement 对象。
  举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到 namespace 为com.mybatis3.mappers.StudentDao 下面 id 为findStudentById 的 MapperStatement。
  Mapper 接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。

9,Mybatis 返回主键

(1)insert 标签中 useGeneratedKeys = true

<insert id="autoincrement$Test" parameterType="com.demo.mybatis.entity.Test" useGeneratedKeys="true" keyProperty="id">
   insert into test(name, type) value (#{name}, #{type})
</insert>

(2)insert 标签中添加 selectKey

<insert id="autoincrement$Test2">
   <selectKey keyProperty="id" order="AFTER" resultType="int">
        select LAST_INSERT_ID()
   </selectKey>
   insert into test(name, type) value (#{name}, #{type})
</insert>
//如果在DAO层使用@Param注解传递参数,则 keyProperty 属性 需要通过 “注解”+“主键id” 的格式,否则无法返回主键。
//如果在DAO层只有单个参数传递(不需要使用@Param注解穿传递参数),则 keyProperty 属性可以直接 = “主键id” 来返回主键。

这里有一个坑:这个返回主键,似乎是 mybatis 先查询当前表中自增 id,然后一条一条累计的。如果使用了 insert ignore 这种忽略自增索引异常的情况,会导致数据错乱。

10,传递多个参数

1,顺序传参

  //方法
  public User selectUser(String name, int deptId);
 
  //xml
  <select id="selectUser" resultMap="UserResultMap">
      select * from user
      where user_name = #{0} and dept_id = #{1}
  </select>

2,@Param 传参

  public User selectUser(@Param("userName") String name, int @Param("deptId") deptId);
 
  <select id="selectUser" resultMap="UserResultMap">
      select * from user
      where user_name = #{userName} and dept_id = #{deptId}
  </select>

3,Map 传参

  public User selectUser(Map<String, Object> params);
 
  <select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap">
      select * from user
      where user_name = #{userName} and dept_id = #{deptId}
  </select>

4,对象传参

  public User selectUser(User params);
 
  <select id="selectUser" parameterType="com.test.User" resultMap="UserResultMap">
      select * from user
      where user_name = #{userName} and dept_id = #{deptId}
  </select>

11,动态 SQL

Mybatis的 动态 SQL 是指在进行 SQL 操作的时候,传入的参数对象或者参数值,根据匹配的条件,有可能需要动态的去判断是否为空,循环,拼接等情况。

 <if/>
 <choose/>、<when/>、<otherwise/>
 <trim/>、<where/>、<set/>
 <foreach/>
 <bind/>

动态SQL

12,半自动映射框架和全自动映射框架

  • Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。
  • 而 Mybatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。

13,一对一,一对多

  • 方式一:sqlMapper配置文件
    一对一:在resultMap标签中使用 association 标签
    一对多:在resultMap 标签中使用collection 标签
//使用一个 resultMap
<resultMap id="workflowColorMap" type="com.bdip.domain.vo.WorkflowColorVo">
    <result property="requestId" column="requestId"></result>
    <result property="requestName" column="requestName"></result>
    <result property="enable" column="enable"></result>
    <collection property="nodeList" ofType="com.bdip.domain.vo.WorkflowColorVo$WorkflowNodeColorVo">
        <result property="nodeId" column="nodeId"></result>
        <result property="nodeName" column="nodeName"></result>
        <result property="color" column="color"></result>
    </collection>
</resultMap>
  • 方式二:注解
    一对一:在@Results 注解中的@Result注解中使用@One注解
    一对多:在@Results 注解中的@Result 注解中使用@Many注解

14,mybatis 延迟加载

只支持 association 关联对象和 collection 关联集合对象的延迟加载

//两种配置方式
<settings>
     <!--开启延迟加载-->
     <!--(1)此方式为:设置所有的分解式(N+1)查询都为延迟加载,注意:这种方式很少使用,因为不可能为所以分解式查询都设置为延迟加载-->
     <setting name="lazyLoadingEnabled" value="true"/>
     <!--(2)此方式为:映射文件中<collection>或<association>中设置了fetchType的方法触发延迟加载,设置为空字符串即可。-->
     <setting name="lazyLoadTriggerMethods" value=""/>
</settings>

15,Mybatis 接口绑定

myabtis 中任意定义接口,然后把接口方法和 SQL 语句绑定,就可以直接调用接口方法。

注解和配置文件两种方式

(1)配置文件绑定
   配置文件与接口同名,nameSpace = 接口全限定名
(2)注解绑定
   接口名上直接用 @Select 等绑定 sql

接口绑定要求:

  • 1、Mapper 接口方法名和 mapper.xml 中定义的每个 sql 的 id 相同
  • 2、Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的 parameterType 的类型相同
  • 3、Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的 resultType 的类型相同
  • 4、Mapper.xml 文件中的 namespace 即是 mapper 接口的类路径

16,Mybatis 四大内置对象

四大内置对象:

ParameterHandler:处理SQL的参数
ResultSetHandler:处理SQL的返回结果集
StatementHandler:数据库的处理对象,用于执行SQL语句。
Executor:MyBatis的执行器,用于执行增删改查操作,执行器负责整个SQL执行过程的总体控制。

17,四大组件

1. 四大组件简介

四大核心组件:

  • SqlSessionFactoryBuiler:用来创建 SqlSessionFactory,创建完成之后就不会用到它了,所以 SqlSessionFactoryBuiler 生命周期极短。
  • SqlSessionFactory:主要是用来生成SqlSession对象,而SqlSession对象是需要不断被创建的,所以SqlSessionFactory是全局都存在的,也没有必要重复创建,所以这是一个单例对象。
  • SqlSession:是用来操作xml文件中我们写好的sql语句,每次操作数据库我们都需要一个SqlSession对象,SqlSession是用来和数据库中的事务进行对接的,所以SqlSession里面是包含了事务隔离级别等信息的。
  • Mapper:是一个接口,没有任何实现类。主要作用就是用来映射Sql语句的接口,映射器的接口实例从SqlSession对象中获取。

生命周期:

对象 生命周期
SqlSessionFactoryBuiler 方法局部(Method)使用完成即可被丢弃
SqlSessionFactory 应用级别(Application),全局存在,是一个单例对象
SqlSession 请求或方法(Request/Method)
Mapper 方法(Method)

mybatis 完成一次操作:

  • 1、加载配置文件
  • 2、获取SqlSessionFactoryBuiler对象
  • 3、通过SqlSessionFactoryBuiler和配置文件流来获取SqlSessionFactory对象
  • 4、利用SqlSessionFactory对象来打开一个SqlSession
  • 5、通过SqlSession来获得对应的Mapper对象
  • 6、通过Mapper对象调用对应接口来查询数据库

2. SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由对它进行清除或重建。使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏味道(bad smell)”。因此 SqlSessionFactory 的最佳范围是应用范围。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

1. DefaultSqlSessionFactory

提供了 SqlSessionFactory 默认实现,一般用的就是这个,非线程安全。

2. SqlSessionManager

实现 SqlSessionFactory 的同时,实现了 SqlSession,并且封装了一个 SqlSessionFactory。

SqlSessionManager 实现了Session接口。意味着,SqlSessionManager集成了 sqlSessionFactory和session 的功能。通过SqlSessionManager,开发者可以不在理会SqlSessionFacotry的存在,直接面向Session编程。

SqlSessionManager 内部提供了一个sqlSessionProxy,这个sqlSessionProxy提供了所有Session接口的实现,而实现中正是使用了上面提到的本地线程保存的session实例。

这样,在同一个线程实现不同的sql操作,可以复用本地线程session,避免了DefaultSqlSessionFactory实现的每一个sql操作都要创建新的session实例。

18,实体类和数据库数据类型的转换

resultMap 中属性:

  • javaType 用来指定对象所属的java数据类型,也就是private Listposts 的ArrayList类型
  • ofType用来指定对象的所属javaBean类,也就是尖括号的泛型private Listposts
  • typeHandler 数据库类型和java类型的转换

example:将 sql 中字符串转换为 Integer[]

  • resultMap:
<resultMap id="workflowNodeColorMap" type="com.bdip.domain.vo.WorkflowColorVo$WorkflowNodeColorVo">
    <result column="nodeName" property="nodeName"></result>
    <result column="color" property="color"></result>
    <result column="nodeId" property="nodeId" typeHandler="com.bdip.hander.StringToArrayHandler"></result>
</resultMap>
  • sql:
<select id="findWorkflowNodeColor" resultMap="workflowNodeColorMap">
    select group_concat(node_id) nodeId, `name` nodeName, color
      from ecology_workflow_color_node
     where workflow_color_id = #{workflowColorId}
      group by `name`
</select>
  • StringToArrayHandler:
package com.bdip.hander;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;

import java.sql.*;

/**
 * @author 实体类 Integer[] 与数据库中 varchar 的转换
 */
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(Integer[].class)
public class StringToArrayHandler extends BaseTypeHandler<Integer[]> {

    //将 java 中的数据类型转化为数据库需要的类型
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, Integer[] integers, JdbcType jdbcType) throws SQLException {

    }

    //将 数据库中的数据类型转换为 java 需要的数据类型
    @Override
    public Integer[] getNullableResult(ResultSet resultSet, String s) throws SQLException {
        String[] item = resultSet.getString(s).split(",");
        Integer[] integers = new Integer[item.length];
        for(int i = 0; i < integers.length;i++){
            integers[i] = Integer.parseInt(item[i]);
        }
        return integers;
    }

    @Override
    public Integer[] getNullableResult(ResultSet resultSet, int i) throws SQLException {
        return new Integer[0];
    }

    @Override
    public Integer[] getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
        return new Integer[0];
    }
}
  • 实体类 WorkflowNodeColorVo:
@Data
public static class WorkflowNodeColorVo{

    //workflow_flownode.nodeId 节点 id
    private Integer[] nodeId;

    private String nodeName;

    private String color;
}

19. MyBatis 事务管理

1. MyBatis 两种事务管理策略

1. 使用JDBC的事务管理机制。

这种机制就是利用java.sql.Connection对象完成对事务的提交

2. 使用MANAGED的事务管理机制。

这种机制mybatis自身不会去实现事务管理,而是让程序的Web容器或者Spring容器来实现对事务的管理。

2. Transaction 接口

事务接口定义在org.apache.ibatis.transaction.Transaction。

public interface Transaction {

  /**
   * Retrieve inner database connection.
   * @return DataBase connection
   * @throws SQLException
   *           the SQL exception
   */
  Connection getConnection() throws SQLException;

  /**
   * Commit inner database connection.
   * @throws SQLException
   *           the SQL exception
   */
  void commit() throws SQLException;

  /**
   * Rollback inner database connection.
   * @throws SQLException
   *           the SQL exception
   */
  void rollback() throws SQLException;

  /**
   * Close inner database connection.
   * @throws SQLException
   *           the SQL exception
   */
  void close() throws SQLException;
}

Mybatis 提供两个实现类,JdbcTransactionManagedTransaction 对应两种策略。在配置文件中选其一:

Mybatis 根据配置创建对应的 TransactionFactory 事务工厂用来创建 Transaction 实例对象。

20. 拦截器

1. Interceptor 接口

自定义拦截器需要实现 Interceptor 接口,并添加拦截注解 @Intercepts。

public interface Interceptor {
  
  /**
   * 这个方法很好理解
   * 作用只有一个:我们不是拦截方法吗,拦截之后我们要做什么事情呢?
   *      这个方法里面就是我们要做的事情
   *
   * 解释这个方法前,我们一定要理解方法参数 {@link Invocation} 是个什么鬼?
   * 1 我们知道,mybatis拦截器默认只能拦截四种类型 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler
   * 2 不管是哪种代理,代理的目标对象就是我们要拦截对象,举例说明:
   *      比如我们要拦截 {@link Executor#update(MappedStatement ms, Object parameter)} 方法,
   *      那么 Invocation 就是这个对象,Invocation 里面有三个参数 target method args
   *          target 就是 Executor
   *          method 就是 update
   *          args   就是 MappedStatement ms, Object parameter
   *
   *   如果还是不能理解,我再举一个需求案例:看下面方法代码里面的需求
   *
   *  该方法在运行时调用
   */
  Object intercept(Invocation invocation) throws Throwable;
  
  /**
   * 这个方法也很好理解
   * 作用就只有一个:那就是Mybatis在创建拦截器代理时候会判断一次,当前这个类 MyInterceptor 到底需不需要生成一个代理进行拦截,
   * 如果需要拦截,就生成一个代理对象,这个代理就是一个 {@link Plugin},它实现了jdk的动态代理接口 {@link InvocationHandler},
   * 如果不需要代理,则直接返回目标对象本身
   *
   * Mybatis为什么会判断一次是否需要代理呢?
   * 默认情况下,Mybatis只能拦截四种类型的接口:Executor、StatementHandler、ParameterHandler 和 ResultSetHandler
   * 通过 {@link Intercepts} 和 {@link Signature} 两个注解共同完成
   * 试想一下,如果我们开发人员在自定义拦截器上没有指明类型,或者随便写一个拦截点,比如Object,那Mybatis疯了,难道所有对象都去拦截
   * 所以Mybatis会做一次判断,拦截点看看是不是这四个接口里面的方法,不是则不拦截,直接返回目标对象,如果是则需要生成一个代理
   *
   *  该方法在 mybatis 加载核心配置文件时被调用
   */
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  
  /**
   * 这个方法最好理解,如果我们拦截器需要用到一些变量参数,而且这个参数是支持可配置的,
   *  类似Spring中的@Value("${}")从application.properties文件获取
   * 这个时候我们就可以使用这个方法
   *
   * 如何使用?
   * 只需要在 mybatis 配置文件中加入类似如下配置,然后 {@link Interceptor#setProperties(Properties)} 就可以获取参数
   *      <plugin interceptor="liu.york.mybatis.study.plugin.MyInterceptor">
   *           <property name="username" value="LiuYork"/>
   *           <property name="password" value="123456"/>
   *      </plugin>
   *      方法中获取参数:properties.getProperty("username");
   *
   * 问题:为什么要存在这个方法呢,比如直接使用 @Value("${}") 获取不就得了?
   * 原因是 mybatis 框架本身就是一个可以独立使用的框架,没有像 Spring 这种做了很多依赖注入的功能
   *
   *  该方法在 mybatis 加载核心配置文件时被调用,在 SpringBoot 环境下是不生效的,反正也用不着,不管
   */
  default void setProperties(Properties properties) {
    // NOP
  }

}

2. Invocation 类

简单理解成一个方法的封装

public class Invocation {
  
  //当前对象
  private final Object target;
  //方法
  private final Method method;
  //方法参数
  private final Object[] args;

  public Invocation(Object target, Method method, Object[] args) {
    this.target = target;
    this.method = method;
    this.args = args;
  }

  public Object getTarget() {
    return target;
  }

  public Method getMethod() {
    return method;
  }

  public Object[] getArgs() {
    return args;
  }
    
  //通过反射来执行方法
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }
}

3. Plugin 代理类

//Plugin 类其实就是一个代理类,因为它实现了jdk动态代理接口 InvocationHandler
public class Plugin implements InvocationHandler {
   
  //如果看懂了代码案例1的例子,那么这个方法很理解,这个方法就是 mybatis 提供给开发人员使用的一个工具类方法,
  //目的就是帮助开发人员省略掉 反射解析注解 Intercepts 和 Signature,有兴趣的可以去看看源码 Plugin#getSignatureMap 方法
  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }
  
  //这个方法就是根据 wrap 方法的解析结果,判断当前拦截器是否需要进行拦截,
  //如果需要拦截:将 目标对象+目标方法+目标参数 封装成一个 Invocation 对象,给我们自定义的拦截器 MyInterceptor 的 intercept 方法
  //这个时候就刚好对应上了上面案例1中对 intercept 方法的解释了,它就是我们要处理自己逻辑的方法,
  //处理好了之后是否需要调用目标对象的方法,比如上面说的 打印了sql语句,是否还要查询数据库呢?答案是肯定的
  //如果不需要拦截:则直接调用目标对象的方法
  //比如直接调用 Executor 的 update 方法进行更新数据库
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
}

4. @Intercepts 注解

  1. 标识我们的类,是一个拦截器。

  2. Signature 署名:则是指明我们的拦截器需要拦截哪一个接口的哪一个方法

    1. type 对应四类接口中的某一个,比如是 Executor
    2. method 对应接口中的哪类方法,比如 Executor 的 update 方法
    3. args 对应接口中的哪一个方法,比如 Executor 中 query 因为重载原因,方法有多个,args 就是指明参数类型,从而确定是哪一个方法
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  /**
   * Returns method signatures to intercept.
   *
   * @return method signatures
   */
  Signature[] value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  /**
   * Returns the java type.
   *
   * @return the java type
   */
  Class<?> type();

  /**
   * Returns the method name.
   *
   * @return the method name
   */
  String method();

  /**
   * Returns java types for method argument.
   * @return java types for method argument
   */
  Class<?>[] args();
}

5. 实例

自定义一个拦截器,拦截 ResultSetHandler 对象参数类型为 {Statement.class} 的 handelResultSets 方法

@Component
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
public class ResultSetHandlerInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("ResultSetHandlerInterceptor-invocation---前置");
        Object proceed = invocation.proceed();
        System.out.println("ResultSetHandlerInterceptor-invocation---后置");
        return proceed;
    }

    @Override
    public Object plugin(Object target) {
        System.out.println("ResultSetHandlerInterceptor-plugin---target:" + target.getClass());
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        System.out.println("ResultSetHandlerInterceptor-setProperties---");
    }
}

参考文献

缓存相关设置
深入浅出 MyBatis 的一级、二级缓存机制
MyBatis 分析
MyBatis 官方文档

posted @ 2024-02-19 10:42  primaryC  阅读(5)  评论(0编辑  收藏  举报