输出映射:关联嵌套标签association和集合嵌套标签collection

前言

为了配置好这两个标签并实现其功能,从数据库录入数据到持久化实体类创建,SQL映射配置和测试等都手动肝了一遍,虽然有点小累,但配置好,测试正确后,长舒了一口气,想起之前参加华为云开发者沙龙时看到的一句话:“选择不凡”。现在写下这一篇日志,总结一下这两个输出映射标签的使用,以及自己的配置过程。既然两个标签都是在输出映射(resultMap)中配置,在讲这两个标签之前,先来简单回顾MyBatis中的两种输出映射配置,觉得清楚resultType和resultMap的可以跳过这一部分。

输出映射resultType和resultMap

MyBatis中有两种方法把数据库查询返回的结果映射到对应的JavaBean中,一种是resultType标签,它比较简单,负责定义SQL映射语句中的记录返回值的映射类型,例如我们根据学号查询某一个学生的姓名(在数据库中存储为字符串类型),返回值的映射类型resultType=“String”。resultType中可以配置基本数据类型,基本数据类型的包装类(Integer、String、Double等)和我们自己定义的JavaBean。resultType在映射用户自定义的Java类时,有两种情况:

第一,如果返回的记录中,列名和自定义类中的属性名里,至少有一个是一致的,就会创建该类的对象,并把列名的数据映射到对象的相应属性中。

第二种情况,如果返回的记录中,所有的列名和自定义类中的属性都不对应,那么就不会创建自定义类的对象。

resultType支持这么多数据类型的映射,还支持自定义的包装类型,看似足够强大了,但其实还是有一个比较大的缺点,就是resultType映射自定义包装类时,类中的属性名必须和数据库的列名相同,否则,你就无法从返回结果集记录中,把想要的结果映射到类的对象中。在日常维护时,如果想把类中的属性名修改,就不得不一起把数据库中的列名也做修改,反过来也是,这样做工作量大,所以resultType适合于一些简单的,基本数据类型,基本数据包装类或自定义类中,字段名无需任何转换的场景。

如果想解决上面这个问题,可以使用resultMap结果集配置,来自己设定数据库列名和自定义Java实体类中属性的一一映射关系,并设定返回的结果集记录最终生成的Java实体类型。看个例子:

<!-- id属性是resultMap的标识id -->
	<resultMap type="com.mybatis.po.User" id="userResultMap">
	<!-- id标签设置查询结果集中的标识(主键),column为标识在数据库中的列名,property为标识在Java类中的属性名 -->
	<id property="id" column="user_id"/>
	<!-- 配置User类中的映射关系 -->
	<result property="username" column="user_name"/>
	<result property="gender" column="user_gender"/>
	<result property="province" column="user_province"/>
	<result property="city" column="user_city"/>
	</resultMap>

在resultMap标签对中,type表示返回的结果集记录最终映射的Java实体类,id属性是这个resultMap的唯一标识,我们可以根据需要设置多个resultMap。下一行的id标签中设置返回结果集中的唯一标识为“id”,(在User类中主键是“id”)。下面的result标签就是对返回结果集中其他列的映射配置,property对应的是Java实体类中的属性,而column对应的是数据库中的列名,也就是返回结果集中的列名,每一个result标签都会根据property和column来进行映射,这样我们就可以自定义配置不同的列名应该和哪一个属性对应,以后在我们修改数据库列名或者Java实体类的属性时,只需修改这个resultMap中的映射关系即可,而无需去对数据库或实体类做修改。

 

结果集的关联嵌套

上面花了一大段回顾了两种结果集的输出映射,原因为了引出resultMap,一个可以自定义映射关系的结果集映射配置,利用它可以配置一些十分复杂的映射关系。来看一个例子,假如我们的商场数据库中有一张顾客信息表,其对应的Java实体类如下:

package com.mybatis.po;

import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private int id;
	private String username;
	private String password;
	private String gender;
	private String email;
	private String province;
	private String city;
	private Date birthday;
	private int age;
	
	public User() {
//		System.out.println("进入目标类无参数构造方法....");
	}
	// 构造方法初始化
	public User(String username, String password, String gender, String email, String province,
			String city, Date birthday) {
		super();
//		System.out.println("进入目标类有参数构造方法....");
		this.username = username;
		this.password = password;
		this.gender = gender;
		this.email = email;
		this.province = province;
		this.city = city;
		this.birthday = birthday;
	}

    // .......get()和set()方法省略
}

数据库中又有一张购物车表,存放不同顾客购物车中购买的商品,其对应的实体类为:

