Mybatis多表关联映射
查询结果集ResultMap
resultMap 元素是 MyBatis 中最重要最强大的元素。它就是让你远离 90%的需要从结果 集中取出数据的 JDBC 代码的那个东西,而且在一些情形下允许你做一些 JDBC 不支持的事 情。 事实上, 编写相似于对复杂语句联合映射这些等同的代码,也许可以跨过上千行的代码。
有朋友会问,之前的示例中我们没有用到结果集,不是也可以正确地将数据表中的数据映射到Java对象的属性中吗?是的。这正是resultMap元素设计的初衷,就是简单语句不需要明确的结果映射,而很多复杂语句确实需要描述它们的关系。我们对Mybatis的设置进行了详细的讲解,不知道朋友们是否还记得有这样一个设置变量autoMappingBehavior,其默认值为PARTIAL,这就意味着Mybatis会自动映射没有定义在resultMap中的字段。前提是Java中的属性名称与数据表中的字段名称完全一样(大小写敏感),又或者是Java中使用了驼峰命名规则,但数据表中使用是下划线连词规则,且我们把Mybatis中的设置变量mapUnderscoreToCamelCase设置为true。
- resultMap元素中,允许有以下直接子元素:
- constructor - 类在实例化时,用来注入结果到构造方法中(本文中暂不讲解)
- id - 作用与result相同,同时可以标识出用这个字段值可以区分其他对象实例。可以理解为数据表中的主键,可以定位数据表中唯一一笔记录
- result - 将数据表中的字段注入到Java对象属性中
- association - 关联,简单的讲,就是“有一个”关系,如“用户”有一个“帐号”
- collection - 集合,顾名思议,就是“有很多”关系,如“客户”有很多“订单”
- discriminator - 使用结果集决定使用哪个个结果映射(暂不涉及)
每个元素的用法及属性我会在下面结合使用进行讲解。下面就正式进入今天的主题。
案例背景
在第三讲《Mybatis之简单示例》的基础上,我们在数据库中额外创建三张数据表,分别表示销售人员、客户,以及销售和客户多对多的对应关系。每个销售、客户都有一个登录帐号。
- CREATE TABLE `customer` (
- `customer_id` int(10) NOT NULL AUTO_INCREMENT,
- `customer_name` varchar(200) NOT NULL,
- `user_id` int(10) DEFAULT NULL,
- `is_valid` tinyint(4) NOT NULL DEFAULT '1',
- `created_time` datetime NOT NULL,
- `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (`customer_id`),
- KEY `customer_name` (`customer_name`) USING BTREE
- ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
- CREATE TABLE `salesman` (
- `sales_id` int(10) NOT NULL AUTO_INCREMENT,
- `sales_name` varchar(64) NOT NULL,
- `sales_phone` varchar(32) DEFAULT NULL,
- `sales_fax` varchar(32) DEFAULT NULL,
- `sales_email` varchar(100) DEFAULT NULL,
- `user_id` int(10) DEFAULT NULL,
- `report_to` int(10) DEFAULT '0',
- `is_valid` tinyint(4) NOT NULL DEFAULT '1',
- `created_time` datetime DEFAULT NULL,
- `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (`sales_id`),
- KEY `sales_name` (`sales_name`)
- ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
-
- CREATE TABLE `customer_sales` (
- `id` int(10) NOT NULL AUTO_INCREMENT,
- `customer_id` int(10) NOT NULL,
- `sales_id` int(10) NOT NULL,
- `created_time` datetime NOT NULL,
- `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (`id`),
- UNIQUE KEY `customer_id` (`customer_id`,`sales_id`) USING BTREE,
- KEY `sales_id` (`sales_id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
实现销售与登录用户一对一关系
为了巩固上一篇文章《Mybatis系列之接口式编程》中的知识,这里采用Mybatis的接口式编程。无论是对单表进行映射,还是对多表映射,步骤都是相同的,唯一的不同就在映射文件的编写上,所以,这里我们只把重点放在映射文件上,其他部分就一笔提过。
首先,我们需要销售创建一个Java类,其中的userInfo属性对应销售的登录用户信息的。
第二步,编写Mybatis映射文件,需要注意的是映射文件的名称空间,要与我们编写的接品的全限定名一致(包名+接口名)。
第三步,将映射文件注册到Mybatis中。这一步很简单,简单到很多朋友会忘掉。却也是最重要的一环。
- <mappers>
- <mapper resource="com/emerson/learning/mapping/User.xml" />
- <mapper resource="com/emerson/learning/mapping/Sales.xml" />
- </mappers>
第四步,编写接口。
- package com.emerson.learning.dao;
-
- import com.emerson.learning.pojo.Sales;
-
- public interface ISalesDao {
-
- public Sales getById(int id);
-
- }
第四步,编写测试用例。
- package com.emerson.learning.dao;
-
- import static org.junit.Assert.*;
-
- 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.After;
- import org.junit.Before;
- import org.junit.Test;
-
- import com.emerson.learning.dao.ISalesDao;
- import com.emerson.learning.pojo.Sales;
-
- public class SalesDaoTest {
-
- private Reader reader;
- private SqlSessionFactory sqlSessionFactory;
-
- @Before
- public void setUp() throws Exception {
- try {
- reader = Resources.getResourceAsReader("mybatis.xml");
- } catch (IOException e) {
- e.printStackTrace();
- }
- sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
- }
-
- @After
- public void tearDown() throws Exception {
- }
-
- @Test
- public void getById() {
- SqlSession session = sqlSessionFactory.openSession();
- try {
- ISalesDao sd = session.getMapper(ISalesDao.class);
- Sales sales = sd.getById(2);
- assertNotNull(sales);
- System.out.println(sales);
- } finally {
- session.close();
- }
- }
-
- }
下面我们就针对第二步,映射文件中的resultMap编写进行详细讲解。
- <resultMap id="salesResultMap" type="com.emerson.learning.pojo.Sales">
- <id property="salesId" column="sales_id" />
- <result property="salesName" column="sales_name" />
- <result property="phone" column="sales_phone" />
- <result property="fax" column="sales_fax" />
- <result property="email" column="sales_email" />
- <result property="isValid" column="is_valid" />
- <result property="createdTime" column="createdTime" />
- <result property="updateTime" column="update_time" />
-
-
- <association property="userInfo" column="user_id" javaType="User" select="selectUser">
- <id property="userId" column="userId" />
- <result property="userName" column="user_name" />
- <result property="userPassword" column="user_password" />
- <result property="nickName" column="nick_name" />
- <result property="email" column="email" />
- <result property="isValid" column="is_valid" />
- <result property="createdTime" column="created_time" />
- <result property="updateTime" column="update_time" />
- </association>
- </resultMap>
和其他元素一样,我们都需要为其取一个唯一的id,并指定其在Java中对应的类型,由于我没有在Mybatis配置文件中为Sales类指定别名,所以这里使用的是全限定名。
- <association property="userInfo" column="user_id" javaType="User" select="selectUser" />
我们还需要告诉Mybatis,加载关联的方式。MyBatis 在这方面会有两种不同的方式:
- 嵌套查询:通过执行另外一个 SQL 映射语句来返回预期的复杂类型。
- 嵌套结果:使用嵌套结果映射来处理重复的联合结果的子集。
- <select id="selectUser" resultType="User">
- SELECT user_id, user_name, user_password, nick_name, email, is_valid, created_time
- FROM sys_user WHERE user_id = #{id}
- </select>
当对Sales进行映射的时候,Mybatis会使用这个名为selectUser的查询语句去获取相关联的数据信息。这种方法使用起来很简单。但是简单,不代表最好。对于大型数据集合和列表这种方式将会有性能上的问题,就是我们熟知的 “N+1 查询问题”。概括地讲,N+1 查询问题可以是这样引起的:
- 你执行了一个单独的 SQL 语句来获取结果列表(就是“+1”)。
- 对返回的每条记录,你执行了一个查询语句来为每个加载细节(就是“N”)。
这个问题会导致成百上千的 SQL 语句被执行。这通常不是期望的。
MyBatis 能延迟加载这样的查询就是一个好处,因此你可以分散这些语句同时运行的消耗。然而,如果你加载一个列表,之后迅速迭代来访问嵌套的数据,你会调用所有的延迟加载,这样的行为可能是很糟糕的。
嵌套结果
下面我们就来讲一下另一种实式方式:嵌套结果。使用这种方式,就可以有效地避免了N+1问题。
- <?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.emerson.learning.dao.ISalesDao">
- <resultMap id="salesResultMap" type="com.emerson.learning.pojo.Sales">
- <id property="salesId" column="sales_id" />
- <result property="salesName" column="sales_name" />
- <result property="phone" column="sales_phone" />
- <result property="fax" column="sales_fax" />
- <result property="email" column="sales_email" />
- <result property="isValid" column="is_valid" />
- <result property="createdTime" column="created_time" />
- <result property="updateTime" column="update_time" jdbcType="TIMESTAMP" />
-
-
- <association property="userInfo" resultMap="userResult" />
- </resultMap>
-
- <resultMap id="userResult" type="User">
- <id property="userId" column="user_id" />
- <result property="userName" column="user_name" />
- <result property="userPassword" column="user_password" />
- <result property="nickName" column="nick_name" />
- <result property="email" column="user_email" />
- <result property="isValid" column="user_is_valid" />
- <result property="createdTime" column="user_created_time" />
- <result property="updateTime" column="user_update_time" />
- </resultMap>
-
- <select id="getById" parameterType="int" resultMap="salesResultMap">
- SELECT
- sales_id, sales_name, sales_phone, sales_fax, sales_email,
- salesman.is_valid, salesman.created_time, salesman.update_time,
- sys_user.user_id as user_id, user_name, user_password, nick_name,
- email as user_email,
- sys_user.is_valid as user_is_valid, sys_user.created_time as
- user_created_time,
- sys_user.update_time as user_update_time
- FROM
- salesman left outer join sys_user using(user_id)
- WHERE sales_id=#{id}
- </select>
- </mapper>
和嵌套查询相比,使用嵌套结果方式,在映射文件上主要有以下三处修改:
一、修改association元素,无需指定column,另外将resultType改为使用resultMap。为什么?这是因为后面我们会把select语句改为多表关联查询,这样就会有些字段名是冲突的,我们不得不使用别名。这一点对于Mybatis而言,就相当于字段名发生了变化,那么就需要我们手工来维护映射关系。另外,我们也无需指定javaType属性了,因为在resultMap中,已经指定了对应的Java实体类,这里就可以省略了。
- <association property="userInfo" resultMap="userResult" />
二、为关联结果集编写映射关系,大家可以看到,好多字段名称已经发生了变化,如is_valid这个字段由于salesman和sys_user表中都存在这个字段,所以我们不得不为其起了一个别名user_is_valid。
- <resultMap id="userResult" type="User">
- <id property="userId" column="user_id" />
- <result property="userName" column="user_name" />
- <result property="userPassword" column="user_password" />
- <result property="nickName" column="nick_name" />
- <result property="email" column="user_email" />
- <result property="isValid" column="user_is_valid" />
- <result property="createdTime" column="user_created_time" />
- <result property="updateTime" column="user_update_time" />
- </resultMap>
三、修改查询语句,由单表查询改表多表关联查询
- <select id="getById" parameterType="int" resultMap="salesResultMap">
- SELECT sales_id, sales_name, sales_phone, sales_fax, sales_email,
- salesman.is_valid, salesman.created_time, salesman.update_time,
- sys_user.user_id as user_id, user_name, user_password, nick_name,
- email as user_email,
- sys_user.is_valid as user_is_valid, sys_user.created_time as
- user_created_time,
- sys_user.update_time as user_update_time
- FROM salesman left outer join sys_user using(user_id)
- WHERE sales_id=#{id}
- </select>
至此,关联映射已讲解完了。还有集合映射没有讲,哇咔咔,内空实在是太多了〜〜〜〜今晚通宵也未必能写得完了。暂时先写到这儿吧,下回再继续讲解如何实现多对多的集合映射。