MyBatis 学习3
延迟加载
在一对多关系中,1个人可以有100个账号,
立即加载就是当查询个人信息时,不管你需不需要看账号信息也立刻把个人对应的账号信息也查询出来,这样可能会消耗很多内存。
延迟加载就是当你只需要看个人信息时就只查了个人信息没有查账号信息,当你需要账号信息时再查给你。
一对多,多对多:通常用延迟加载。
多对一,一对一:通常用立即加载。(MyBatis 把多对一也看作一对一)
一对一延迟加载演示:
为了观看效果引入了日志依赖
<!--日志-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
Account 类
public class Account implements Serializable {
private Integer id;
private Integer uid;
private Double money;
private User user;
//省略getter、setter、toString方法
}
MyBatis 配置文件 SqlMapConfig.xml 开启延迟加载
<settings>
<!--开启延迟加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- aggressiveLazyLoading 当开启时,任何方法的调用都会加载该对象的所有属性。
否则,每个属性会按需加载-->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
IAccountDao.xml
<resultMap id="accountUserLazyMap" type="Account">
<id property="id" column="id"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!-- 一对一关系映射,封装 user 的内容
select属性:查询用户的唯一标识
column属性:根据id查询时,所需要的值
-->
<association property="user" column="uid" javaType="user" select="com.rgb3.dao.IUserDao.findById"></association>
</resultMap>
<!--一对一延迟查询全部-->
<select id="lazyFindAll" resultMap="accountUserLazyMap">
select * from t_account;
</select>
<association> 里面
select属性:查询用户的唯一标识,这里指定了 IUserDao.xml 里的findById 方法
column属性:根据id查询时,所需要的值
IUserDao.xml
<resultMap id="userMap" type="user">
<!--主键字段的对应-->
<id property="userId" column="id"></id>
<!--非主键字段的对应-->
<result property="userName" column="username"></result>
<result property="userBirthday" column="birthday"></result>
<result property="userAddress" column="address"></result>
</resultMap>
<!--根据id查询-->
<select id="findById" parameterType="int" resultMap="userMap">
select * from t_user where id=#{id}
</select>
数据表 t_account
t_user
IAccountDao 接口添加测试方法
//延迟查询
List<Account> lazyFindAll();
测试方法
@Test
public void lazyFindAll(){
List<Account> all = accountDao.lazyFindAll();
/*for (Account account:all){
System.out.println(account);
System.out.println(account.getUser());
}*/
}
这里只是查询所有账号数据
把测试方法的注释解开
@Test
public void lazyFindAll(){
List<Account> all = accountDao.lazyFindAll();
for (Account account:all){
System.out.println(account);
System.out.println(account.getUser());
}
}
这里因为代码中用到了个人 user 对象的属性,即打印 user 数据,所以 MyBatis 就把 user 的数据查询了。
实现了延迟查询,用到才查,用不到就不查。
一对多延迟加载演示:
1个人有多个账号的情况。
User 类
public class User implements Serializable {
private int userId;
private String userName;
private Date userBirthday;
private String userAddress;
private List<Account> accounts;
//省略getter、setter、toString方法
}
Account 类
public class Account implements Serializable {
private Integer id;
private Integer uid;
private Double money;
//省略getter、setter、toString方法
}
IUserDao 接口添加方法
//一对多延迟查询
List<User> lazyFindUserAll();
IAccountDao.xml
<!--根据uid查询账号-->
<select id="findByUid" resultType="account">
select * from t_account where uid = #{uid};
</select>
IUserDao.xml
<!--一对多结果映射,延迟加载-->
<resultMap id="userAccountLazyMap" type="user">
<id property="userId" column="id"></id>
<result property="userName" column="username"></result>
<result property="userBirthday" column="birthday"></result>
<result property="userAddress" column="address"></result>
<!--配置 user 对象的 accounts 集合映射-->
<collection property="accounts" ofType="account" select="com.rgb3.dao.IAccountDao.findByUid" column="id"></collection>
</resultMap>
这里<collection>里的 column 属性填id,因为 t_user 表里是 id 字段。
数据表 t_account
数据表 t_user
UserAccountTest 类测试方法
//一对多查询,延迟加载
@Test
public void lazyFindAccountAll(){
List<User> all = userDao.lazyFindUserAll();
/*for (User user : all){
System.out.println(user);
System.out.println(user.getAccounts());
System.out.println("------------------------");
}*/
}
这里没有用到 account 对象的数据,所以只查询了个人信息
现在把测试方法的注释解开
//一对多查询,延迟加载
@Test
public void lazyFindAccountAll(){
List<User> all = userDao.lazyFindUserAll();
for (User user : all){
System.out.println(user);
System.out.println(user.getAccounts());
System.out.println("------------------------");
}
}
这里用到account 对象的数据,所以查询个人信息后也查询了账号信息,实现了延迟加载。
MyBatis 的缓存
使用缓存可以减少和数据库交互的次数,提高执行效率。
-
适合用缓存的数据:经常要查询且不常改变的,数据的正确与否对最终结果影响不大的。
-
不合适用缓存的数据:经常改变的数据,数据的正确与否对最终结果影响大的(商品的库存等)。
MyBatis 中有一级缓存和二级缓存
一级缓存
指 MyBatis 中 SqlSession 对象的缓存,当我们执行查询后,查询结果会同时存入 SqlSession 的一块区域中,这个区域结构是 Map。当我们再次查询同样的数据时 MyBatis 会先去 SqlSession 中查询有没有,有就直接拿来用,就不用再去数据库查询了。
当 SqlSession 对象消失时,它的一级缓存也会消失。
t_user 表
User 类
public class User implements Serializable {
private int userId;
private String userName;
private Date userBirthday;
private String userAddress;
//省略getter、setter方法
//为了演示效果就不重写 toString 方法了
}
IUserDao.xml
<resultMap id="userMap" type="user">
<!--主键字段的对应-->
<id property="userId" column="id"></id>
<!--非主键字段的对应-->
<result property="userName" column="username"></result>
<result property="userBirthday" column="birthday"></result>
<result property="userAddress" column="address"></result>
</resultMap>
MyBatisTest 测试类添加测试方法:
//测试一级缓存,根据id查询
@Test
public void firstCache(){
User user1 = userDao.findById(33);
System.out.println(user1);
User user2= userDao.findById(33);
System.out.println(user2);
}
可以看到在代码里让它查询2次,但实际上,通过控制台可以看出它只从数据库查询了1次,第二的结果就是用了缓存里面的,所以是同一个 User 对象。
试试清空缓存
@Test
public void firstCache(){
User user1 = userDao.findById(33);
System.out.println(user1);
session.clearCache(); //清除缓存
User user2= userDao.findById(33);
System.out.println(user2);
System.out.println(user1 == user2);
}
}
清空缓存后,第二次查询就需要再次去数据库里面查了。
一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加、删除、commit()、close() 等方法,就会清空一级缓存。
IUserDao.xml
<!--修改用户-->
<update id="updateUser" parameterType="com.rgb3.domain.User">
update t_user set username=#{userName},birthday=#{userBirthday} where id=#{userId};
</update>
测试方法:
@Test
public void firstCache(){
User user1 = userDao.findById(33);
System.out.println(user1);
//session.clearCache(); //清除缓存
user1.setUserName("mily");
userDao.updateUser(user1);
User user2= userDao.findById(33);
System.out.println(user2);
System.out.println(user1 == user2);
}
t_user 表
可以看到,在更新数据后,缓存里的数据也没了需要再次去数据库里面查,使查出来的数据是最新,而不是之前存在缓存里旧的数据。
二级缓存
SecondCacheTest 类
public class SecondCacheTest {
private InputStream in ;
private SqlSessionFactory factory;
@Before//测试方式运行前运行
public void init() throws Exception {
//读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactory工厂
factory = new SqlSessionFactoryBuilder().build(in);
}
@After//测试方法运行后运行
public void destroy() throws Exception{
in.close();
}
//测试二级缓存
@Test
public void secondCache(){
//用工厂生产SqlSession对象
SqlSession sqlSession1 = factory.openSession();
//用SqlSession创建Dao接口的代理对象
IUserDao userDao1 = sqlSession1.getMapper(IUserDao.class);
User user1 = userDao1.findById(33);
sqlSession1.close(); //清空一级缓存
System.out.println(user1);
SqlSession sqlSession2 = factory.openSession();
IUserDao userDao2 = sqlSession2.getMapper(IUserDao.class);
User user2 = userDao2.findById(33);
sqlSession2.close(); //清空一级缓存
System.out.println(user2);
System.out.println(user1 == user2);
}
}
这里分别用2个 SqlSession 对象进行查询,可以看到它查询1次后,再次查询它还是会再次去数据库里面查询,即默认二级缓存是关闭的
开启二级缓存:
-
在 MyBatis 配置文件 SqlMapConfig.xml 中开启二级缓存的支持
<settings> <!--开启二级缓存的支持,这里默认是true的,不写也行--> <setting name="cacheEnabled" value="true"/> </settings>
在相关的 dao.xml 映射配置文件里开启二级缓存的支持,这里是 IUserDao.xml
<!--开启二级缓存的支持--> <cache/>
-
配置 statement 上面的 useCache 属性,这里是 IUserDao.xml 里的<select> 标签
<!--根据id查询--> <select id="findById" parameterType="int" resultMap="userMap" useCache="true"> select * from t_user where id=#{id} </select>
再次启动测试方法
可以看到有2个查询结果,但是它只向数据库查询了1次,所以第二次就是从缓存里面取出。
提示:当我们在使用二级缓存时,所缓存的类一定要实现序列化接口,如这里的 User 类
public class User implements Serializable {
//...
}
MyBatis 注解方式
之前一直都是用 xml 配置文件的形式使用 MyBatis,MyBatis 也支持注解形式使用。
注意,MyBatis 的主配置文件还是要以 xml 配置文件的形式存在,之前像 IUserDao.xml 这些映射文件就可以不写了。
-
@Insert:实现新增
-
@Update:实现更新
-
@Delete:实现删除
-
@Select:实现查询
-
@Result:实现结果集封装
-
@Results:可以与@Result 一起使用,封装多个结果集
-
@ResultMap:实现引用@Results 定义的封装
-
@One:实现一对一结果集封装
-
@Many:实现一对多结果集封装
-
@SelectProvider: 实现动态 SQL 映射
-
@CacheNamespace:实现注解二级缓存的使用
单表演示:
查询全部
User 类
public class User implements Serializable {
private int id;
private String username;
private Date birthday;
private String address;
//省略 getter setter toString方法
}
IUserDao 接口
public interface IUserDao {
//查询所有
@Select("select * from t_user")
List<User> findAll();
}
MyBatis 主配置文件 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">
<!--mybatis主配置文件-->
<configuration>
<properties resource="jdbcConfig.properties"></properties>
<!--配置别名,只能配置 domain 中类的别名-->
<typeAliases>
<!--指定要配置别名的包,该包下的实体类都会注册别名,它的类名就是别名,不区分大小写-->
<package name="com.rgb3.domain"></package>
</typeAliases>
<!--配置环境-->
<environments default="mysql">
<!--配置mysql的环境-->
<environment id="mysql">
<!--配置事务的类型-->
<transactionManager type="JDBC"></transactionManager>
<!--配置数据源(连接池)-->
<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>
<!-- package 指定有注解的 dao接口所在包,可以代替<mapper>-->
<package name="com.rgb3.dao"></package>
</mappers>
</configuration>
//测试注解类
public class MyBatisTest2 {
private InputStream in ;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;
@Before//测试方式运行前运行
public void init() throws Exception {
//读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactory工厂
factory = new SqlSessionFactoryBuilder().build(in);
//用工厂生产SqlSession对象
session = factory.openSession();
//用SqlSession创建Dao接口的代理对象
userDao = session.getMapper(IUserDao.class);
}
@After//测试方法运行后运行
public void destroy() throws Exception{
//提交事务
session.commit(); //保存、修改、删除操作时,如果你没这步提交事务,数据是不能成功写入数据库的
in.close();
session.close();
}
@Test
public void findAll(){
List<User> all = userDao.findAll();
for (User user:all){
System.out.println(user);
}
}
}
数据表t_user
插入数据
IUserDao 接口 添加方法
@Insert("insert into t_user(username,birthday,address) values(#{username},#{birthday},#{address})")
void saveUser(User user);
测试方法
@Test
public void saveUser(){
User user = new User();
user.setUsername("Bob");
user.setBirthday(new Date());
user.setAddress("北京");
userDao.saveUser(user);
}
修改数据
IUserDao 接口添加方法
//修改数据
@Update("update t_user set username=#{username},birthday=#{birthday},address=#{address} where id =#{id}")
void updateUser(User user);
测试方法
//修改数据
@Test
public void updateUser(){
User user = new User();
user.setId(42);
user.setUsername("Bob2");
user.setBirthday(new Date());
user.setAddress("上海");
userDao.updateUser(user);
}
IUserDao 接口添加方法
//根据id查询
@Select("select * from t_user where id = #{id}")
User findById(Integer id);
测试方法
//根据id查询
@Test
public void findById(){
User user = userDao.findById(39);
System.out.println(user);
}
删除
IUserDao 接口添加方法
//删除数据
@Delete("delete from t_user where id = #{id}")
void deleteUser(Integer id);
测试方法
//删除数据
@Test
public void deleteUser(){
userDao.deleteUser(42);
}
模糊查询
IUserDao 接口添加方法
//根据名字模糊查询
@Select("select * from t_user where username like #{username}")
List<User> findByName(String username);
测试方法
//根据名字模糊查询
@Test
public void findByUserName(){
List<User> list = userDao.findByName("%t%");//注意这里要自己加百分号%
for (User user:list){
System.out.println(user);
}
}
这里的 SQL 语句用的是占位符的形式,还有一种是字符串拼接的方式
@Select("select * from t_user where username like '%${value}%'")
这样在 Java 中使用就不用自己加百分号
List<User> list = userDao.findByName("t");
聚合函数查询总数
IUserDao 接口添加方法
//聚合函数查询总数
@Select("select count(*) from t_user")
int findTotal();
测试方法
//查询总数
@Test
public void findTotal(){
int total = userDao.findTotal();
System.out.println(total);
}
数据库字段名与 Java 变量名不一致时的注解配置
User 类
public class User implements Serializable {
private int userId;
private String userName;
private Date userBirthday;
private String userAddress;
//省略 getter、setter、toString 方法
}
数据表字段
IUserDao 接口
//查询所有
@Select("select * from t_user")
@Results(id="userMap",value = {
@Result(id = true,column = "id",property = "userId"),
@Result(column = "username",property = "userName"),
@Result(column = "birthday",property = "userBirthday"),
@Result(column = "address",property = "userAddress")
})
List<User> findAll();
//根据id查询
@Select("select * from t_user where id = #{id}")
@ResultMap("userMap")
User findById(Integer id);
@Result( id = true )表示这一字段对应的是 id 字段,类似 XML 配置方式里的 <id> 的作用。column 属性 表示数据表里的字段名,property 属性表示 Java 类里面的属性名。
@Results( id = “userMap”) 这里的 id 属性值表示给这个结果映射加一个唯一标识,这样在其他方法上要使用的时候用 @ResultMap() 引用一下就可以了。
@ResultMap("userMap")
多对一查询
多对一查询在 MyBatis 里即 一对一查询。
Account 类
public class Account implements Serializable {
private Integer id;
private Integer uid;
private Double money;
//注解多对一,即一对一的查询,1个账号只属于1个人
private User user;
//省略 getter、setter、toString 方法
}
IAccountDao 接口
public interface IAccountDao {
@Select("select * from t_account")
@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 = "com.rgb3.dao.IUserDao.findById",fetchType = FetchType.EAGER))
})
List<Account> findAll();
}
@Result(column = "uid",property = "user",one = @One(select = "com.rgb3.dao.IUserDao.findById",fetchType = FetchType.EAGER)
这里@Result 的 column 属性 表示根据 uid 去查 账号对应的 user 。
@One 里面的 select 属性表示去哪个接口的哪个方法查 user,注意是要全限定类名,fetchType = FetchType.EAGER 表示积极的查询(即立即加载,因为现实中,一般情况多对一,一对一都用立即加载,一对多用延迟加载),还有个延迟的查询(即延迟加载),后面会提到。
测试类
public class AccountTest2 {
private InputStream in ;
private SqlSessionFactory factory;
private SqlSession session;
private IAccountDao accountDao;
@Before//测试方式运行前运行
public void init() throws Exception {
//读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactory工厂
factory = new SqlSessionFactoryBuilder().build(in);
//用工厂生产SqlSession对象
session = factory.openSession();
//用SqlSession创建Dao接口的代理对象
accountDao = session.getMapper(IAccountDao.class);
}
@After//测试方法运行后运行
public void destroy() throws Exception{
//提交事务
session.commit(); //保存、修改、删除操作时,如果你没这步提交事务,数据是不能成功写入数据库的
in.close();
session.close();
}
//查询全部
@Test
public void findAll(){
List<Account> all = accountDao.findAll();
for (Account account:all){
System.out.println(account);
System.out.println(account.getUser());
}
}
}
一对多查询
1个人可以有多个账号的关系演示
User 类
public class User implements Serializable {
private int userId;
private String userName;
private Date userBirthday;
private String userAddress;
//一对多关系,1个人可以有多个账号
private List<Account> accounts;
//省略 getter、setter、toString 方法
}
IAccountDao 接口 添加方法
//根据个人id 查询账号信息
@Select("select * from t_account where uid = #{uid}")
List<Account> findByUid(Integer uid);
IUserDao 接口添加方法
//查询所有
@Select("select * from t_user")
@Results(id="userMap",value = {
@Result(id = true,column = "id",property = "userId"),
@Result(column = "username",property = "userName"),
@Result(column = "birthday",property = "userBirthday"),
@Result(column = "address",property = "userAddress"),
@Result(column = "id",property = "accounts",many = @Many(select = "com.rgb3.dao.IAccountDao.findByUid",fetchType = FetchType.LAZY))
})
List<User> findAll();
@Result(column = "id",property = "accounts",many = @Many(select = "com.rgb3.dao.IAccountDao.findByUid",fetchType = FetchType.LAZY))
这里与多对一查询里面类似,@Result() 里 column 属性表示根据 用户的 id 字段去查询 这个 id 有哪些账号,property 属性指 User 类里面的变量。many 属性表示这里是使用的是一对多,所以后面使用 @Many。
@Many 与 多对一的 @One 作用类似,不同的是这里我设置了 fetchType = FetchType.LAZY 表示延迟加载。
测试方法
//查询全部
@Test
public void findAll(){
List<User> all = userDao.findAll();
for (User user:all){
System.out.println(user);
System.out.println(user.getAccounts());
}
}
可以看到有些人有多个账号的是可以打印出来的。
现在试试把打印代码注释掉
@Test
public void findAll(){
List<User> all = userDao.findAll();
/*for (User user:all){
System.out.println(user);
System.out.println(user.getAccounts());
}*/
}
可以看到它只去查询了 user 的数据,没有查 user 对应的 accounts 数据,即实现了延迟查询。
注解方式使用二级缓存
XML 方式和注解方式的一级缓存使用默认是开启的,所以这里讲下注解方式的使用二级缓存。
User 类
public class User implements Serializable {
private int userId;
private String userName;
private Date userBirthday;
private String userAddress;
//省略 getter、setter、toString 方法
}
IUserDao 接口
public interface IUserDao {
//查询所有
@Select("select * from t_user")
@Results(id="userMap",value = {
@Result(id = true,column = "id",property = "userId"),
@Result(column = "username",property = "userName"),
@Result(column = "birthday",property = "userBirthday"),
@Result(column = "address",property = "userAddress"),
})
List<User> findAll();
//根据id查询
@Select("select * from t_user where id = #{id}")
@ResultMap("userMap")
User findById(Integer id);
}
测试类
public class SecondCacheTest2 {
private InputStream in ;
private SqlSessionFactory factory;
@Before//测试方式运行前运行
public void init() throws Exception {
//读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//创建SqlSessionFactory工厂
factory = new SqlSessionFactoryBuilder().build(in);
}
@After//测试方法运行后运行
public void destroy() throws Exception{
in.close();
}
//根据id查询
@Test
public void findById(){
SqlSession sqlSession1 = factory.openSession();
IUserDao userDao1 = sqlSession1.getMapper(IUserDao.class);
User user1 = userDao1.findById(39);
System.out.println(user1);
sqlSession1.close();//释放一级缓存
SqlSession sqlSession2 = factory.openSession();
IUserDao userDao2 = sqlSession2.getMapper(IUserDao.class);
User user2 = userDao2.findById(39);
System.out.println(user2);
sqlSession2.close();//释放一级缓存
}
}
默认没有开启二级缓存,所以这里我在代码中手动关闭了 SqlSession 后,再创建,再查询同样的内容,它还是会去数据库里面查。
开启二级缓存步骤:
MyBatis 的主配置文件 SqlMapConfig.xml 开启二级缓存(与 XML 方式类似)
<settings>
<!--开启二级缓存的支持,这里默认是true的,不写也行-->
<setting name="cacheEnabled" value="true"/>
</settings>
IUserDao 接口上添加注解
@CacheNamespace(blocking = true) //使用二级缓存
public interface IUserDao {...}
再次启动测试方法
可以看到只向数据库里查询了1次,即使用了二级缓存。