简介
-
开源免费,原名叫iBatis
-
是一种数据访问层框架,底层是对JDBC的封装。
配置
-
导入Jar包,下载地址:Github发布页
-
配置XML文件
-
创建XML文件,没有名称和位置要求,一般配置在src文件目录下。如果使用maven管理项目,一般放在resources文件夹下
-
需要引入DTD文件或者Schema典型的配置文件如下所示
<?xml version="1.0" encoding="UTF-8" ?> <!-- 引入远程dtd文件 --> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 数据库环境配置,可以指定一个默认环境 --> <environments default="mysql"> <!-- 这里可以配置多个数据库环境 --> <environment id="mysql"> <!-- 使用 JDBC 进行数据库事务管理 --> <transactionManager type="JDBC"></transactionManager> <!-- 使用MyBatis自带的数据库连接池 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/ssm"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <mapper resource="com/joey/mapper/FlowerMapper.xml"></mapper> </mappers> </configuration>
-
-
使用MyBatis时,不需要编写实现类,只需要写需要执行的SQL命令。新建一个mapper包,包下存放一些包含了 SQL 命令的 XML 文件,这些文件的作用就是替代『数据访问层的实现类』。
典型的 Mapper XML 文件如下:
<?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"> <!-- namespace指定了此实现类的完整名称:包名 + 类名 --> <mapper namespace="com.joey.mapper.FlowerMapper"> <!-- select标签可以看作是定义了此实现类的方法,resultType则定义的是返回值类型 --> <select id="selAll" resultType="com.joey.pojo.Flower"> select * from flower </select> <select id="selByName" resultType="com.joey.pojo.Flower"> select * from flower where name = "玫瑰" </select> <select id="countAll" resultType="int"> select count(*) from flower </select> </mapper>
-
测试MyBatis
package com.joey.test; import com.joey.pojo.Flower; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; import java.util.List; import java.util.Map; /** * 测试MyBatis框架 * 单独测试时才需要单独写这些代码,SSM 整合在一起时,其实是不需要这些代码的 * * @Author: Joey * @Date: 15/03/2020 4:17 PM */ public class Test { public static void main(String[] args) { // MyBatis提供了一个 Resources 类用于简化使用 ClassLoader 获取资源文件的过程 InputStream is = Resources.getResourceAsStream("mybatis.xml"); // 构建者模式 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); // 工厂模式 SqlSession sqlSession = sqlSessionFactory.openSession(); // 适用于返回结果为多个对象的查询 List<Flower> flowerList = sqlSession.selectList("com.joey.mapper.FlowerMapper.selAll"); for (Flower flower : flowerList) { System.out.println(flower); } // 返回一个Map对象,第二个参数指定的是『以返回的实体类的哪一个属性为key进行Map储存』 Map<Object, Object> map = sqlSession.selectMap("com.joey.mapper.FlowerMapper.selByName", "name"); System.out.println(map); // 适用于返回结果为一个值或者单个对象的查询 Object count = sqlSession.selectOne("com.joey.mapper.FlowerMapper.countAll"); System.out.println(count); sqlSession.close(); try { if (is != null) { is.close(); } } catch (IOException e) { e.printStackTrace(); } } }
**MyBatis的学习重点在与如何编写Mapper XML文件,这些文件会被MyBatis通过XML解析并被反射成数据库访问层的实现类对象。**这个过程是依靠反射和 JDK 动态代理来实现的。
MyBatis 将数据库中的结果返回时,会对数据表字段名和封装类的字段名进行比对,如果相同(==不区分大小写==),则调用其setter方法进行赋值,因此在数据库和实体类的设计阶段,就需要将数据库表中的字段名和实体类的属性名保持一致。如果不一致,可以通过给SQL中查询字段添加别名的方式来解决。这个过程叫Auto Mapping。
如果查询出来的表字段没有找到相对应的返回类属性,或者返回类属性中没有相对应的表字段,都不会报错,只是不进行映射而已。
-
全局配置文件
<transactionManager type="JDBC"></transactionManager>
type 描述 JDBC 使用JDBC原生事务管理方式,这是 MyBatis 的默认设置。MySQL 的 JDBC 操作是自动提交的,虽然 MyBatis 的底层是 JDBC 的封装,且默认使用 JDBC 进行事务管理,但是它的 CRUD 操作却不是自动提交的,需要手动调用 SqlSession 对象的 commit 方法进行提交。 MANAGED 将事务管理转交给其他容器,例如MyBatis和Spring框架一起使用时,就可以将数据库的事务管理转交给Spring框架 <dataSource type="POOLED"></dataSource>
Type属性值 描述 UNPOOLED 不使用数据库连接池,默认设置,和直接使用JDBC一样 POOLED 使用 MyBatis 自带的数据库连接池 JNDI The Java Naming and Directory Interface(Java命名目录接口技术)
<settings>
标签
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25000"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
设置日志
MyBatis provides logging information through the use of an internal log factory. The internal log factory will delegate logging information to one of the following log implementations:
- SLF4J
- Apache Commons Logging
- Log4j 2
- Log4j
- JDK logging
The logging solution chosen is based on runtime introspection by the internal MyBatis log factory. The MyBatis log factory will use the first logging implementation it finds (implementations are searched in the above order). If MyBatis finds none of the above implementations, then logging will be disabled.
一些运行环境会自带 Commons Logging 的 Jar 包,比如 Tomcat 和 WebSphere。在这种 classpath
中包含 Commons Logging 的环境中,MyBatis 会忽略 Log4J 的配置文件,因为 MyBatis 会自动使用 Commons Logging,因此需要配置配置文件才能让 MyBatis 使用 Log4J。
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
SQL语句的参数
#{}
方式
单个参数(没有使用注解)
-
参数是一个对象,使用
#{fieldName}
取出对象的属性作为参数(使用的是对象的fieldName
属性的getter
方法,如果不存在这个属性,那么会出现异常)。 -
参数是一个
Map
,使用#{key}
取出Map
中key
对应的属性作为参数(使用的是Map
的get
方法,如果不存在这个key
,将会返回 null)。 -
如果仅有一个参数,且为基本类型或
String
,那么#{identifier}
中的内容可以用任何标识符,包括任意整数。
多个参数
多个参数传入的情况下,MyBatis会将所有的参数使用一个 Map 存储起来,在 Mapper Statements 中使用 #{key}
的形式进行获取。但是不同的情况下,这个 Map 的 key 是不一样的。下面将进行详细分析。
-
没有使用注解
key 的形式是
[0, 1, param1, param2]
。因此在 Mapper Statements 中使用参数的方式有两种:#{index}
,index 是从 0 开始的整数,表示第 index+1 个传入的参数#{paramX}
,X 是从 1 开始的整数,表示第 X 个传入的参数- 如果
#{index}
和#{paramX}
取出的是对象,那么还可以使用#{index.fieldName}
或者#{paramX.fieldName}
取出该对象的属性。 - 如果取出的是 Map,还可以使用
#{index.key}
和#{paramX.key}
这种方式取出 Map 中对应 key 的 value
-
参数使用了注解
@Param("key")
用注解
@Param("key")
修饰参数,就是将映射到这个参数的 key 从[index, paramX]
变成了[key, paramX]
。void insertStudent(@Param("stu") Student student, Map map, @Param("i") Integer id, Integer score);
这个接口方法的可用参数,也就是 MyBatis 为这些参数生成的 Map 的 key 为
[1, 3, stu, i, param3, param4, param1, param2]
上述的 #{}
的使用方法只是一个简单的总结。如果想要完全了解 #{}
的特性,需要去了解它的解析方式,但是比较遗憾的是 MyBatis 的手册貌似不是特别详细,竟然连一个 #{}
的语法规定都没有。
${}
方式
首先给出观点:不建议使用 ${}
来传入参数。
#{}
和 ${}
对比
#{}
的底层是 PreparedStatement
,比较安全,会对传入的参数进行解析和检查。
${}
的底层是 String Substitution
,也就是 Statement
传入参数的做法,可能会出现 SQL 注入的危险。
// SQL 注入示例
String id = "5 or 1 = 1";
String sql = "DELETE from user where id = " + id;
stmt.execute(sql);
${}
的使用
${}
括号中的表达式是 OGNL
表达式,类似于 EL
表达式。
${}
可以也用来传入参数,特点和 #{}
有一些相似,也有很大的不一样。
${0}
,${1}
,${2}
… ,和#{index}
不同,${index}
不是代表参数,而是直接表示{ }
中的数字,和直接写{ }
中的数字效果是相同的。- 对于上述的
#{}
的使用中,除了#{index}
没有对应的${index}
用法外,其他的都可以使用$
替代#
,并且取到相应的对象或者属性。但是#
会根据 MyBatis 内置或者自定义的各种TypeHandler
对参数进行转化,并使用PreparedStatement
的合适的setXXX
方法将参数添加到 SQL 语句中。而$
就很直接了,它直接将该对象或者属性转化为字符串,直接替代 SQL 语句中${}
的占位,这一步在PreparedStatement
设置参数之前就已经完成了。 - 如果仅有一个没有注解的参数,且为基本类型或
String
,${}
方式无法取到该参数1。请使用#{}
。
由于这些特点,一般参数的占位符不会使用 ${}
,而是在需要传入 metadata
信息(表名,列名)的时候使用,详情可以去看手册的 String Substitution
一节。
@Param("key")
注解
只要接口方法中使用了此注解,MyBatis 在解析 Mapper Statement 时,会将所有的参数转变为一个 ParameterMap
,在 Mapper Statement 中可以通过这个 Map 的 key 进行获取。方式是:#{key}
。接口方法中的每个参数均有两个 key,注解修饰的参数的 key 的形式是 key
和 paramX
,而未被注解修饰的参数的 key 的形式是 index
和 paramX
。
考虑这样一个情况:
Integer countLink(Integer status);
<select id="countLink" parameterType="integer" resultType="Integer">
SELECT COUNT(*) FROM link
<where>
<if test="status != null">
link_status = #{status}
</if>
</where>
</select>
这个接口方法只有一个参数,也就是 status
,而且它是一个简单参数,因此在 Mapper Statement 中可以使用 ${identifier}
的方式取出该参数,这里的 identifier
可以是任意标识符。
但是如果通过 SqlSesion
获取到该接口的实例,并调用该方法,会出现异常。
这是因为在动态 SQL 标签 <if test="status != null">
中的 status
是取不到传入参数的。它会认为传入的
是一个对象,status
是这个对象的一个属性,并尝试通过该属性的 getter 方法获取到该属性的值,但是 Integer
是没有这样一个属性的,所以会产生异常。因此正确的接口方法声明应该要添加注解。
Integer countLink(@Param("status") Integer status);
比较有意思的是,上述的接口方法参数使用了注解后,#{identifier}
就不能是任意的表示符了,可用的参数名称变成了 [param1, status]
。想一想为什么?
分页查询
<select id="paginate" resultType="com.joey.pojo.Flower" parameterType="map">
select * from flower limit #{pageStart}, #{pageSize}
</select>
// 一页显示的数量
int pageSize = 2;
// 第几页
int pageNum = 2;
int pageStart = pageSize * (pageNum - 1);
Map map = new HashMap();
map.put("pageSize", pageSize);
map.put("pageStart", pageStart);
logger.debug(sqlSession.selectList("com.joey.mapper.FlowerMapper.paginate", map));
别名
在全局配置文件中可以为类型设置别名,这样在Mapper XML文件中声明resultType
和parameterType
的时候就可以直接使用别名,而不是完全限定名(包名+类名)。
<typeAliases>
<typeAlias type="com.joey.pojo.Flower" alias="Flower"></typeAlias>
</typeAliases>
<select id="paginate" resultType="Flower" parameterType="map">
select * from flower limit #{pageStart}, #{pageSize}
</select>x
另外还可以使用package
标签直接为包内所有的类进行别名设置,直接省略包名。
<typeAliases>
<!--这样设置的别名,在Mapper XML文件中就可以直接使用类名,甚至类名都可以不用区分大小写-->
<package name="com.joey.pojo"/>
</typeAliases>
Mybatis
中的别名,包括 3 种:
- 系统内置别名,一般就是将类型换成小写
- 使用
typeAlias
设置类型的别名 - 使用
package
标签直接为包内所有的类进行别名设置
事务
概念 | 含义 |
---|---|
功能 | 从应用程序的角度出发,软件具有哪些功能 |
业务 | 完成功能的逻辑,对应service中的一个方法 |
事务 | 从数据库角度出发,完成业务时需要执行的 SQL 集合,统称为一个事务 |
==**原生JDBC进行数据库访问时,是默认自动提交的。MyBatis中默认关闭了JDBC的自动提交功能。**==
每一个SqlSession
默认都是不自动提交事务的。需要在创建SqlSession
时传入参数设置其自动提交:
SqlSession openSession(boolean autoCommit);
在默认不自动提交状态下,可以通过SqlSession
对象调用commit
方法手动提交。
/**
* Flushes batch statements and commits database connection.
* Note that database connection will not be committed if no updates/deletes/inserts were called.
* To force the commit call {@link SqlSession#commit(boolean)}
*/
void commit();
MyBatis中的<insert>, <delete>, <update>
分别对应SQL中的增删改语句,这些标签没有resultType
属性,Mybatis认为这些语句的返回值都是int
。
MyBatis 接口绑定
按照指定规则创建与Mapper XML文件一一对应的接口,这样在 service 层中就可以使用 SqlSession
的 <T> T getMapper(Class<T> type)
方法,反射出此接口的代理类对象(此代理类实现了接口)。从而可以使用接口方法调用的方式来完成数据库操作。
-
新建的接口文件,其接口名与包名需要与 Mapper XML文件中的
mapper
标签的namespace
相同,其方法名称与 Mapper Statement 标签,也就是<insert>,<select>,<update>
等标签的id
相同。 -
在全局配置文件中,配置接口或 XML 文件的扫描。
<?xml version="1.0" encoding="UTF-8" ?> <!-- 引入远程dtd文件 --> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="logImpl" value="LOG4J"/> </settings> <typeAliases> <package name="com.joey.pojo"/> </typeAliases> <!-- 默认数据库环境 --> <environments default="mysql"> <!-- 这里可以配置多个数据库环境 --> <environment id="mysql"> <transactionManager type="JDBC"></transactionManager> <!-- 使用数据库连接池 --> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/ssm"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <!--配置需要自动生成 Mapper 实现类所在的包--> <package name="com.joey.mapper"/> </mappers> </configuration>
值得注意的是
package
和mapper
两个标签配置一种就行,不要两种都配置。package 标签配置之后,扫描的是该包下的所有接口文件。而 mapper 标签则可以配置用于配置接口文件的位置、也可以用于配置 Mapper XML 文件的位置。使用 package 标签配置扫描接口,需要 Mapper XML 文件名和接口文件名相同,并且在同一个包下。<mappers> <!-- Using classpath relative resources --> <mapper resource="org/mybatis/builder/AuthorMapper.xml"/> <!-- Using url fully qualified paths --> <mapper url="file:///var/mappers/BlogMapper.xml"/> <!-- Using mapper interface classes --> <mapper class="org.mybatis.builder.PostMapper"/> <!-- Register all interfaces in a package as mappers --> <package name="org.mybatis.builder"/> </mappers>
多参数传递
package com.joey.mapper;
import com.joey.pojo.TransferLog;
import java.util.List;
import java.util.Map;
public interface TransferLogMapper {
List<TransferLog> selAll();
int countAll();
List<TransferLog> paginate(Map map);
void insTransferLog(TransferLog transferLog);
List<TransferLog> selByAccInAccOut(String accInAccNo, String accOutAccNo);
}
<?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.joey.mapper.TransferLogMapper">
<insert id="insTransferLog" parameterType="TransferLog">
insert into transferLog values(default, #{accOutAccNo}, #{accInAccNo}, #{amount})
</insert>
<select id="paginate" resultType="TransferLog" parameterType="map">
select * from transferLog limit #{pageStart}, #{pageSize}
</select>
<select id="countAll" resultType="int">
select count(*) from transferLog
</select>
<select id="selAll" resultType="TransferLog">
select * from TransferLog
</select>
<!-- 需要传递多参数时,不需要设置parameterType参数 -->
<select id="selByAccInAccOut" resultType="TransferLog">
select * from transferLog where accInAccNo = #{0} and accOutAccNo = #{1}
</select>
</mapper>
这样配置好之后,就可以在代码中使用类进行数据库操作。
class TransferLogMapperTest {
static InputStream is;
static SqlSessionFactory factory;
static SqlSession sqlSession;
@BeforeAll
static void init() {
try {
is = Resources.getResourceAsStream("mybatis.xml");
factory = new SqlSessionFactoryBuilder().build(is);
sqlSession = factory.openSession();
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
void selAll() {
// 使用getMapper方法实例化一个接口
TransferLogMapper transferLogMapper = sqlSession.getMapper(TransferLogMapper.class);
// 调用接口的方法
List<TransferLog> transferLogs = transferLogMapper.selAll();
for (TransferLog tl :
transferLogs) {
System.out.println(tl);
}
}
@AfterAll
static void destroy() {
sqlSession.close();
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在接口的方法声明中还可以使用注解 @Param
修饰参数。
List<TransferLog> selByAccInAccOut(@Param("accin") String accInAccNo, @Param("accout") String accOutAccNo);
select * from transferLog where accInAccNo = #{accin} and accOutAccNo = #{accout}
动态SQL
根据条件不同,生成不同的SQL语句。在MyBatis中就是在Mapper XML文件中添加逻辑判断。
<if>
标签
if
标签中的test
属性是一种叫做 OGNL(Object Graphic Navigation Language) 的表达式,直接使用key
或者属性名来取得参数。
<!-- 官方示例 -->
<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>
<where>
标签
The where element knows to only insert “WHERE” if there is any content returned by the containing tags. Furthermore, if that content begins with “AND” or “OR”, it knows to strip it off.
<select id="findBlog" resultType="Blog">
SELECT * FROM BLOG
<!-- 自动生成where子句,会自动添加where, 删除第一个AND -->
<where>
<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>
<!-- 上述例子也可以直接使用 if 进行替换,但是效率没有上面的高 -->
<select id="findBlog" resultType="Blog">
SELECT * FROM BLOG WHERE 1=1
<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>
choose,when,otherwise
Let’s use the example above, but now let’s search only on title if one is provided, then only by author if one is provided. If neither is provided, let’s only return featured blogs (perhaps a strategically list selected by administrators, instead of returning a huge meaningless list of random blogs).
<select id="findActiveBlogLike"
resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<!-- choose子标签里一旦有一个条件成立,就不会判断后面的条件,相当于自带break -->
<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>
set
标签
用于修改SQL中的set
从句,可以自动去掉最后一个逗号,如果set
标签内有内容,则生成set
关键字,如果没有,就不会生成。
<update id="updateAuthorIfNecessary">
update Author
<set>
<!-- 防止出现语法错误,即下面所有if都不成立时 -->
id=#{id},
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
<if test="bio != null">bio=#{bio}</if>
</set>
where id = #{id}
</update>
trim
标签
<!-- 等效于where标签 -->
<trim prefix="WHERE" prefixOverrides="AND | OR ">
...
</trim>
<!-- 等效于set标签 -->
<trim prefix="SET" suffixOverrides=",">
...
</trim>>
bind
The bind element lets you create a variable out of an OGNL expression and bind it to the context.
常用于模糊查询和在原内容前后添加内容。
<select id="selectBlogsLike" resultType="Blog">
<bind name="title" value="'%' + title + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
foreach
标签
用于循环参数内容,具备在内容的前后添加内容,还具备添加分隔符功能。
适用于in
查询和批量插入。
Another common necessity for dynamic SQL is the need to iterate over a collection, often to build an IN condition.
<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>
MyBatis的foreach
标签效率比较低,虽然执行SQL语句时,批量插入比多条数据单独插入效率要高,但是由于其foreach
标签的效率较低,因此使用foreach
的批量插入比单独一条一条的插入效率要低。
如果要使用批量新增,必须指定Executor.BATCH
,底层就是JDBC的PreparedStatement.addBatch()
SqlSession sqlSession = sqlSessionFactory.openSession(Executor.BATCH);
sql,include
标签
实现SQL的复用。
<sql id="my">
id,accIn,accOut,amount
</sql>
<select id="selAll">
select <include refid="my"></include> from log
</select>
ThreadLocal
与OpenSessionInView
ThreadLocal
就是想在多线程环境下,保证成员变量的安全。可以将 ThreadLocal
看作一个 Map,key 是线程信息,value 是该线程存储在其中的内容。每个线程去访问这个ThreadLocal
对象。
常用于为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样同一个线程的所有调用到的方法都可以非常方便的访问这些资源。只要线程不发生变化,存储在 ThreadLocal
中的对象就不会发生变化。
JDK 建议将ThreadLocal
定义为private static
。
package com.joey.test.mutlithread;
/**
* 在使用ThreadLocal的过程中,要注意Thread的上下文
* 另外如果使用 InheritableThreadLocal 创建 ThreadLocal 对象,则子线程会继承父线程的值(拷贝一份)
*/
public class TestThreadLocal {
// 泛型,不添加<>,则默认为<Object>,jdk1.7之后后面的泛型可以省略,JVM自己推导
// 每个线程都会在ThreadLocal中开辟一块空间,用来存储数据
private static ThreadLocal<Integer> tl = new ThreadLocal<>();
// 使用lambda表达式进行初始化
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()-> 100);
// 使用匿名内部类进行初始化
private static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 200;
}
};
public static void main(String[] args) {
// 通过get方法进行访问
// 输出为null,因为没有初始化,Integer类型默认值为null
System.out.println(Thread.currentThread().getName() + " tl : " + tl.get());
// 输出100,因为通过lambda表达式进行了初始化
System.out.println(Thread.currentThread().getName() + " threadLocal : "
+ threadLocal.get());
System.out.println(Thread.currentThread().getName() + " threadLocal2 : "
+ threadLocal2.get());
// 使用set方法进行修改
tl.set(520);
System.out.println(Thread.currentThread().getName() + " tl : " + tl.get());
new Thread(()->{
// 上述对tl的修改不会影响此线程保存的数据
System.out.println(Thread.currentThread().getName() + " tl : " + tl.get());
System.out.println(Thread.currentThread().getName() + " threadLocal : "
+ threadLocal.get());
System.out.println(Thread.currentThread().getName() + " threadLocal2 : "
+ threadLocal2.get());
}).start();
}
}
MyBatis 和 Hibernate 框架都是数据访问层框架。Spring 框架提出了一个技术叫openSessionInView
,意思就是在View
层进行SqlSession
的创建,关闭,提交以及回滚等操作统统放在 View
层,比如放在Filter
中。
其实现的原理就是基于一个事实:『从 Filter 到 Servlet 到 Service,以及 MyBatis 访问数据库,这些操作都是在一个线程下执行的』。Servlet 是基于责任链模式设计的,对于同一个请求,Filter 中的代码和 Servlet 的代码是在一个线程中执行的,本质都是普通方法调用。
如果没有 OpenSessionInView
,我们一般会在 Service
层进行 SqlSession
的创建,关闭和异常的回滚。这样就要求我们每个 Service
方法都需要进行这些操作,代码十分的冗余。
因此我们引入一个工具类,它的作用就是提供专属于调用线程的 SqlSession
对象,这样我们就可以对所有的请求在 Filter
中统一进行事务管理,因此每个请求都会分配一个线程进行处理。而这个工具类实现提供专属于调用线程的SqlSession
对象,所依赖的就是 ThreadLocal
。
package com.joey.filter;
import com.joey.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
/**
* @Author: Joey
* @Date: 17/03/2020 10:03 AM
*/
@WebFilter(filterName = "OpenSessionInView", value = "/*")
public class OpenSessionInView implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");
SqlSession sqlSession = MyBatisUtil.getSession();
try {
chain.doFilter(req, resp);
sqlSession.commit();
} catch (Exception e) {
sqlSession.rollback();
e.printStackTrace();
} finally {
MyBatisUtil.closeSession();
}
}
public void init(FilterConfig config) throws ServletException {
}
}
工具类的封装中,将 SqlSessionFactory
创建放在static块中,只初始化一次,因为工厂的创建一般是比较耗时的。然后将 SqlSession
对象放在 ThreadLocal
中,以后调用 session
对象就会使用ThreadLocal
中存放的SqlSession
对象,为此线程的所有对象所共用,提高效率。
package com.joey.util;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
/**
* @Author: Joey
* @Date: 17/03/2020 10:05 AM
*/
public class MyBatisUtil {
// 只实例化一个工厂
private static SqlSessionFactory factory;
private static ThreadLocal<SqlSession> tl = new ThreadLocal<>();
static {
try (InputStream is = Resources.getResourceAsStream("mybatis.xml")) {
factory = new SqlSessionFactoryBuilder().build(is);
} catch (IOException e) {
e.printStackTrace();
}
}
// 提供给外部调用,获取 SqlSession 对象,由于这个 SqlSession 对象是存放在 ThreadLocal 中的,
// 因此不同的线程访问这个方法,将会得到不同的 SqlSession,但是同一线程上的调用则会返回同一个
// SqlSession 对象。
public static SqlSession getSession() {
SqlSession sqlSession = tl.get();
if (sqlSession == null) {
tl.set(factory.openSession());
}
return tl.get();
}
public static void closeSession() {
SqlSession sqlSession = tl.get();
if (sqlSession != null) {
sqlSession.close();
}
tl.set(null);
}
}
缓存
SqlSession
缓存
MyBatis默认开启SqlSession
缓存。
- 同一个
SqlSession
对象调用同一个select Statement
时,只有第一次会访问数据库,第一次查询的结果会被缓存在该SqlSession
的缓存区中(内存) - 缓存的是该
<select>
标签对应的Statement
对象 - 每一个
SqlSession
对象,都有一个单独的缓存区。
SqlSessionFactory
缓存
另外 MyBatis 还提供了一个 SqlSessionFactory
缓存,又叫二级缓存。如果开启了二级缓存,则只要是同一个SqlSessionFactory
创建的 SqlSesion
,就会有一块共享的缓存空间。
开启二级缓存,只需要在 Mapper XML 文件中,添加 <cache />
标签即可。
二级缓存开启后,它的默认行为是:
- 本文件内的所有
select statement
的结果都会被缓存(Local cache) - 本文件中的所有
insert
,update
,delect
语句均会清空缓存(Local and 2nd level caches) - 缓存失效的算法使用 LRU(Least Recently Used)
- 缓存大小为 1024 个对象或者 List 的引用
- 缓存默认为『可读可写』缓存
SqlSession
的缓存和 SqlSessionFactory
的缓存是独立的空间,当SqlSession
关闭或提交时,SqlSession
会把自己缓存的数据(一级缓存区,Local Cache)刷到 SqlSesionFactory
缓存区(二级缓存区,2nd level cache)中。
简单 LRU 实现
class LRUCache {
private DoublyLinkedListNode head;
private DoublyLinkedListNode tail;
private int capacity;
private int size;
private Map<Integer, DoublyLinkedListNode> nodes = new HashMap<>();
public LRUCache(int capacity) {
this.capacity = capacity;
head = new DoublyLinkedListNode();
tail = new DoublyLinkedListNode();
head.next = tail;
tail.pre = head;
}
public int get(int key) {
DoublyLinkedListNode node = nodes.get(key);
if (node != null) {
update(node, node.value);
return node.value;
}
return -1;
}
/**
* update the node's value, and move the node to the head of the list.
*
* @param node
* @param value
*/
private void update(DoublyLinkedListNode node, int value) {
node.value = value;
unlink(node);
link(node, head);
}
/**
* unlink the given node from the list.
*
* @param node
*/
private void unlink(DoublyLinkedListNode node) {
node.next.pre = node.pre;
node.pre.next = node.next;
node.next = null;
node.pre = null;
}
/**
* insert a given node after the given previous node.
*
* @param node
* @param previous
*/
private void link(DoublyLinkedListNode node, DoublyLinkedListNode previous) {
node.next = previous.next;
node.pre = previous;
previous.next.pre = node;
previous.next = node;
}
public void put(int key, int value) {
if (nodes.containsKey(key)) {
// set new value, and update the ordering
DoublyLinkedListNode node = nodes.get(key);
update(node, value);
} else {
// insert a new node, take care of the capacity
if (size == capacity) {
invalidate();
}
insert(key, value);
}
}
/**
* insert the new node at the head.
*
* @param key
* @param value
*/
private void insert(int key, int value) {
DoublyLinkedListNode node = new DoublyLinkedListNode(key, value);
nodes.put(key, node);
link(node, head);
size++;
}
/**
* invalidate the node at the tail, which is exactly the LRU, i.e. the least recently used node.
*/
private void invalidate() {
DoublyLinkedListNode lru = tail.pre;
nodes.remove(lru.key);
unlink(lru);
size--;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
DoublyLinkedListNode current = head.next;
while (current != tail) {
sb.append(current).append("->");
current = current.next;
}
if (sb.length() > 2) {
sb.deleteCharAt(sb.length() - 1);
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
static class DoublyLinkedListNode {
int key;
int value;
DoublyLinkedListNode pre;
DoublyLinkedListNode next;
public DoublyLinkedListNode() {
}
public DoublyLinkedListNode(int key, int value) {
this.key = key;
this.value = value;
}
@Override
public String toString() {
return "DoublyLinkedListNode{" +
"key=" + key +
", value=" + value +
'}';
}
}
}
可读可写缓存
MyBatis 检查到当前查询可以直接返回缓存对象时,会通过序列化和反序列化,将缓存的对象复制一份发送给查询者,因此查询者获得的结果和缓存中的结果是两个不同的对象。即查询者对查询结果进行修改不会影响到缓存。所以此缓存就是可读也可写的缓存。
由于需要进行序列化,所以开启二级缓存后,缓存中缓存的实体类需要实现可序列化接口。
只读缓存
<cache readOnly="true" />
如果有缓存可用,那么查询者将获得缓存对象。
多表查询
实现方式
- 业务装配:即在 Service 中,对多个表进行单表查询,最后在 service 方法中将查询结果进行关联
- Auto-Mapping 特性,通过别名完成两表联合查询
- 使用 MyBatis 的
<resultMap>
标签
多表查询时,一般会让一个实体类中包含另一个实体类,根据包含的类型不同,可以分为:
- 包含单个对象
- 包含多个对象(集合)
分别对应于对象与对象的一对一和一对多的关系。
resultMap
Mapper XML中的SQL查询结果会被MyBatis的自动映射特性(Auto Mapping)解析,生成一个POJO对象(由resultType
属性指定)。在这个过程中,需要查询出来的字段名和实体类的定义中的字段名相同(不相同,则需要设置别名)。
但是当select
标签不设置resultType
属性,而是使用resultMap
属性引用一个定义好了的resultMap
标签时,MyBatis查询结果生成的实体类行为将由resultMap
来控制。
<mapper namespace="com.joey.mapper.TeacherMapper">
<!-- 使用resultType属性 -->
<select id="selById" parameterType="int" resultType="Teacher">
select * from teacher where id = #{param1}
</select>
</mapper>
<mapper namespace="com.joey.mapper.TeacherMapper">
<!-- 使用resultMap标签 -->
<resultMap type="Teacher" id="mymap">
<!-- id用于配置主键,result用于配置其它列
column设置表中的字段名,property设置其对应的实体类中的属性名 -->
<id column="id" property="tid" />
<result column="name" property="tname" />
</resultMap>
<select id="selById" parameterType="int" resultMap="mymap">
select * from teacher where id = #{param1}
</select>
</mapper>
resultMap
实现多表查询
N+1查询方式:先查询出某个表的全部信息,再根据这个表的信息查询另一个表的信息。和业务装配一样,只是这里可以使用resultMap
由MyBatis进行操作。
N + 1方式配合<association>
标签实现对象属性填充
// POJO
// 这里放在了一起,并省略了getter,setter
public class Student {
private int id;
private String name;
private int age;
private int tid;
private Teacher teacher;
}
public class Teacher {
private int id;
private String name;
private List<Student> list;
}
-- 数据库表定义
create table teacher(
id int(10) primary key auto_increment,
name varchar(20)
);
create table student(
id int(10) primary key auto_increment,
name varchar(20),
age int(3),
tid int(10),
constraint fk_teacher foreign key (tid) references teacher(id)
);
Mapper XML文件写法:
<?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.joey.mapper.TeacherMapper">
<select id="selById" parameterType="int" resultType="Teacher">
select * from teacher where id = #{param1}
</select>
</mapper>
<?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.joey.mapper.StudentMapper">
<!-- 先查询出student表的信息,然后根据tid查询Teacher表的信息 -->
<resultMap type="Student" id="stuMap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="tid" property="tid"/>
<!-- 设置一个查询结果映射到teacher属性,可以通过column指定某一列作为查询的参数 -->
<association property="teacher" select="com.joey.mapper.TeacherMapper.selById" column="tid"></association>
</resultMap>
<select id="selAll" resultMap="stuMap">
select * from student
</select>
</mapper>
在使用N+1方式进行多表联合查询时,可以不配置属性名和数据表列名相同的映射关系,MyBatis会Auto Mapping,但是作为参数传给association
标签的列名需要配置,因为MyBatis在Auto Mapping的过程会把这一列忽略。因此上述Mapper XML文件还可以写成如下形式:
<?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.joey.mapper.StudentMapper">
<resultMap type="Student" id="stuMap">
<result column="tid" property="tid"/>
<association property="teacher" select="com.joey.mapper.TeacherMapper.selById" column="tid"></association>
</resultMap>
<select id="selAll" resultMap="stuMap">
select * from student
</select>
</mapper>
N + 1方式配合<collection>
标签实现集合属性填充
<!-- MyBatis使用N+1方式结合resultMap加载集合对象,自动以id进行合并 -->
<resultMap id="teaMap2" type="Teacher">
<id property="id" column="id"></id>
<collection property="list" select="com.joey.mapper.StudentMapper.selByTid" column="id"></collection>
</resultMap>
<select id="selAll2" resultMap="teaMap2">
select * from teacher
</select>
<?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.joey.mapper.StudentMapper">
<select id="selByTid" resultType="Student" parameterType="int">
select * from student where tid = #{param1}
</select>
</mapper>
联表查询实现集合属性填充
<!-- MyBatis使用联表查询结合resultMap加载集合对象,自动以id进行合并 -->
<resultMap id="teaMap" type="Teacher">
<id property="id" column="tid"></id>
<result property="name" column="tname"></result>
<collection property="list" ofType="Student">
<id property="id" column="sid"></id>
<result property="name" column="sname"></result>
<result property="age" column="age"></result>
<result property="tid" column="tid"></result>
</collection>
</resultMap>
<select id="selAll" resultMap="teaMap">
select t.id tid, t.name tname, s.id sid, age, s.name sname
from teacher t left join student s
on t.id = s.tid
order by tid
</select>
Auto Mapping实现多表查询
只能使用联表查询方式,不能使用N+1了。
<select id="selAll" resultType="Student">
select t.id `teacher.id`, t.name `teacher.name`, s.id id, age, s.name name
from teacher t right join student s
on t.id = s.tid
order by tid
</select>
MyBatis在自动映射时,会将teacher.id
映射到Student对象中的teacher属性的id属性。
注意需要使用反单引号转义关键符号:·
。
Auto Mapping可以用于加载单一对象,但是无法加载集合对象。
MyBatis的注解
-
作用:写在类文件中,简化Mapper XML文件。
-
如果涉及到到动态SQL,还是需要使用Mapper XML文件。
-
Mapper XML文件和注解可以同时存在。
使用注解,**需要在全局配置文件中的mappers
标签下使用package
标签或者mapper
标签的class
属性进行设置。**注意mapper
标签的resource
属性是用于加载资源文件,也就是Mapper XML文件。
package com.joey.mapper;
import com.joey.pojo.Teacher;
import org.apache.ibatis.annotations.Select;
public interface TeacherMapper {
@Select("select * from teacher where id = #{param1}")
Teacher selById(int id);
@Results(value = {
@Result(id = true, property = "id", column = "id"),
@Result(property = "name", column = "name"),
@Result(property = "list", column = "id",
many = @Many(select = "com.joey.mapper.StudentMapper.selByTid"))
})
@Select("select * from teacher")
List<Teacher> selAll();
}
直接在接口上添加相应的注解,上述代码中selById
方法上的注解相当于在相应的Mapper XML文件中添加:
<select id="selById" parameterType="int" resultType="Teacher">
select * from teacher where id = #{param1}
</select>
除了@Select
之外,还有@Insert,@Update,@Delete
等注解。
package com.joey.mapper;
import com.joey.pojo.Student;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface StudentMapper {
@Select("select s.id id, s.name name, s.age age, s.tid tid, t.id `teacher.id`, t.name `teacher.name`" +
" from student s left join teacher t on s.tid = t.id")
List<Student> selAll();
@Select("select * from student where tid = #{param1}")
List<Student> selByTid(int tid);
}
注解还可以用来替代resultMap
,可以说是非常强大,但是相对来说,易读性不是特别好。
MyBatis 的使用流程
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
...
inputStream = Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
Resources
工具类加载全局配置文件,生成一个流对象- 实例化
SqlSessionFactoryBuilder
对象(工厂构建器) XMLConfigBuilder
根据流对象中的信息,实例化一个Configuration
对象,存放配置文件信息SqlSessionFactoryBuilder
根据Configuration
对象,实例化SqlSessionFactory
接口的实现类DefaultSqlSessionFactory
- 由
TransactionFactory
创建一个事务Transaction
对象 - 创建执行器
Executor
对象 - 实例化
SqlSession
接口的实现类DefaultSqlSession
- 创建(Create)、更新(Update)、读取(Retrieve)和删除(Delete)等数据库操作(CURD)
- 根据执行情况,操作不同。如果执行成功,就提交;如果执行失败,则回滚到第 5 步。
- 关闭
SqlSession
-
事实上,还是有方法可以取到参数的,只是这种方式属于室内打伞,多此一举。比如如果传入的参数是
Integer
类型,就可以通过${value}
取到这个参数的值,因为Integer
对象内有一个 ``private final int value` 保存了此对象代表的整数大小。虽然是私有的,MyBatis 还是有办法取到这个属性的值的。还有一种办法,就是加上注解。 ↩︎