1. 简介及引入
普通原生JDBC的问题:
- 每次查询都要开关数据库连接,性能不高(解决方案:连接池)
- SQL语句、Statement、ResultSet硬编码在类中,通用性不高(解决方案:配置到XML中、查询结果自动映射为Java对象)
MyBatis可以帮我们封装输入参数、输出结果。
官方文档:MyBatis
Maven引入依赖:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>
<!-- 导入Log4J方便调试 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
2. MyBatis框架概览
- SqlMapConfig.xml:MyBatis的全局配置文件,因为是我们手动加载的,可以任意命名
- mapper.xml:映射文件,命名方式为XXXMapper.xml
- SqlSessionFactory:用来创建SqlSession
- SqlSession:用来发出SQL语句
- Executor:SqlSession内部通过Executor操作数据库。
- mapped statement:封装SQL语句、输入参数、输出结果
使用步骤如下:
第一步创建MyBatis的全局配置XML,指定Mapper:
<?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>
<!-- 环境列表,及默认环境。整合Spring后将不使用environments -->
<environments default="development">
<environment id="development">
<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>
<!-- 声明映射文件,以classpath为根路径,使用/代替. -->
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>
第二步配置映射文件Mapper.xml,每一个语句的定位由mapper的namespace和语句的id唯一确定:
<?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="org.example.UserMapper">
<!-- select语句。id指定sql语句封装成的MappedStatement的id,resultType指定输出数据类型 -->
<select id="findUserById" resultType="com.example.domain.User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
第三步读取MyBatis配置文件创建SqlSessionFactory。SqlSessionFactory
使用单例模式进行访问,避免重复创建,提高效率:
public static void main(String[] args) {
private static SqlSessionFactory factory;
static {
Reader resource = null;
try {
// 以classpath为根路径,包名分隔符使用/代替.
resource = Resources.getResourceAsReader("/package/to/mybatis.xml");
factory = new SqlSessionFactoryBuilder().build(resource);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (resource != null) {
try {
resource.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
第四步使用SqlSessionFactory
创建SqlSession
进行查询,使用selectOne()
、selectList()
、insert()
、update()
、delete()
进行对应的增删改查操作,接收参数为第二步中的语句坐标,以及查询参数。MyBatis根据传入的语句坐标执行具体语句。
public void selectUserById() {
SqlSession session = sqlSessionFactory.openSession();
try {
// 使用mapper的namespace + statement的id
User user = (User)session.selectOne("com.example.mybatis.UserMapper.selectUserById", 101);
} finally {
session.close();
}
}
SqlSession
具有一些属性,同时是线程不安全的,因此最好放到方法体当中创建为局部变量,就可以避免多线程操作冲突。
IDEA中需要将XML等资源文件加入到classpath中,才能打包到最后的包中。
加入classpath的方式有两种,第一种是放置到resources类型的目录下
第二种是编译时将指定格式的资源文件也编译进去<!-- pom.xml --> <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build>
3. Mapper声明及使用
Mapper中的SQL语句中的参数可以使用两种符号
#{}
:对应SQL语句中的占位符,例如?
,会自动对输入参数添加引号''
,基本数据类型的参数可以使用任意变量名,POJO类型需要使用属性名。${}
:直接拼接输入的参数到SQL中,所以需要手动添加''
,也可以添加匹配符%
,基本数据类型的参数名必须为value,POJO类型与#{}
一样使用属性名。缺点是可能会被SQL注入。
3.1 纯XML方式
Mapper声明,此时的namespace
只起到区分Mapper的作用:
<mapper namespace="com.example.mapper">
<select id="findUserById" resultType="com.example.mybatis.domain.User">
SELECT * FROM users WHERE id = #{value}
</select>
</mapper>
Mapper使用,需要通过namespace
+id
引用具体Mapper:
SqlSession session = factory.openSession();
User user = session.selectOne("com.example.mapper.findUserById", 3);
3.2 XML+接口方式
首先需要声明一个接口作为Mapper接口,方法名为具体的查询方法:
package com.example.mapper;
public Interface UserMapper {
User findUserById(int id);
}
然后声明Mapper XML,此时namespace需要为Mapper接口的完整类名,id为mapper接口中的对应方法:
<mapper namespace="com.example.mybatis.mapper.UserMapper">
<select id="findUserById" resultType="com.example.mybatis.domain.Commodity">
SELECT * FROM commodity WHERE id = #{value}
</select>
</mapper>
然后获取Mapper接口类实例进行查询,MyBatis会根据接口的完整路径在XML中查找相对应的mapper语句,然后帮我们进行接口实现,调用接口的具体方法就可以实现查询了:
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.findUserById(6);
3. 全局配置
全局配置包含的字标签内容有:
- properties(属性)
- settings(全局配置)
- typeAliases(别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境)
- mappers(映射器)
3.1 配置方式
全局配置可以使用纯XML方式,也可以使用XML+properties的方式。
3.1.2 纯XML方式
如果有&
需要使用&
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
3.1.3 XML结合properties
可以在XML中声明引用的外部properties,同样是以classpath为根路径,最好加上一层变量封装,避免跟操作系统自带变量重名,例如使用jdbc.username
代替username
<!-- 指定使用的资源文件 -->
<properties resource="mybatis-config.properties" />
<environments>
<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>
</environments>
// mybatis-config.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=
如果使用中文查询无法正常查询出结果集时,需要设置连接的URL添加characterEncoding=UTF-8参数,同时还需要配置MySQL的服务端字符集character-set-server为uft8
// my.ini [mysqld] character-set-server=utf8
也可以在创建SqlSessionFactory时传入读取的props
is = Resources.getResourceAsStream("mybatis-config.xml");
Properties props = Resources.getResourceAsProperties("mybatis-config.properties");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is, props);
3.2 全局配置项
全局配置项为<configuration>
标签中的可以设置的事项
3.2.1 properties
可以通过resource属性指定要加载的属性文件,或者通过内部的property属性指定具体属性值:
<configuration>
<properties resource="">
<property name="" value=""/>
</properties>
</configuration>
3.2.2 settings
MyBatis的配置:
<configuration>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="autoMappingBehavior" value="FULL"/>
</settings>
</configuration>
其中一些重要的配置:
mapUnderscoreToCamelCase
:自动映射的过程中把数据库字段名中的_
转换为Java变量通用的驼峰式命名法,例如user_id
可以映射为userId
,默认为false
。autoMappingBehavior
:自动映射行为,默认为PARTIAL
,即只自动映射单层ResultMap(包含ResultType),不自动映射多层ResultMap。设置为FULL
启用多层ResultMap映射。
3.2.3 typeAliases
Mapper XML中需要指定parameterType和resultType的具体类型,为了方便具体类型的引用,MyBatis内置了对于基础数据类型及包装类型的别名,例如_int对应int类型,int对应Integer类型。其他自定义的POJO就需要我们在MyBatis全局配置文件中设置别名了,例如:
<typeAliases>
<!-- 单个别名 -->
<typeAlias alias="Author" type="domain.blog.User"/>
<!-- 批量设置别名,声明包名,则包下的类自动赋予与类名相同的别名,首字母大小写都可以-->
<package name="com.example"/>
</typeAliases>
Mapper中就可以直接使用:
<select resultType="User" />
3.2.4 TypeHandlers(类型处理器)
MyBatis中通过TypeHandlers完成从Java类型转换为JDBC类型,MyBatis内置提供了很多的TypeHandlers。
3.2.5 Mapper
加载Mapper的两种方式:
- 单个加载:
- resource属性加载以classpath为根路径的mapper XML
<mapper resource="mapper/UserMapper.xml"/>
- url属性加载绝对物理路径的mapper XML
<mapper url="file:///xxxxxx\xxx\xxx\UserMapper.xml" />
- class属性加载mapper XML对应的Mapper接口,XML与接口类需要同名且同包
<mapper class="com.example.UserMapper"/>
- resource属性加载以classpath为根路径的mapper XML
- 批量加载某个包
<!-- 批量加载,在通过Mapper接口加载的前提下,声明Mapper接口的包名,则会自动加载该包名下的Mapper接口及Mapper XML --> <package name="com.example"/>
4. 查询示例
4.1 查询多条记录
<select id="selectBlogByName" parameterType="java.lang.String" resultType="Blog"> <!-- ${}必须使用value作为参数名 -->
select * from Blog where name = '%${value}%'
</select>
Blog blog = (Blog)session.selectList("org.mybatis.example.BlogMapper.selectBlog", 101);
查询单条时使用selectOne()
或者selectList()
都可以,但是查询多条时必须使用selectList()
,否则抛出TooManyResultsException
。
4.2 插入记录
<!-- 输入对象为POJO时,#{}必须以其属性名命名 -->
<insert id="insertUser" parameterType="xx.xx.User">
insert into user(id,name) values (#{id}, ${name})
</insert>
sqlSession.insert("insertUser", new User());
// 需要提交才会实际进行修改
sqlSession.commit();
sqlSession.close();
获取自增主键自动生成的主键的方式有两种
-
设置
useGeneratedKeys=true
,并且设置keyProperty
为自增主键的字段名<insert id="insertUser" useGeneratedKeys="true" keyProperty="id"> INSERT INTO user (name) values ('root') </insert>
生成的主键值就会赋值给传入的POJO对象中。
-
使用MySQL的
last_insert_id()
函数,会将获取后的主键值设置到传入的POJO对象中:<insert id="insertUser" parameterType="xx.xx.User"> <selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer"> SELECT LAST_INSERT_ID() </selectKey> insert into user(name) values (${name}) </insert>
非自增主键可以首先通过MySQL的uuid()
函数生成35位字符串,作为主键的值,然后insert语句执行后,取出id值:
<insert id="insertUser" parameterType="xx.xx.User">
<selectKey keyProperty="id" order="BEFORE" resultType="java.lang.String">
SELECT UUID()
</selectKey>
insert into user(id,name) values (#{id}, ${name})
</insert>
4.3 删除记录
<delete id="deleteUser" parameterType="java.lang.Integer">
delete from user where id=#{id}
</delete>
4.3 更新记录
<update id="updateUser" parameterType="xx.xx.User">
update user set username=#{username} where id=#{id}
</update>
5. DAO
5.1 原生DAO
原始DAO即自己创建DAO接口和具体实现类,例如:
public interface UserDao {
User findUserById(int id) throws Exception;
int insertUser(User user) throws Exception;
boolean deleteUser(User user) throws Exception;
}
public class UserDaoImpl implements UserDao {
private SqlSessionFactory factory;
public UserDaoImpl(SqlSessionFactory factory) {
this.factory = factory;
}
User findUserById(int id) {
SqlSession session = this.factory.openSession();
// statement的id, 传入参数
session.selectOne("xxx", 2);
session.close();
}
// ...
}
原始DAO的缺点:
- 每个方法内存在一些重复代码
- statement的id硬编码在代码中
- 传入参数为Object,编译时无法发现错误
5.2 Mapper代理接口
鉴于原始DAO的缺点,MyBatis提供了Mapper接口的机制。Mapper接口相当于DAO接口,只要遵循了Mapper的规范,MyBatis就可以为我们创建实现类。
开发规范:
- Mapper XML文件的namespace为Mapper接口的完整类名
- Mapper接口中的方法名与Mapper XML中的statement的id一致,方法参数类型与parameterType一致,返回值类型与resultType一致。
实现原理:通过类名、方法名、方法参数、方法返回值通过反射调用对应的Mapper接口,生成实现类作为DAOImpl供我们调用。
Mapper方法内部根据返回参数是否是集合对象来决定调用selectOne()
或selectList()
。
限于Mapper XML中的parameterType只有一个,可以使用POJO类型实现多个参数的传递。
6. 输入映射
通过parameterType指定输入参数的类型,可以是基础类型、HashMap、POJO类型
6.1 hashmap
以某个键值对的key和value为参数值:
<select id="selectUser" parameterType="hashmap">
select * from user where name = #{some_key} and age = #{some_value}
</select>
6.2 POJO
可以将多个查询参数包装为自定义的POJO类型,然后Mapper中调用POJO类型的属性。
public class UserQueryVo{
private User user;
private Order order;
}
<select id="selectUser" parameterType="com.example.UserQueryVo"> <!-- ${}必须使用value作为参数名 -->
select * from user where name LIKE '%${user.name}%' AND id = #{order.id}
</select>
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setName("小王");
Order order = new Order();
order.setId(4);
UserQueryVo query = new UserQueryVo(user, order);
mapper.selectUser(query);
7. 输出映射
输出映射分为resultType和resultMap。其实resultType也是通过resultMap实现的。
解决查询结果集字段名与POJO对象属性名不一致的方法有多种:
- 可以给结果集字段名取别名,与POJO属性名一致,然后指定resultType为对应POJO即可。这种方式是结果集去适应POJO。
- 也可以自定义一个新的POJO,属性名与查询的结果集字段名一一对应,通过指定resultType即可。这种方式是POJO去适应结果集
- 定义一个resultMap,其中结果集字段名与POJO属性名一致的可以自动映射,不一致的可以通过
<id>
或<result>
标签进行映射。这种方式是以resultMap为桥梁连接结果集和POJO。
7.1 resultType
resultType可以为简单数据类型、POJO、hashmap。hashmap以字段名为key,字段值为value。
与parameterType一样,值都是集合中的类型或单个POJO的类型,不是集合类型。
使用resultType进行输出映射时,只有查询出的字段名与POJO中的属性名一致,该字段值才会赋给对应属性。
如果查询出的字段名与POJO中的属性完全不一样,则不会创建POJO对象。只要有一个一样,就会创建POJO。
7.2 resultMap
解决resultType中结果字段名称和POJO属性名称不一致、或者POJO中包含了其他POJO,导致无法直接映射的问题。
<mapper>
<!-- type是最终映射的Java类型,可以为aliases。id为resultMap的id -->
<resultMap type="user" id="userResultMap">
<!-- id标签为结果集中的唯一标识的字段。column为结果字段名,property为POJO的属性名 -->
<id column="id_" property="id" />
<!-- result标签为普通字段 -->
<result column="name_" property="name"/>
</resultMap>
<!-- resultMap中以已定义的resultMap的id为值,如果是其他Mapper中定义的resultMap,需要在前面加上namespace -->
<select id="findUserById" resultMap="userResultMap">
<!-- 给字段取别名 -->
select id id_, name name_ from user where id = #{value}
</select>
<mapper>
POJO复杂嵌套的情况时使用association
(一对一)、collection
(一对多),其实这两个也是一种resultMap。
8. 动态SQL
通过表达式动态拼装SQL语句。
IF判断
<select id="findUsers">
select * from user
<!-- where标签可以自动去除内部生成表达式的第一个and -->
<where>
<if test="userCustom != null">
<if test="userCustom.name != ''">
and name = #{userCustom.name}
</if>
<if test="userCustom.mobile != ''">
and mobile = #{userCustom.mobile}
</if>
</if>
</where>
</select>
SQL片段
定义可复用的SQL语句片段,一般基于单表进行定义,也尽量不要包含where标签,可以更好地复用。
<sql id="query_user_where">
<if test="userCustom != null">
<if test="userCustom.name != ''">
and name = #{userCustom.name}
</if>
<if test="userCustom.mobile != ''">
and mobile = #{userCustom.mobile}
</if>
</if>
</sql>
<select id="findUsers">
select * from user
<where>
<!-- 引用sql片段,如果定义在其他Mapper中,需要在前面加上namespace -->
<include refid="query_user_where"/>
</where>
</select>
foreach
如果传入参数是数组或List时,可以使用foreach标签循环输入的参数:(collection和item不用加#{},里面的参数需要加#{})
<select id="findUsers" parameterType="int">
select * from user
<where>
<!-- 输入参数中是否包括ids属性 -->
<if test="ids != null">
<!-- collection是要遍历的集合,item是每一项的变量名,open是遍历前添加的字符串,close是遍历后添加的字符串,separator是两次遍历间的字符串 -->
<!-- 组合成类似 AND (1 OR 2 OR 3) -->
<foreach collection="ids" item="id" open="AND (" close=")" separator="OR">
id = #{id}
</foreach>
<!-- 组合成类似 IN (1, 2, 3) -->
<foreach collection="ids" item="id" open="IN (" close=")" separator=",">
#{id}
</foreach>
</if>
</where>
</select>
9. 多表查询
一对一查询
一对一查询的例子,比如订单对用户是一对一(一个订单对应一个用户)
-
使用resultType的话,直接继承自
Order
(或User
类),并把复制User
(或Order
)中的属性到继承的子类中,使得POJO的属性与查询结果字段能一一对应即可。 -
使用resultMap的话,直接改写
Order
类,添加一个成员变量User
,然后配置resultMap
如下:<resultMap id="ordersUserResultMap" type="com.example.Order"> <!-- 首先配置Order类原先的数据。 --> <!-- id标签为唯一标识Order实体的字段,column为字段名,property为Order中的对应属性 --> <id column="id" property="id"/> <!-- result标签为普通字段 --> <result column="user_id" property="userId" /> <result column="orderno" property="orderno" /> <!-- 然后使用association把查询到的其他字段配置到相关联的单个对象中。property为要映射到的Order中的关联属性名,javaType为其类型 --> <association property="user" javaType="com.example.User"> <!-- 然后映射结果字段到关联对象属性中 --> <!-- 首先要注意id标签指明查询结果中唯一标识关联对象的字段user_id,对应Order中属性user中的id属性 --> <id column="user_id" property="id"/> <!-- 然后是普通字段 --> <result column="name" property="name"/> </association> </resultMap> <select resultMap="ordersUserResultMap"> select * from orders, user where orders.user_id = user.id </select>
使用resultType和resultMap的对比:
- resultType相对比较简单,只要根据查询结果自定义新POJO即可。
- resultMap可以实现复杂数据的映射,例如集合对象。
- resultMap可以实现延迟加载。
一对多查询
一对多查询,例如根据订单查询订单详情。因为一对多查询时,POJO中需要使用一个集合类型的属性来保存另一个表中的多条数据,无法实现结果字段到POJO属性的直接一一对应,需要使用resultMap来实现。
<resultMap id="OrderAndOrderDetailMap" type="com.tim.domain.Order">
<id column="id" property="id"/>
<result column="orderno" property="orderNo"/>
<!-- collection是映射成集合,ofType指定集合所包含的类型。 -->
<collection property="orderDetails" ofType="com.tim.domain.OrderDetail">
<!-- 同样的column是查询结果中的字段名,如果有多个同名,查询时需要取别名。property是集合所包含类型里的属性 -->
<id column="ordersdetail_id" property="id"/>
<result column="goods_id" property="goodsId"/>
<result column="goods_num" property="goodsNum"/>
</collection>
</resultMap>
<!-- 一对多,实际查询结果有多条,MyBatis会自动将同个Order.id的记录合并为一条,把多条记录里的OrderDetail根据Collection标签映射成集合。 -->
<select id="findOrderAndOrderDetail" resultMap="OrderAndOrderDetailMap" parameterType="int">
select
orders.id,
user_id,
orderno,
ordersdetail.id ordersdetail_id,
goods_id,
goods_num
from orders, ordersdetail where orders.id = ordersdetail.orders_id and orders.id = #{value}
</select>
collection和association可以互相嵌套,例如User(一)——Order(多)——(OrderDetail)(多),一名用户可以有多个订单,User
中就包含List<Order>
,同时单个订单中又有多个订单明细,Order
包含List<OrderDetail>
,resultMap的结构示例如下:
<resultMap>
<id/>
<result/>
<collection>
<collection></collection>
</collection>
</resultMap>
使用resultType也可以实现,只不过要自己手动添加内嵌POJO的各个字段,同时因为会查出多条记录,要自己合并同个Order.id的记录。
根据实际需求选择resultType或者resultMap。如果最终结果是个列表,列表所含类型中没有POJO属性,则可以使用resultType。如果最终结果类型中包含了POJO属性,则使用resultMap的association(一对一)或collection(一对多)。
resultMap间可以相互继承。
延迟加载
resultMap中的association和collection支持延迟加载。
首先延迟加载要开启全局配置:
<configuration>
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
</configuration>
假设需要获取订单对应的用户信息:
<resultMap id="OrderAndUserMap" type="com.tim.domain.Order">
<id column="id" property="id"/>
<result column="orderno" property="orderNo"/>
<!-- association或collection,select表示查询懒加载数据所使用的select标签id, column表示查询结果中用来查询懒加载数据的字段名 -->
<association property="user" javaType="com.tim.domain.User" select="findUserById" column="user_id"/>
</resultMap>
<!--主句中进行简单的单表查询,不进行关联查询或子查询 -->
<select id="findOrdersAndUser" resultMap="OrderAndUserMap" parameterType="int">
select *
from orders
where id = #{value};
</select>
<!-- 懒加载使用的select -->
<select id="findUserById" resultType="User">
select * from users where id = #{id}
</select>
final Order order = sqlSession.selectOne("com.tim.mapper.UserMapper.findOrdersAndUser", 1);
// 调用关联查询的属性时才会发出查询
final User user = order.getUser();
10. 查询缓存
一级缓存
一级缓存是SqlSession级别的缓存。SqlSession对象中具有一个HashMap来存储缓存数据。不同SqlSession的缓存数据是不一样。
MyBatis会将SQL语句和对应的查询结果封装为key-value,当再次发起同样的请求时直接返回结果,提高响应速度,降低数据库压力。
如果对数据库进行了insert、delete、update时,一级缓存会清空,避免脏读。
二级缓存
二级缓存是Mapper级别的缓存。多个SqlSession操作同个Mapper的select语句,会保存到该Mapper下的HashMap,二级缓存是跨SqlSession的。注意实际上是按照namespace进行区分的,不同namespace的Mapper具有不同的二级缓存,相同namespace的Mapper的二级缓存是相同的。
与一级缓存一样,如果进行了增删改,则会清空二级缓存。
二级缓存的开启步骤:
- 开启全局配置
<setting name="cacheEnabled" value="true" />
- Mapper
<mapper namespace=""> <!-- 开启本namespace下的二级缓存 --> <cache /> </mapper>
二级缓存需要让对应的POJO类实现序列化接口,因为二级缓存可能在内存中,也可能在硬盘中。
SqlSession
的close()
会把数据写到二级缓存中。
statement级别的禁用本句二级缓存:
<select useCache="false"></select>
MyBatis整合ehcache
ehcache是一个分布式缓存。分布式缓存包括redis、memcached、ehcache。使用分布式缓存,可以对缓存数据集中管理。
MyBatis中有个Cache接口,用来实现二级缓存功能。MyBatis中默认的实现类是PerpectualCache
。
整合ehcache,首先要导入相关库,然后在Mapper中指定cache:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.1.0</version>
</dependency>
<mapper>
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
</mapper>
接着创建配置文件ehcache.xml放置到classpath中(可选)。
二级缓存的应用场景
对于访问较多,同时实时性要求不是很高的请求,或者耗时较高的统计分析等场景,可以使用二级缓存。此时设置一个刷新间隔flushInterval,到期后自动清空缓存,再进行查询。
二级缓存的局限性
MyBatis的二级缓存是Mapper级别的,当进行了某个语句的增删改时,其他查询的缓存都会被清空,导致需要重新查询。
11. Spring整合MyBatis
11.1 整合步骤
整体思路是通过Spring单例管理SqlSessionFactory,具体首先要在MyBatis库的基础上导入Spring及其他相关库:
<!-- Spring相关库 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
<!--mybatis-spring整合包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<!-- 线程池使用DBCP -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.5.0</version>
</dependency>
然后要在Spring中配置DPCP的dataSource:
<context:property-placeholder location="classpath:db.properties"/>
<bean id="datasource" class="org.apache.tomcat.dbcp.dbcp2.BasicDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数 -->
<property name="acquireIncrement" value="5"/>
<!-- 初始连接池大小 -->
<property name="initialPoolSize" value="10"/>
<!-- 连接池中连接最小个数 -->
<property name="minPoolSize" value="5"/>
<!-- 连接池中连接最大个数 -->
<property name="maxPoolSize" value="20"/>
</bean>
注意,Spring中读取Properties文件,默认情况下系统变量会覆盖我们自定义的同名变量,例如username
,导致数据库连接参数与实际配置参数不同,此时可以修改我们配置的变量名,或者加一层命名空间jdbc.username
。或者设置system-properties-mode
为FALLBACK
或NEVER
,参考Access denied for user ‘Administrator’@‘localhost’ (using password: YES)
11.2 Spring结合DAO
11.2.1 原生DAO
DAO实现类可以继承SqlSessionDaoSupport
类,该类提供了SqlSessionFactory
的注入,可以通过调用this.getSession()
获取SqlSession
,获取的SqlSession
由Spring进行管理,无需手工调用sqlSession.close()
关闭。
DAO的bean配置:
<context:property-placeholder location="classpath:db.properties"/>
<bean id="datasource" class="org.apache.tomcat.dbcp.dbcp2.BasicDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="datasource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>
<bean id="userDao" class="com.example.dao.UserDaoImpl">
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {
@Override
public User findUserById(int id) {
final SqlSession sqlSession = this.getSqlSession();
final User user = sqlSession.selectOne("org.tim.UserMapper.findUserById", 2);
return user;
}
}
11.2.2 Mapper代理
在原来Mapper代理的前提条件设置下,可以进行单个Mapper配置
<context:property-placeholder location="classpath:db.properties"/>
<bean id="datasource" class="org.apache.tomcat.dbcp.dbcp2.BasicDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="datasource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>
<!-- 单个Mapper -->
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="com.example.mapper.UserMapper"/>
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>
<!-- 批量扫描包里的Mapper接口,自动创建代理对象,并在容器中注册。生成的bean id为Mapper接口的名字首字母小写 -->
<!-- XxxMapper.java和XxxMapper.xml需要同名同包。配置该项后MyBatis的package标签可以不用配置了。 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 多个包使用半角逗号分隔 -->
<property name="basePackage" value="com.example.mapper"/>
<!-- 注意此处使用sqlSessionFactoryBeanName,因为要等待datasource加载properties -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
或批量扫描包里的Mapper接口,自动创建代理对象,并在容器中注册。生成的bean id为Mapper接口的名字首字母小写
<context:property-placeholder location="classpath:db.properties"/>
<bean id="datasource" class="org.apache.tomcat.dbcp.dbcp2.BasicDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="datasource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>
<!-- XxxMapper.java和XxxMapper.xml需要同名同包。配置该项后MyBatis的package标签可以不用配置了。 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 多个包使用半角逗号分隔 -->
<property name="basePackage" value="com.example.mapper"/>
<!-- 注意此处使用sqlSessionFactoryBeanName,才不会在datasource前设置SqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
12. 逆向工程
MyBatis提供根据单表自动生成Mapper.java、Mapper.xml、VO等等。
mybatis-generator
13. 注解开发
首先还是需要配置sqlMapConfig.xml
,因为注解开发消除了mapper.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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://localhost/test?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value=""/>
</dataSource>
</environment>
</environments>
<!-- 指定扫描包 -->
<mappers>
<package name="com.tim.dao"/>
</mappers>
</configuration>
Dao:
public interface BrandDao {
@Select("SELECT * FROM tdb_brands")
List<Brand> findAllBrands();
@Insert("INSERT INTO tdb_brands (brand_name) VALUES (#{brand_name})")
void saveBrand(Brand brand);
@UPDATE("UPDATE tdb_brands SET brand_name = #{brand_name}) WHERE brand_id = #{brand_id}")
void updateBrand(Brand brand);
@Delete("DELETE FROM tdb_brands WHERE id = #{id}")
void deleteBrandById(int id);
}
有以下四个注解:
@Select
:代替<select>
@Update
:代替<update>
@Insert
:代替<insert>
@Delete
:代替<delete>
这四个注解的value为具体的SQL语句,占位符有两种
#{目标变量名}
:对于基本数据类型,不管目标变量名为何,直接使用方法变量作为值,如果是字符串类型自动添加引号。对于POJO类型,则是会调用方法变量中目标变量名对应的getter方法。${目标变量名}
:对于基本数据类型,需要使用value
作为目标变量名,否则识别为POJO。对于POJO类型,会调用方法变量中目标变量名对应的getter方法。不是采用PreparedStatement的占位符方式,而是直接修改Statement,直接替换内容,所以如果是字符串不会自动添加引号。
13.1 一对一查询
以查询商品及所对应品牌的场景为例:
品牌表格:
CREATE TABLE `tdb_brands` (
`brand_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`brand_name` varchar(40) NOT NULL DEFAULT '' COMMENT '所属品牌'
PRIMARY KEY (`brand_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
品牌POJO:
public class Brand {
private int brand_id;
private String brand_name;
// 省略getter setter
}
商品表格:
CREATE TABLE `tdb_goods` (
`goods_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`goods_name` varchar(150) NOT NULL COMMENT '商品名称',
`cate_id` smallint(5) unsigned NOT NULL,
`brand_id` smallint(5) unsigned NOT NULL,
`goods_price` decimal(15,3) unsigned NOT NULL DEFAULT '0.000',
PRIMARY KEY (`goods_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
商品POJO:
public class Goods {
private int goods_id;
private String goods_name;
private int cate_id;
private int brand_id;
private float goods_price;
// 省略getter setter
}
提供一个商品+品牌的封装类:
public class GoodsWithBrand extends Goods {
private Brand brand;
// 省略getter setter
}
品牌DAO:
public interface BrandDao {
@Select("SELECT * FROM tdb_brands WHERE brand_id = #{id}")
Brand findBrandById(int id);
}
重点是商品DAO:
public interface GoodsDao {
@Select("SELECT * FROM tdb_goods WHERE goods_id = #{id}")
@Results({
@Result(property = "goods_id", column = "goods_id", id = true),
@Result(property = "goods_name", column = "goods_name"),
@Result(property = "cate_id", column = "cate_id"),
@Result(property = "brand_id", column = "brand_id"),
@Result(property = "goods_price", column = "goods_price"),
@Result(property = "brand", column = "brand_id", javaType = Brand.class,
one = @One(select = "com.tim.dao.BrandDao.findBrandById",
fetchType = FetchType.DEFAULT))
})
GoodsWithBrand selectGoodsWithBrandById(int id);
}
其中商品的查询SQL仍然使用@Select
,对于品牌的一对一映射,需要配置@Results
标签,里面接收一个@Result
数组,@Result
中的property
对应结果POJO的字段,column
对应查询结果的字段,使用id
表示当前字段为主键。
对于结果POJO中的内嵌POJO,column
指明商品表中的外键字段名,添加javaType
属性指明具体类,添加one
属性表示一对一关系,里面的select属性接收一个DAO方法的完整方法名,MyBatis会以外键字段的值作为参数调用这个select语句,查出嵌套的对象。另外还可以指定一个fetchType
表示加载模式,DEFAULT为EAGER,即立即加载。LAZY为懒加载。
因为可以自动映射,所以可以省略非关键字段如下。因为column中的brand_id
在最后映射给了内嵌POJO,如果省略了@Result(property = "brand_id", column = "brand_id")
,则POJO中的brand_id
就会为0。
public interface GoodsDao {
@Select("SELECT * FROM tdb_goods WHERE goods_id = #{id}")
@Results({
@Result(property = "goods_id", column = "goods_id", id = true),
@Result(property = "brand_id", column = "brand_id"),
@Result(property = "brand", column = "brand_id", javaType = Brand.class,
one = @One(select = "com.tim.dao.BrandDao.findBrandById",
fetchType = FetchType.DEFAULT))
})
GoodsWithBrand selectGoodsWithBrandById(int id);
}
13.2 一对多查询
品牌表格:
CREATE TABLE `tdb_brands` (
`brand_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`brand_name` varchar(40) NOT NULL DEFAULT '' COMMENT '所属品牌'
PRIMARY KEY (`brand_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
品牌POJO:
public class Brand {
private int brand_id;
private String brand_name;
// 省略getter setter
}
商品表格:
CREATE TABLE `tdb_goods` (
`goods_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT,
`goods_name` varchar(150) NOT NULL COMMENT '商品名称',
`cate_id` smallint(5) unsigned NOT NULL,
`brand_id` smallint(5) unsigned NOT NULL,
`goods_price` decimal(15,3) unsigned NOT NULL DEFAULT '0.000',
PRIMARY KEY (`goods_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
商品POJO:
public class Goods {
private int goods_id;
private String goods_name;
private int cate_id;
private int brand_id;
private float goods_price;
// 省略getter setter
}
提供一个品牌+商品的封装类:
public class BrandWithGoods extends Brand {
private List<Goods> goods;
// 省略getter setter
}
商品DAO:
public interface GoodsDao {
@Select("SELECT * FROM tdb_goods WHERE brand_id = #{id}")
List<Goods> selectGoodsByBrandId(int id);
}
重点是品牌DAO:
public interface BrandDao {
@Select("SELECT * FROM tdb_brands WHERE brand_id = #{id}")
@Results({
@Result(property = "brand_id", column = "brand_id", id = true),
@Result(property = "goods", column = "brand_id", javaType = List.class,
many = @Many(select="com.tim.dao.GoodsDao.selectGoodsByBrandId"))
})
BrandWithGoods selectBrandWithGoodsById(int id);
}
与一对一不同的是,one
改成了many
,javaType
改成了List.class
,实质上思想还是根据一个字段值调用另一个查询方法,把结果映射到对应的字段上。
14. 以Map为输入参数
void updateTypes(@Param("types") Map types);
collection以types.key为遍历目标,也可以遍历types.value。遍历key取value时,使用#{types[${key}]}
q为value值。
<update id="updateTypes" parameterType="map">
<foreach collection="types.keys" item="key" index="index">
update shoes_info set content = #{types[${key}]} where type = #{key};
</foreach>
</update>
15. 以Map为输出参数
@MapKey("date")
Map<String, CustomObject> query();
<select id="query" resultType="com.example.CustomObject">
select val1, val2 from my_table
</select>
select语句指定resultType或resultMap,接口层添加@MapKey注解,value的值为CustomObject的一个属性,以这个属性为key,把返回的数据列表转为map。
16. Spring + MyBatis + Druid 批量执行语句
首先jdbc的url需要加上allowMultiQueries=true
参数。
然后druid如果配置了wall-filter,需要配置multiStatementAllow
和noneBaseStatementAllow
参数:
<bean id="wallFilter" class="com.alibaba.druid.wall.WallFilter">
<property name="dbType" value="mysql"/>
<property name="config" ref="wallConfig"/>
</bean>
<bean id="wallConfig" class="com.alibaba.druid.wall.WallConfig">
<property name="multiStatementAllow" value="true"/>
<property name="noneBaseStatementAllow" value="true"/>
</bean>
DruidDataSource再设置proxyFilters为我们自定义的Filter:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${db.driverClassName}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
<property name="initialSize" value="3"/>
<property name="minIdle" value="3"/>
<property name="maxActive" value="20"/>
<property name="maxWait" value="60000"/>
<!-- <property name="filters" value="stat,wall"/>-->
<property name="proxyFilters">
<list>
<ref bean="statFilter"/>
<ref bean="wallFilter"/>
</list>
</property>
</bean>
17. JSON字段自动转换
步骤
- 创建类继承自
BaseTypeHandler
,指定泛型类,实现字段转换的逻辑。 - 把实体类中对应成员变量的类型从String改为JSON对应的实体类型。
- 设置mybatis的type-handler的扫描包或具体类,例如
yybatisSqlSessionFactoryBean.setTypeHandlersPackage("")
或yybatisSqlSessionFactoryBean.setTypeHandlers()
public class ListTypeHandler extends BaseTypeHandler<List<Test>> {
private static final Logger LOGGER = LoggerFactory.getLogger(ListTypeHandler.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final TypeReference<List<T>> TYPE_REFERENCE = new TypeReference<List<T>>() {
};
/**
* 根据参数设置字段值
*/
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int columnIndex, List<Test> list, JdbcType jdbcType) throws SQLException {
if (CollectionUtils.isEmpty(list)) {
preparedStatement.setString(columnIndex, "[]");
} else {
try {
String json = OBJECT_MAPPER.writeValueAsString(list);
preparedStatement.setString(columnIndex, json);
} catch (JsonProcessingException e) {
LOGGER.error("序列化失败", e);
throw new ServiceException("序列化失败");
}
}
}
/**
* 根据字段名获取实体值
*/
@Override
public List<Test> getNullableResult(ResultSet resultSet, String columnLabel) throws SQLException {
String json = resultSet.getString(columnLabel);
if (StringUtils.isBlank(json)) {
return new ArrayList<>();
}
try {
return OBJECT_MAPPER.readValue(json, TYPE_REFERENCE);
} catch (IOException e) {
LOGGER.error("反序列化失败", e);
throw new ServiceException("反序列化失败");
}
}
/**
* 根据字段下标获取实体值
*/
@Override
public List<Test> getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
String json = resultSet.getString(columnIndex);
try {
return OBJECT_MAPPER.readValue(json, TYPE_REFERENCE);
} catch (IOException e) {
LOGGER.error("反序列化失败", e);
throw new ServiceException("反序列化失败");
}
}
/**
* 根据字段下标获取实体值
*/
@Override
public List<Test> getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
String json = callableStatement.getString(columnIndex);
try {
return OBJECT_MAPPER.readValue(json, TYPE_REFERENCE);
} catch (IOException e) {
LOGGER.error("反序列化失败", e);
throw new ServiceException("反序列化失败");
}
}
}