前言
关联关系是面向对象分析、面向对象设计最终的思想,Mybatis完全可以理解这种关联关系,如果关系得当,Mybatis的关联映射将可以大大简化持久层数据的访问。关联关系大致可以分为以下情况:
1、一对一,比如一个人只能有一张身份证,而一张身份证只能属于一个人;
2、一对多,比如一个部门有多个员工,而一个员工只能属于一个部门;
3、多对多,比如在购物系统中,一个订单可以有多个商品,而一个商品也可以属于多个订单。
当然,还有更复杂的关系,同样在购物系统中,一个用户可以有多个订单,而一个订单只能属于一个用户,再加上商品的关系就是一对多夹杂多对多的关系,但是万变都不离其中。
下面就围绕这几种关系来看看Mybatis是怎么处理的。
一对一
1、搭建mybatis环境和日志环境(参考第一篇和第二篇)并创建如下两张表tb_user和tb_card,插入两条数据:
2、创建相应的实体类对象(用user去关联card)、dao层接口和mapper文件
持久化对象如下:
public class Card {
private Integer cardId;//主键id
private String cardCode;//身份证号
//省略相应的get/set和构造方法
}
public class User{
private Integer userId;//主键userId
private String userName;//姓名
private String userGender;//性别
private Integer userAge;//年龄
private Card card;//人和身份证是一对一的关系
//省略相应的set/get方法和构造方法
}
dao层接口:
public interface CardDao {
Card getCardById(Integer cardId);
}
public interface UserDao {
User getUserById(Integer userId);
}
mapper文件(记得在mybais的配置文件中扫描mapper文件)
<?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.zepal.mybatis.dao.UserDao">
<resultMap type="com.zepal.mybatis.domain.User" id="userMap">
<id column="user_id" property="userId"/>
<result column="user_name" property="userName"/>
<result column="user_gender" property="userGender"/>
<result column="user_age" property="userAge"/>
<!-- 一对一关联,select属性是通过namespace引入的card查询 -->
<association property="card" column="card_id"
select="com.zepal.mybatis.dao.CardDao.getCardById"
javaType="com.zepal.mybatis.domain.Card"/>
</resultMap>
<select id="getUserById" parameterType="int" resultMap="userMap">
SELECT * FROM tb_user WHERE user_id = #{userId};
</select>
</mapper>
<?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.zepal.mybatis.dao.CardDao">
<resultMap type="com.zepal.mybatis.domain.Card" id="cardMap">
<id column="card_id" property="cardId"/>
<result column="card_code" property="cardCode"/>
</resultMap>
<select id="getCardById" parameterType="int" resultMap="cardMap">
SELECT * FROM tb_card WHERE card_id = #{cardId};
</select>
</mapper>
说明:在查询user的时候,通过<association>标签引入了card的查询,其中select属性表示使用column属性的card_id值作为参数去执行CardMapper.xml中定义的getCardById,注意,它是通过CardMapper.xml中的命名空间namespqce引入的。
在<association>还有子标签,可以直接将需要关联对象的集合映射直接写在<association>里面,一般这种情况用在连接查询的时候,也就是sql中内连接、外连接之类的,注意上面称为关联查询,比如:
<association property="card" column="card_id"
select="com.zepal.mybatis.dao.CardDao.getCardById"
javaType="com.zepal.mybatis.domain.Card">
<id column="card_id" property="cardId"/>
<result column="card_code" property="cardCode"/>
</association>
3、测试代码和结果
public class Test {
public static void main(String[] args) {
InputStream is = Test.class.getResourceAsStream("/mybatis/mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
Integer userId = 1;
UserDao userDao = sqlSession.getMapper(UserDao.class);
User user = userDao.getUserById(userId);
System.out.println(user.toString());
sqlSession.close();
}
}
DEBUG [main] - ==> Preparing: SELECT * FROM tb_user WHERE user_id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
User [userId=1, userName=张三, userGender=男, userAge=18, card=Card [cardId=2, cardCode=12345619900801123x]]
一对多
1、按照上举例的部门的和员工的关系创建如下两张表tb_dept和tb_emp,并插入数据。Mybatis的配置环境继续延用。
2、创建相应的持久层对象、dao层接口和mapper文件
持久层对象
public class Emp {
private Integer empId;
private String empName;
private Dept dept;//一个员工只能属于一个部门
//省略get/set和构造方法
}
public class Dept {
private Integer deptId;
private String deptName;
private List<Emp> emps;//一个部门有多个员工
//省略get/set和构造方法
}
dao层接口(这里在EmpDao里面写了两个方法,很明显,如果我要通过dept去关联查询emp,应该调用的是getEmpByDeptId,我应该是通过dept_id去查询,这里注意一下)
public interface DeptDao {
Dept getDeptById(Integer deptId);
}
public interface EmpDao {
Emp getEmpById(Integer empId);
Emp getEmpByDeptId(Integer deptId);
}
mapper文件
<mapper namespace="com.zepal.mybatis.dao.DeptDao">
<resultMap type="com.zepal.mybatis.domain.Dept" id="deptMap">
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
<collection property="emps" column="dept_id"
javaType="java.util.ArrayList" ofType="com.zepal.mybatis.domain.Emp"
select="com.zepal.mybatis.dao.EmpDao.getEmpByDeptId"
fetchType="lazy"/>
</resultMap>
<select id="getDeptById" parameterType="int" resultMap="deptMap">
SELECT * FROM tb_dept WHERE dept_id = #{deptId};
</select>
</mapper>
<mapper namespace="com.zepal.mybatis.dao.EmpDao">
<resultMap type="com.zepal.mybatis.domain.Emp" id="empMap">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<!-- 注意这个地方 -->
<association property="dept" column="dept_id"
select="com.zepal.mybatis.dao.DeptDao.getDeptById"
javaType="com.zepal.mybatis.domain.Dept"/>
</resultMap>
<select id="getEmpById" parameterType="int" resultMap="empMap">
SELECT * FROM tb_emp WHERE emp_id = #{empId};
</select>
<resultMap type="com.zepal.mybatis.domain.Emp" id="empResultMap">
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<!-- 注意这个地方 -->
<!-- <association property=""></association> -->
</resultMap>
<select id="getEmpByDeptId" parameterType="int" resultMap="empResultMap">
SELECT * FROM tb_emp WHERE dept_id = #{deptId};
</select>
</mapper>
这里又有一个注意的地方,在通过dept去关联查询emp的同时,不能又反过来关联查询dept,这是一个无限递归逻辑,会造成内存溢出的(相当于A去找B,B又去找A,A再找B....),当然,也不能光写一个<association>标签,不加select属性,然后通过<result>去做集合映射,同样的道理,既然通过dept去查emp的同时,不能通过emp去查dept,那么emp里面就不包含dept,这个时候做集合映射是无意义的,结果同样是null,那么,只能单独通过emp去查询dept(这时候就是个一对一关系,同样也不能在emp查dept的同时用<collection>标签去关联查询emp,这里就不演示通过emp去查询dept了,一对一关系在上面已经演示过了)
这里同样是在<collection>利用select属性通过namesqpce去找到EmpMapper中相关的查询方法。这里的javaType是ArrayList,(List<Emp> emps,同样是全路径,虽然Mybatis为常用的java数据类型作了处理,能直接使用ArrayList,但我还是坚持全路径,不然真的不好排错,除了团队沟通很好、开发规范文档很完善),然后在通过ofType(字面意思就是javaType指向的对象的type,我是这么记的^_^)属性去设置list里面的泛型类。
这里多了一个fetchType属性,它有两种属性值lazy(懒加载)和eager(立即加载),这里mybatis为关联查询做的优化。比如说,当查询数据量特别大的时候,不仅查询费时,而且网络传输还费时。所以开启懒加载之后,当查询结果不需要用到关联对象时,mybatis并不会发送SQL语句去查询关联对象,只有当查询结果用到了关联查询的数据,它才会按需加载。
开启懒加载有两种方式,一种是全局开关,开启之后,所有的关联查询都会被懒加载,第二种就是上面的单独指定,第二种方式优先级是高于全局的,所以如果全局的和局部的冲突的话,会以局部为准。同时,开启懒加载需要mybatis设置中的aggressiveLazyLoading支持并将其设为false,在第二篇已经罗列过mybatis的所有设置。下面直接贴上示例
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="logImpl" value="LOG4J"/>
</settings>
3、测试代码和结果
开启懒加载不使用关联结果(mybatis将不会发送SQL语句去关联查询)
public class Test {
public static void main(String[] args) {
InputStream is = Test.class.getResourceAsStream("/mybatis/mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
Integer deptId = 1;
DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
Dept dept = deptDao.getDeptById(deptId);
//只打印deptName,不打印emp
System.out.println(dept.getDeptName());
sqlSession.close();
}
}
DEBUG [main] - ==> Preparing: SELECT * FROM tb_dept WHERE dept_id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
研发中心
开启懒加载使用关联结果
public class Test {
public static void main(String[] args) {
InputStream is = Test.class.getResourceAsStream("/mybatis/mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new
SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();
Integer deptId = 1;
DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
Dept dept = deptDao.getDeptById(deptId);
//只打印deptName,不打印emp
//System.out.println(dept.getDeptName());
//打印dept对象,使用emp对象
System.out.println(dept.toString());
sqlSession.close();
}
}
DEBUG [main] - ==> Preparing: SELECT * FROM tb_dept WHERE dept_id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
DEBUG [main] - ==> Preparing: SELECT * FROM tb_emp WHERE dept_id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 2
Dept [deptId=1, deptName=研发中心, emps=[Emp [empId=1, empName=张三, dept=null], Emp [empId=2, empName=李四, dept=null]]]
很明显懒加载生效,且在查询结果中使用到关联对象的时候,发送了两条SQL。
多对多
1、按照上举例的订单的和商品的关系创建如下表tb_order、tb_product、tb_order_product_relation,并插入数据。Mybatis的配置环境继续延用。简单描述一下,订单1(张三的订单)中有红牛、冰红茶、雪碧三件商品,订单2(李四的订单)中有红牛和脉动两件商品。
2、创建相应的持久层对象、dao层接口和mapper文件
持久层对象
public class Order {
private Integer orderId;
private String orderDesc;
private List<Product> products;//一个订单可以有多件商品
//省略get/set和构造方法
}
public class Product {
private Integer productId;
private String productName;
private List<Order> orders;//一件商品可以出现在多个商品中
}
dao层接口
public interface OrderDao {
Order getOrderById(Integer orderId);
//查询商品的时候可以关联查询该商品被哪些订单买了
Order getOrderByProductId(Integer productId);
}
public interface ProductDao {
Product getProductById(Integer productId);
//查询订单的时候关联查询该订单有哪些商品
Product getProductByOrderId(Integer orderId);
}
mapper文件
<mapper namespace="com.zepal.mybatis.dao.OrderDao">
<resultMap type="com.zepal.mybatis.domain.Order" id="orderMap">
<id column="order_id" property="orderId"/>
<result column="order_desc" property="orderDesc"/>
<collection property="products" column="order_id"
javaType="java.util.ArrayList"
ofType="com.zepal.mybatis.domain.Product"
select="com.zepal.mybatis.dao.ProductDao.getProductByOrderId"
fetchType="lazy"/>
</resultMap>
<select id="getOrderById" parameterType="int" resultMap="orderMap">
select * from tb_order where order_id = #{orderId};
</select>
</mapper>
<mapper namespace="com.zepal.mybatis.dao.ProductDao">
<resultMap type="com.zepal.mybatis.domain.Product" id="productMap">
<id column="product_id" property="productId"/>
<result column="product_name" property="productName"/>
</resultMap>
<select id="getProductByOrderId" parameterType="int" resultMap="productMap">
SELECT * FROM tb_product WHERE product_id
in
(SELECT r.product_id FROM tb_order o, tb_order_product_relation r
WHERE o.order_id = r.order_id AND o.order_id = #{orderId});
</select>
</mapper>
3、测试代码及运行结果
DEBUG [main] - ==> Preparing: select * from tb_order where order_id = ?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
DEBUG [main] - ==> Preparing: SELECT * FROM tb_product WHERE product_id in (SELECT r.product_id FROM tb_order o, tb_order_product_relation r WHERE o.order_id = r.order_id AND o.order_id = ?);
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 3
Order [orderId=1, orderDesc=张三的订单, products=[Product [productId=1, productName=红牛, orders=null], Product [productId=3, productName=冰红茶, orders=null], Product [productId=5, productName=雪碧, orders=null]]]
总结
在mybatis中,不管表与表之间的关系多复杂,mybatis都可以通过关联映射查询去获取结果。核心标签就是<collection>和association。抓住以下几点:
1、一个对象关联的是单个对象就用<association>,一个对象关联的是多个对象就用<collection>。
2、完成关联映射的写法多种多样,唯一的核心就是抓住源头和目标,即通过谁去关联查询谁,就可以从中抽丝剥茧。唯一要注意的就是当通过A去关联映射B的同时,不能用再在B中去关联映射A,这是一个无限递归的逻辑,会报内存溢出异常的。
3、懒加载对Mybatis有一定优化作用。