前言
在上篇MyBatis基础篇中我们独立使用MyBatis构建了一个简单的数据库访问程序,可以实现单表的基本增删改查等操作,通过该实例我们可以初步了解MyBatis操作数据库需要的一些组成部分(配置文件、实体类、SQL映射文件、Mapper接口等等)和重要对象(SqlSession、Mapper实例等等)。有了整体认知后,我们就可以进一步深入学习MyBatis的使用,resultMap本文主要围绕resultMap展开。
resultMap作为MyBatis的Sql映射文件中重要的元素之一,主要用来实现复杂的结果映射,其子元素结构如下:
constructor
constructor是构造器的意思,对反射有基本了解的都应该不会陌生。借此我们先回顾一下前面定义的一个简单resulMap映射内容
<resultMap type="person" id="personResultMap" >
<id column="id" property="id" />
<result column="name" property="name" />
<result column="gender" property="gender" />
</resultMap>
上面是一个典型的在resultMap中定义数据表与实体类的映射关系,type这里用的别名指向Person类,id为该映射的唯一标识,用于在后面我们的定义语句中引用,内部的id和result分别对应主键和普通字段定义,column指数据表中字段名,property指实体类中属性名。在之前的示例中,通过这个一个映射关系,我们查询出来的结果就转化为了一个Person类对象。
这个Person类对象并非由我们创建出来,而是由mybatis调用了Person类的默认无参构造函数创建对象,再调用set方法为对象赋值,这样才返回了我们想要的结果。下面结合之前的示例,做一些改动以便观察。
package com.mmm.pojo;
/**
* Person实体类
* */
public class Person {
private String id;
private String name;
private String gender;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
System.out.println("为主键属性id赋值");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
System.out.println("为属性name赋值");
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
System.out.println("为属性gender赋值");
}
public Person() {
System.out.println("调用无参构造创建对象");
}
}
<select id="selectById"resultMap="personResultMap">
select * from `person` where id = #{id}
</select>
调用该查找方法后会看到控制台输出如下信息:
说明确实是mybatis调用了这些方法帮我们创建了对象,基于构造器。说起构造器,在我们定义的pojo实体类中,简单的私有属性加上set/get方法声明即完成,在java类中,会默认提供一个无参的构造方法,例如这里的Person类
public Person(){}
我们使用语句 Person p = new Person();这里即调用的该无参构造。方法都可以重载,构造方法也不例外。但是很重要的一点,我们在重载构造方法后,原本默认提供的无参构造就无法使用了,需要显示声明,这点下面会给出示例。resultMap中constructor就是基于重载的带参构造方法创建对象,如下:
<resultMap type="person" id="personResultMap" >
<constructor>
<idArg column="id" javaType="string" />
<arg column="name" javaType="string" />
<arg column="gender" javaType="string" />
</constructor>
</resultMap>
在实体类中添加重载的带参构造方法
public Person(String id, String name, String gender) {
this.id = id;
this.name = name;
this.gender = gender;
System.out.println("直接调用带参构造函数创建对象");
}
再次调用查找方法后会看到控制台输出如下信息:
这样一来,使用constructor同样拿到了数据。
这个时候如果我们把实体类中的默认无参构造方法的显示声明删除,即只有一个带参的构造方法,这样我们再把Sql映射文件中<resultMap>内容还原成文中一开始定义的典型<id><result>等,再调用方法,为了更明显,在地址栏中通过controller层层调用到该方法,报错乍一看,一大串异常,其实标题末尾一句话就出来了
不存在该方法,该方法指的就是Person实体类中的无参构造方法,在之前我们没有显示声明没有问题,那是因为在类中如果没有任何的构造方法声明,会默认提供一个无参构造;但是我们使用construtor的时候在实体类中重载了一个带参的构造方法,使得原本默认的无参构造无法使用了,这里如果还要需要使用,可以在实体类中显示声明。
id、result
前面的单表典型基本映射就是由这两个元素组成,所以应该比较好理解。id和result都用于将表中字段和实体类中属性建立起映射关系,区别是id是用于主键字段的映射,result用于普通字段映射。两个元素中都有5个属性供定义,分别是column、property、javaType、jdbcType、typeHandler。
column:这里用于单表查询的话可以简单的理解为数据表中字段,前面也是类似处理,但是实际上并不是这么简单,例如我们在写SQL语句时经常会使用到别名,别名的好处就不复述了,特别是进行多表查询时,这里我们在SQL映射文件的<select>语句中也可以使用别名,这个别名与这里的column就对应上了,不注意这点的话可能出问题,后面介绍关联查询会有示例。
property:实体类中定义的与表字段对应的属性
javaType:类型这里可以使用全名,也可以使用别名,例如上面的string就是一个别名。我们在定义基本的实体类映射时,mybatis一般可以自动确定类型。
支持下面的 JDBC 类型。
typeHandler:在使用数据库创建表,定义字段名,选择字段类型时,例如MySql中字符串类型varchar,然后我们使用mybatis映射到实体类中变成Java字符串类型String,这个转换过程是怎么实现的?其中mybatis就已经预定义了一些类型处理器,也就是这个typeHandler,我们也可以根据需要自定义一些类型处理器。
association(对一)
association意思为联系、关联,针对单个(一个)对象,典型的一对一、多对一关联映射关系都可以通过该元素实现,下面是基本定义内容。
<association property="关联的对象在本对象实体类中的属性名" column="关联表的主键在本表中的字段名" javaType="关联对象的实体类类型(可用别名)">
<id column="ID" property="id" />
<result column="数据表字段名" property="实体类属性名" />
<result column="数据表字段名" property="实体类属性名" />
...
</association>
association>内部的<id><result>等元素,我们不一定要这样定义,也可以通过额外定义一个resultMap填入这些元素,然后<association>有一个resultMap属性,值就填额外的reslutMap的id,这样也可以实现关联映射。除此之外,也可以通过一个关联的嵌套查询语句实现,即额外定义一个针对关联表的<select>元素,然后<association>有一个select属性,值就填<select>的id,这样同样能实现。
collection(对多)
collection意为集合,这里主要用于关联多个对象的映射关系定义,例如典型的一对多,下面是基本定义内容。
<collection property="关联的对象在本对象实体类中的属性名" column="本表的主键在关联表中的字段名" ofType="关联集合中对象的实体类类型(可用别名)">
<id column="ID" property="id" />
<result column="数据表字段名" property="实体类属性名" />
<result column="数据表字段名" property="实体类属性名" />
...
</collection>
关联映射应用得较多,关联属性也相对复杂一些,光凭文字和片段描述很难快速理解,下面以一个一对多和多对一结合的示例来整体应用一次。
关联映射实例(一对多/多对一)
背景关系
这里以部门作为查询的实体对象,从图中不难理解,部门与公司是多对一关系,部门与员工是一对多关系,我们查询一个部门对象时,要把这个部门所属的公司信息查出来,还要把部门下面的所有员工信息查出来。
准备工作
这里对象分公司、部门、员工一共三类,所以首先准备三张数据表,结构如下
这里要理解一点,基于主键关联,不论是一对多,还是多对一,在“多“”的一方表中,都有一个与“一”的一方表主键对应的字段,例如这里的部门表中COMPANY_ID和员工表中的DEPT_ID,用来关联对应表。
新建maven工程,这里不需要建web项目,普通的java项目就行。pom依赖可以参照SSM框架开发web项目系列(一) 环境搭建篇。整体结构如下所示
然后新建三个对象实体类,如下所示
package com.mmm.pojo;
//公司
public class Company {
private Integer id; //主键
private String companyName; //公司名称
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
@Override
public String toString() {
return "Company [id=" + id + ", companyName=" + companyName + "]";
}
}
package com.mmm.pojo;
import java.util.List;
//部门
public class Dept {
private Integer id; //主键
private String deptName; //部门名称
private Company company; //部门所属公司---------多对一
private List<Emp> empList; //部门下面员工---------一对多
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
public List<Emp> getEmpList() {
return empList;
}
public void setEmpList(List<Emp> empList) {
this.empList = empList;
}
@Override
public String toString() {
return "Dept [id=" + id + ", deptName=" + deptName + ", company=" + company + ", empList=" + empList + "]";
}
}
package com.mmm.pojo;
//员工实体类
public class Emp {
private Integer id; //主鍵
private String name; //员工姓名
private String gender; //员工性别
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 String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Emp [id=" + id + ", name=" + name + ", gender=" + gender + "]";
}
}
配置文件(mybatis-config.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">
<configuration>
<!-- 这里可以定义类的别名,在mapper.xml文件中应用会方便很多 -->
<typeAliases>
<typeAlias alias="emp" type="com.mmm.pojo.Emp" />
<typeAlias alias="dept" type="com.mmm.pojo.Dept" />
<typeAlias alias="company" type="com.mmm.pojo.Company" />
</typeAliases>
<!-- 环境配置 -->
<environments default="envir">
<environment id="envir">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/ssm?characterEncoding=utf-8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/mmm/mapper/deptMapper.xml"/>
</mappers>
</configuration>
mapper接口及对应sql映射文件
package com.mmm.mapper;
import com.mmm.pojo.Dept;
public interface DeptMapper {
public Dept selectById(Integer id);
}
<?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="com.mmm.mapper.DeptMapper">
<resultMap type="dept" id="deptResultMap">
<!-- 下面的column就是前面说的要注意的,如果简单的认为是字段名会出问题 -->
<id column="d_id" property="id" />
<result column="d_name" property="deptName" />
<!-- 一个公司下面多个部门,部门与公司,多对一关系 -->
<association property="company" column="COMPANY_ID" javaType="company" resultMap="companyResultMap">
<!-- <id column="ID" property="id" />
<result column="COMPANY_NAME" property="companyName" /> -->
</association>
<!-- 一个部门下有多个员工,部门与员工,一对多关系 -->
<collection property="empList" column="DEPT_ID" ofType="emp">
<id column="ID" property="id" />
<result column="EMP_NAME" property="name" />
<result column="EMP_GENDER" property="gender" />
</collection>
</resultMap>
<!-- 也可以通过类似下面的定义然后引入到association中,association的resultMap属性值对应下面resultMap的id -->
<!-- <resultMap type="company" id="companyResultMap">
<id column="ID" property="id" />
<result column="COMPANY_NAME" property="companyName" />
</resultMap> -->
<!-- 根据主键id查找记录 -->
<select id="selectById" resultMap="deptResultMap">
select e.*,d.ID AS d_id,d.DEPT_NAME AS d_name,c.* from `TBL_EMP` e, `TBL_DEPT` d, `TBL_COMPANY` c where d.ID = #{id} and d.COMPANY_ID = c.id and d.ID = e.DEPT_ID;
</select>
</mapper>
最后,测试一下是否能实现关联查询
package com.mmm.test;
import java.io.IOException;
import java.io.Reader;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
import com.mmm.mapper.DeptMapper;
import com.mmm.pojo.Dept;
public class TestMyBatis {
@Test
public void testCore() throws IOException {
//直接实例SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//MyBatis配置文件路径
String path = "mybatis-config.xml";
//通过路径获取输入流
Reader reader = Resources.getResourceAsReader(path);
//通过reader构建sessionFactory
SqlSessionFactory sessionFactory = builder.build(reader);
//获取SqlSession对象
SqlSession sqlSession = sessionFactory.openSession();
//获取Mapper实例
DeptMapper mapper = sqlSession.getMapper(DeptMapper.class);
//根据id查找制定的部门,并关联查询出其公司和员工信息
Dept dept = mapper.selectById(10001);
System.out.println(dept);
}
}
控制台输出信息过长,复制下来粘贴如下内容:
Dept [id=10001, deptName=资产部, company=Company [id=10003, companyName=公司一], empList=[Emp [id=10001, name=员工一, gender=男], Emp [id=10002, name=员工二, gender=男], Emp [id=10003, name=员工三, gender=男]]]
可以看到,至此基本实现了,查询部门时,也关联查询出了所属公司信息、下面所有部门员工信息。
我们在浏览网站,点击链接跳转,每个页面的刷新,大多是不断在请求查询数据库中的数据,当然也不全是查询,例如会有日志记录,也是插入操作等等。查询是一个比较重要的内容,怎样关联查询,怎么优化查询语句,减少查询次数,提高查询速度等等都是需要去思考和学习的地方。