package com.mybatis.po;

import java.io.Serializable;

public class ShoppingCart implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private int cartId; // 购物车id
	private int userId; // 用户id
	private int productId; // 商品id
	private String productName; // 商品名
	private int number; //商品数量
	private double price; //商品价格
	
	public ShoppingCart() {
//		System.out.println("进入ShoppingCart类无参数构造方法");
	}
	
	public ShoppingCart(int cartId, int userId, int productId, String productName, int number, 
			double price) {
//		System.out.println("进入ShoppingCart类有参数构造方法");
		this.cartId = cartId;
		this.userId = userId;
		this.productId = productId;
		this.productName = productName;
		this.number = number;
		this.price = price;
		//this.user = user;
	}
    // ........get()和set()方法省略
}

里面的属性有购物车id,顾客id,顾客选购的商品和价格数量等信息。

在这两张表中我们看到,无论哪一个属性,它的数据类型都是基本数据类型或基本数据类型的包装类,假如我们查顾客信息表,我们可以查出顾客的姓名电子邮件等,把这些信息以一条记录返回,用User类来映射接收结果集的映射,创建对象,这没问题。假如我们查购物车表,我们可以查出顾客购买的商品,商品的价格和数量等,然后用一个ShoppingCart类去映射接收返回的结果集映射。

假如现在有这么一个需求,我们想根据顾客id,来查询某一个顾客的购物车信息,以及这位顾客的部分信息,如姓名和性别,这些信息分别存放在两张表中,也就是我们要做综合查询。你可能会想,SQL语句实现很简单,多表连接查询,根据两张表共同列的相关性来查询就可以了,例如顾客表和购物车表里都有顾客id,根据这个id来进行多表查询即可。对没错,SQL语句是这样些,那么查询得到的返回结果集,要怎么映射呢?这些结果信息,及不完全属于User类,也不完全属于ShoppingCart类。

解决的方法是“关联嵌套”,有一个reultMap中有一个association配置,可以让我们在结果集中关联其他的结果集,下面通过一个例子,慢慢来看这个关联嵌套是怎么做。

查询包装类

既然要查询的信息不在一个表中,无法映射到一个Java实体类中,那么我们首先创建一个查询包装类:

package com.mybatis.po;

import java.io.Serializable;

// 购物车表查询包装类
public class ShoppingCartInstance extends ShoppingCart implements Serializable {	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private User user; //对应用户
	
	public ShoppingCartInstance() {
		
	}
	
	public ShoppingCartInstance(int cartId, int userId, int productId, 
			String productName, int number, 
			double price, User user) {
		super(cartId, userId, productId, productName, number, price);
		this.user = user;
	}
	
	public User getUser() {
		return user;
	}

	public void setUser(User user) {
		this.user = user;
	}
	
}

在购物车查询包装类中,我们让这个类继承ShoppingCart类,这样就能获得购物车类的所有属性,也就是能映射接收查询结果集中的购物信息。接着我们在包装类中,嵌套一个User类的实例对象,这个user对象就是负责映射接收返回结果集中的顾客信息数据。这样做可以吗?可以,得益于resultMap中的association标签,来看看这个包装类的SQL映射配置文件怎么写:

<select id="queryShoppingCartInstance" parameterType="int" resultMap="shoppingCartResult">
		SELECT
			S.cartId	as cart_id,
			S.productName	as product_name,
			S.userId	as cartUser_id,
			S.price	as product_price,
			S.number	as product_number,
			U.id	as user_id,
			U.username	as user_name,
			U.gender	as user_gender,
			U.province	as user_province,
			U.city	as user_city
		FROM ShoppingCart S left outer join user U on S.userId = U.id 
		WHERE S.userId = #{id}
	</select>

SQL语句,查询两表中需要的信息,然后多表连接。可以看到,结果集返回类型使用我们自己定义的resultMap,接下来看看这个结果集映射的配置。

association映射配置

上面我们说了,解决综合查询的映射问题,我们是创建了一个包装类,这个包装类用来接收结果集中查询购物车表的信息,类中嵌套了一个User类的实例对象,它用来接收结果集中查询顾客表得到的信息,可以这么说,我们在结果集中嵌套了另一个结果集,在购物车表包装类中嵌套了顾客表类。为了让结果集可以正确映射到两个类中,下面来配置这个包装类的SQL映射配置文件。

