Mybatis 框架
1,JDBC
全称:java database connectivity,简称jdbc, 翻译就是 Java 数据库连接,就是 java 用来操作数据库的。
JDBC 使用
- 导入 jar 包
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.33</version> </dependency>
- 测试类
//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, 注册驱动 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 使用
- 导入 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>
- 配置文件
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>
- mapper 接口
public interface TestMapper { List<Test> findList(); }
- 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>
- 测试类
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,关闭或者让一级缓存不起作用
-
在 mapper 的 select 标签中设置 statementType=STATEMENT
-
在 mapper 的 select 标签中设置 flushCache=“true”,直接刷新 cache
-
全局设置 localCacheScope = STATEMENT
//SESSION: 当前会话数据保存在会话缓存中,使用一级缓存。 //STATEMENT:可以禁用一级缓存。 <setting name="localCacheScope" value="STATEMENT"/>
3.2,二级缓存
Mybatis 二级缓存是mapper级别的 需要手动开启,他的作用范围更广也就是mapper文件的一个命名空间(namespace)中。
1,二级缓存需要手动开启
- 全局配置文件中开启
<setting name="cacheEnabled" value="true"/>
- 在 mapper.xml 文件中加入下边配置 或者 使用注解SQL开发的话使用 @CacheNamespace 来开启
<cache/> 或者 <cache-ref namespace="com.xxx.xxx.mapper.UserMapper"/>
-
查询返回结果集实体类需要实现序列化接口
-
当第一次查询事务提交后,才会真正存取二级缓存中。
-
如果某个 sql 不想走缓存的话 需要在像如下操作 useCache=false
<select id="findLogs" resultType="map" useCache="false">
2,清除二级缓存
-
执行增删改后会清除二级缓存。
-
二级缓存设置的时间过期
-
mapper 的 select 标签下,useCache = “false” 不走缓存。
3.3,三级缓存
一二级缓存都是本地缓存,在分布式系统上不好使。因此我们需要三级缓存,即分布式缓存。自定义缓存对象,需要实现 org.apache.ibatis.cache.Cache 接口。这里以 redis 为例。
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 分页插件
- 导入 jar 包
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.11</version> </dependency>
- 测试类
/** * 分页插件 * */ 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/>
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 List
posts 的ArrayList类型 - ofType用来指定对象的所属javaBean类,也就是尖括号的泛型private List
posts - 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 提供两个实现类,JdbcTransaction
和 ManagedTransaction
对应两种策略。在配置文件中选其一:
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 注解
-
标识我们的类,是一个拦截器。
-
Signature 署名:则是指明我们的拦截器需要拦截哪一个接口的哪一个方法
- type 对应四类接口中的某一个,比如是 Executor
- method 对应接口中的哪类方法,比如 Executor 的 update 方法
- 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---"); } }
参考文献
本文作者:primaryC
本文链接:https://www.cnblogs.com/cnff/p/17073997.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步