1 动态 SQL 语句-更复杂的查询业务需求
1.1 动态 SQL-官方文档
(1)文档地址: mybatis – MyBatis 3 | 动态 SQL
(2)为什么需要动态 SQL
- 动态 SQL 是 MyBatis 的强大特性之一
- 使用 JDBC 或其它类似的框架,根据不同条件拼接 SQL 语句非常麻烦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号等
- SQL 映射语句中的强大的动态 SQL 语言, 可以很好的解决这个问题
1.2 动态 SQL-基本介绍
- 在一个实际的项目中,sql 语句往往是比较复杂的
- 为了满足更加复杂的业务需求,MyBatis 的设计者,提供了动态生成 SQL 的功能。
动态 SQL 必要性
- 比如我们查询Monster 时,如果程序员输入的age 不大于0, 我们的sql语句就不带age 。
- 更新 Monster 对象时,没有设置的新的属性值,就保持原来的值,设置了新的值,才更新
解决方案分析
- 从上面的需求我们可以看出,有时我们在生成 sql 语句时,在同一个方法中,还要根据 不同的情况生成不同的 sql 语句
- 解决方案: MyBatis 提供的动态 SQL 机制
动态 SQL 常用标签
动态 SQL 提供了如下几种常用的标签,类似我们 Java 的控制语句:
(1)if [判断]
(2)where [拼接 where 子句]
(3)choose/when/otherwise [类似 java 的 switch 语句, 注意是单分支]
(4)foreach [类似 in ]
(5)trim [替换关键字/定制元素的功能]
(6)set [在 update 的 set 中,可以保证进入 set 标签的属性被修改,而没有进入 set 的,保持原来的值]
1.3 动态 SQL-案例演示
1.3.1 新建 Module dynamic-sql
(1)在原来的项目中,新建 dynamic-sql 模块演示动态 SQL 的使用
(2)新建 Module 后,先创建需要的包,再将需要的文件/资源拷贝过来(这里我们拷贝 Monster.java、resources/jdbc.properties 和 mybatis-config.xml)
(3)创建 MonsterMapper.java MonsterMapper.xml 和 MonsterMapperTest.java
1.3.2 if 标签应用实例
需求:请查询 age 大于 10 的所有妖怪,如果程序员输入的 age 不大于 0, 则输出所有 的妖怪
代码实现
(1)修改 MonsterMapper.java ,增加接口方法
//根据age查询结果
public List<Monster> findMonsterByAge(@Param(value = "age") Integer age);
(2)修改 MonsterMapper.xml,sql实现
<!--
1. 配置方法public List<Monster> findMonsterByAge(@Param(value="age") Integer age);
2. 请查询age 大于 10 的所有妖怪,如果程序员输入的age 不大于 0, 则输出所有的妖怪
3. 如果使用原来的#{age} 在test表达式是取不出入参值
4. 解决方案是使用@Param
-->
<select id="findMonsterByAge" parameterType="int" resultType="monster">select * from `monster` where 1 = 1<if test="age >= 0">and `age` > ${age}</if>
</select>
(3)修改 MonsterMapperTest.java 增加测试方法
@Test
public void findMonsterByAge() {List<Monster> monsters =monsterMapper.findMonsterByAge(-1);for (Monster monster : monsters) {System.out.println("monster--" + monster);}if (sqlSession != null) {sqlSession.close();}System.out.println("操作成功~");
}
测试效果
1.3.3 where 标签应用实例
需求:查询 id 大于 20 的,并且名字包含 "狐狸精" 的所有妖怪, 如果名字为空 , 就不带名字条件, 如果输入的 id 小于 0, 就不带 id 的条件
(1)修改 MonsterMapper.java ,增加接口方法
//根据id和名字来查询结果
public List<Monster> findMonsterByIdAndName(Monster monster);
(2)修改 MonsterMapper.xml,sql实现
<!--1、配置public List<Monster> findMonsterByIdAndName(Monster monster);2. 如果我们入参是对象,test表达式中, 直接使用对象的属性名即可3. where标签,会在组织动态sql时,加上where4. mybatis底层自动的去掉多余的AND
-->
<select id="findMonsterByIdAndName" parameterType="monster" resultType="monster">select * from `monster`<where><if test="age >= 0">`age` > #{age}</if><if test="name != null and name != ''">and `name` like '%${name}%'</if></where>
</select>
(3)修改 MonsterMapperTest.java 增加测试方法
@Test
public void findMonsterByIdAndName(){Monster monster = new Monster();monster.setAge(20);monster.setName("狐狸精");List<Monster> monsters =monsterMapper.findMonsterByIdAndName(monster);for (Monster o : monsters) {System.out.println("monster--" + o);}if (sqlSession != null){sqlSession.close();}System.out.println("操作成功~");
}
测试效果
1.3.4 choose/when/otherwise 应用实例
需求:如果给的 name 不为空,就按名字查询妖怪,name 为空并且 id>0,就按 id 来查询妖怪, 如果name 为空 并且 id<0,则以 id > 10 为查询条件。要求使用 choose/when/otherwise 标签实现, 传入参数要求使用 Map
(1)修改 MonsterMapper.java ,增加接口方法
//测试choose标签的使用
public List<Monster> findMonsterByIdOrName_choose(Map<String, Object> map);
(2)修改 MonsterMapper.xml,sql实现
<!--1、配置/实现public List<Monster> findMonsterByIdOrName_choose(Map<String, Object> map)2、 1) 如果给的name不为空,就按名字查询妖怪,2) 如果指定的id>0,就按id来查询妖怪3) 如果前面两个条件都不满足, 就默认查询 `age` > 10的4) 使用mybatis 提供choose-when-otherwise-->
<select id="findMonsterByIdOrName_choose" parameterType="map" resultType="monster">select * from `monster`<where><choose><when test="name != null and name != ''">`name` like '%${name}%'</when><when test="age >= 0">`age` > #{age}</when><otherwise>`age` > 10</otherwise></choose></where>
</select>
(3)修改 MonsterMapperTest.java 增加测试方法
@Test
public void findMonsterByIdOrName_choose(){HashMap<String, Object> map = new HashMap<>();map.put("age",-1);map.put("name","");List<Monster> monsters =monsterMapper.findMonsterByIdOrName_choose(map);for (Monster o : monsters) {System.out.println("monster--" + o);}if (sqlSession != null){sqlSession.close();}System.out.println("操作成功~");
}
测试效果
1.3.5 forEach 标签应用实例
forEach 标签说明:可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。
需求:查询 monster_id 为 20, 22, 34 的妖怪
(1)修改 MonsterMapper.java ,增加接口方法
//测试foreach的标签使用
public List<Monster> findMonsterById_forEach(Map<String, Object> map)
(2)修改 MonsterMapper.xml,sql实现
<!--1、配置/实现public List<Monster> findMonsterById_forEach(Map<String, Object> map);2、查询id 为 5, 6, 7 的妖怪3、是foreach标签4. 入参map 中 会如何传入id值 k-v ids - [集合,比如List 10,12,14], 即map 入参中应当有 ids-[10,12,14]-->
<select id="findMonsterById_forEach" parameterType="map" resultType="monster">select * from `monster`<!-- 解读3. 如果ids不为空,则使用foreach标签进行遍历4. collection="ids" 对应入参map的 key 为 ids5. item="id" 在遍历ids集合时,每次取出的值,对应的变量id6. open="where id in (" 表示开头的字符串7. separator="," 遍历出来的多个值之间的 分隔符号8. close=")" 表示结尾的字符串9. #{id} 对应的就是 item="id",即遍历出来的值--><if test="ids != null and ids != ''"><foreach collection="ids" index="" item="id" open="where id in ("separator="," close=")">#{id}</foreach></if>
</select>
(3)修改 MonsterMapperTest.java 增加测试方法
@Test
public void findMonsterById_forEach() {Map<String, Object> map = new HashMap<>();map.put("ids", Arrays.asList(5, 6, 7));List<Monster> monsters =monsterMapper.findMonsterById_forEach(map);for (Monster monster : monsters) {System.out.println("monster--" + monster);}if (sqlSession != null) {sqlSession.close();}System.out.println("操作成功~");}
测试效果
1.3.6 trim 标签应用实例
trim 可以替换一些关键字
要求:按名字查询妖怪,在动态添加where,并且如果where 子句以开头 and | or 就去除。即使用trim 标签 实现 等价于 where标签的效果
(1)修改 MonsterMapper.java ,增加接口方法
//trim标签的使用
public List<Monster> findMonsterByName_Trim(Map<String, Object> map);
(2)修改 MonsterMapper.xml,sql实现
suffixOverrides 表示行末尾,prefixOverrides 表示行开头
<!--
1. 配置/实现 public List<Monster> findMonsterByName_Trim(Map<String, Object> map);
2. 按名字和年龄 查询妖怪,如果sql语句开头有 and | or 就替换成 where
3. 如果要实现这个功能,其实使用where标签是一样的效果 [加入where 同时会去掉多余的and]
4. trim prefix="WHERE" prefixOverrides="and|or|wwj" 若子句的开头为 “AND” 或 “OR 或"wwj"
, 就去除
-->
<select id="findMonsterByName_Trim" parameterType="map" resultType="Monster">SELECT * FROM `monster`<trim prefix="WHERE" prefixOverrides="and|or|wwj"><if test="name != null and name != ''">AND `name` = #{name}</if><if test="age != null and age != ''">AND `age` > #{age}</if></trim>
</select>
(3)修改 MonsterMapperTest.java 增加测试方法
@Test
public void findMonsterByName_Trim() {Map<String, Object> map = new HashMap<>();map.put("name", "狐狸精-100");map.put("age", 10);List<Monster> monsters =monsterMapper.findMonsterByName_Trim(map);for (Monster monster : monsters) {System.out.println("monster--" + monster);}if (sqlSession != null) {sqlSession.close();}System.out.println("操作成功~");}
测试效果
1.3.7 set 标签应用实例
set标签说明:
set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号
需求: 请对指定 id 的妖怪进行 修改,如果没有设置新的属性,则保持原来的值
(1)修改 MonsterMapper.java ,增加接口方法
//测试Set标签
public void updateMonster_set(Map<String, Object> map);
(2)修改 MonsterMapper.xml,sql实现
<!--
1. 配置/实现 public void updateMonster_set(Map<String, Object> map);
2. 请对指定id 的妖怪进行 修改,如果没有设置新的属性,则保持原来的值
3. 入参要根据sql语句来配合 map [age-10, email-'xx@sohu.com'...]
4. set标签会处理多余的 ,
-->
<update id="updateMonster_set" parameterType="map">UPDATE `monster`<set><if test="age != null and age != ''">`age` = #{age} ,</if><if test="email != null and email != ''">`email` = #{email} ,</if><if test="name != null and name != ''">`name` = #{name} ,</if><if test="birthday != null and birthday != ''">`birthday` = #{birthday} ,</if><if test="salary != null and salary != ''">`salary` = #{salary} ,</if><if test="gender != null and gender != ''">`gender` = #{gender} ,</if></set>WHERE id = #{id}
</update>
(3)修改 MonsterMapperTest.java 增加测试方法
@Test
public void updateMonster_set() {Map<String, Object> map = new HashMap<>();map.put("id", 1);map.put("name", "牛魔王5-100");map.put("age", 76);map.put("email", "wwj@qq.com");monsterMapper.updateMonster_set(map);//修改需要有commitif (sqlSession != null) {sqlSession.commit();sqlSession.close();}System.out.println("操作成功~");
}
测试效果
2 映射关系一对一
2.1 映射关系-官方文档
文档地址: mybatis – MyBatis 3 | XML 映射器
2.2 映射关系 1 对 1-基本介绍
基本介绍:项目中 1 对 1 的关系是一个基本的映射关系,比如:Person(人) --- IDCard(身份证)
2.3 映射关系 1 对 1-映射方式
2.3.1 映射方式
(1)通过配置 XxxMapper.xml 实现 1 对 1 [配置方式]
(2)通过注解的方式实现 1 对 1 [注解方式]
2.3.2 配置 Mapper.xml 的方式-应用实例
2.3.2.1 方式1
说明:通过配置 XxxMapper.xml 的方式来实现下面的 1 对 1 的映射关系,实现级联查询,通过 person 可以获取到对应的 idencard 信息
完成功能示意(如下)
person--Person{id=1, name='张三', card=IdenCard{id=1,card_sn='111111111111110'}}
代码实现
(1)创建 person 表和 idencard 表
CREATE TABLE idencard(
id INT PRIMARY KEY AUTO_INCREMENT,
card_sn VARCHAR(32) NOT NULL DEFAULT ''
);CREATE TABLE person(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(32) NOT NULL DEFAULT '',
card_id INT , --对应idencard的主键id
FOREIGN KEY (card_id) REFERENCES idencard(id) --外键
);INSERT INTO idencard VALUES(1,'111111111111110');
INSERT INTO person VALUES(1,'张三',1);
SELECT * FROM person;
SELECT * FROM idencard;
(2)创建新的 module(mybatis-mapping), 相关配置文件可以从上一个 module 拷贝 (这里我们拷贝resources/jdbc.properties 和 mybatis-config.xml)。嫌麻烦这一步可以省略,这里这是为了直观,所以一个章节使用一个module
(3)创 建 entity: src\main\java\com\entity\IdenCard.java 和 src\main\java\com\entity\Person.java
package com.entity;public class IdenCard {private Integer id;private String card_sn;//通过查询IdenCard 可以级联查询得到personprivate Person person;public Person getPerson() {return person;}public void setPerson(Person person) {this.person = person;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getCard_sn() {return card_sn;}public void setCard_sn(String card_sn) {this.card_sn = card_sn;}@Overridepublic String toString() {return "IdenCard{" +"id=" + id +", card_sn='" + card_sn + '\'' +", person=" + person +'}';}
}
package com.entity;public class Person {private Integer id;private String name;//因为我们的需要实现一个级联操作, 一个人需要对应一个身份证//这里需要直接定义IdenCard对象属性private IdenCard card;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public IdenCard getCard() {return card;}public void setCard(IdenCard card) {this.card = card;}@Overridepublic String toString() {return "Person{" +"id=" + id +", name='" + name + '\'' +", card=" + card +'}';}
}
(4)创建 com\mapper\IdenCardMapper.java接口 和 com\mapper\IdenCardMapper.xml映射文件
package com.hspedu.mapper;import com.hspedu.entity.IdenCard;public interface IdenCardMapper {//根据id获取到身份证序列号public IdenCard getIdenCardById(Integer id);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.mapper.IdenCardMapper"><select id="getIdenCardById" parameterType="integer" resultType="idenCard">select * from `idencard` where `id`=#{id}</select>
</mapper>
(5)创建 PersonMapper.java 和 PersonMapper.xml
package com.mapper;import com.entity.Person;public interface PersonMapper {//通过Person的id获取到Person,包括这个Person关联的IdenCard对象[级联查询]public Person getPersonById(Integer id);}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.mapper.PersonMapper"><!--1、配置/实现public Person getPersonById(Integer id);2、完成通过Person的id获取到Person,包括这个Person关联的IdenCard对象[级联查询]3. 自定义resultMap 搞定 映射返回的结果--><resultMap id="PersonResultMap" type="Person"><!--id标签 – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能1.property="id" 表示person 属性 id ,通常是主键2.column="id" 表示对应表的字段--><id property="id" column="id"/><result property="name" column="name"/><!--association标签 – 一个复杂类型的关联1. property="card" 表示 Person对象的 card 属性2. javaType="IdenCard" 表示card 属性 的类型3. column="id" 是从我们的 下面这个语句查询后返回的字段SELECT * FROM `person`,`idencard` WHERE `person`.id=1AND `person`.card_id = `idencard`.id--><association property="card" javaType="IdenCard"><result property="id" column="id"/><result property="card_sn" column="card_sn"/></association></resultMap><select id="getPersonById" parameterType="Integer"resultMap="PersonResultMap">SELECT * FROM `person`,`idencard` WHERE `person`.id = #{id}AND `person`.card_id = `idencard`.id</select></mapper>
(6)创建 src\test\java\com\mapper\IdenCardMapperTest.java 和src\test\java\com\map per\PersonMapperTest.java , 进行测试
package com.mapper;import com.entity.IdenCard;
import com.util.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Before;
import org.junit.Test;public class IdenCardMapperTest {SqlSession sqlSession;IdenCardMapper idenCardMapper;@Beforepublic void init(){sqlSession = MyBatisUtils.getSqlSession();idenCardMapper = sqlSession.getMapper(IdenCardMapper.class);}@Testpublic void getIdenCardById(){int id = 1;IdenCard idenCard = idenCardMapper.getIdenCardById(id);System.out.println("idenCard=" + idenCard);}
}
package com.mapper;import com.entity.Person;
import com.util.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Before;
import org.junit.Test;public class PersonMapperTest {SqlSession sqlSession;PersonMapper personMapper;@Beforepublic void init(){sqlSession = MyBatisUtils.getSqlSession();personMapper = sqlSession.getMapper(PersonMapper.class);}@Testpublic void getPersonById(){Person person = personMapper.getPersonById(1);System.out.println("person--" + person);if (sqlSession != null){sqlSession.close();}}}
测试效果
2.3.2.2 方式2
(1)修改PersonMapper.java 接口增加方法 和 PersonMapper.xml 配置相应sql映射
//通过Person的id获取到Person,包括这个Person关联的IdenCard对象,方式2
public Person getPersonById2(Integer id);
<!--1、方式21) 先通过 SELECT * FROM `person` WHERE `id` = #{id} 返回 person信息2) 再通过 返回的card_id 值,再执行操作,得到IdenCard 数据
-->
<resultMap id="PersonResultMap2" type="Person"><id property="id" column="id"/><result property="name" column="name"/><!--1. mybatis第二种方式核心思想: 将多表联查,分解成单表操作 , 这样简洁,而且易于维护 ,推荐2. property="card": 表示 Person对象的 card 属性3. column="card_id" 这个是SELECT * FROM `person` WHERE `id` = #{id} 返回的 字段 card_id 信息/数据4. 返回的 字段 card_id 信息/数据 作为getIdenCardById入参, 来执行--><association property="card" column="card_id"select="com.hspedu.mapper.IdenCardMapper.getIdenCardById" />
</resultMap>
<select id="getPersonById2" parameterType="Integer" resultMap="PersonResultMap2">SELECT * FROM `person` WHERE `id` = #{id}
</select>
(2)修改 PersonMapperTest.java 进行测试
@Test
public void getPersonById2(){Person person = personMapper.getPersonById2(1);System.out.println("person--" + person);if (sqlSession != null){sqlSession.close();}
}
测试效果
2.3.3 注解的方式实现-应用实例
说明:通过注解的方式来实现下面的 1 对 1 的映射关系,实现级联查询,通过 person 可以获取到 对应的 identcard 信息 (在实际开发中还是推荐使用配置方式)
(1)创建 com\mapper\IdenCardMapperAnnotaion.java
package com.mapper;import com.entity.IdenCard;
import org.apache.ibatis.annotations.Select;/*** IdenCardMapperAnnotation: 使用注解方式实现1对1的映射*/
public interface IdenCardMapperAnnotation {//根据id获取到身份证//这个方法不需要返回任何级联对象@Select("SELECT * FROM `idencard` WHERE `id` = #{id}")public IdenCard getIdenCardById(Integer id);
}
(2)创建 com\mapper\PersonMapperAnnotation.java
package com.mapper;import com.entity.Person;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;public interface PersonMapperAnnotation {//这里注解实现方法//注解的形式就是对前面xml配置方式的体现@Select("SELECT * FROM `person` WHERE `id` = #{id}")@Results({@Result(id = true, property = "id", column = "id"),@Result(property = "name", column = "name"),@Result(property = "card", column = "card_id",one = @One(select = "com.mapper.IdenCardMapper.getIdenCardById"))})public Person getPersonById(Integer id);
}
(3)创建 com\mapper\PersonMapperAnnotationTest.java 完成测试
package com.mapper;import com.entity.Person;
import com.util.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Before;
import org.junit.Test;public class PersonMapperAnnotationTest {//属性private SqlSession sqlSession;private PersonMapperAnnotation personMapperAnnotation;//初始化@Beforepublic void init() {//获取到sqlSessionsqlSession = MyBatisUtils.getSqlSession();personMapperAnnotation = sqlSession.getMapper(PersonMapperAnnotation.class);}@Testpublic void getPersonById() {Person person = personMapperAnnotation.getPersonById(1);System.out.println("person----" + person);if(sqlSession != null) {sqlSession.close();}}
}
测试效果
3 映射关系多对一
3 .1 映射关系-官方文档
文档地址: mybatis – MyBatis 3 | XML 映射器
3.2 映射关系多对 1-基本介绍
基本介绍
(1)项目中多对 1 的关系是一个基本的映射关系, 多对 1, 也可以理解成是 1 对多
- User --- Pet: 一个用户可以养多只宠物
- Dep ---Emp : 一个部门可以有多个员工
特别说明
(1)在实际的项目开发中, 要求会使用双向的多对一的映射关系
(2)说明:什么是双向的多对一的关系 : 比如通过 User 可以查询到对应的 Pet, 反过来,通过 Pet 也可以级联查询到对应的 User 信息
(3)多对多的关系,是在多对 1 的基础上扩展即可.
3.3 映射关系多对 1-映射方式
3.3.1 映射方式
(1)方式 1:通过配置 XxxMapper.xml 实现多对 1
(2)方式 2:通过注解的方式实现 多对 1
3.3.2 配置 Mapper.xml 方式-应用实例
需求说明: 实现级联查询,通过 user 的 id 可以查询到用户信息,并可以查询到关联的 pet 信息,反过来,通过 Pet 的 id 可以查询到 Pet 的信息,并且可以级联查询到它的主人 User 对象信息
(1)创建 mybatis_user 和 mybatis_pet 表
CREATE TABLE mybatis_user(
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(32) NOT NULL DEFAULT ''
);
CREATE TABLE mybatis_pet(
id INT PRIMARY KEY AUTO_INCREMENT,
nickname VARCHAR(32) NOT NULL DEFAULT '',
user_id INT ,
FOREIGN KEY (user_id) REFERENCES mybatis_user(id)
);
INSERT INTO mybatis_user
VALUES(NULL,'宋江'),(NULL,'张飞');
INSERT INTO mybatis_pet
VALUES(1,'黑背',1),(2,'小哈',1);
INSERT INTO mybatis_pet
VALUES(3,'波斯猫',2),(4,'贵妃猫',2);
SELECT * FROM mybatis_user;
SELECT * FROM mybatis_pet;
(2)创 建 entity : entity\Pet.java 和 entity\User.java
package com.entity;import java.util.List;public class User {private Integer id;private String name;//因为一个user可以养多个宠物,mybatis 使用集合List<Pet>体现这个关系private List<Pet> pets;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public List<Pet> getPets() {return pets;}public void setPets(List<Pet> pets) {this.pets = pets;}//这toString会带来麻烦?=>会造成StackOverFlow//@Override//public String toString() {// return "User{" +// "id=" + id +// ", name='" + name + '\'' +// ", pets=" + pets +// '}';//}
}
package com.entity;public class Pet {private Integer id;private String nickname;//一个pet对应一个主人 User对象private User user;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getNickname() {return nickname;}public void setNickname(String nickname) {this.nickname = nickname;}public User getUser() {return user;}public void setUser(User user) {this.user = user;}}
(3)创建 PetMapper.java 和 UserMapper.java
package com.mapper;import com.entity.Pet;import java.util.List;public interface PetMapper {//通过User的id来获取pet对象,可能有多个,因此使用List接收public List<Pet> getPetByUserId(Integer userId);//通过pet的id获取Pet对象, 同时会查询到pet对象关联的user对象public Pet getPetById(Integer id);}
package com.mapper;import com.entity.User;public interface UserMapper {//通过id获取User对象public User getUserById(Integer id);
}
(4)创建 UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.mapper.UserMapper"><!--1、配置/实现 public User getUserById(Integer id);2、思路(1) 先通过user-id 查询得到user信息 (2) 再根据user-id查询对应的pet信息并映射到User-List<Pet> pets--><resultMap id="userMap" type="com.entity.User"><id property="id" column="id"/><result property="name" column="name"/><!--因为pets属性是集合,因此这里需要是collection标签来处理1. ofType="Pet" 指定返回的集合中存放的数据类型Pet2. collection 表示 pets 是一个集合3. property="pets" 是返回的user对象的属性 pets4. column="id" SELECT * FROM `mybatis_user` WHERE `id` = #{id} 返回的id字段对应的值--><collection property="pets" column="id" ofType="pet"select="com.mapper.PetMapper.getPetByUserId"/></resultMap><select id="getUserById" parameterType="integer" resultMap="userMap">select * from `mybatis_user` where id=#{id}</select></mapper>
(5) 创建 PetMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.mapper.PetMapper"><!--1、通过User的id来获取pet对象,可能有多个,因此使用List接收2、public List<Pet> getPetByUserId(Integer userId);--><resultMap id="PetResultMap" type="Pet"><id property="id" column="id"/><result property="nickname" column="nickname"/><association property="user" column="user_id"select="com.mapper.UserMapper.getUserById" /></resultMap><select id="getPetByUserId" parameterType="integer" resultMap="PetResultMap">select * from `mybatis_pet` where user_id=#{userId}</select><!--1. 这里直接复用PetResultMap2. 实现/配置public Pet getPetById(Integer id);3. 通过pet的id获取Pet对象--><select id="getPetById"parameterType="Integer"resultMap="PetResultMap">SELECT * FROM `mybatis_pet` WHERE `id` = #{id}</select>
</mapper>
(6)创建 PetMapperTest.java 完成测试
package com.mapper;import com.entity.Pet;
import com.entity.User;
import com.util.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Before;
import org.junit.Test;import java.util.List;public class PetMapperTest {//属性private SqlSession sqlSession;private PetMapper petMapper;//初始化@Beforepublic void init() {//获取到sqlSessionsqlSession = MyBatisUtils.getSqlSession();petMapper = sqlSession.getMapper(PetMapper.class);}@Testpublic void getPetByUserId() {List<Pet> pets = petMapper.getPetByUserId(2);for (Pet pet : pets) {System.out.println("pet信息-" + pet.getId() + "-" + pet.getNickname());User user = pet.getUser();System.out.println("user信息 name-" + user.getName());}if(sqlSession != null) {sqlSession.close();}}@Testpublic void getPetById() {Pet pet = petMapper.getPetById(2);System.out.println("pet信息-" + pet.getId() + "-" + pet.getNickname());User user = pet.getUser();System.out.println("user信息-" + user.getId() + "-" + user.getName());if(sqlSession != null) {sqlSession.close();}}}
测试效果
3.3.3 注解实现多对 1 映射-应用实例
需求说明: 通过注解的方式来实现下面的多对 1 的映射关系,实现级联查询,完成前面完成 的任务,通过 User-->Pet 也可 Pet->User , 在实际开发中推荐使用配置方式来做
(1)创建 UserMapperAnnotation.java
package com.mapper;import com.entity.User;
import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;/*** UserMapperAnnotation:以注解的方式来配置多对一*/
public interface UserMapperAnnotation {//通过id获取User对象@Select("SELECT * FROM `mybatis_user` WHERE `id` = #{id}")@Results({@Result(id = true, property = "id", column = "id"),@Result(property = "name", column = "name"),//这里请小伙伴注意,pets属性对应的是集合@Result(property = "pets",column = "id",many = @Many(select = "com.mapper.PetMapperAnnotation.getPetByUserId"))})public User getUserById(Integer id);
}
(2)创建 PetMapperAnnotation.java
package com.mapper;import com.entity.Pet;
import org.apache.ibatis.annotations.*;import java.util.List;public interface PetMapperAnnotation {//通过User的id来获取pet对象,可能有多个,因此使用List接收//id = "PetResultMap" 就是给我们的Results[Result Map] 指定一个名字//,目的是为了后面复用@Select("SELECT * FROM `mybatis_pet` WHERE `user_id` = #{userId}")@Results(id = "PetResultMap", value = {@Result(id = true, property = "id", column = "id"),@Result(property = "nickname", column = "nickname"),@Result(property = "user",column = "user_id",one = @One(select = "com.mapper.UserMapperAnnotation.getUserById"))})public List<Pet> getPetByUserId(Integer userId);//通过pet的id获取Pet对象, 同时会查询到pet对象关联的user对象/*** @ResultMap("PetResultMap") 使用/引用我们上面定义的 Results[ResultMap]*/@Select("SELECT * FROM `mybatis_pet` WHERE `id` = #{id}")@ResultMap("PetResultMap")public Pet getPetById(Integer id);
}
(3)创建 UserMapperAnnotationTest.java 完成测试
package com.mapper;import com.entity.Pet;
import com.entity.User;
import com.util.MyBatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Before;
import org.junit.Test;import java.util.List;public class UserMapperAnnotationTest {//属性private SqlSession sqlSession;private UserMapperAnnotation userMapperAnnotation;//初始化@Beforepublic void init() {//获取到sqlSessionsqlSession = MyBatisUtils.getSqlSession();userMapperAnnotation = sqlSession.getMapper(UserMapperAnnotation.class);}@Testpublic void getUserById() {User user = userMapperAnnotation.getUserById(2);System.out.println("user信息-" + user.getId() + "-" + user.getName());List<Pet> pets = user.getPets();for (Pet pet : pets) {System.out.println("宠物信息-" + pet.getId() + "-" + pet.getNickname());}if(sqlSession != null) {sqlSession.close();}}
}
测试效果
4 缓存-提高检索效率的利器
4.1 缓存-官方文档
文档地址:mybatis – MyBatis 3 | XML 映射器
4.2 一级缓存
4.2.1 基本介绍
(1)默认情况下,mybatis 是启用一级缓存的/本地缓存/local Cache,它是 SqlSession 级别的
(2)同一个 SqlSession 接口对象调用了相同的 select 语句,会直接从缓存里面获取,而不是再去查询数据库
(3)一级缓存原理图
4.2.2 一级缓存快速入门
需求: 当我们第 1 次查询 id=1 的 Monster 后,再次查询 id=1 的 monster 对象,就会直接 从一级缓存获取,不会再次发出 sql
(1)创建新module: mybatis_cache , 必要的文件和配置直接从mybatis_quickstart module 拷贝即可,需要拷贝的文件和配置如图
(2)使用 MonsterMapperTest.java , 运行 getMonsterById() 通过查看日志输出, 结论我们多次运行,总是会发出 SQL
(3)修改MonsterMapperTest.java, 增加测试方法, 测试一级缓存的基本使用(无需配置,因为默认的就是一级缓存)
//测试一级缓存
@Test
public void level1CacheTest() {//查询id=1的monsterMonster monster = monsterMapper.getMonsterById(1);System.out.println("monster=" + monster);//再次查询id=1的monster//当我们再次查询 id=1的Monster时,直接从一级缓存获取,不会再次发出sqlSystem.out.println("--一级缓存默认是打开的,当你再次查询相同的id时, 不会再发出sql----");Monster monster2 = monsterMapper.getMonsterById(1);System.out.println("monster2=" + monster2);if (sqlSession != null) {sqlSession.close();}
}
4.2.3 一级缓存失效分析
(1)关闭 sqlSession 会话后, 再次查询,会到数据库查询
(2)当执行 sqlSession.clearCache() 会使一级缓存失效
(3)当对同一个 monster 修改(update),该对象在一级缓存会失效
4.3 二级缓存
4.3.1 基本介绍
(1)二级缓存和一级缓存都是为了提高检索效率的技术
(2)最大的区别就是作用域的范围不一样,一级缓存的作用域是 sqlSession 会话级别,在一次 会话有效,而二级缓存作用域是全局范围,针对不同的会话都有效
(3)二级缓存原理图
4.3.2 二级缓存快速入门
(1)mybatis-config.xml 配置中开启二级缓存
<configuration><properties resource="jdbc.properties"/><settings><!--配置MyBatis自带的日志输出-查看原生的sql--><setting name="logImpl" value="STDOUT_LOGGING"/><!--1、全局性地开启或关闭所有映射器配置文件中已配置的任何缓存, 可以理解这是一个总开关2、默认就是: true--><setting name="cacheEnabled" value="true"/></settings>
。。。。。
</configuration>
public class Monster implements Serializable {
(3)在对应的 XxxMapper.xml 中设置二级缓存的策略
<mapper namespace="com.mapper.MonsterMapper"><!--1、配置二级缓存: 是mybatis自带2、eviction="FIFO" – 先进先出:按对象进入缓存的顺序来移除它们。默认LRU,最近最少使用。可选属性还有"SOFT"软引用,"WEAK"弱引用3. flushInterval 刷新间隔 是毫秒单位 60000 表示 60s4. size="512": 引用数目, 属性可以被设置为任意正整数, 默认10245. readOnly="true": (只读)属性可以被设置为 true 或 false: 如果我们只是用于读操作,建议设置成 true, 这样可以提示效率, 如果有修改操作,设置成 false, 默认就是false--><cache eviction="FIFO" flushInterval="60000"size="512" readOnly="true"/>。。。。。。。
</mapper>
(4)修改 MonsterMapperTest.java , 完成测试
//测试二级缓存的使用
@Test
public void level2CacheTest() {//查询id=1的monsterMonster monster = monsterMapper.getMonsterById(1);System.out.println("monster=" + monster);//这里关闭sqlSessionif (sqlSession != null) {sqlSession.close();}//重新获取sqlSessionsqlSession = MyBatisUtils.getSqlSession();//重新获取了monsterMappermonsterMapper = sqlSession.getMapper(MonsterMapper.class);//再次查询id=3的monsterSystem.out.println("--虽然前面关闭了sqlSession,因为配置二级缓存, " +"当你再次查询相同的id时, 依然不会再发出sql, 而是从二级缓存获取数据----");Monster monster2 = monsterMapper.getMonsterById(1);System.out.println("monster2=" + monster2);if (sqlSession != null) {sqlSession.close();}
}
测试效果
4.3.3 注意事项和使用陷阱
(1)理解二级缓存策略的参数
<cache eviction="FIFO" flushInterval="30000" size="360" readOnly="true"/
上面的配置意思如下:
创建了 FIFO 的策略,每隔 30 秒刷新一次,最多存放 360 个对象而且返回的对象被认为是 只读的。
- eviction:缓存的回收策略
- flushInterval:时间间隔,单位是毫秒
- size:引用数目,内存大就多配置点,要记住你缓存的对象数目和你运行环境的可用内存 资源数目。默认值是 1024
- readOnly:true,只读
(2)四大策略
- LRU – 最近最少使用的:移除最长时间不被使用的对象,它是默认
- FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
- SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
- WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
(3)如何禁用二级缓存
1) 在 mybatis-config.xml
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
<!--全局性地开启或关闭所有映射器配置文件中已配置的任何缓存, 默认就是 true-->
<setting name="cacheEnabled" value="false"/>
</settings>
2)在 mapper\MonsterMapper.xml 中把下面这一行注释掉
<!--<cache eviction="FIFO" flushInterval="30000" size="360" readOnly="true"/>-->
3) 或者更加细粒度的, 在配置方法上指定
设置 useCache=false 可以禁用当前 select 语句的二级缓存,即每次查询都会发出 sql 去查询, 默认情况是 true,即该 sql 使用二级缓存。 注意:一般我们不需要去修改,使用默认的即可
(4)mybatis 刷新二级缓存的设置
<update id="updateMonster" parameterType="Monster" flushCache="true">UPDATE mybatis_monster SET NAME=#{name},age=#{age} WHERE id=#{id}
</update>
insert、update、delete 操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读 默认为 true,默认情况下为 true 即刷新缓存,一般不用修改。
(5)不会出现一级缓存和二级缓存中有同一个数据。因为二级缓存(数据)是在一级缓存关闭 之后才有的
4.4 Mybatis 的一级缓存和二级缓存执行顺序
缓存执行顺序是:二级缓存-->一级缓存-->数据库
说明:先看二级缓存有没有数据,如果没有再看一级缓存,一级缓存也没有数据,才会连接数据库。当我们关闭一级缓存的时候,如果配置了二级缓存,那么一级缓存的数据,会放入到二级缓存
小实验,测试缓存执行顺序
(1)修改 MonsterMapperTest.java,增加如下方法
//演示二级缓存->一级缓存->DB执行的顺序
@Test
public void cacheSeqTest() {System.out.println("查询第1次");//DB, 会发出SQL, 分析cache hit ratio 0.0Monster monster1 = monsterMapper.getMonsterById(1);System.out.println(monster1);//这里关闭sqlSession, 一级缓存数据没有//当我们关闭一级缓存的时候,如果你配置二级缓存,那么一级缓存的数据,会放入到二级缓存sqlSession.close();sqlSession = MyBatisUtils.getSqlSession();monsterMapper = sqlSession.getMapper(MonsterMapper.class);System.out.println("查询第2次");//从二级缓存获取id=1 monster , 就不会发出SQL, 分析cache hit ratio 0.5Monster monster2 = monsterMapper.getMonsterById(1);System.out.println(monster2);System.out.println("查询第3次");//从二级缓存获取id=1 monster, 不会发出SQL, 分析cache hit ratio 0.6666Monster monster3 = monsterMapper.getMonsterById(1);System.out.println(monster3);if (sqlSession != null) {sqlSession.close();}System.out.println("操作成功");}
(2)运行结果:
4.5 EhCache 缓存
4.5.1 基本介绍
(1)配置文档(官方解释): https://www.cnblogs.com/zqyanywn/p/10861103.html
(2)文档说明(通俗解释): https://www.taobye.com/f/view-11-23.html
(3)EhCache 是一个纯 Java 的缓存框架,具有快速、精干等特点
(4)MyBatis 有自己默认的二级缓存(前面我们已经使用过了),但是在实际项目中,往往使用的是更加专业的第三方缓存产品 作为 MyBatis 的二级缓存,EhCache 就是非常优秀的缓存产品
4.5.2 配置和使用 EhCache
(1)加入相关依赖, 修改 mybatis_cache\pom.xml
<dependencies><!--引入ehcache核心库/jar--><dependency><groupId>net.sf.ehcache</groupId><artifactId>ehcache-core</artifactId><version>2.6.11</version></dependency><!--引入需要使用的slf4j--><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.25</version></dependency><!--引入mybatis整合ehcache库/jar--><dependency><groupId>org.mybatis.caches</groupId><artifactId>mybatis-ehcache</artifactId><version>1.2.1</version></dependency>
</dependencies>
(2)mybatis-config.xml 仍然打开二级缓存
<settings><!--1、全局性地开启或关闭所有映射器配置文件中已配置的任何缓存, 可以理解这是一个总开关2、默认就是: true--><setting name="cacheEnabled" value="true"/>
</settings>
(3)加入 mybatis_cache\src\main\resources\ehcache.xml 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache><!--diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:user.home – 用户主目录user.dir – 用户当前工作目录java.io.tmpdir – 默认临时文件路径--><diskStore path="java.io.tmpdir/Tmp_EhCache"/><!--defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。--><!--name:缓存名称。maxElementsInMemory:缓存最大数目maxElementsOnDisk:硬盘最大缓存个数。eternal:对象是否永久有效,一但设置了,timeout将不起作用。overflowToDisk:是否保存到磁盘,当系统宕机时timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。可理解为: TTI用于设置对象在cache中的最大闲置时间,就是 在一直不访问这个对象的前提下,这个对象可以在cache中的存活时间。timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。可理解为: TTL用于设置对象在cache中的最大存活时间,就是 无论对象访问或是不访问(闲置),这个对象在cache中的存活时间diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。clearOnFlush:内存数量最大时是否清除。memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。FIFO,first in first out,这个是大家最熟的,先进先出。LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。--><defaultCacheeternal="false"maxElementsInMemory="10000"overflowToDisk="false"diskPersistent="false"timeToIdleSeconds="1800"timeToLiveSeconds="259200"memoryStoreEvictionPolicy="LRU"/><cachename="cloud_user"eternal="false"maxElementsInMemory="5000"overflowToDisk="false"diskPersistent="false"timeToIdleSeconds="1800"timeToLiveSeconds="1800"memoryStoreEvictionPolicy="LRU"/></ehcache>
(4)在 XxxMapper.xml 中启用 EhCache , 记得把原来 MyBatis 自带的缓存配置注销
<!--配置/启用ehcache-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
(5)修改 MonsterMapperTest.java , 增加测试方法, 完成测试
//测试ehCache级缓存
@Test
public void ehCacheTest() {//查询id=1的monsterMonster monster = monsterMapper.getMonsterById(1);//会发出SQL, 到db查询System.out.println("monster=" + monster);//这里关闭sqlSession, 一级缓存[数据]失效.=> 将数据放入到二级缓存 (ehcache)if (sqlSession != null) {sqlSession.close();}//重新获取sqlSessionsqlSession = MyBatisUtils.getSqlSession();//重新获取了monsterMappermonsterMapper = sqlSession.getMapper(MonsterMapper.class);//再次查询id=1的monsterSystem.out.println("--虽然前面关闭了sqlSession,因为配置二级缓存(ehcache), " +"当你再次查询相同的id时, 不会再发出sql, 而是从二级缓存(ehcache)获取数据----");Monster monster2 = monsterMapper.getMonsterById(1);System.out.println("monster2=" + monster2);//再次查询id=3的monster, 仍然到二级缓存(ehcache), 获取数据, 不会发出sqlMonster monster3 = monsterMapper.getMonsterById(1);System.out.println("monster3=" + monster3);if (sqlSession != null) {sqlSession.close();}}
测试效果
4.5.3 EhCache 缓存-细节说明
如何理解 EhCache 和 MyBatis 缓存的关系
(1)MyBatis 提供了一个接口 Cache【如右图,找到 org.apache.ibatis.cache.Cache ,关联源 码包就可以看到 Cache 接口】
(2)只要实现了该 Cache 接口,就可以作为二级缓存产品和 MyBatis 整合使用,Ehcache 就是实现了该接口
(3)MyBatis 默认情况(即一级缓存)是使用的 PerpetualCache 类实现 Cache 接口的
(4)当我们使用了 Ehcahce 后,就是 EhcacheCache 类实现 Cache 接口的.
(5)我们看一下源码,发现缓存的本质就是 Map<Object,Object>