mybatis进阶学习
内容介绍
- 动态sql
- mybatis中多表查询
- mybatis中嵌套查询
- 懒加载(延迟加载)
- 缓存
一 映射文件-动态sql
当我们要根据不同的条件,来执行不同的sql语句的时候,需要用到动态sql。
例如:多条件查询,修改密码和修改用户基本信息
1 if标签
需求:查询用户,若usename不为空添加username条件,若sex不为空添加sex条件
public interface UserDao {
List<User> find4If(User user);
}
<select id="find4If" resultType="user">
select * from user where 1=1
<if test="username != null and username != ''">
and username = #{username}
</if>
<if test="sex != null and sex != ''">
and sex = #{sex}
</if>
</select>
@Test
public void testIf() {
//0. 模拟条件
User user = new User();
//user.setUsername("王小二");
user.setSex("女");
//1.获取sqlsession
SqlSession sqlSession = MybatisUtils.openSession();
//2.获取dao
UserDao userDao = sqlSession.getMapper(UserDao.class);
//3.调用dao中方法
List<User> list = userDao.find4If(user);
for (User user1 : list) {
System.out.println(user1);
}
//4.提交事务 释放资源
MybatisUtils.commitAndClose(sqlSession);
}
注意: sql中判断并且的时候不要使用 “&&” , 应该使用 “and”
2 where标签
我们使用where之后就不需要再写1=1(处理第一个条件and或者or),可以判断是否所有的条件都不成立,若都不成立就不加where ;若有一个成立 就添加where 且还会把第一个条件前面的and或者or处理掉
需求:根据用户名性别查询用户,有哪些条件就添加那些条件,没有就查询所有
List<User> find4IfAndWhere(User user);
<select id="find4IfAndWhere" resultType="user">
select * from user
<where>
<if test="username != null and username != ''">
and username = #{username}
</if>
<if test="sex != null and sex != ''">
and sex = #{sex}
</if>
</where>
</select>
@Test
public void testIfAndWhere() {
//0. 模拟条件
User user = new User();
user.setUsername("王小二");
user.setSex("女");
//1.获取sqlsession
SqlSession sqlSession = MybatisUtils.openSession();
//2.获取dao
UserDao userDao = sqlSession.getMapper(UserDao.class);
//3.调用dao中方法
List<User> list = userDao.find4IfAndWhere(user);
for (User user1 : list) {
System.out.println(user1);
}
//4.提交事务 释放资源
MybatisUtils.commitAndClose(sqlSession);
}
3 set标签
需求:根据id更新用户信息,那些属性有值就更新那些属性
set标签可以将set中最后一个字段的","去掉
int update4Set(User user);
<update id="update4Set">
update user
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="sex != null and sex != ''">
sex = #{sex},
</if>
<if test="birthday != null">
birthday = #{birthday},
</if>
<if test="address != null and address != ''">
address = #{address},
</if>
</set>
where id = #{id}
</update>
@Test
public void testSet() {
//0. 模拟条件
User user = new User();
user.setId(50);
user.setUsername("睡觉1");
user.setSex("男");
user.setBirthday(new Date());
//1.获取sqlsession
SqlSession sqlSession = MybatisUtils.openSession();
//2.获取dao
UserDao userDao = sqlSession.getMapper(UserDao.class);
//3.调用dao中方法
userDao.update4Set(user);
//4.提交事务 释放资源
MybatisUtils.commitAndClose(sqlSession);
}
## 4 foreach标签
foreach主要是用来做数据的循环遍历
例如:`select * from user where id in (1,2,3) `在这样的语句中,传入的参数部分必须依靠 foreach遍历才能实现。
> <foreach>标签主要用于遍历集合,它的属性:
> • collection:代表要遍历的集合元素
> 若遍历集合,属性值为colletion或者list
> 若遍历数组,属性值为array
> 若遍历对象中的list或者array,属性值为对象中属性名
> • open:代表语句的开始部分
> • close:代表结束部分
> • item:代表遍历集合的每个元素,生成的变量名
> • separator:代表分隔符
### 遍历list
```java
List<User> find4ForeachByList(List ids);
<select id="find4ForeachByList" resultType="user">
select * from user where id in
<foreach collection="list" open="(" close=")" separator="," item="n">
#{n}
</foreach>
</select>
遍历数组
List<User> find4ForeachByArray(int[] ids);
<select id="find4ForeachByArray" resultType="user">
select * from user where id in
<foreach collection="array" open="(" close=")" separator="," item="n">
#{n}
</foreach>
</select>
遍历bean中的list或者数组属性
List<User> find4ForeachByQueryVo(QueryVo vo);
<select id="find4ForeachByQueryVo" resultType="user">
select * from user where id in
<foreach collection="ids" open="(" close=")" separator="," item="n">
#{n}
</foreach>
</select>
QueryVo类
/*
vo:value object 值对象(封装数据的对象)
*/
public class QueryVo {
private int[] ids;
public int[] getIds() {
return ids;
}
public void setIds(int[] ids) {
this.ids = ids;
}
}
5 sql片段
映射文件中可将重复的 sql 提取出来,使用时用 include 引用即可,最终达到 sql 重用的目的
例如:查询的时候,开发中一般不使用select * 而使用的 select 字段名,这样话效率比较高效.
<sql id="BaseSelectSQL">
select id,username,birthday,sex,address from user
</sql>
<select id="find4ForeachByQueryVo" resultType="user">
<include refid="BaseSelectSQL"/>
where id in
<foreach collection="ids" open="(" close=")" separator="," item="n">
#{n}
</foreach>
</select>
二 多表关系映射及查询
1 实体关系
表关系:
- 一对多(多对一) : 部门和员工 分类和线路 线路和线路图片 商家和线路 用户和订单
- 多对多 : 用户收藏线路 用户和角色 学生和课程 订单和商品
- 一对一 : 公民和身份证号 丈夫和妻子
表设计:
- 一对多:一般在多表上添加一个外键字段,指向一表的主键
- 多对多:添加一个中间表,表中至少保留另外俩表的主键,可以将多对多拆分成俩一对多
- 一对一:
- 俩表合二为一
- 唯一外键对应
- 主键对应
java实体关系和设计:
从实际业务角度出发的话,只存在一对多和一对一这两种情况
一对多关系来说:在当前类中存放对方的集合
一对一关系来说:在当前类中存放对方的对象
2 一对多
需求 : 查询id=41的用户的信息及其订单信息
sql分析:
SELECT u.*,o.id oid,o.ordertime,o.money
FROM USER u
LEFT JOIN orders o
ON u.id = o.uid
WHERE u.id = 41;
分析:
返回的应该是一个用户对象.对象中包含ta的订单的信息
步骤分析:
- 把orders表创建出来,把Order实体类创建出来
- 在User类中添加订单的集合(Order),提供get和set方法 表示用户有那些订单
- 在UserDao中添加方法
User findUserByUidWithOrders(int uid)
- 在UserDao的映射文件中提供statement标签
- 处理一对多
代码实现:
1.创建orders表
CREATE TABLE `orders` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) DEFAULT NULL,
`ordertime` datetime DEFAULT NULL,
`money` double DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `uid` (`uid`),
CONSTRAINT `orders_ibfk_1` FOREIGN KEY (`uid`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*Data for the table `orders` */
insert into `orders`(`id`,`uid`,`ordertime`,`money`) values (1,41,'2019-05-20 02:58:02',999.5),(2,45,'2019-02-14 07:58:00',1399),(3,41,'2019-06-01 21:00:02',1666);
2.编写Order实体
public class Order {
private Integer id;
private Date orderTime;
private Double money;
//get和set自己生成
}
3.在OrderDao中编写方法
User findUserByUidWithOrders(int uid);
4.在OrderDao.xml中编写一对多的映射
<resultMap id="WithOrdersMap" type="user">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
<!--
一对多的映射
将查询的行记录封装成订单对象,然后加入user的orderList属性中
property:对方集合在当前类中的属性名称
ofType:对方的全限定类名-支持别名
-->
<collection property="orderList" ofType="order">
<!--查询结果和订单对象的映射关系-->
<id property="id" column="oid"/>
<result property="orderTime" column="ordertime"/>
<result property="money" column="money"/>
</collection>
</resultMap>
<select id="findUserByUidWithOrders" resultMap="WithOrdersMap">
SELECT u.*,o.id oid,o.ordertime,o.money
FROM USER u
LEFT JOIN orders o
ON u.id = o.uid
WHERE u.id = #{uid};
</select>
public class B_TestManyTable {
@Test
public void testOne2Many() {
//获取session
SqlSession sqlSession = MybatisUtils.openSession();
//获取dao
UserDao userDao = sqlSession.getMapper(UserDao.class);
//调用dao方法
User user = userDao.findUserByUidWithOrders(41);
System.out.println(user);
List<Order> orderList = user.getOrderList();
if (orderList != null) {
for (Order order : orderList) {
System.out.println(order);
}
}
//提交事务
MybatisUtils.commitAndClose(sqlSession);
}
}
3 一对一
需求 : 查询id=1的订单信息及其所属用户信息
sql分析:
SELECT o.id oid,o.ordertime,o.money,u.*
FROM orders o
JOIN USER u
ON o.uid = u.id
WHERE o.id = 1;
返回一个订单对象,订单的信息封装给订单对象,用户的信息封装给订单中的用户对象
步骤分析:
- 在Order类中添加一个用户对象,用来表示当前订单属于那个用户
- 编写OrderDao接口,提供对应的映射文件
- 在OrderDao中编写方法 findOrderByOidWithUser,及其在映射文件中提供相应的statement标签即可
方式1:使用resultMap进行封装结果集
- 在OrderDao提供方法
Order findOrderByOidWithUser_1(int oid);
- 在OrderDao.xml提供相应的statement标签
方式2:使用resultMap+association标签(一对一映射配置)完成封装
- 在OrderDao提供方法
Order findOrderByOidWithUser_2(int oid);
- 在OrderDao.xml提供相应的statement标签
方式1:使用resultMap进行封装结果集
public interface OrderDao {
Order findOrderByOidWithUser_1(int oid);
}
<resultMap id="WithUser_1" type="order">
<id property="id" column="oid"/>
<result property="orderTime" column="ordertime"/>
<result property="money" column="money"/>
<result property="user.id" column="id"/>
<result property="user.username" column="username"/>
<result property="user.birthday" column="birthday"/>
<result property="user.sex" column="sex"/>
<result property="user.address" column="address"/>
</resultMap>
<select id="findOrderByOidWithUser_1" resultMap="WithUser_1">
SELECT o.id oid,o.ordertime,o.money,u.*
FROM orders o
JOIN USER u
ON o.uid = u.id
WHERE o.id = #{oid};
</select>
方式2:使用resultMap+association标签(一对一映射配置)完成封装
public interface OrderDao {
Order findOrderByOidWithUser_1(int oid);
Order findOrderByOidWithUser_2(int oid);
}
<resultMap id="WithUser_2" type="order">
<id property="id" column="oid"/>
<result property="orderTime" column="ordertime"/>
<result property="money" column="money"/>
<!--
一对一的映射:将查询出来的对象的信息封装指定的对象
property:对方在当前类中的属性名
javaType:对象的全限定类名
-->
<association property="user" javaType="user">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
</association>
</resultMap>
<select id="findOrderByOidWithUser_2" resultMap="WithUser_2">
SELECT o.id oid,o.ordertime,o.money,u.*
FROM orders o
JOIN USER u
ON o.uid = u.id
WHERE o.id = #{oid};
</select>
三 嵌套查询
将多表查询的复杂sql语句拆分成多个简单的查询语句,再由mybatis框架进行嵌套
举个栗子
* 需求:查询一个订单,同时查询该订单的用户
* 联合查询:
select *
from orders o
join user u
on o.uid = u.id
where o.id = 1;
* 嵌套查询
1.先查询订单信息
select * from orders where id = 1;-- 可以查询到uid = 41;
2.在查询用户信息
select * from user where id = 41;-- 根据上面查询的结果来查询用户
嵌套查询是延迟加载的基础.
1 一对一
需求:查询一个订单,与此同时查询出该订单所属的用户
1.先查询订单信息
select * from orders where id = 1;-- 可以查询到uid = 41;
2.在查询用户信息
select * from user where id = 41;-- 根据上面查询的结果来查询用户
步骤分析
- 在UserDao中提供一个方法: User findUserByUid(int uid);
- 在UserDao.xml中提供上述方法的statement即可
- 在OrderDao中提供一个方法: Order findOrderByOidWithUser_QianTao(int oid)
- 在OrderDao.xml中提供上述方法的statement
- 要配置嵌套查询,将查询订单信息的结果中uid作为UserDao中查询用户信息条件,最后将用户信息封装给订单中的用户
代码实现
UserDao
User findUserByUid(int uid);
UserDao.xml配置如下
<select id="findUserByUid" resultType="user">
select * from user where id = #{uid}
</select>
OrderDao
Order findOrderByOidWithUser_QianTao(int oid);
OrderDao.xml中代码
<resultMap id="WithUser_QianTao" type="order">
<id property="id" column="id"/>
<result property="orderTime" column="ordertime"/>
<result property="money" column="money"/>
<!--
一对一的嵌套查询
将查询结果中的uid获取到,作为userDao中某个statement的条件查询数据,结果类型为user,赋值给order中的user对象
-->
<association property="user" column="uid" select="com.itheima.dao.UserDao.findUserByUid" javaType="user"/>
</resultMap>
<select id="findOrderByOidWithUser_QianTao" resultMap="WithUser_QianTao">
select * from orders where id = #{oid}
</select>
2 一对多
需求:查询一个用户,与此同时查询出该用户具有的订单
1.先根据用户的id 查询出用户的信息
select * from user where id = 41;
2.再根据用户的id 查询订单表中当前用户的所有订单
select * from orders where uid = 41;
步骤分析
- 在OrderDao中提供方法: List findAllOrdersByUid(int uid);
- 在OrderDao.xml中提供上述方法对应的statement即可
- 在UserDao中提供方法: User findUserByUidWithOrders_QianTao(int uid);
- 在UserDao.xml中配置上述方法的statement
- 配置嵌套查询,将用户的id作为查询订单的条件,查询所有的订单
代码实现
OrderDao
List<Order> findAllOrdersByUid(int uid);
OrderDao.xml
<select id="findAllOrdersByUid" resultType="order">
select * from orders where uid = #{uid}
</select>
UserDao
User findUserByUidWithOrders_QianTao(int uid);
UserDao.xml
<resultMap id="WithOrdersMap_QianTao" type="user">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
<!--
一对多的嵌套查询
在查询的结果中获取column对应的值(id)作为查询条件,去执行orderDao中某个statement,将查询的结果赋值给用户中订单集合
-->
<collection property="orderList" ofType="order" column="id"
select="com.itheima.dao.OrderDao.findAllOrdersByUid"/>
</resultMap>
<select id="findUserByUidWithOrders_QianTao" resultMap="WithOrdersMap_QianTao">
select * from user where id = #{uid}
</select>
3 知识小结
一对一(多对一)配置:使用<resultMap>+<association>做配置,通过column条件,执行select查询
一对多(多对多)配置:使用<resultMap>+<collection>做配置,通过column条件,执行select查询
优点:简化多表查询操作,是懒加载的基础
缺点:执行多次sql语句,浪费数据库性能
四 加载策略
在企业开发时,不是在查询所有的数据的时候都会去查询关联数据,这个时候就要用到延迟加载机制
* 一个用户,该用户下有1000个订单
我们在查询用户的时候,要不要把关联的订单一起查询? 不用
我们在查询订单详情的时候,要不要把关联的用户一起查询?用
* 延迟加载策略
就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
* 应用场景
一对多或者多对多来说,一般采用懒加载去查询关联数据
一对一或者多对一来说,一般采用立即加载去查询关联数据
* 注意
懒加载是基于嵌套查询实现的
分类:
- 全局懒加载
- 局部懒加载
- 注意:
- 都需要手动去开启
- 局部懒加载优先级高于全局懒加载
1 全局延迟加载
在核心配置文件中,设置全局延迟加载开启
<settings>
<!--开启全局懒加载-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--设置任何方法都不触发懒加载,除属性的get方法-->
<setting name="lazyLoadTriggerMethods" value=""/>
</settings>
在debug的时候或者直接打印改对象的时候,会执行对象的toString方法,这时候会触发延迟加载,所有我们还需要在核心配置文件中配置toString方法不触发懒加载
注意:局部加载策略优先于全局加载策略,通常可以将一对一设置为立即加载
2 局部延迟加载
<association> 和 <collection>标签,有一个共同的属性
fetchType="lazy | eager"
lazy 懒加载
eager 立即加载
<association property="user" javaType="cn.itcast.domain.User" column="uid"
select="cn.itcast.dao.UserDao.findUserByUid" fetchType="eager"/>
<collection property="orderList" ofType="cn.itcast.domain.Order" column="id"
select="cn.itcast.dao.OrderDao.findAllOrdersByUid" fetchType="lazy"/>
五 缓存
什么是缓存?
- 存储在内存中的数据
为什么使用缓存?
- 因为查询内存中的数据效率高于查询磁盘中的数据
什么样的数据适合使用缓存?
- 经常访问 不会经常修改的数据且不太敏感使用缓存
什么样的数据不适合使用缓存?
- 敏感度比较高的数据 : 银行账户、商品数量、股票牌价
MyBatis是一款优秀的ORM框架,内置缓存机制
- 一级缓存:默认开启,且不能关闭,SqlSession级别的缓存,和SqlSession的生命周期一样.
- 二级缓存:默认开启,要想使用还需要进行一些配置,映射器级别(SqlSessionFactory级别)的缓存
1 一级缓存
一级缓存是SqlSession级别的缓存,是默认开启且无法关闭的。
- 查询数据的时候优先去缓存中查询数据,若缓存中直接返回;若缓存中没有数据,去磁盘中查询数据.查询到的数据根据一定的规则放入缓存中,方便下次查询使用;
- 当session关闭的时候或者清空的时候clearCache()就是把session的一级缓存清空
- 清空缓存的操作:保存,更新,删除操作(为了保证数据的一致性)
- 一级缓存中存放的是查询出来的对象
演示一级缓存:在一个sqlSession查询一个对象两次即可
@Test
//测试一级缓存的存在:在一个sqlSession中查询某个对象两次
public void testFirstCache() {
SqlSession sqlSession = MybatisUtils.openSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
User user1 = userDao.findUserByUid(42);
System.out.println(user1);
System.out.println("------再次查询-------");
User user2 = userDao.findUserByUid(42);
System.out.println(user2);
System.out.println(user1 == user2);
MybatisUtils.commitAndClose(sqlSession);
}
演示清空一级缓存:
@Test
//测试清除一级缓存
public void testClearFirstCache() {
SqlSession sqlSession = MybatisUtils.openSession();
UserDao userDao = sqlSession.getMapper(UserDao.class);
User user1 = userDao.findUserByUid(42);
System.out.println(user1);
System.out.println("------清空缓存-------");
sqlSession.clearCache();
System.out.println("------再次查询-------");
User user2 = userDao.findUserByUid(42);
System.out.println(user2);
System.out.println(user1 == user2);
MybatisUtils.commitAndClose(sqlSession);
}
2 二级缓存
二级缓存是属于Mapper映射器级别,需要手动开启<cache>,还需要让实体类实现序列化serializable接口
二级缓存是跨sqlSession的.使用同一个sqlSession,不会去二级缓存中查询.
当sqlsession关闭的时候,mybatis会把数据放到二级缓存中去.
二级缓存只保存数据,不保存对象.
使用步骤
1. 在核心配置文件开启二级缓存的支持【默认开启】
<!--开启二级缓存支持-->
<setting name="cacheEnabled" value="true"/>
2. 在指定Mapper映射文件中开启二级缓存 <cache/>
<mapper namespace="cn.itcast.dao.UserDao">
<!--当前映射开启二级缓存-->
<cache/>
3. 修改实体类实现serializable序列化接口
4. 测试二级缓存
@Test
//测试二级缓存的存在:使用两个sqlSession中查询某个对象两次,切记,第一个sqlsession查询完毕之后需要先关闭
public void testSecondCache() {
SqlSession sqlSession1 = MybatisUtils.openSession();
UserDao userDao1 = sqlSession1.getMapper(UserDao.class);
User user1 = userDao1.findUserByUid(42);
System.out.println(user1);
//关闭sqlSession
MybatisUtils.commitAndClose(sqlSession1);
System.out.println("------再次查询-------");
SqlSession sqlSession2 = MybatisUtils.openSession();
UserDao userDao2 = sqlSession2.getMapper(UserDao.class);
User user2 = userDao2.findUserByUid(42);
System.out.println(user2);
System.out.println(user1 == user2);
//关闭sqlSession
MybatisUtils.commitAndClose(sqlSession2);
}