本人小白一枚,欢迎大家一起讨论学习,如有错误,还望大家指教。
Mybatis延迟加载策略
什么是延迟加载?
就是在我们进行级联操作时,需要的数据进行加载,不需要的数据就不加载,可以理解为按需加载,延迟加载又称懒加载。
延迟加载的优点: 先从单表查询,需要时再从关联表中去关联查询,大大提高了数据库性能,因为查询单表比关联查询多张表速度要快。
延迟加载的缺点: 因为只有当需要用到数据时才进行数据库查询,在存在大批量数据查询时,查询的工作要消耗很多时间,可能会造成用户等待时间过长,造成用户体验下降。
使用延迟加载的时机: 数据表有四种对应关系,有一对多、多对一、一对一、多对多。当一对多或者多对多时,我们通常采用延迟加载。当多对一或者一对一时我们通常采用立即加载。
演示案例:
现有两张表,account账户表和user用户表,一个用户可以有多个账户,但一个账户只能有一个用户,也就是用户和账户是一对多的关系,而账户和用户是一对一的关系。当我们查询账户信息时关联查询用户信息,如果只查账户信息即可满足要求就不用再去查询用户信息了,当我们查询用户信息再想查询用户信息时,把对用户信息的按需查询这就是延迟加载了。
- account账户表以及user用户表的详情如下图所示,并在工程中创建对应的实体类,这里我特意将user实体类中的字段与表中的字段没有对应。
public class Account {
private int id;
private int uid;
private Double money;
// 用来存储对应的用户信息
private User user;
// get/set等方法自行补齐............
}
public class User {
private int userId;
private String username;
private LocalDate userBirthday;
private String userSex;
private String userAddress;
// get/set等方法自行补齐............
}
- 在账户的持久层接口中添加findAll()的方法,同时在用户的持久层接口中添加findById()方法
public interface AccountMapper {
/**
* 查询所有账户并查询所属用户信息
* @return
*/
List<Account> findAll();
}
public interface UserMapper {
/**
* 根据用户id查询用户信息
* @param id 用户主键
* @return
*/
public User findById(int id);
}
- 在账户的映射配置文件和用户的映射配置文件添加如下内容,这里要注意select属性,该属性的内容是我们要调用的select映射的id。column属性的内容是我们传递给select映射的参数。
<mapper namespace="mapper.AccountMapper">
<resultMap id="accountUserMap" type="account">
<id property="id" column="id"/>
<result property="uid" column="uid"/>
<result property="money" column="money"/>
<association property="user" javaType="user" select="mapper.UserMapper.findById" column="uid"/>
</resultMap>
<select id="findAll" resultMap="accountUserMap">
SELECT * FROM account
</select>
</mapper>
<mapper namespace="mapper.UserMapper">
<cache readOnly="true"></cache>
<resultMap id="userMap" type="user">
<id column="id" property="userId"/>
<result column="username" property="username"/>
<result column="address" property="userAddress"/>
<result column="sex" property="userSex"/>
<result column="birthday" property="userBirthday"/>
</resultMap>
<select id="findById" resultMap="userMap" parameterType="int">
SELECT * FROM user WHERE id = #{userId}
</select>
</mapper>
- 在主配置文件开启延迟加载,这两个参数的设置我们在下面会具体叙述
<!--开启懒加载-->
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
- 添加测试方法并查看测试结果,当我们只调用了账户信息,没有查看用户信息时,我们可以看到,用户信息没有加载。
public class Demo {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
@Before
public void init() throws Exception{
in = Resources.getResourceAsStream("MapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
}
@Test
public void test1() {
AccountMapper accountMapper = session.getMapper(AccountMapper.class);
List<Account> accounts = accountMapper.findAll();
accounts.forEach(item -> System.out.println(item.getId()));
}
@After
public void destory() throws Exception {
session.commit();
session.close();
in.close();
}
}
- 添加测试方法并查看测试结果,当我们查看用户的信息时,他才会对用户的信息进行加载。
@Test
public void test1() {
AccountMapper accountMapper = session.getMapper(AccountMapper.class);
List<Account> accounts = accountMapper.findAll();
accounts.forEach(item -> System.out.println(item.getUser()));
}
下面我们具体叙述下lazyLoadingEnabled
和aggressiveLazyLoading
属性。
lazyLoadingEnabled
属性的含义是开启全局懒加载的开关,也就是真正开启懒加载的属性只有lazyLoadingEnabled这个属性,默认值为false。aggressiveLazyLoading
可以控制具有懒加载特性对象的属性的加载情况。true表示如果对具有懒加载特性的对象任意的调用都会导致懒加载,而false表示只有调用具有懒加载特性的属性的get方法时,才会进行懒加载,这个属性在3.4.1(包含)之前默认都为true,之后默认为false。
当我们不想将全部的级联操作都设计成懒加载的时候怎么办呢?
Mybaits在association
和collection
标签提供fetchType
属性,可以使用该属性覆盖全局的懒加载状态,eager
表示这个级联不使用懒加载即使用立即加载,lazy
表示使用懒加载。注意:discriminator
没有fetchType
属性,因此它只能根据全局的懒加载的配置。同时也要注意,我们可以直接使用fetchType
这个属性来控制懒加载,不用配置全局的也是可以的。
Mybatis缓存
像大多数的持久层框架一样,Mybatis框架也提供了缓存策略,通过缓存策略来减少对数据的查询次数,从未提高性能。Mybatis中的缓存分为一级缓存和二级缓存。一级缓存不需要任何配置,Mybatis默认开启一级缓存。
- 测试一级缓存并查看结果,可以看到只执行了一次查询,第二次是从一级缓存中读取出来的没有查询数据库,并且这两个对象是同一个地址
@Test
public void test3() {
UserMapper userMapper = session.getMapper(UserMapper.class);
User user1 = userMapper.findById(46);
System.out.println(user1);
User user2 = userMapper.findById(46);
System.out.println(user2);
System.out.println(user1 == user2);
}
- 一级缓存是SqlSession范围的缓存,当调用SqlSession的修改、添加、删除、commit()方法、close()方法等,就会清空一级缓存。其实一级缓存就是一个Map,例如我们第一次查询id为1的用户信息时,先去找缓存中是否有id为1的用户信息,如果没有,从数据查询用户。将得到的用户信息存到一级缓存中,如果SqlSession去执行commit操作(执行插入、更新、删除等)就会清空一级缓存,这样做的目的是存储最新的信息,避免脏读。
- 清除一级缓存测试并查看结果,就会发现当我们清除缓存之后,它在查询缓存中没有时就会去数据库中去查询
@Test
public void test3() {
UserMapper userMapper = session.getMapper(UserMapper.class);
User user1 = userMapper.findById(46);
System.out.println(user1);
// 清除缓存
session.clearCache();
User user2 = userMapper.findById(46);
System.out.println(user2);
System.out.println(user1 == user2);
}
- 二级缓存是Mapper映射级别的缓存,由同一个SqlSessionFactory对象创建的多个SqlSession去操作同一个Mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
- 第一步:在主配置文件开启耳机缓存,因为cacheEnabled默认就是true,所以这一步也可以省略不写,true表示开启,false表示关闭。
<settings>
<!--开启二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
- 第二步:配置相关的Mapper映射文件,<cache>标签当前这个mapper映射将使用二级缓存,区分的标准就看mapper的namespace值。
<?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="mapper.UserMapper">
<!--开启二级缓存的支持-->
<cache></cache>
</mapper>
- 第三步:配置statement上面的
useCache
属性,如果每次查询都需要最新的数据,要设置成useCache=“false”,禁用二级缓存。
<resultMap id="userMap" type="user">
<id column="id" property="userId"/>
<result column="username" property="username"/>
<result column="address" property="userAddress"/>
<result column="sex" property="userSex"/>
<result column="birthday" property="userBirthday"/>
</resultMap>
<select id="findById" resultMap="userMap" parameterType="int" useCache="true">
SELECT * FROM user WHERE id = #{userId}
</select>
</resultMap>
- 第四步:要让实体类实现Serializable序列化接口,具体原因在下面叙述。
public class User implements Serializable {
private int userId;
private String username;
private LocalDate userBirthday;
private String userSex;
private String userAddress;
// get/set等方法自行补齐............
}
- 第五步:测试代码并查看结果。
@Test
public void test4() {
SqlSession sqlSession1 = factory.openSession();
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = userMapper1.findById(46);
sqlSession1.close();
SqlSession sqlSession2 = factory.openSession();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.findById(46);
System.out.println(user1 == user2);
}
总结:可能大家对于上面的案例有两个问题,一个就是为什么实体类要实现Serializable接口,还有就是为什么打印的结果是false。原因就是Mybatis使用SerializedCache序列化缓存来实现可读写缓存类,并通过反序列化从缓存获取数据时,得到的当然是一个新的实例。如果配置成只读缓存,Mybatis就会使用Map来存储缓存值,这种情况下,从缓存中获取的对象就是同一个实例。如何配置只读缓存呢?在<cache>标签有个readOnly属性,将readOnly设置为true表示将缓存设置成只读缓存。只读缓存将对所有调用者返回同一个实例,因为对象没有进行序列化,所以速度最快,可写的缓存将通过反序列化返回一个缓存对象,速度慢,并且得到的对象是一个新的对象,线程安全。Mybatis的二级缓存默认就是可写缓存,即readOnly=false。那为什么使用序列化缓存呢?因为当我们将对象序列化成二进制在缓存时,节省了内存,并且反序列化得到缓存时,线程是安全的。
使用Mybatis注解实现基本的CRUD
- 这里同样使用上面的实体类User,注意这里实体类的属性名和表中的列名不一致。
public class User implements Serializable {
private int userId;
private String username;
private Date userBirthday;
private String userSex;
private String userAddress;
// get/set等方法自行补齐............
}
- 使用注解方式开发持久层接口
public interface UserMapper {
/**
* 查询所有用户
*/
@Results(id = "userMap", value={
@Result(id = true, property = "userId", column = "id"),
@Result(property = "username", column = "username"),
@Result(property = "userAddress", column = "address"),
@Result(property = "userSex", column = "sex"),
@Result(property = "userBirthday", column = "birthday")
})
@Select("SELECT * FROM user")
List<User> findAll();
/**
* 保存用户信息,同时将插入的主键保存到实体中,注意这里的主键是自增长的
*/
@Insert("INSERT INTO user(username, address, sex, birthday) VALUES(#{username}, #{userAddress}, #{userSex}, #{userBirthday})")
@SelectKey(keyColumn = "id", keyProperty = "userId", resultType = int.class, before = false, statement = {"SELECT last_insert_id()"})
int save(User user);
/**
* 更新用户信息
*/
@Update("UPDATE user set username=#{username}, address=#{userAddress}, sex=#{userSex}, birthday=#{userBirthday} WHERE id=#{userId}")
int update(User user);
/**
* 多条件查询,Mybatis提供了注解方式的动态SQL,在<script>标签中声明动态SQL
*/
@Select({"<script>",
"SELECT * FROM user",
"<where>",
"<if test=\"username != null and username != ''\">",
"AND username LIKE \"%\"#{username}\"%\"",
"</if>",
"<if test=\"userSex != null and userSex != ''\">",
"AND sex LIKE \"%\"#{userSex}\"%\"",
"</if>",
"</where>",
"</script>"})
@ResultMap("userMap")
List<User> find(User user);
/**
* 删除用户信息
*/
@Delete("DELETE FROM user WHERE id=#{id}")
int delete(int id);
/**
* 查询记录的总条数
*/
@Select("SELECT COUNT(*) FROM user")
int findTotal();
/**
* 根据id查询用户信息
*/
@ResultMap("userMap")
@Select("SELECT * FROM user WHERE id=#{id}")
User findById(int id);
}
- 编写主配置文件
<?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 resource="dp.properties"/>
<!--注册别名-->
<typeAliases>
<package name="entity"/>
</typeAliases>
<!--配置数据源-->
<environments default="mysql">
<environment id="mysql">
<transactionManager type="JDBC"/>
<dataSource type="datasource.DruidDataSourceFactory">
<property name="driverClass" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<!--注册映射文件-->
<mappers>
<package name="mapper"/>
</mappers>
</configuration>
使用注解实现复杂关系映射
使用注解实现一对一复杂关系映射及延迟加载:加载账户信息时并加载账户的用户的信息,使用注解方式
- 在Account实体类添加User类型的属性
- 添加账户的持久层接口(注解方式)
public interface AccountMapper {
@Results(id = "accountMap", value = {
@Result(id=true, column = "id", property = "id"),
@Result(column = "uid", property = "uid"),
@Result(column = "money", property = "money"),
@Result(column = "uid", property = "user", one=@One(select = "mapper.UserMapper.findById", fetchType = FetchType.LAZY))
})
@Select("SELECT * FROM account")
List<Account> findAll();
@Select("SELECT * FROM account WHERE uid=#{uid}")
List<Account> findByUid(int uid);
}
使用注解实现一对多复杂关系映射:查询用户信息时,也要查询出关联的账户列表,使用注解方式实现
- 在User实体类中添加Account集合的属性
- 在用户持久层添加对应的注解
- 在账户的持久层添加对应的注解
mybatis基于注解的二级缓存 - 在主配置文件开启二级缓存
<!--开启二级缓存-->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
- 在持久层接口是使用注解配置二级缓存