1,理解Mybatis多对多关系
2,掌握Mybatis的延迟加载
3,掌握Mybatis缓存
4,掌握逆向工程生成mapper和实体类
通过前面的学习,我们已经掌握了 Mybatis 中一对一,一对多,多对多关系的配置及实现,可以实现对象的关联查询。实际开发过程中有时候我们并不需要在加载用户信息时,就加载他的账户信息。 而是在使用用户账号的时候,再向数据库查询,此时就是我们所说的延迟加载。
即时加载:
// 条件查询
@Test
public void findAll() throws Exception {
// SELECT u.*,a.* FROM USER u LEFT JOIN account a ON a.UID=u.id;
// 执行userDao.findAll()方法,执行查询用户、账户信息,一次加载所有的数据,这种叫做及时加载。
List<User> list = userDao.findAll();
for(User u : list){
System.out.println(u);
System.out.println(u.getAccounts());
}
}
延迟加载:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载.
好处: 先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
坏处: 因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
如何实现延迟加载:前面实现多表操作时,我们使用了resultMap 来实现一对一,一对多,多对多关系的操作。主要是通过 association、 collection 实现一对一及一对多映射。 association、 collection 具备延迟加载功能。
思考:什么是及时加载?什么是延时加载?
及时加载:一次加载所有数据。
延迟加载:也叫做懒加载,再需要用到数据时候再查询数据。
需求:查询Account账户信息时候,也要显示User用户信息,但User用户信息用到的时候再向数据库发送查询语句。
数据库中实现:
-- 一对一延迟加载
-- 需求:查询账户,同时也要显示用户。但用户信息是再用到的时候再查询.
-- 实现过程:
-- 1) 查询账户
SELECT * FROM account
-- 3) 使用用户对象数据时候,查询用户
SELECT * FROM USER WHERE id=46
public class Account {
private int accountId;
private int uid;
private double money;
// 一个账户对应一个用户
private User user;
}
public class User {
private int id;
private String username;
private Date birthday;
private String sex;
private String address;
// 一个用户,对应多个账户
private List<Account> accounts;
}
public interface AccountMapper {
/**
* 查询账户
*/
List<Account> findAll();
}
public interface UserMapper {
/**
* 根据用户id查询
*/
User findById(int id);
}
<mapper namespace="com.itdfbz.dao.UserMapper" >
<select id="findById" resultType="user">
select * from user where id=#{id}
select>
mapper>
<mapper namespace="com.itdfbz.dao.AccountMapper">
<resultMap id="accountResultMap" type="account">
<id property="accountId" column="accountId">id>
<result property="uid" column="uid">result>
<result property="money" column="money">result>
<association property="user" javaType="user" column="uid"
select="com.itdfbz.dao.UserMapper.findById">association>
resultMap>
<select id="findAll" resultMap="accountResultMap">
SELECT * FROM account
select>
mapper>
@Test
public void test1() {
AccountMapper accountMapper = session.getMapper(AccountMapper.class);
List<Account> accountList = accountMapper.findAll();
for (Account account : accountList) {
System.out.println(account.getAccountId());
}
}
执行结果:
发现,我们在测试中,只输出了账户信息,并没有使用用户信息,但还是查询了用户,并没有应用懒加载。如何解决?
查看mybatis官网
http://www.mybatis.org/mybatis-3/zh/configuration.html
打开页面,找到settings
然后在SqlMapConfig.xml中配置开启懒加载支持
最后,再次执行测试
1.测试类中没有使用Account账户的User信息
@Test
public void find() throws Exception{
List<Account> list = accountDao.findAccounts();
for (Account a : list) {
System.out.println(a.getAccountId() + ","+a.getUid() + "," +a.getMoney());
}
}
测试结果
2.测试类中有使用Account账户的User信息
@Test
public void test1() {
AccountMapper accountMapper = session.getMapper(AccountMapper.class);
List<Account> accountList = accountMapper.findAll();
for (Account account : accountList) {
System.out.println(account.getAccountId()+"----"+account.getUser());
}
}
测试结果: 可以看到使用对象数据的时候,才向数据库发送查询的SQL
(1) 注释SqlMapConfig.xml中全局懒加载的的开启
(2) 修改映射文件
通过打印的SQL可以看到,懒加载一样生效:
通过本示例,我们可以发现 Mybatis 的延迟加载对于提升软件性能这是一个不错的手段。实现的关键: association 的配置
<resultMap id="accountResultMap" type="account">
<id property="accountId" column="accountId">id>
<result property="uid" column="uid">result>
<result property="money" column="money">result>
<association property="user" javaType="user" column="uid" fetchType="lazy"
select="com.itdfbz.dao.UserMapper.findById">association>
resultMap>
查询用户信息,再使用用户账户时候再查询账户。
数据库实现:
-- 一对多延迟加载
-- 1) 查询用户
SELECT * FROM USER;
-- 2) 使用用户的账户对象数据时候再查询账户
SELECT * FROM account WHERE uid=46;
public class User {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
// 一个用户,对应多个账户
private List<Account> accounts;
}
public interface UserMapper {
/**
* 查询所有用户
*/
List<User> findAll();
}
public interface AccountMapper {
/**
* 根据用户的id查询
*/
List<Account> findAccountByUserId(Integer userId);
}
<select id="findAccountByUserId" resultType="account">
select * from account where uid=#{userId}
select>
<resultMap id="userResultMap" type="user">
<id property="id" column="id">id>
<result column="username" property="username">result>
<result column="sex" property="sex">result>
<result column="address" property="address">result>
<collection property="accounts" column="id"
ofType="account"
select="com.itdfbz.dao.AccountMapper.findAccountByUserId">
collection>
resultMap>
<select id="findAll" resultMap="userResultMap">
select * from user
select>
@Test
public void test1() {
UserMapper userMapper = session.getMapper(UserMapper.class);
List<User> userList = userMapper.findAll();
for (User user : userList) {
System.out.println(user.getSex());
}
}
测试结果:
// 查询用户
@Test
public void find() throws Exception{
List<User> list = userDao.findUsers();
for (User u : list) {
System.out.println(u.getId() + "," + u.getUsername());
System.out.println(u.getAccounts());
}
}
(1) 账户与用户,一对一
public class Account implements Serializable {
private int accountId;
private int uid;
private double money;
//一个账号对应一个账户
private User user;
}
(2) 用户与账户,一对多
public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
//一个用户对应多个账号
private List<Account> accountList;
}
public interface UserMapper {
// 根据用户id查询
@Select("select * from user where id=#{id}")
User findById(int id);
}
package com.itdfbz.dao;
import com.itdfbz.entity.Account;
import com.itdfbz.entity.User;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;
public interface AccountMapper {
/*
*
* @One:代表一对一
* select:要映射到本表属性的数据提供方法
* user:本类中要映射的属性名
* uid:外键,要传入findById方法的参数
* fetchType: 配置懒加载
* LAZY:懒加载
* EAGER:立即加载
*
* */
@Select("select * from account where accountId=#{id}")
@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.itdfbz.dao.UserMapper.findById",fetchType = FetchType.EAGER )
)
})
Account findById(Integer id);
}
@Test
public void test1(){
AccountMapper mapper = session.getMapper(AccountMapper.class);
Account account = mapper.findById(1);
System.out.println(account.getAccountId());
System.out.println(account.getUser());
}
/**
* many:代表一对多
* accountList:代表本类中的多方属性名
* id:要传入findByUserId的参数
* List.class:多方集合类型
* FetchType.LAZY:懒加载
*
*/
@Select("select * from user where id=#{id}")
@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 = "accountList",column = "id",javaType = List.class,
many=@Many(select = "com.itdfbz.dao.AccountMapper.findByUserId",fetchType = FetchType.LAZY)
)
})
@Test
public void test2() {
UserMapper userMapper = session.getMapper(UserMapper.class);
User user = userMapper.findById(46);
System.out.println(user.getUsername());
List<Account> accountList = user.getAccountList();
for (Account account : accountList) {
System.out.println(account);
}
}
像大多数的持久化框架一样, Mybatis 也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。Mybatis 中缓存分为一级缓存,二级缓存。
从图中可以看出什么 ? 一级缓存是基于SqlSessoion的缓存,一级缓存的内容不能跨sqlsession。由mybatis自动维护。二级缓存是基于映射文件的缓存,缓存范围比一级缓存更大。不同的sqlsession可以访问二级缓存的内容。哪些数据放入二级缓存需要自己指定。
同一个SqlSession的一级缓存的测试:
@Test
public void test1() {
SqlSession session = factory.openSession();
UserMapper userMapper1 = session.getMapper(UserMapper.class);
User user1 = userMapper1.findById(41); //发送sql查询出来,放入一级缓存
System.out.println(user1.getUsername());
User user2 = userMapper1.findById(41); //直接从一级缓存中查询(不发送sql)
System.out.println(user2.getUsername());
session.close(); //session关闭(此session的一级缓存销毁)
SqlSession session2 = factory.openSession();
UserMapper userMapper2 = session2.getMapper(UserMapper.class);
User user3 = userMapper2.findById(41); //发送sql查询一级缓存
System.out.println(user3.getUsername());
}
测试结果分析:
小结
tips:如果更改了一级缓存中的任何数据那么一级缓存会被清空
二级缓存是 mapper 映射级别的缓存,多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession 的。
二级缓存结构图:
首先开启 mybatis的二级缓存。
sqlSession1去查询用户信息,查询到用户信息会将查询数据存储到二级缓存中。sqlSession2去查询与 sqlSession1 相同的用户信息, 首先会去缓存中找是否存在数据,如果存在直接从缓存中取出数据。如果 SqlSession3 去执行相同 mapper 映射下 sql,执行 commit 提交, 将会清空该 mapper映射下的二级缓存区域的数据。
因为 cacheEnabled 的取值默认就为 true,所以这一步可以省略不配置。为 true代表开启二级缓存;为 false 代表不开启二级缓存。
官网介绍:
<mapper namespace="com.itdfbz.dao.UserMapper">
<cache>cache>
<select id="findById" parameterType="int" resultType="user" useCache="true">
select * from user where id=#{id}
select>
mapper>
将 UserMapper.xml 映射文件中的标签中设置 useCache=”true”代表当前这个 statement 要使用二级缓存,如果不使用二级缓存可以设置为 false。
注意: 针对每次查询都需要最新的数据 sql,要设置成useCache=false,禁用二级缓存。
当我们在使用二级缓存时,所缓存的类一定要实现 java.io.Serializable 接口:
public void test1() throws IOException {
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder=new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
SqlSession session1 = factory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.findById(41); //发送sql查询,放入一级缓存
System.out.println(user1);
session1.close(); //关闭一级缓存,并将一级缓存的内容写入二级缓存
SqlSession session2 = factory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.findById(41); //从二级缓存查询(反序列化)直接返回(此时一级缓存还是空的)
User user3 = mapper2.findById(41); //查询二级缓存(反序列化)
System.out.println(user3==user2); //false
session2.close();
is.close();
}
测试结果
二级缓存执行原理:
tips:如果更改了任何数据那么二级缓存和一级缓存都将被清空
@Test
public void test2() throws IOException {
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
SqlSession session1 = factory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.findById(41); //发送sql查询,放入一级缓存
user1.setAddress("NanChang");
mapper1.update(user1); //清空一级缓存 update delete
User user2 = mapper1.findById(41); //再次发送sql
session1.close(); //关闭一级缓存,并将一级缓存的内容写入二级缓存
}
@Test
public void test3() throws IOException {
InputStream is = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(is);
SqlSession session1 = factory.openSession();
UserMapper mapper1 = session1.getMapper(UserMapper.class);
User user1 = mapper1.findById(41); //发送sql查询,放入一级缓存
System.out.println(user1);
session1.close(); //关闭一级缓存,并将一级缓存的内容写入二级缓存
SqlSession session2 = factory.openSession();
UserMapper mapper2 = session2.getMapper(UserMapper.class);
User user2 = mapper2.findById(41); //从二级缓存查询(反序列化)直接返回(此时一级缓存还是空的)
mapper2.update(user2); //清空二级缓存、一级缓存
User user3 = mapper2.findById(41); //查询二级缓存--->一级缓存--->数据库
User user4 = mapper2.findById(41); //查询二级缓存(空的)---->查询一级缓存---返回结果
System.out.println(user3 == user4); //true
session2.close();
is.close();
}
一级缓存和二级缓存的执行顺序为:二级缓存---->一级缓存---->数据库
查询语句始终是由一级缓存发送的
一级缓存默认由MyBatis维护,我们只需了解使用即可
二级缓存需要我们开启:
1、在SqlMapConfig.xml中开启二级缓存(默认开启)
<settings>
<setting name="cacheEnabled" value="true"/>
settings>
2、在mapper文件中加入标签代表本mapper文件的所有查询结果放入二级缓存,并保证useCache不为false(默认为true)
<cache>cache>
<select id="findById" parameterType="int" resultType="user" useCache="true">
select * from user where id=#{id}
select>
3、MyBatis的二级缓存默认采用序列化/反序列化来保证对象的存取,所以所有的entity对象都应该实现serializable
接口
A. 分布式的项目架构下,也就是最少使用两个服务器,如果使用两个服务器mybatis的缓存技术就无法在两个服务器通用就是,也就是两个服务器无法达到数据通用,比如我在一个服务器存储了我的信息,但是我转跳到另一个服务器那使用mybatis数据就是需要从新加载,这里就是一个非常大的问题。
B. mybatis无法实现细粒度的缓存管理,当你查询大量数据的时候而且将数据存储到mybatis二级缓存中的时候,但是一旦队一个数据操作增加,删除,修改,这里二级缓存就全部清空,而mybatis无法实现对这里单个信息的修改
问题:如何解决这个缺陷?
答:使用第三方的缓存如:ehcache、redis
ehcache需要slf4j这个日志包支持
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.25version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.35version>
dependency>
<dependency>
<groupId>org.mybatis.cachesgroupId>
<artifactId>mybatis-ehcacheartifactId>
<version>1.1.0version>
dependency>
classpath下添加:ehcache.xml(EhcacheCache实现类的默认配置文件)
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="F:\temp\cache"/>
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
diskPersistent="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
memoryStoreEvictionPolicy="LRU">
defaultCache>
ehcache>
EhcacheCache 是ehcache对Cache接口的实现;修改mapper.xml文件,在cache标签的type属性中指定EhcacheCache。覆盖其默认实现
根据需求调整缓存参数:(根据条件对EhcacheCache实现类的参数调整,可选)
结果:观察缓存命中,且在写入磁盘配置路径处可以看到生成文件
idea的逆向工程,通过maven的插件来实现
在maven项目的pom.xml 添加mybatis-generator-maven-plugin 插件
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generatorgroupId>
<artifactId>mybatis-generator-maven-pluginartifactId>
<version>1.3.2version>
plugin>
plugins>
build>
更新下载依赖包
在maven项目下的src/main/resources 目录下建立名为 generatorConfig.xml的配置文件,作为mybatis-generator-maven-plugin 插件的执行目标,模板如下:
<generatorConfiguration>
<properties resource="mybatis.properties"/>
<classPathEntry location="D:\.m2\repository\mysql\mysql-connector-java\5.1.35\mysql-connector-java-5.1.35.jar" />
<context id="testTables" targetRuntime="MyBatis3">
<commentGenerator>
<property name="suppressAllComments" value="true" />
commentGenerator>
<jdbcConnection driverClass="${jdbc.driver}"
connectionURL="${jdbc.url}" userId="${jdbc.username}"
password="${jdbc.password}">
jdbcConnection>
<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
javaTypeResolver>
<javaModelGenerator targetPackage="cn.nyse.entity"
targetProject="src/main/java">
<property name="enableSubPackages" value="false" />
<property name="trimStrings" value="true" />
javaModelGenerator>
<sqlMapGenerator targetPackage="cn.nyse.mapper"
targetProject="src/main/java">
<property name="enableSubPackages" value="false" />
sqlMapGenerator>
<javaClientGenerator type="XMLMAPPER"
targetPackage="cn.nyse.mapper"
targetProject="src/main/java">
<property name="enableSubPackages" value="false" />
javaClientGenerator>
<table tableName="${jdbc.table.items}">table>
context>
generatorConfiguration>
mybatis.properties:
jdbc.driver = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/mybatis
jdbc.username = root
jdbc.password = 123456
#数据库中要生成的表
jdbc.table.items = student
项目目录如下
观察控制台输出结果:
生成的文件:
mybatis.properties:
```properties
jdbc.driver = com.mysql.jdbc.Driver
jdbc.url = jdbc:mysql://localhost:3306/mybatis
jdbc.username = root
jdbc.password = 123456
#数据库中要生成的表
jdbc.table.items = student
项目目录如下
观察控制台输出结果:
生成的文件: