有了前面几章的基础,对一些简单的应用是可以处理的,但在实际项目中,经常是关联表的查询,比如:最常见到的一对一,一对多等。这些查询是如何处理的呢,这一讲就讲这个问题。前面节中介绍的都是单表映射的一些操作,然而在我们的实际项目中往往是用到多表映射。在 Java 实体对象对中,一对多可以根据 List 和 Set 来实现,两者在 mybitis 中都是通过 collection 标签来配合来加以实现。这篇介绍的是多表中的多对一表关联查询。
创建两张表,假设一个老师对应一个班级
CREATE TABLE `teacher` (
`t_id` int(11) NOT NULL AUTO_INCREMENT,
`t_name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`t_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert into `teacher`(`t_id`,`t_name`) values (1,'张三');
insert into `teacher`(`t_id`,`t_name`) values (2,'李四');
CREATE TABLE `class` (
`c_id` int(11) NOT NULL AUTO_INCREMENT,
`c_name` varchar(20) DEFAULT NULL,
`teacher_id` int(11) DEFAULT NULL,
PRIMARY KEY (`c_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert into `class`(`c_id`,`c_name`,`teacher_id`) values (1,'Java',1);
insert into `class`(`c_id`,`c_name`,`teacher_id`) values (2,'UI',2);
CREATE TABLE `student` (
`s_id` int(11) NOT NULL AUTO_INCREMENT,
`s_name` varchar(20) DEFAULT NULL,
`class_id` int(11) DEFAULT NULL,
PRIMARY KEY (`s_id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
insert into `student`(`s_id`,`s_name`,`class_id`) values (1,'AA',1);
insert into `student`(`s_id`,`s_name`,`class_id`) values (2,'BB',1);
insert into `student`(`s_id`,`s_name`,`class_id`) values (3,'CC',1);
insert into `student`(`s_id`,`s_name`,`class_id`) values (4,'DD',2);
insert into `student`(`s_id`,`s_name`,`class_id`) values (5,'EE',2);
insert into `student`(`s_id`,`s_name`,`class_id`) values (6,'FF',2);
创建实体
public class Teacher {
private int id;
private String name;
public class Classes {
private int id;
private String name;
private Teacher teacher;
ClassesMapper.xml 查询将出现两张方式
方式一:级联查询
<resultMap id="classResultMap" type="cn.hxzy.mybatis.entity.Classes">
<id column="c_id" property="id"/>
<result column="c_name" property="name"/>
<result column="t_id" property="teacher.id"/>
<result column="t_name" property="teacher.name"/>
</resultMap>
<select id="selectAll" resultMap="classResultMap">
SELECT * FROM `class` c LEFT JOIN `teacher` t ON t.`t_id`=c.`teacher_id`
</select>
方式二:嵌套结果,使用嵌套结果映射来处理重复的联合结果的子集封装联表查询的数据(去除重复的数据)
<resultMap id="classResultMap2" type="cn.hxzy.mybatis.entity.Classes">
<id column="c_id" property="id"/>
<result column="c_name" property="name"/>
<association property="teacher" javaType="cn.hxzy.mybatis.entity.Teacher">
<id property="id" column="t_id"/>
<result property="name" column="t_name"/>
</association>
</resultMap>
<select id="selectAll" resultMap="classResultMap2">
SELECT * FROM `class` c LEFT JOIN `teacher` t ON t.`t_id`=c.`teacher_id`
</select>
方式三:嵌套查询,通过执行另外一个 SQL 映射语句来返回预期的复杂类型
<select id="getClass2" resultMap="ClassResultMap2">
select * from class
</select>
<resultMap type="cn.hxzy.mybatis.entity.Classes" id="ClassResultMap2">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<association property="teacher" column="teacher_id" select="getTeacher"></association>
</resultMap>
<select id="getTeacher" parameterType="int" resultType="cn.hxzy.mybatis.entity.Teacher">
SELECT t_id id, t_name name FROM teacher WHERE t_id=#{id}
</select>
association 定义关联对象的封装规则
select:表明当前属性是调用 select 指定的方法查出的结果。
column:指定将哪一列的值传给这个方法。
注意:$ 与 # 的区别:
#{} 使用占位符 ?的方式编译和发送 SQL;好处:防止 SQL 注入(推荐)
${} 将用户填入的参数直接拼接到 SQL。坏处:SQL 注入;
注意:使用 #{} 不能生成表名和字段名,所以在字段和表名处,必须使用 ${}。
将 #{id} 转成 ${id} 会报 There is no getter for property named ‘id’ in ‘class java.lang.Integer’ ,只需要将参数换成 ${_parameter} 或者换成更高的 3.5.6 即可。
练习:
1.创建项目 mybatis11,分别使用嵌套查询和嵌套结果完成如下用户角色的查询。表数据下载。
需使用两种查询完成如下功能:
一、查询单个用户时将其所属角色查询出来。
参考代码:
<mapper namespace="cn.hxzy.mapper.UserMapper">
<!-- 嵌套结果-->
<resultMap id="userResultMap1" type="cn.hxzy.entity.User">
<id column="u_id" property="id"/>
<result column="u_name" property="name"/>
<result column="login_name" property="loginName"/>
<result column="login_password" property="loginPassword"/>
<result column="u_create_time" property="createTime"/>
<result column="u_update_time" property="updateTime"/>
<association property="role" javaType="cn.hxzy.entity.Role">
<id column="r_id" property="id"/>
<result column="r_name" property="name"/>
<result column="remark" property="remark"/>
<result column="r_create_time" property="createTime"/>
<result column="r_update_time" property="updateTime"/>
</association>
</resultMap>
<sql id="user">u.id u_id,u.name u_name,u.login_name login_name, u.login_password login_password,
u.create_time u_create_time,u.update_time u_update_time</sql>
<sql id="role">r.id r_id,r.name r_name,r.remark remark,r.create_time r_create_time,r.update_time r_update_time</sql>
<select id="findByIdWithRole1" resultMap="userResultMap1">
select
<include refid="user"/>,
<include refid="role"/>
from user u left join role r on u.role_id=r.id where u.id=#{id}
</select>
<!-- 嵌套查询-->
<resultMap id="userResultMap2" type="cn.hxzy.entity.User">
<id column="u_id" property="id"/>
<result column="u_name" property="name"/>
<result column="login_name" property="loginName"/>
<result column="login_password" property="loginPassword"/>
<result column="u_create_time" property="createTime"/>
<result column="u_update_time" property="updateTime"/>
<association property="role" javaType="cn.hxzy.entity.Role" column="role_id" select="getRole">
</association>
</resultMap>
<select id="findByIdWithRole2" resultMap="userResultMap2">
select
<include refid="user"/>,role_id
from user u where id=#{id}
</select>
<select id="getRole" resultType="cn.hxzy.entity.Role">
select id,name,remark,create_time createTime,update_time updateTime from role where id=#{id}
</select>
</mapper>
实体类
public class Student {
private int id;
private String name;
public class Classes {
private int id;
private String name;
private Teacher teacher;
private List<Student> students;
方式一:嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集
<select id="findAll" resultMap="ClassResultMap3">
SELECT * FROM `class` c LEFT JOIN `student` s ON c.`c_id` =s.`class_id`
</select>
<resultMap type="cn.hxzy.mybatis.entity.Classes" id="ClassResultMap3">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<collection property="students" ofType="cn.hxzy.mybatis.entity.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
</collection>
</resultMap>
注意:ofType 指定 students 集合中的对象类型
方式二:嵌套查询,通过执行另外一个SQL映射语句来返回预期的复杂类型
<select id="findAll" resultMap="ClassResultMap4">
select * from class
</select>
<resultMap type="cn.hxzy.mybatis.entity.Classes" id="ClassResultMap4">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<collection property="students" ofType="cn.hxzy.mybatis.entity.Student"
column="c_id" select="getStudent"></collection>
</resultMap>
<select id="getStudent" parameterType="int" resultType="cn.hxzy.mybatis.entity.Student">
SELECT s_id id, s_name name FROM student WHERE class_id=#{id}
</select>
collection 定义关联集合类型的属性的封装规则
ofType:指定集合里面元素的类型
在 mybatis 中关联关系常用如下:
一对一 javaType
一对多 ofType
练习:(默写 30 分钟)
1.在上面项目 mybatis11 中,使用嵌套查询完成用户角色资源的查询。完成如下功能:
一、查询单个角色时将其角色下的所有资源全部查询出来。
二、查询单个用户时将其所属的角色以及角色拥有的资源全部查询出来。
参考代码:
UserDao.xml
<?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="cn.hxzy.dao.UserDao">
<resultMap type="cn.hxzy.entity.User" id="UserMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="loginName" column="login_name" jdbcType="VARCHAR"/>
<result property="loginPassword" column="login_password" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<association property="role" column="role_id" select="cn.hxzy.dao.RoleDao.queryById"></association>
</resultMap>
<!--查询单个-->
<select id="queryById" resultMap="UserMap">
select
id, name, login_name, login_password, role_id, create_time, update_time
from mybatis.user
where id = #{id}
</select>
</mapper>
RoleDao.xml
<?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="cn.hxzy.dao.RoleDao">
<resultMap type="cn.hxzy.entity.Role" id="RoleMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="remark" column="remark" jdbcType="VARCHAR"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<collection property="resources" ofType="cn.hxzy.entity.Resource" column="id"
select="cn.hxzy.dao.ResourceDao.queryByRoleId"></collection>
</resultMap>
<!--查询单个-->
<select id="queryById" resultMap="RoleMap">
select
id, name, remark, create_time, update_time
from mybatis.role
where id = #{id}
</select>
</mapper>
ResourceDao.xml
<?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="cn.hxzy.dao.ResourceDao">
<resultMap type="cn.hxzy.entity.Resource" id="ResourceMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="url" column="url" jdbcType="VARCHAR"/>
<result property="pid" column="pid" jdbcType="INTEGER"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="orderNumber" column="order_number" jdbcType="INTEGER"/>
</resultMap>
<!--查询单个-->
<select id="queryByRoleId" resultMap="ResourceMap">
select
id, name, url, pid, create_time, update_time, order_number
from resource re left join role_resource rr on re.id = rr.resource_id where rr.role_id=#{roleId}
</select>
</mapper>
1.一级缓存:基于 PerpetualCache 的 HashMap本地缓存,其存储作用域为 Session,当 Session clearCache 或 close 之后,该 Session中的所有 Cache 就将清空。 (默认开启)
2.二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace 命名空间),并且可自定义存储源,如 Ehcache。
3.对于缓存数据更新机制,当某一个作用域(一级缓存 Session /二级缓存 Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
一级缓存就是 Session 不关闭的情况下多次查询同一个对象不发 SQL,mybatis 默认开启一级缓存。
二级缓存通常需要在 *Mapper.xml 中添加一个 ,还要在 mybatis 配置文件中全局开启。
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
注意:第一个一级缓存关闭时会将数据刷入 SqlSessionFactory 另外一个 session 才能获取二级缓存内容,即前面的 session 必须关闭或 commit,后面的 session 才能获取。
SqlSessionFactory factory = MybatisUtils.getFactory();
SqlSession session1 = factory.openSession();
SqlSession session2 = factory.openSession();
String statement = "com.ibaits.mapper.userMapper.getUser";
CUser user = session1.selectOne(statement, 1);
session1.commit();
System.out.println(user);
user = session2.selectOne(statement, 1);
session2.commit();
System.out.println(user);
映射语句文件中的所有 select 语句将会被缓存。
映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
缓存会根据指定的时间间隔来刷新。
缓存会存储 1024 个对象
flushInterval=“60000” //自动刷新时间60s
size=“512” //最多缓存512个引用对象
readOnly=“true”/> //只读
需要用到 cglib 做代理模式。使用如下方式导入 cglib 依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
然后在全局配置文件加入
<!-- 全局配置参数 -->
<settings>
<!-- 延迟加载总开关 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 设置按需加载 -->
<setting name="aggressiveLazyLoading" value="false" />
</settings>
如上配置,所有的嵌套查询就会延迟加载,但是所有的嵌套结果方式没有影响。
mybatis 可以使用 discriminator 判断某列的值,然后根据某列的值改变封装行为。
案例:封装 Employee
如果查出的是女生:就把部门信息查询出来,否则不查询;
如果是男生,把 last_name 这一列的值赋值给 email;
<resultMap type="cn.hxzy.mybatis.bean.Employee" id="MyEmpDis">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="email" property="email"/>
<result column="gender" property="gender"/>
<discriminator javaType="string" column="gender">
<!--女生 resultType:指定封装的结果类型;不能缺少。/resultMap-->
<case value="0" resultType="employee">
<association property="dept" select="cn.hszy.mybatis.mapper.DepartmentMapper.getDeptById"
column="d_id"></association>
</case>
<!--男生 ;如果是男生,把last_name这一列的值赋值给email; -->
<case value="1" resultType="employee">
<id column="id" property="id"/>
<result column="last_name" property="lastName"/>
<result column="last_name" property="email"/>
<result column="gender" property="gender"/>
</case>
</discriminator>
</resultMap>
创建表和存储过程。
create table p_user(
id int primary key auto_increment,
name varchar(10),
sex char(2)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into p_user(name,sex) values('A',"男");
insert into p_user(name,sex) values('B',"女");
insert into p_user(name,sex) values('C',"男");
创建存储过程(查询得到男性或女性的数量,如果传入的是 0 就女性否则是男性)
DELIMITER $
CREATE PROCEDURE ges_user_count(IN sex_id INT, OUT user_count INT)
BEGIN
IF sex_id=0 THEN
SELECT COUNT(*) FROM p_user WHERE p_user.sex='女' INTO user_count;
ELSE
SELECT COUNT(*) FROM p_user WHERE p_user.sex='男' INTO user_count;
END IF;
END
$
调用存储过程
DELIMITER ;
SET @user_count = 0;
CALL ges_user_count(1, @user_count);
SELECT @user_count;
查询得到男性或女性的数量,如果传入的是0就女性否则是男性
mapper.xml
<mapper namespace="cn.hxzy.mybatis.test7.userMapper">
<select id="getCount" statementType="CALLABLE" parameterMap="getCountMap">
call ges_user_count(?,?)
</select>
<parameterMap type="map" id="getCountMap">
<parameter property="sex_id" mode="IN" jdbcType="INTEGER"/>
<parameter property="user_count" mode="OUT" jdbcType="INTEGER"/>
</parameterMap>
</mapper>
执行查询
Map<String, Integer> parameterMap = new HashMap<String, Integer>();
parameterMap.put("sex_id", 1);
parameterMap.put("user_count", -1);
session.selectOne(statement, parameterMap);
Integer result = parameterMap.get("user_count");
System.out.println(result);
分页可以分为逻辑分页和物理分页。逻辑分页是我们的程序在显示每页的数据时,首先查询得到表中的 1000 条数据,然后成熟根据当前页的“页码”选出其中的 100 条数据来显示。
物理分页是程序先判断出该选出这 1000 条的第几条到第几条,然后数据库根据程序给出的信息查询出程序需要的 100 条返回给我们的程序。
mybatis 的分页通常在开发中有时借助第三方分页插件进行分页
添加如下两个第三方 jar 包
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.0.0</version>
</dependency>
mybatis 配置文件加入插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
在查询之前调用静态方法即可完成查询
@Test
public void testGetAll() {
SqlSessionFactory factory = MybatisUtils.getFactory();
SqlSession session = factory.openSession();
String statement = "com.day03_mybaits.test2.userMapper.getAllUsers";
Page<User> startPage = PageHelper.startPage(1, 12);
List<User> list = session.selectList(statement);
session.close();
for (User user : list) {
System.out.println(user);
}
System.out.println(startPage.getPages());
}
Page 使用 json 序列化工具会导致其他属性(总页数,总条数)不见,可使用 PageInfo 弥补该问题
public PageInfo<MenuGroup> findAll(Integer page, Integer size) {
Page<MenuGroup> objects = PageHelper.startPage(page, size);
menuGroupMapper.selectByExample(new MenuGroupExample());
return new PageInfo<>(objects);
}
由于 jdk7 之前通过反射无法获取方法参数的名字,jdk8 有该功能但是未开启。所以在接口中方法有多个参数时xml中无法正常识别。
List<Resource> queryAll(int start,int size,String name);
xml
<select id="queryAll" resultType="cn.hxzy.entity.Resource">
select id, name, url, pid, create_time, update_time, order_number
from resource where name like #{name} limit #{start},#{size}
</select>
解决的办法有如下几个:
1.搜索打开 jdk8 反射获取参数名,或者升级 jdk9。
2.使用 [arg2, arg1, arg0, param3, param1, param2] 代替#{name}、#{start}和#{size}
3.像如下方式加入 mybatis 的注解 org.apache.ibatis.annotations.Param。
List<Resource> queryAll(@Param("start") int start,@Param("size") int size,@Param("name") String name);