1. 延迟加载
1.1 概念
- 在需要用到数据时才进行加载,不需要用到数据时就不加载数据。也称作懒加载
- 好处:先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能
- 缺点:在大批量数据查询时,由于查询会耗时,可能导致用户等待时间变长,影响用户体验
其中:mybatis的association、collection标签具备延迟加载功能
及时加载:一次加载所有数据。
1.2 一对一实现延时加载
以账户表和用户表一对一关系为例
需求:查询账户时只查询到账户,只有使用账户查询对应的用户时才查询用户
1.2.1 创建项目和创建实体类
按照“mybatis-CERD操作”里的架构创建项目,添加pom依赖、添加SqlMapConfig.xml、log4j.properties、jdbc.properties配置文件。引入“mybatis-多表查询”中的User类、Account类
1.2.2 创建IUserDao接口和IUserDao.xml
IUserDao接口
public interface IUserDao {
/*根据用户id查询用户,一个账户只对应一个用户*/
User findUserById(int id);
}
IUserDao.xml映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namesppace名称空间,用于定义是哪个类的映射文件,这里需要写映射接口的类全名-->
<mapper namespace="com.azure.dao.IUserDao">
<select id="findUserById" parameterType="int" resultType="user">
select * from user where id=#{id}
</select>
</mapper>
1.2.3 创建IAccount接口和映射配置文件
在一对一关系中使用延迟加载,需要利用到association标签的select属性和column属性调用延迟加载的方法
- select属性:格式:接口类全名.方法名,用于设置需要查询一对一关系对应的表数据时执行哪个接口的哪个方法
- column属性:如果使用select标签,则column用于将指定的数据传递给select方法作为参数。如果参数有多个,则使用集合形式,如column="{参数1=accountsMap的某column字段,参数2=accountsMap的某column字段,…}"
- fetchType=“lazy”:局部开启延迟加载,会覆盖全局延迟加载设置。默认不开启。如果全局延迟加载已设置,这里可省略不设置
<?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.azure.dao.IAccountDao">
<!--返回集的类型是Account类型,里面需封装account表数据和User对象-->
<resultMap id="accountsMap" type="account">
<!--先建立account对象属性与account表字段的映射关系-->
<id property="accountId" column="accountId"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!--建立user对象与User表字段的映射关系-->
<!--
使用association标签标示一对一关系
- property:一对一关系的对应对象属性名(本例是指账户对象对应的user对象属性);
- JavaType:对应对象属性类型
- column:外键字段
配置延迟加载
- select:格式:接口类全名.方法名,用于设置需要查询一对一关系对应的表数据时执行哪个接口的哪个方法
- column:如果使用select标签,则column用于将指定的数据传递给select方法作为参数
- fetchType="lazy":局部开启延迟加载,会覆盖全局延迟加载设置。默认不开启。如果全局延迟加载已设置,这里可省略不设置
-->
<association property="user" javaType="user" column="uid" select="com.azure.dao.IUserDao.findUserById" fetchType="lazy"></association>
</resultMap>
<!--使用resultMap明确一对一关系-->
<select id="findAll" resultMap="accountsMap">
select * from account
</select>
</mapper>
1.2.4 测试类
public class AccountDaoTest {
private InputStream is;
private SqlSession session;
private IAccountDao accountDao;
/**
* 每次执行Junit前都会执行
* @throws IOException
*/
@Before
public void before() throws IOException {
is = Resources.getResourceAsStream("SqlMapConfig.xml");
session = new SqlSessionFactoryBuilder().build(is).openSession();
accountDao = session.getMapper(IAccountDao.class);
}
/**
* 每次执行Junit后都会执行,提交事务和关闭资源
* @throws IOException
*/
@After
public void close() throws IOException {
//手动提交事务
session.commit();
//关闭资源
session.close();
is.close();
}
@Test
public void findAll() {
List<Account> list = accountDao.findAll();
for (Account account : list) {
System.out.println(account.getAccountId() +"--" + account.getMoney());
//使用账户查询用户信息,此时才会查询用户表,实现延迟加载
System.out.println(account.getUser().getUsername());
}
}
}
注意:如果测试类直接打印list,由于Account类中包含User对象,此时系统会自动查询user,无法显示延迟加载的效果。实际上是有实现延迟加载。
1.2.5 延迟加载支持的开启
1.2.5.1 使用延迟加载全局开关进行开启
开启方式:在主配置文件中设置settings标签,将lazyLoadingEnabled的属性值设置为true
1.2.5.2 使用局部延迟加载
开启方式:在dao映射配置文件的<association>
或<collection>
中设置fetchType属性值为lazy
1.2.5.3 两种延迟加载方式的取舍
一般只要开启全局延迟加载即可。如果只是局部需要延迟加载,那可以只配局部延迟加载。
1.3 一对多实现延迟加载
以User与Account为一对多关系为例
需求:查询用户时候,只查询用户信息; 使用用户的账户信息时候才查询账户信息
1.3.1 dao接口
1.3.1.1 IAccountDao接口
/*根据uid获得账户*/
List<Account> findByUid(int uid);
1.3.1.2 IUserDao接口
/*查询所有用户*/
List<User> findAll();
1.3.2 dao映射文件
1.3.2.1 IAccountDao.xml
<!--根据用户id查询账户信息-->
<select id="findByUid" resultType="Account">
select * from account where UID=#{uid}
</select>
1.3.2.2 IUserDao.xml
在一对多关系中使用延迟加载,需要利用到collection标签的select属性和column属性调用延迟加载的方法
- select属性:格式:接口类全名.方法名,用于设置需要查询一对多关系对应的表数据时执行哪个接口的哪个方法
- column属性:如果使用select标签,则column用于将指定的数据传递给select方法作为参数。如果参数有多个,则使用集合形式,如column="{参数1=accountsMap的某column字段,参数2=accountsMap的某column字段,…}"
- fetchType=“lazy”:局部开启延迟加载,会覆盖全局延迟加载设置。默认不开启。如果全局延迟加载已设置,这里可省略不设置
<resultMap id="userMap" type="user">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="birthday" column="birthday"></result>
<result property="sex" column="sex"></result>
<result property="address" column="address"></result>
<!--
select:用于指定延迟加载的数据进行调用的方法
column:在select标签存在的情况下,column用于传递方法所需参数,参数是userMap中的column值。如果参数有多个,采用集合方式
-->
<collection property="accounts" ofType="account" column="id" select="com.azure.dao.IAccountDao.findByUid" fetchType="lazy"></collection>
</resultMap>
<select id="findAll" resultMap="userMap">
select * from user
</select>
1.3.3 测试类
@Test
public void findAll() {
List<User> list = userDao.findAll();
for (User user : list) {
System.out.println(user.getId() +"--"+ user.getUsername());
//利用用户获得账户信息,此时才会加载对应的方法查询,实现延迟加载
System.out.println(user.getAccounts());
}
}
注意:如果测试类直接打印list,由于User类中包含List<Account>
对象,此时系统会自动查询account表,无法显示延迟加载的效果。实际上是有实现延迟加载。
2. mybatis缓存机制
Mybatis 提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。Mybatis 中缓存分为一级缓存,二级缓存。
2.1 一级、二级缓存
- 一级缓存
- 基于SqlSession的缓存,不能跨sqlSession;
- mybatis自动维护,无法认为影响哪些数据会存入一级缓存;
- 关闭sqlSession后一级缓存会失效;
- 效果:第一次查询会到数据库查询,然后将查询结果存储到一级缓存中。之后的查询会先到缓存中查找,找到就不查询数据库,找不到再到数据库查询。
- 二级缓存
- 基于映射文件的缓存,缓存范围比一级缓存更大。不同的sqlsession可以操作同一个 Mapper 映射的 sql 语句、访问二级缓存的内容。可以跨sqlSession
- 哪些数据放入二级缓存需要自行指定。一般存入二级缓存的数据不会经常修改;
- 步骤:
- 开启mybatis的二级缓存:在主配置文件中的settings中配置cacheEnabled属性为true(默认已经开启)
<settings>
<!--配置二级缓存,默认开启,可选配置-->
<setting name="cacheEnabled" value="true"></setting>
</settings>
- 哪些映射文件中的SQL查询的结果需要放入二级缓存,需要在映射文件中配置
<cache/>
,并在需要使用二级缓存的方法中配置userCache=true
- 实体类实现可序列化接口(Serializable)
2.2 Redis和Mybatis的缓存使用场景
Redis可以集群缓存;Mybatis只适合单机缓存,且缓存不会持久化。故可以使用Mybatis操作redis
3. Mybatis注解开发
3.1 注解说明
@Insert:实现新增
@Update:实现更新
@Delete:实现删除
@Select:实现查询
@Result:实现结果集封装
@Results:可以与@Result 一起使用,封装多个结果集
@One:实现一对一结果集封装
@Many:实现一对多结果集封装
@Param 当方法参数有多个时候,建立sql语句中的占位符参数值与方法参数的映射关系。
3.2使用注解实现CRUD操作
以IUserdao实现CRUD操作为例
3.2.1 dao接口
- 直接使用注释配置sql语句
public interface IUserDao {
/*实现单表查询功能*/
@Select("select * from user where id=#{id}")
User findById(int id);
/*实现修改功能,里面的参数需要对应实体类的属性,mybatis会自动将参数赋值到参数中*/
@Update("update user set username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} where id=#{id}")
void update(User user);
/*实现新增功能,mybatis会自动封装User*/
@Insert("insert into user values (null,#{username},#{birthday},#{sex},#{address})")
void save(User user);
/*实现删除功能*/
@Delete("delete from user where id=#{id}")
void delete(int id);
3.2.2 测试类
public class UserDaoTest {
private InputStream is;
private SqlSession session;
private IUserDao userDao;
/**
* 每次执行Junit前都会执行
* @throws IOException
*/
@Before
public void before() throws IOException {
is = Resources.getResourceAsStream("SqlMapConfig.xml");
session = new SqlSessionFactoryBuilder().build(is).openSession();
userDao = session.getMapper(IUserDao.class);
}
/**
* 每次执行Junit后都会执行,提交事务和关闭资源
* @throws IOException
*/
@After
public void close() throws IOException {
//手动提交事务
session.commit();
//关闭资源
session.close();
is.close();
}
@Test
public void find() {
User user = userDao.findById(48);
System.out.println(user);
}
@Test
public void update(){
User user = new User();
user.setId(51);
user.setUsername("大狗");
user.setBirthday(new Date());
user.setSex("男");
user.setAddress("北极");
userDao.update(user);
}
@Test
public void insert(){
User user = new User();
user.setUsername("大狗");
user.setBirthday(new Date());
user.setSex("男");
user.setAddress("北极");
userDao.save(user);
}
@Test
public void delete() {
userDao.delete(51);
}
}
3.2.3 多参数的CRUD操作
- sql语句中需要传递两个及以上参数时,需要使用多参数CRUD操作
- 以查询为例,其余操作的写法与此雷同
/*多参数实现单表查询功能*/
//报错org.apache.ibatis.exceptions.PersistenceException:
// Parameter 'start' not found. Available parameters are [arg1, arg0, param1, param2]
@Select("select * from user limit #{start},#{length}")
List<User> findByPage (int start, int length);
上述代码报错的原因:代码没有定义start和length,mybatis不会识别出#{start}是要传入start还是length,所以要指定占位符要传入哪个参数
正确写法1
虽然代码没有定义start和length,但是根据报错可知,mybatis有默认的参数定义,arg0、arg1…,表明第一个参数、第二个参数…那么可以通过这种已定义的参数名进行参数值传入
/*多参数解决方法1*/
@Select("select * from user limit #{arg0},#{arg1}")
List<User> findByPage (int start, int length);
正确写法2
既然原来的代码报错的原因是没有定义参数名,如果我不改sql语句,如何实现功能。mybatis可以通过注解@Param
定义参数,注意:注解定义的参数名要与sql语句占位符的名称保持一致
/*多参数解决方法2,使用注解@Param定义方法参数
* 注意,注解定义的参数名要与sql语句占位符的名称保持一致*/
@Select("select * from user limit #{start},#{length}")
List<User> findByPage2 (@Param("start") int start,@Param("length") int length);
3.3 注解实现一对一映射及延迟加载
以Account与User的一对一关系为例
3.3.1 dao层接口
3.3.1.1 IUserDao
public interface IUserDao {
/*实现查询用户功能*/
@Select("select * from user where id=#{id}")
User findById(int id);
}
3.3.1.2 IAccountDao
配置一对一关系的注解标签
public interface IAccountDao {
/**
* 使用注释方法实现一对一和一对多关系
* @Results 建立多个查询列与对象属性的映射关系,格式:@Results({@Result(...),@Result(...),...})。如果查询列只有一个,大括号可以省略
* @Result 建立每一个查询列与对象属性的映射关系
* - id 标记主键字段,默认为false.如果是主键字段,手动设置为true
* - property 对象属性
* - column 对象属性对应的查询列
* - javaType 对象属性类型,注意写法:类名.class。与配置文件写法不同。可以省略
* - one 此属性表示一对一关系
* @One one属性的值的类型,就是一个注解类型,表示一对一
* - select 延迟加载查询。对应用户接口中,根据用户id查询用户的方法,格式:接口类全名.方法名。
* - fetchType 是否开启延迟加载支持
*/
@Select("select * from account")
@Results({
@Result(id = true,property = "accountId",column = "accountId"),
@Result(property = "uid",column = "uid"),
@Result(property = "money",column = "money"),
//配置一对一关系
@Result(property = "user",column = "uid",javaType = User.class,
one = @One(select = "com.azure.dao.IUserDao.findById",fetchType= FetchType.LAZY))
})
List<Account> findAll();
}
注意事项
- 注解写法与配置文件写法有细微的差别,必须要留意写法的不同。例如,注解写法只有javaType属性,而配置文件有javaType和ofType两种属性。等等。
3.3.2 小结
3.4 注解实现一对多映射及延迟加载
以用户与账户一对多为例
3.4.1 dao接口
3.4.1.1 IAccountDao
/*
根据用户id查询账户
*/
@Select("select * from account where uid=#{uid}")
List<Account> findByUserId(int uid);
3.4.1.2 IUserDao
配置一对多关系
/**
* 使用注释方法实现一对多关系
* @Results 建立多个查询列与对象属性的映射关系,格式:@Results({@Result(...),@Result(...),...})。如果查询列只有一个,大括号可以省略
* @Result 建立每一个查询列与对象属性的映射关系
* - id 标记主键字段,默认为false.如果是主键字段,手动设置为true
* - property 对象属性
* - column 对象属性对应的查询列
* - javaType 对象属性类型。与配置文件写法不同,没有ofType属性。且属性类型是List,javaType的值为List.class,而不是User!可以省略
* - many 此属性表示一对多关系
* @Many many属性的值的类型,就是一个注解类型,表示一对多
* - select 延迟加载查询。对应用户接口中,根据用户id查询用户的方法,格式:接口类全名.方法名。
* - fetchType 是否开启延迟加载支持
*/
@Select("select * from user")
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "username",column = "username"),
@Result(property = "birthday",column = "birthday"),
@Result(property = "sex",column = "sex"),
@Result(property = "address",column = "address"),
//配置一对多关系
@Result(property = "accounts",column = "id",javaType = List.class,
many = @Many(select = "com.azure.dao.IAccountDao.findByUserId",fetchType = FetchType.LAZY))
})
List<User> findAllUsers();
疑问
为何配置一对一关系中,@Result(property = "user",column = "uid",javaType = User.classs,one = @One(...)
,column的值是uid?
解答:
1、column是传入给@One中方法的参数,而One的方法是findById,所需要的参数是id。
2、但是要注意,findById查询的是user表,user表自然有id列,而column是当前account表的查询列,account表只有accountId、uid、money三个查询列,没有id列。如果此时column的值写成id,mybatis没有从account表中找到id列,就会报错java.lang.NullPointerException。
3、留意到user表的id和account表的uid是主外键关系,可以通过uid传入id值,所以这里写的是uid。
4、一对多关系的column值同理