<?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="ShoppingCartTest">
	<!-- resultMap配置查询返回结果中列名和Java类中属性名的映射关系,type是最终映射的Java类型 -->
	<resultMap type="shoppingCartInstance" id="shoppingCartResult">
		<!-- id是查询返回的结果集中的主键 -->
		<id property="cartId" column="cart_id"/>
		<!-- result标签对是对普通列的定义,column是查询结果集的列名,property是映射Java类型中的属性名 -->
		<result property="productName" column="product_name"/>
		<result property="price" column="product_price"/>
		<result property="number" column="product_number"/>
		<!-- association标签:关联的嵌套结果配置 -->
		<association property="user" column="cartUser_id" javaType="com.mybatis.po.User" 
		resultMap="userResult"/>
	</resultMap>
	
	<resultMap type="com.mybatis.po.User" id="userResult">
		<id property="id" column="user_id"/>
		<!-- 配置User类中的映射关系 -->
		<result property="username" column="user_name"/>
		<result property="gender" column="user_gender"/>
		<result property="province" column="user_province"/>
		<result property="city" column="user_city"/>
	</resultMap>

在resultMap中,第16行的association标签,配置关联的结果集映射,属性名为user(即购物车包装类ShoppingCartInstance中的User类实例user),javaType指定user的数据类型为User类的实例对象,association标签中最后一个resultMap指定了这个user对象的映射对应关系,它又是一个resultMap。

      user的resultMap在第20到27行,里面定义了一些属性和列名的对应关系。这是一种配置方法,你也可以把user的映射关系嵌套配置在association标签对里,例如这样:

配置好SQL映射文件,最后别忘了在全局配置文件里配置这个mapper!接下来就可以编写我们的测试用例了。

关联嵌套测试用例

// 关联的嵌套结果
	public void TestAssociationQuery() throws IOException {
		SqlSession sqlSession = dataConn.getSqlSession();
		List<ShoppingCartInstance> resultList = sqlSession.selectList("queryShoppingCartInstance", 2);
		StringBuffer result = new StringBuffer();
		double totalAmount;
		
		System.out.println("顾客姓名: " + resultList.get(1).getUser().getUsername());
		System.out.println("性别: " + resultList.get(1).getUser().getGender());
		System.out.println("商品清单:" + "\r\n");
		
		for(int i=0; i<resultList.size(); i++) {
			ShoppingCartInstance shoppingCartInstance = resultList.get(i);
			result.append("商品名:" + shoppingCartInstance.getProductName() + "\r\n");
			result.append("商品价格:" + shoppingCartInstance.getPrice() + "\r\n");
			result.append("商品数量:" + shoppingCartInstance.getNumber() + "\r\n");
			totalAmount = (shoppingCartInstance.getPrice()*shoppingCartInstance.getNumber());
			result.append("商品总价:" + String.format("%.2f", totalAmount) + "\r\n");
			
			System.out.println(result.toString());
			result.setLength(0);
		}
		
		sqlSession.close();
	}

由于购物车上的商品可能不止一个,所以第40行用列表List<JavaBean>来接收返回结果集,每一个ShoppingCartInstance实例对象都是一个商品清单,清单上除了商品信息外,还有购买该商品的部分顾客信息,我们的需求是查询出某一id的用户的购物信息,所以其实对象里的User实例都是一样的,第44-45行输出顾客信息,只需获取一次即可,ShoppingCartInstance类中有嵌套的User类实例,并且有对该对象的get()和set()方法,所以可以上述44-45行的方法获得user对象,从而获得里面的信息。

      商品信息则不止一种,一个顾客可能购买了多件商品,所以用循环遍历结果集,输出顾客购物车上的所有商品信息:

 

结果集的集合嵌套

在结果集中嵌套(也可以说关联)其他的结果集,除了上面说的情况,还有另一种情况,如在实体类中嵌套另一个实体类的集合,具体情景就是,假如我们想根据id,查询某一的商品信息,以及顾客对这些商品的评价,这些信息同样分别位于两张表中,商品信息在商品表里,而顾客对商品的评价在商品评价表里:

package com.mybatis.po;

import java.io.Serializable;

public class Product implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private int productId; // 商品id
	private String productName; // 商品名
	private double productPrice; // 商品价格
	// ......商品的其他属性
	
	public Product() {
		
	}
	
	public Product(int productId, String productName, double productPrice) {
		this.productId = productId;
		this.productName = productName;
		this.productPrice = productPrice;
	}
    // ......get()和set()方法省略
}

商品表的实体类。

package com.mybatis.po;

import java.io.Serializable;

