实际开发过程中很多时候并不需要总是在加载用户信息时就一定要加载他的订单信息。此时就是我们所说的延迟加载。
在一对多中,当有一个用户,它有个100个订单;在查询用户时,用户下的订单应该是,什么时候用,什么时候查询;在查询订单时,订单所属的用户信息应该是随着订单一起查询出来。
延迟加载:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
一对多,多对多:通常情况下采用延迟加载。
一对一(多对一):通常情况下采用立即加载。
注意:延迟加载是基于嵌套查询来实现的。
在 association
和 collection
标签中都有一个 fetchType
属性,通过修改它的值,可以修改局部的加载策略。
OrderMapper.xml
<!--
fetchType="lazy" : 延迟加载策略
fetchType="eager": 立即加载策略
-->
<resultMap id="orderMap2" type="com.renda.domain.Orders">
<id property="id" column="id"/>
<result property="ordertime" column="ordertime"/>
<result property="total" column="total"/>
<result property="uid" column="uid"/>
<association property="user" javaType="com.renda.domain.User"
select="com.renda.mapper.UserMapper.findById" column="uid" fetchType="lazy"/>
</resultMap>
<select id="findAllWithUser2" resultMap="orderMap2">
SELECT * FROM orders
</select>
在配置了延迟加载策略后,发现即使没有调用关联对象的任何方法,在调用当前对象的 equals
、clone
、hashCode
、toString
方法时也会触发关联对象的查询。
OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
List<Orders> allWithUser2 = orderMapper.findAllWithUser2();
for (Orders orders : allWithUser2) {
// 因为 Orders 的 toString 没有开启延迟加载
// 配置了延迟加载的关联对象 User 还是被打印出来
System.out.println(orders);
}
可以在配置文件 sqlMapConfig.xml
中使用 lazyLoadTriggerMethods
配置项覆盖掉上面四个方法。
<settings>
<!-- 所有 toString 方法都会触发延迟加载 -->
<setting name="lazyLoadTriggerMethods" value="toString()"/>
</settings>
在 MyBatis 的核心配置文件中可以使用 setting 标签修改全局的加载策略。
<settings>
<!-- 开启全局延迟加载功能 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 所有 toString 方法都会触发延迟加载 -->
<setting name="lazyLoadTriggerMethods" value="toString()"/>
</settings>
配置完全局延迟加载功能后,需要加载关联的对象就需要调用它的 toString
方法:
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> allWithOrder2 = userMapper.findAllWithOrder2();
for (User user : allWithOrder2) {
System.out.println(user);
// 需要用到用户关联的订单
System.out.println(user.getOrdersList());
}
注意:局部的加载策略优先级高于全局的加载策略;
所以,在开启全局延迟加载后,为了实现订单能立即加载关联的用户信息,就可以在局部开启立即加载策略:
<!--
fetchType="lazy" : 延迟加载策略
fetchType="eager": 立即加载策略
-->
<resultMap id="orderMap2" type="com.renda.domain.Orders">
<id property="id" column="id"/>
<result property="ordertime" column="ordertime"/>
<result property="total" column="total"/>
<result property="uid" column="uid"/>
<association property="user" javaType="com.renda.domain.User"
select="com.renda.mapper.UserMapper.findById" column="uid" fetchType="eager"/>
</resultMap>
<select id="findAllWithUser2" resultMap="orderMap2">
SELECT * FROM orders
</select>
当用户频繁查询某些固定的数据时,第一次将这些数据从数据库中查询出来,保存在缓存中。当用户再次查询这些数据时,不用再通过数据库查询,而是去缓存里面查询。减少网络连接和数据库查询带来的损耗,从而提高我们的查询效率,减少高并发访问带来的系统性能问题。
一句话概括:经常查询一些不经常发生变化的数据,使用缓存来提高查询效率。
像大多数的持久化框架一样,MyBatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。MyBatis 中缓存分为一级缓存,二级缓存。
一级缓存是 SqlSession
级别的缓存,是默认开启的。
所以在参数和 SQL 完全一样的情况下,我们使用同一个 SqlSession
对象调用一个 Mapper 方法,往往只执行一次 SQL,因为使用 SqlSession
第一次查询后,MyBatis 会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession
都会取出当前缓存的数据,而不会再次发送 SQL 到数据库。
资源目录 resources
下增加 log4j.properties
:
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### direct messages to file mylog.log ###
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=C:/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
### set log levels - for more verbose logging change 'info' to 'debug' ###
log4j.rootLogger=debug, stdout
pom.xml
中导入 log4j 的依赖,从而可以查看底层调用 JDBC 的 log:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.7</version>
</dependency>
编写代码验证 MyBatis 中的一级缓存:
@Test
public void testOneCache() throws IOException {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 根据 id 查询用户信息
// 第一次查询,查询的数据库
User user1 = userMapper.findById();
System.out.println(user1);
// 第二次查询,查询的是一级缓存
User user2 = userMapper.findById();
System.out.println(user2);
}
可以发现,虽然在上面的代码中查询了两次,但最后只执行了一次数据库操作,这就是 MyBatis 提供的一级缓存在起作用了。因为一级缓存的存在,导致第二次查询 id 为 1 的记录时,并没有发出 SQL 语句从数据库中查询数据,而是从一级缓存中查询。
一级缓存是 SqlSession
范围的缓存,执行 SqlSession
的 C(增加)U(更新)D(删除)操作,或者调用 clearCache()
、commit()
、close()
方法,都会清空缓存。
sqlSession
去执行 commit
操作(执行插入、更新、删除),清空 SqlSession
中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。sqlSession.clearCache()
手动清空一级缓存:
@Test
public void testOneCache() throws IOException {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 根据 id 查询用户信息
// 第一次查询,查询的数据库
User user1 = userMapper.findById();
System.out.println(user1);
// clearCache: 手动清空缓存
sqlSession.clearCache();
// 第二次查询,查询的是一级缓存
User user2 = userMapper.findById();
System.out.println(user2);
}
flushCache="true"
自动清空一级缓存:
<select id="findById" resultType="com.renda.domain.User" parameterType="int" flushCache="true">
SELECT * FROM `user` WHERE id = #{id}
</select>
二级缓存是 namspace
级别(跨 sqlSession
)的缓存,是默认不开启的。
实现二级缓存的时候,MyBatis 要求返回的 POJO 必须是可序列化的,也就是要求实现 Serializable 接口。
二级缓存的开启需要进行配置,配置方法很简单,只需要在映射 XML 文件配置 <cache/>
就可以开启二级缓存了。
<settings>
...
<!--
cacheEnabled 的取值默认为 true,所以这一步可以省略不配置。
为 true 代表支持二级缓存;为 false 代表不支持二级缓存。
-->
<setting name="cacheEnabled" value="true"/>
</settings>
<mapper namespace="com.renda.mapper.UserMapper">
<!-- 当前映射开启二级缓存 -->
<cache></cache>
<!--
根据 id 查询用户
useCache="true" 代表当前这个 statement 是使用二级缓存
-->
<select id="findById" resultType="com.renda.domain.User" parameterType="int" useCache="true">
SELECT * FROM `user` WHERE id = #{id}
</select>
</mapper>
public class User implements Serializable {
private static final long serialVersionUID = 7898016747305399302L;
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
private List<Orders> ordersList;
private List<Role> roleList;
...
}
@Test
public void testTwoCache() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession1 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
// 第一次查询
User user = userMapper1.findById();
System.out.println(user);
// 只有执行 sqlSession.commit 或者 sqlSession.close,那么一级缓存中内容才会刷新到二级缓存
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 第二次查询
User user2 = userMapper2.findById();
System.out.println(user2);
sqlSession2.close();
}
二级缓存是 mapper
映射级别的缓存,多个 SqlSession
去操作同一个 Mapper
映射的 sql
语句,多个 SqlSession
可以共用二级缓存,二级缓存是跨 SqlSession
的。
select
语句将会被缓存。insert
、update
和 delete
语句会刷新缓存。注意问题:MyBatis 的二级缓存因为是 namespace
级别,某个 namespace
的增删改只会刷新它自己的缓存,会导致不同 namespace
缓存了别的 namespace
的旧值,所以在进行多表查询时会产生脏读问题。
Redis
解决问题。这几年来注解开发越来越流行,MyBatis 也可以使用注解开发方式,这样我们就可以减少编写 Mapper 映射文件了。我们先围绕一些基本的 CRUD 来学习,再学习复杂映射多表操作。
@Insert
:实现新增,代替了@Delete
:实现删除,代替了@Update
:实现更新,代替了@Select
:实现查询,代替了@Result
:实现结果集封装,代替了@Results
:可以与@Result 一起使用,封装多个结果集,代替了@One
:实现一对一结果集封装,代替了 <association></association>
@Many
:实现一对多结果集封装,代替了public interface UserMapper {
/**
* 查询用户
*/
@Select("select * from user")
List<User> findAll();
/**
* 添加用户
*/
@Insert("insert into user(username,birthday,sex,address) values(#{username},#{birthday},#{sex},#{address})")
void save(User user);
/**
* 更新用户
*/
@Update("update user set username=#{username}, birthday=#{birthday}, sex=#{sex} where id = #{id}")
void update(User user);
/**
* 删除用户
*/
@Delete("delete from user where id = #{id}")
void delete(Integer id);
}
sqlMapConfig.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>
<!-- 加载 properties 文件 -->
<properties resource="jdbc.properties"></properties>
<settings>
<!-- 开启全局延迟加载功能 -->
<setting name="lazyLoadingEnabled" value="false"/>
<!-- 所有方法都会延迟加载 -->
<setting name="lazyLoadTriggerMethods" value="toString()"/>
<!--
因为 cacheEnabled 的取值默认就为 true,所以这一步可以省略不配置。
为 true 代表开启二级缓存;为 false 代表不开启二级缓存。
-->
<setting name="cacheEnabled" value="true"/>
</settings>
<!-- 设置别名 -->
<typeAliases>
<package name="com.renda.domain"/>
</typeAliases>
<!-- environments: 运行环境 -->
<environments default="development">
<environment id="development">
<!-- 当前的事务事务管理器是 JDBC -->
<transactionManager type="JDBC"></transactionManager>
<!-- 数据源信息 POOLED:使用 mybatis 的连接池 -->
<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 类所在的包 -->
<package name="com.renda.mapper"/>
</mappers>
</configuration>
public class MyBatisTest {
private SqlSessionFactory sqlSessionFactory;
private SqlSession sqlSession;
@Before
public void before() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
sqlSession = sqlSessionFactory.openSession();
}
@After
public void after(){
sqlSession.commit();
sqlSession.close();
}
/*
测试查询方法
*/
@Test
public void testSelect() {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> all = userMapper.findAll();
for (User user : all) {
System.out.println(user);
}
}
/*
测试添加方法
*/
@Test
public void testInsert(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setUsername("布莱尔");
user.setSex("女");
user.setBirthday(new Date());
user.setAddress("江苏");
mapper.save(user);
}
@Test
public void testUpdate(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = new User();
user.setUsername("柳岩");
user.setBirthday(new Date());
user.setSex("女");
user.setId();
mapper.update(user);
}
@Test
public void testDelete(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
mapper.delete();
}
}
在映射文件中通过配置 <resultMap>
、<association>
、<collection>
来实现复杂关系映射。
使用注解开发后,可以使用 @Results
、@Result
,@One
、@Many
注解组合完成复杂关系的配置。
@Results
:
@Result
注解,也可也使用 @Result
聚合。@Results ({@Result () , @Result() })
或 @Results (@Result () )
@Result
:
@Result
中属性介绍:column
- 数据库的列名,property
- 需要装配的属性名,one
- 需要使用的 @One
注解 @Result (one=@One) ()
,many
- 需要使用的 @Many
注解 @Result (many=@many) ()
@One
(一对一):
@One
属性介绍:select - 指定用来多表查询的 SQL Mapper,使用格式 - @Result(column="", property="", one=@One(select=""))
@Many
(一对多):
@Result(property="", column="", many=@Many(select=""))
需求:查询一个订单,与此同时查询出该订单所属的用户
SELECT * FROM orders;
SELECT * FROM `user` WHERE id = #{ 订单的 uid };
OrderMapper
接口
@Select("select * from orders")
@Results({
@Result(property="id",column = "id",id = true),
@Result(property="ordertime",column = "ordertime"),
@Result(property="total",column = "total"),
@Result(property="uid",column = "uid"),
@Result(property="user",column = "uid", javaType = User.class,
one = @One(select = "com.renda.mapper.UserMapper.findById",fetchType = FetchType.EAGER))
})
List<Orders> findAllWithUser();
UserMapper
接口
@Select("select * from user where id = #{uid}")
User findById(Integer uid);
测试代码
@Test
public void testOneToOne() {
OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
List<Orders> allWithUser = mapper.findAllWithUser();
for (Orders orders : allWithUser) {
System.out.println(orders);
}
}
需求:查询一个用户,与此同时查询出该用户具有的订单
SELECT * FROM `user`;
SELECT * FROM orders where uid = #{ 用户 id };
UserMapper
接口
@Select("select * from user")
@Results({
@Result(property="id",column="id",id=true),
@Result(property="username",column="username"),
@Result(property="birthday",column="birthday"),
@Result(property="sex",column="sex"),
@Result(property="address",column="address"),
@Result(property="ordersList",column="id",javaType=List.class,
many=@Many(select="com.renda.mapper.OrderMapper.findOrderByUid",fetchType=FetchType.LAZY))
})
List<User> findAllWithOrder();
OrderMapper
接口
@Select("select * from orders where uid = #{uid}")
List<Orders> findOrderByUid(Integer uid);
测试代码
@Test
public void testOneToMany(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allWithOrder = mapper.findAllWithOrder();
for (User user : allWithOrder) {
System.out.println(user);
System.out.println(user.getOrdersList());
}
}
需求:查询所有用户,同时查询出该用户的所有角色
SELECT * FROM `user`;
SELECT * FROM sys_role r INNER JOIN sys_user_role ur ON r.`id` = ur.`rid` WHERE ur.`uid` = #{ 用户 id };
UserMapper
接口
@Select("select * from user")
@Results({
@Result(property="id",column="id",id=true),
@Result(property="username",column="username"),
@Result(property="birthday",column="birthday"),
@Result(property="sex",column="sex"),
@Result(property="address",column="address"),
@Result(property="roleList",column="id",javaType=List.class,
many=@Many(select="com.renda.mapper.RoleMapper.findAllByUid")),
})
List<User> findAllWithRole();
RoleMapper
接口
@Select("SELECT * FROM sys_role r INNER JOIN sys_user_role ur ON ur.roleid = r.id WHERE ur.userid = #{uid}")
List<Role> findAllByUid(Integer uid);
测试代码
@Test
public void testManyToMany(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allWithRole = mapper.findAllWithRole();
for (User user : allWithRole) {
System.out.println(user);
System.out.println(user.getRoleList());
}
}
配置 SqlMapConfig.xml
文件开启二级缓存的支持
<settings>
...
<!--
因为 cacheEnabled 的取值默认就为 true,所以这一步可以省略不配置。
为 true 代表开启二级缓存;为 false 代表不开启二级缓存。
-->
<setting name="cacheEnabled" value="true"/>
</settings>
在 Mapper 接口中使用注解配置二级缓存
@CacheNamespace
public interface UserMapper {
...
}
测试代码
@Test
public void cacheTest(){
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user1 = userMapper1.findById();
System.out.println(user1);
// 关闭 sqlSession, 将内容从一级缓存刷新到二级缓存
sqlSession1.close();
User user2 = userMapper2.findById();
System.out.println(user2);
sqlSession2.close();
}
不管是一对一还是一对多 ,在注解配置中都有 fetchType
的属性
fetchType = FetchType.LAZY
表示懒加载fetchType = FetchType.EAGER
表示立即加载fetchType = FetchType.DEFAULT
表示使用全局配置注解开发和 XML 配置优劣分析