public class ProductAppraise implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private int productId; // 商品id
	private double productScore; // 商品评分
	private int userId; // 评分顾客id
	// ......其他属性
	
	public ProductAppraise() {
		
	}
	
	public ProductAppraise(int productId, double productScore, int userId) {
		this.productId = productId;
		this.productScore = productScore;
		this.userId = userId;
	}
    // .......get()和set()方法省略
}

商品评价表的实体类。

由于一个商品,可以有许多顾客的评价,所以,我们查询出来的一个商品信息,里面对这个商品的评价不止条,即商品评价表里查询出来的结果一定有多条,它可以用一个集合来存储。也就是说,这个综合查询,实在一个结果集中,嵌套了一个集合,如下面的商品表包装类:

package com.mybatis.po;

import java.io.Serializable;
import java.util.List;

// 商品表查询包装类
public class ProductInstance extends Product implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;
	private List<ProductAppraise> appraiseList; // 商品的顾客评价信息列表
	
	public ProductInstance() {
		super();
	}
	
	public ProductInstance(int productId, String productName, double productPrice, 
			List<ProductAppraise> productAppraiseList) {
		super(productId, productName, productPrice);
		this.appraiseList = productAppraiseList;
	}

	public List<ProductAppraise> getProductAppraiseList() {
		return appraiseList;
	}

	public void setProductAppraiseList(List<ProductAppraise> productAppraiseList) {
		this.appraiseList = productAppraiseList;
	}
}

商品表包装类继承了商品表实体类,然后添加了一个新的属性,一个商品评价表实体类的集合,集合里面每一个实体都是一个用户对某一个商品的评价信息。这样的商品包装类,可以让我们在根据某一商品id查询到商品信息的同时,保存该商品的所有评价信息。来看看SQL配置文件怎么写。

collection映射配置

<?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="ProductTest">
	
	<resultMap type="productInstance" id="productResult">
		<id property="id" column="p_id"/>
		<result property="productId" column="product_id"/>
		<result property="productName" column="product_name"/>
		<result property="productPrice" column="product_price"/>
		<collection property="appraiseList" ofType="com.mybatis.po.ProductAppraise" 
		resultMap="ProductAppraiseResult"/>
	</resultMap>
	
	<resultMap type="productAppraise" id="ProductAppraiseResult">
		<id property="id" column="pa_id"/>
		<result property="productId" column="productAppraise_id"/>
		<result property="productScore" column="product_score"/>
		<result property="userId" column="user_id"/>
	</resultMap>
	
	<select id="queryProductInstance" parameterType="int" resultMap="productResult">
		SELECT
			P.productId	as product_id,
			P.productName	as product_name,
			P.productPrice	as product_price,
			PA.productScore	as product_score,
			PA.productId	as productAppraise_id,
			PA.userId	as user_id
		FROM Products P LEFT OUTER JOIN ProductAppraise PA on P.productId=PA.productId
		WHERE P.productId=#{id}
	</select>
	
</mapper>

和关联嵌套配置差不多,collection标签可以配置在结果集中嵌套的集合信息,例如第13行,配置集合appraiseList的对应实体类型是ProductAppraise,结果集映射为外部的resultMap=“ProductAppraiseResult”,这个结果集映射就在下面第17行到22行中。配置完后,我们用一个简单的查询测试用例看看结果。

集合嵌套测试用例

package com.mybatis.test;

import java.io.IOException;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import com.mybatis.datasource.DataConnection;
import com.mybatis.po.Product;

public class MyBatisTestProduct {
	public DataConnection dataConn = new DataConnection();
	
	public void queryProductTest() throws IOException {
		SqlSession sqlSession = dataConn.getSqlSession();
		List<Product> resultList = sqlSession.selectList("queryProductInfo");
		StringBuffer result = new StringBuffer();
		
		for(int i=0; i<resultList.size(); i++) {
			Product product = resultList.get(i);
			result.append("商品id:" + product.getProductId() + "\r\n");
			result.append("商品名:" + product.getProductName() + "\r\n");
			result.append("商品价格:" + product.getProductPrice() + "\r\n");
			
			System.out.println(result.toString());
			result.setLength(0);
		}
		
		sqlSession.close();
	}
	public static void main(String[] args) throws IOException {
		MyBatisTestProduct testProduct = new MyBatisTestProduct();
		testProduct.queryProductTest();

	}

}

虽然说查询得到的只有一个商品,但是商品的评价却有很多条,所以结果集还是要用集合来接收返回的结果集。

 

完整代码已上传GitHub:

https://github.com/justinzengtm/SSM-Frame

https://gitee.com/justinzeng/MyBatis/tree/master

发布了97 篇原创文章 · 获赞 71 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/justinzengTM/article/details/96746841