Spring Boot实现高质量的CRUD-2

发布时间 2023-06-12 10:15:47作者: 阿拉伯1999

(续前文)

5、Dao类 ​

​	Dao类提供操作访问数据库表的接口方法。常规的CRUD,将考虑下列接口方法:
​	1)插入单条对象记录;
​	2)批量插入对象记录;
​	3)修改单条对象记录;
​	4)批量修改对象记录;
​	5)删除单条对象记录;
​	6)批量删除对象记录;
​	7)查询多条对象记录;
​	8)查询指定key的对象记录;
​	9)查询记录条数;
​	10)根据唯一字段(或组合字段)查询单条对象记录。

​	Dao类使用@Mapper注解,类中代码应仅实现接口方法定义,不建议使用@Select,@Insert等注解直接在Dao类中实现接口(特殊情况除外),考虑的后期接口功能扩展的可能性,在Mybatis xml脚本文件中实现接口书写更容易规范,功能也更强大。

5.1、新增对象

​	新增对象和批量新增对象的方法名和形式如下所示。其中如果支持自增ID,则返回值为对象ID。
	/**
	 * @methodName	: insertItem
	 * @description	: 新增一个XXX对象
	 * @param item	: XXX对象
	 * @return		: 受影响的记录数
	 * @history		:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int insertItem(XXX item);

	/**
	 * @methodName		: insertItems
	 * @description		: 批量新增XXX对象
	 * @param itemList	: XXX对象列表
	 * @return			: 受影响的记录数
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int insertItems(List<XXX> itemList);
​	批量新增对象接口,在数据库表为关系表时,更需要使用;而数据库表为对象表时,往往因为复杂的数据库一致性校验和关联操作,不常使用。
​	新增对象和批量新增对象的处理性能相差不大,即批量插入100条记录和插入单条记录的时间相差无几。因此需要大量新增对象时,应考虑批量新增对象的接口。

​	下面分别是新增用户和批量新增用户角色关系的方法示例:
	/**
	 * @methodName	: insertItem
	 * @description	: 新增一个用户对象
	 * @param item	: 用户对象
	 * @return		: 受影响的记录数
	 * @history		:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int insertItem(User item);

	/**
	 * @methodName		: insertItems
	 * @description		: 新增一批用户对象
	 * @param itemList	: 用户对象列表
	 * @return			: 受影响的记录数
	 * @history			: 
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int insertItems(List<User> itemList);

5.2、修改对象

​	修改单个对象和批量修改对象的方法名和形式如下所示。​	
	/**
	 * @methodName		: updateItemByKey
	 * @description		: 根据key修改一个XXX对象
	 * @param params	: XXX对象相关属性字段字典,key字段必选,其它字段可选
	 * 	{
	 * 		"keyPropName1"	: 0,	// keyPropName1说明,必选
	 * 		"keyPropName2"	: 0,	// keyPropName2说明,必选
	 * 		......
	 * 	}
	 * @return			: 受影响的记录数
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int updateItemByKey(Map<String, Object> params);

	/**
	 * @methodName		: updateItems
	 * @description		: 批量修改XXX对象
	 * @param params	: 请求参数,所有字段可选,但至少需要一个修改字段和一个条件字段
	 * 	{
	 * 		修改字段:
	 * 		"setPropName1"	: 0,	// setPropName1字段说明,可选
	 * 		......
	 * 		条件字段:
	 * 		"condPropName1"	: 0,	// condPropName1字段说明,可选
	 * 		......
	 * 	}
	 * @return			: 受影响的记录数
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int updateItems(Map<String, Object> params);
​	修改单个对象需要所有的主键属性字段,从而确定唯一的对象。而其它字段都应允许修改,且可选。(记录操作字段中的create_time和update_time字段不在修改字段之列)。
​	不建议使用下列接口形式来实现单个对象的修改:
public int updateItem(XXX item);
​	updateItem这种接口形式的问题是参数形式为实体类对象,需要整个实体类对象的属性,对于前端而言,需要将所有属性都赋值,如有遗漏,会造成信息被默认值覆盖;另外可能前端也未必能获取到对象的全部信息。因此,这种接口形式会提高处理的复杂度。

​	批量修改由于影响多条记录,误操作造成的影响和损失是巨大的,因此,往往要控制至少一个条件字段。另外,此接口如果修改大量记录,与删除大量数据一样,可能会造成数据库死锁。此接口是否需要提供,视业务需求而定。
​	批量修改,往往对状态字段进行修改,或存在父级对象(如主从表)的冗余字段进行修改,可修改的字段是极为有限的。
​	另外批量修改,一般情况下,不建议使用多表联结,可以先将外表的条件转为本表字段的值的列表,使用in条件进行单表操作。

​	下面分别是修改用户和批量修改用户的方法示例:
	/**
	 * @methodName		: updateItemByKey
	 * @description		: 根据key修改一个用户对象
	 * @param params	: 用户对象相关属性字段字典,key字段必选,其它字段可选
	 * 	{
	 * 		"userId"	: 0L,		// 用户ID,必选
	 * 	}
	 * @return			: 受影响的记录数
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int updateItemByKey(Map<String, Object> params);

	/**
	 * @methodName		: updateItems
	 * @description		: 根据条件批量修改用户对象的相关属性字段的值
	 * @param params	: 用户对象相关字段字典,至少需要一个修改字段和一个条件字段,修改字段和条件字段均可选;
	 * 	{
	 * 					  修改字段集如下:
	 * 		"orgId"			: 0,	// 组织机构ID,可选
	 * 		"userType"		: 3,	// 用户类型,1-系统管理员、2-公司内部用户、3-外部用户,可选
	 * 		"deleteFlag"	: 0,	// 记录删除标记,0-正常、1-禁用,可选
	 * 		"operatorName"	: "",	// 操作人账号,可选
	 * 					  条件字段如下:
	 * 		"userIdList"	: [],	// 用户ID列表,list,可选
	 * 		"userName"		: "",	// 用户名,可选
	 * 		"phoneNumber"	: "",	// 手机号码,可选
	 * 	}
	 * @return			: 受影响的记录数
	 * @history			: 
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * 2023/05/17	1.0.0		sheng.zheng		初版
	 *
	 */
	public int updateItems(Map<String, Object> params);

5.3、条件字段的命名规范

​	涉及批量记录,往往与条件有关,包括后面的批量删除记录,查询记录等,都需要使用条件。抛开需要使用Mysql内置函数,以及复杂AND和OR组合的个性化条件,大多数情况,多个条件使用AND进行联结。
​	条件字段有下列可能:等于,不等于,大于,大于等于,小于,小于等于,IS NULL,IS NOT NULL,IN,NOT IN,LIKE(BETWEEN可被大于等于和小于等于替代,不必考虑),NOT BETWEEN AND(内置了OR条件)。
​	同一个参数名,不能适用不同的条件,否则需要更多接口,增加维护的复杂度,因此需要对条件字段的名称进行规范。
​	假设属性字段名称为propName,则条件字段的命名规则如下:
	/**
		等于		: propName
		不等于		: propNameNeq	
		大于等于	: propNameGte
		大于		: propNameGt
		小于等于	: propNameLte
		小于		: propNameLt
		IS NULL		: propNameNull
		IS NOT NULL	: propNameNNull
		IN			: propNameList
		NOT IN		: propNameNList
		LIKE		: propNameLike
		NOT BETWEEN AND	: propNameNBtwl	// NOT BETWEEN AND的下边界值
						  propNameNBtwh	// NOT BETWEEN AND的上边界值,上下边界值必须同时出现。	 
	 */
​	下面是一个条件参数命名示例:
	/**
	 * @param params	: 查询参数,形式如下:
	 * 	{
	 * 		"userType"		: 3,	// 用户类型,1-系统管理员、2-公司内部用户、3-外部用户,可选
	 * 		"userTypeList"	: [],	// 用户类型列表,in,可选
	 * 		"userTypeNList"	: [],	// 用户类型列表,not in,可选
	 * 		"sex"			: 1,	// 性别,1-无值、2-男、3-女、4-其它,可选
	 * 		"deleteFlag"	: 0,	// 记录删除标记,0-正常、1-禁用,可选
	 * 		"userName"		: "",	// 用户名,精确匹配,可选
	 * 		"userNameLike"	: "",	// 用户名,like,可选
	 * 		"phoneNumber"	: "",	// 手机号码,精确匹配,可选
	 * 		"phoneNumberLike": "",	// 手机号码,like,可选
	 * 		"realNameLike"	: "",	// 真实姓名,like,可选
	 * 		"emailLike"		: "",	// Email,like,可选
	 * 		"birthGte"		: "",	// 生日起始值,yyyy-MM-dd格式,gte,可选
	 * 		"birthLte"		: "",	// 生日终止值,yyyy-MM-dd格式,lte,可选
	 * 		"birthNull"		: "",	// 生日,is null,任意字符串值,可选
	 * 		"orgId"			: 1,	// 组织ID,可选
	 * 		"orgIdList"		: [],	// 组织ID列表,in,可选
	 * 	}
	 */
​	字符串类型字段,对于前端页面,常使用模糊匹配来查询记录,以扩大记录集;而内部调用时,往往使用精确匹配查询,以求缩小搜索记录集合和更高的查询性能。有了条件参数命名规则,同一个查询接口可以支持多种查询业务的需求,从而解除Dao层与Service层的紧耦合。

5.4、删除对象

​	删除单个对象和批量删除对象的方法名和形式如下所示。​	
	/**
	 * @methodName		: deleteItemByKey
	 * @description		: 根据key删除一个XXX对象
	 * @param keyPropName1	: 对象xxx的key1属性字段	  
	 * ....					: 	  	  
	 * @param keyPropNameN	: 对象xxx的keyn属性字段,如果只有一个key字段,则无此参数,数据类型依据具体key而定
	 * @return			: 受影响的记录数
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int deleteItemByKey(@Param("keyPropName1") Integer keyPropName1,...,@Param("keyPropNameN") Integer keyPropNameN);

	/**
	 * @methodName		: deleteItems
	 * @description		: 批量删除XXX对象
	 * @param params	: 请求参数,所有字段可选,但至少需要一个条件字段
	 * 	{
	 * 		条件字段:
	 * 		"condPropName1"	: 0,	// condPropName1字段说明,可选
	 * 		......
	 * 	}
	 * @return			: 受影响的记录数
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int deleteItems(Map<String, Object> params);
​	如果对象表,一般不支持物理删除,就无需提供这两个方法;对于关系表,一般支持物理删除,需要删除方法。

​	下面分别是删除用户角色关系和批量删除用户角色关系的方法示例:
	/**
	 * @methodName		: deleteItemByKey
	 * @description		: 根据key删除一个用户和角色关系对象
	 * @param userId	: 用户ID
	 * @param roleId	: 角色ID
	 * @return			: 受影响的记录数
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int deleteItemByKey(@Param("userId") Long userId, @Param("roleId") Integer roleId);

	/**
	 * @methodName		: deleteItems
	 * @description		: 批量删除相关用户和角色关系对象
	 * @param params	: 请求参数,要求至少有一个条件参数,形式如下:
	 * 	{
	 * 		"roleId"	: 0,	// 角色ID,可选
	 * 		"userId"	: 0L,	// 用户ID,可选
	 * 	}
	 * @return			: 受影响的记录数
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public int deleteItems(Map<String, Object> params);

5.5、查询对象

​	查询是使用最频繁的功能,受业务需求驱动,查询的要求也多种多样。此处仅考虑通用的查询需求,不考虑分组(GROUP)查询和联合(UNION)查询及子查询,这些特殊查询属于定制需求。
​	建议一般情况使用单表查询,在主从表(Master-Detail)的情况下,子表可以关联主表查询。建议不要超过3个表的关联查询,否则由于搜索记录空间太过庞大(为笛卡尔乘积),即使有索引,性能也不会太好,何况有些查询条件会使得索引失效,如LIKE,大于等于等等。
​	那么如果一个表的外键比较多,如何处理呢?建议使用分步查询以及缓存来处理,这样可以获取较好的查询性能。
​	通用查询考虑提供下列4个方法:
	/**
	 * 		selectItems		: 根据条件查询对象列表,此方法适用于API调用和内部调用
	 * 		selectItemByKey	: 根据key值查询一个对象
	 * 		selectCount		: 根据条件查询记录数
	 * 		selectItemByXXX	: 根据唯一键(或组合唯一键)查询一个对象
	 */

5.5.1、查询对象列表

​	查询对象列表的方法名和形式如下所示:
	/**
	 * @methodName		: selectItems
	 * @description		: 根据条件查询XXX对象列表
	 * @param params	: 查询条件参数,形式如下:
	 * 	{
	 * 		条件字段:
	 * 		"condPropName1"	: 0,	// condPropName1字段说明,可选
	 * 		......
	 * 		"condPropNameN"	: 0,	// condPropNameN字段说明,可选
	 * 		"offset"		: 0,	// limit记录偏移量,可选
	 * 		"rows"			: 20,	// limit最大记录条数,可选
	 * 		"sortList"		: [],	// 排序选项,SortField对象列表,只支持数据表字段属性,可选
	 * 	}
	 * @return			: XXX对象列表
	 * @history			: 
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public List<XXX> selectItems(Map<String, Object> params);
```	​	
	​	除了条件字段外,selectItems还有3个可选参数,其中offset和rows参数,一方面为配合单元测试辅助工具,获取一定数目的记录样本,另外也为代替分页插件提供了备选方案。分页插件使用了子查询来查询总记录数,导致有时性能很差,必要时,可以使用selectCount结合selectItems方法,取代分页插件。
	​	sortList,排序选项,支持多个字段的排序,一般的方案是支持一个字段的排序,且有sql注入的风险,不可取。排序选项是SortField对象的列表,SortField定义如下:
```java
package com.abc.example.vo;

import lombok.Data;

/**
 * @className	: SortField
 * @description	: 排序字段信息对象
 * @summary		: 用于SQL查询语句的ORDER BY
 * @history		:
 * ------------------------------------------------------------------------------
 * date			version		modifier		remarks                   
 * ------------------------------------------------------------------------------
 * yyyy/mm/dd	1.0.0		author		    初版
 *
 */
@Data
public class SortField {

	// 字段属性名称
	private String fieldName = "";
	
	// 排序次序,0-升序,1-降序
	private int sortOrder = 0;
}
```	​	
	​	sortList,可以将多个字段按各自升序或降序要求,顺序设置,从而实现丰富的排序功能。
	​	没有sortList字段,常规的做法是根据业务特点将排序固化,这样处理代码实现简单,但不灵活。当然支持sortList选项,会增加较多的Mybatis的代码,是否需要支持排序选项,视业务需求而定。

	​	下面是查询用户记录的方法示例:
```java
	/**
	 * @methodName		: selectItems
	 * @description		: 根据条件查询用户对象列表,用于前端和内部查询记录
	 * @param params	: 查询参数,前端调用至少要有一个条件参数,参数形式如下:
	 * 	{
	 * 		"userType"		: 3,	// 用户类型,1-系统管理员、2-公司内部用户、3-外部用户,可选
	 * 		"userTypeList"	: [],	// 用户类型列表,in,可选
	 * 		"userTypeNList"	: [],	// 用户类型列表,not in,可选
	 * 		"sex"			: 1,	// 性别,1-无值、2-男、3-女、4-其它,可选
	 * 		"sexList"		: [],	// 性别列表,in,可选
	 * 		"deleteFlag"	: 0,	// 记录删除标记,0-正常、1-禁用,可选
	 * 		"userName"		: "",	// 用户名,精确匹配,可选
	 * 		"userNameLike"	: "",	// 用户名,like,可选
	 * 		"phoneNumber"	: "",	// 手机号码,精确匹配,可选
	 * 		"phoneNumberLike": "",	// 手机号码,like,可选
	 * 		"realName"		: "",	// 真实姓名,精确匹配,可选
	 * 		"realNameLike"	: "",	// 真实姓名,like,可选
	 * 		"email"			: "",	// Email,精确匹配,可选
	 * 		"emailLike"		: "",	// Email,like,可选
	 * 		"birthGte"		: "",	// 生日起始值,yyyy-MM-dd格式,gte,可选
	 * 		"birthLte"		: "",	// 生日终止值,yyyy-MM-dd格式,lte,可选
	 * 		"birthNull"		: "",	// 生日,is null,任意字符串值,可选
	 * 		"orgId"			: 1,	// 组织ID,可选
	 * 		"orgIdList"		: [],	// 组织ID列表,in,可选	 
	 * 		"offset"		: 0,	// limit记录偏移量,可选
	 * 		"rows"			: 20,	// limit最大记录条数,可选
	 * 		"sortList"		: [],	// 排序选项,SortField对象列表,只支持数据表字段属性,可选
	 * 	}
	 * @return			: 用户对象列表
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public List<User> selectItems(Map<String, Object> params);
```	​	
	​	对于前端而言,枚举类型如用户类型,可能是单选,也可能是多选,因此提供了userType和userTypeList条件字段,以满足不同场景的查询需求。如果枚举项很多,也可以提供类似userTypeNList的条件字段,相当于反选,以减少用户勾选点击次数。
	​	如phoneNumber字段,前端需要模糊匹配,后端内部调用需要精确匹配,因此同时提供phoneNumber和phoneNumberLike条件字段,以满足不同需求。
	​	简而言之,selectItems的条件选项集是一个大拼盘,提供丰富的条件选项,以满足不同场景的需求。	
	​	但是,也没必要所有的字段要按各种条件都给出选项。需要提供哪些条件选项,除了业务需求要求的特殊条件选项外,有以下较为通用的设置原则:
```java
/**
	1、如果某属性不给任何条件,即为空集时,其效果相当于全集。
	2、对于二值枚举类型,只需要'='条件的选项,不需要'in'条件的选项。
	3、对于多值枚举类型,需要'='、'in'条件的选项,枚举项较多时,需要'not in'条件选项。
	4、对于字符串类型,如果为不易改变的字段,如name,phoneNumber等,需要'='和'like'条件的选项,必要时,还需要'in'条件选项;对于如desc、remark等容易改变的信息字段,只需提供'like'条件选项。
	5、对于本表的记录ID字段,提供'='、'in'条件选项,如果记录ID与时间序列高度相关,则可能还需要'gte'和'lt'条件选项。
	6、对于外键ID,提供'='、'in'条件选项。'in'条件选项用于分步查询,如查询:‘组织名称包含xxx的用户记录’,因为用户表没有组织名称字段(orgName),但有组织ID(orgId),因此需要先查询组织表,获取orgIdList,然后就可以在人员表中查询了。
	7、对于数值量和日期字段,如有需要,提供'gte'、'lt'条件选项,特殊情况可再提供'gt'和'lte'以及'not between and'条件选项。
	8、对于默认值为null的字段,提供'is null'和'is not null'条件选项。
	9、与数据权限相关的字段,假设用户表的orgId字段涉及数据权限,用户只能查询本组织或给定组织列表的人员记录,则此字段需要'in'条件选项,以便后端服务层根据通过提供用户的orgId权限列表,过滤查询数据。如果通过其它途径已有了orgIdList集合,后端服务层还需要将数据权限要求的orgIdList与之取交集。
*/

5.5.2、根据key查询对象

​	根据key查询对象的方法名和形式如下所示:
	/**
	 * @methodName		: selectItemByKey
	 * @description		: 根据key查询一个XXX对象
	 * @param keyPropName1	: 对象xxx的key1属性字段	  
	 * ....					: 	  	  
	 * @param keyPropNameN	: 对象xxx的keyn属性字段,如果只有一个key字段,则无此参数,数据类型依据具体key而定
	 * @return			: XXX对象
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public XXX selectItemByKey(@Param("keyPropName1") Integer keyPropName1,...,@Param("keyPropNameN") Integer keyPropNameN);
​	根据key查询对象,经常用于获取对象,以及存在性检查。
​	下面是根据key查询用户对象的方法示例:
	/**
	 * @methodName		: selectItemByKey
	 * @description		: 根据key查询一个用户对象
	 * @param userId	: 用户ID
	 * @return			: 用户对象
	 * @history			:
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public User selectItemByKey(@Param("userId") Long userId);

5.5.3、查询对象记录数

​	查询对象记录数的方法名和形式如下所示:
	/**
	 * @methodName		: selectCount
	 * @description		: 根据条件查询XXX对象的记录数
	 * @param params	: 查询条件参数,形式如下:
	 * 	{
	 * 		条件字段:
	 * 		"condPropName1"	: 0,	// condPropName1字段说明,可选
	 * 		......
	 * 		"condPropNameN"	: 0,	// condPropNameN字段说明,可选
	 * 	}
	 * @return			: XXX对象记录数
	 * @history			: 
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public Integer selectCount(Map<String, Object> params);
​	查询记录条数,经常用于存在性检查,查询计数的性能要比查询记录列表高得多,且可以减少IO和内存的开销,因此也较为常用。
​	selectCount的条件选项一般与selectItems保持一致。
​	如果预见表的记录数规模会很大时,返回值类型可以改为Long型,一般情况,Integer型足够了。

5.5.4、根据唯一键查询对象

​	根据唯一键(或组合唯一键)查询对象的方法名和形式如下所示:
	/**
	 * @methodName		: selectItemByXXX
	 * @description		: 根据XXX查询XXX对象
	 * @param uniPropName1	: 的唯一键组合属性字段1
	 * ....					: 	  	  
	 * @param uniPropNameN	: 的唯一键组合属性字段N,如果只有一个字段,则无此参数,数据类型依据具体key而定
	 * @return			: XXX对象
	 * @history			: 
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public XXX selectItemByXXX(@Param("uniPropName1") String uniPropName1,...,@Param("uniPropNameN") String uniPropNameN);
​	根据唯一键组合查询对象,此方法可以完全被selectItems覆盖,但是由于Map<String, Object>类型的参数构造比较费代码,需要创建Map对象,然后再赋值,查询结果还是一个List类型。对于结果为唯一对象(或为null)的查询,服务层往往希望有简单的接口形式。因此,如果确定某些属性(或组合属性)是唯一时,可以提供此方法。
​	一个对象,可能有多个唯一字段或唯一组合,因此每个都需要提供类似方法。如用户对象,登录名是唯一的。还有一些属性,有值时是唯一的,如手机号码、email账号、身份证号码、微信小程序openid、微信公众号openid等,这些准唯一字段,也需要提供类似方法,但是调用时,需要判断是否为例外情况,如为空串,则不能调用,否则会抛出异常。另外,由于准唯一字段,数据库层面没有使用uniquekey约束,所有的唯一性约束是通过代码层面控制的,数据库记录层面看,并没有唯一性要求,为避免意外(如记录被人工修改失去唯一性),调用时应加try/catch进行保护。
​	下面是根据用户名查询用户对象的方法示例:	
	/**
	 * @methodName		: selectItemByUserName
	 * @description		: 根据用户名(登录)查询用户对象
	 * @param userName	: 用户名
	 * @return			: 用户对象
	 * @history			: 
	 * ------------------------------------------------------------------------------
	 * date			version		modifier		remarks
	 * ------------------------------------------------------------------------------
	 * yyyy/mm/dd	1.0.0		author		    初版
	 *
	 */
	public User selectItemByUserName(@Param("userName") String userName);

6、Mybatis脚本 ​

​	Mybatis提供操作访问数据库表的接口方法的实现,文件命名为XXXDaoMapper.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="com.abc.example.dao.UserDao">

</mapper>

6.1、新增单条记录

​	新增单条记录,方法名为insertItem,使用insert标签,参数为实体类对象。分自增ID和非自增ID。非自增ID,ID可以是全局ID,或用户指定ID,需要在调用dao接口前赋值。
​	由于数据类型可以通过实体类对象反射获取,因此无需表明JDBC数据类型;另外,由于实体类的属性都有默认值,因此,insert语句变得很简单。create_time和update_time字段,由数据库维护,无需新增。

6.1.1、自增ID的新增单条记录

​	示例代码如下:	
    <insert id="insertItem" parameterType="com.abc.example.entity.User">
        <selectKey keyProperty="userId" order="AFTER" resultType="java.lang.Long">
            SELECT LAST_INSERT_ID()
        </selectKey>	
        INSERT INTO exa_users(
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag
        )
        VALUES(
            #{userId},#{userName},#{password},#{salt},#{userType},#{orgId},#{realName},#{email},
            #{phoneNumber},#{sex},#{birth},#{idNo},#{openId},#{woaOpenid},#{remark},
            #{operatorName},#{deleteFlag}
        )
    </insert>

6.1.2、非自增ID的新增单条记录

​	示例代码如下:	
    <insert id="insertItem" parameterType="com.abc.example.entity.User">
        INSERT INTO exa_users(
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag
        )
        VALUES(
            #{userId},#{userName},#{password},#{salt},#{userType},#{orgId},#{realName},#{email},
            #{phoneNumber},#{sex},#{birth},#{idNo},#{openId},#{woaOpenid},#{remark},
            #{operatorName},#{deleteFlag}
        )
    </insert>

6.2、批量新增记录

​	批量单条记录,方法名为insertItems,使用insert标签,参数为实体类对象列表。分自增ID和非自增ID。非自增ID,需要在调用dao接口前赋值。
​	批量新增语句,使用foreach标签来实现对列表的遍历。

6.2.1、自增ID的批量新增记录

​	示例代码如下:	
    <insert id="insertItems" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="userId">
        INSERT INTO exa_users(
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag
        )
        VALUES
        <foreach collection ="list" item="item" index= "index" separator =",">
        (
            #{item.userId},#{item.userName},#{item.password},#{item.salt},#{item.userType},
            #{item.orgId},#{item.realName},#{item.email},#{item.phoneNumber},#{item.sex},
            #{item.birth},#{item.idNo},#{item.openId},#{item.woaOpenid},#{item.remark},
            #{item.operatorName},#{item.deleteFlag}
        )
        </foreach>
    </insert>

6.2.2、非自增ID的批量新增记录

​	示例代码如下:	
    <insert id="insertItems" parameterType="java.util.List">
        INSERT INTO exa_users(
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag
        )
        VALUES
        <foreach collection ="list" item="item" index= "index" separator =",">
        (
            #{item.userId},#{item.userName},#{item.password},#{item.salt},#{item.userType},
            #{item.orgId},#{item.realName},#{item.email},#{item.phoneNumber},#{item.sex},
            #{item.birth},#{item.idNo},#{item.openId},#{item.woaOpenid},#{item.remark},
            #{item.operatorName},#{item.deleteFlag}
        )
        </foreach>
    </insert>

6.3、修改单条记录

​	修改单条记录,方法名为updateItemByKey,使用update标签。参数为Map<String,Object>类型,由于参数项的值的类型为Object,因此需表明JDBC数据类型;另外,允许修改部分字段,即每个修改字段都是可选的,条件字段为对象的key值。create_time和update_time字段,由数据库维护,不允许修改。
​	示例代码如下:	
    <update id="updateItemByKey" parameterType="java.util.Map">
        UPDATE
            exa_users
        <set>
            <if test='userId != null'>
                user_id = #{userId,jdbcType=BIGINT},
            </if>
            <if test='userName != null'>
                user_name = #{userName,jdbcType=VARCHAR},
            </if>
            <if test='password != null'>
                password = #{password,jdbcType=VARCHAR},
            </if>
            <if test='salt != null'>
                salt = #{salt,jdbcType=VARCHAR},
            </if>
            <if test='userType != null'>
                user_type = #{userType,jdbcType=TINYINT},
            </if>
            <if test='orgId != null'>
                org_id = #{orgId,jdbcType=INTEGER},
            </if>
            <if test='realName != null'>
                real_name = #{realName,jdbcType=VARCHAR},
            </if>
            <if test='email != null'>
                email = #{email,jdbcType=VARCHAR},
            </if>
            <if test='phoneNumber != null'>
                phone_number = #{phoneNumber,jdbcType=VARCHAR},
            </if>
            <if test='sex != null'>
                sex = #{sex,jdbcType=TINYINT},
            </if>
            <if test='birth != null'>
                birth = #{birth,jdbcType=TIMESTAMP},
            </if>
            <if test='idNo != null'>
                id_no = #{idNo,jdbcType=VARCHAR},
            </if>
            <if test='openId != null'>
                open_id = #{openId,jdbcType=VARCHAR},
            </if>
            <if test='woaOpenid != null'>
                woa_openid = #{woaOpenid,jdbcType=VARCHAR},
            </if>
            <if test='remark != null'>
                remark = #{remark,jdbcType=VARCHAR},
            </if>
            <if test='operatorName != null'>
                operator_name = #{operatorName,jdbcType=VARCHAR},
            </if>
            <if test='deleteFlag != null'>
                delete_flag = #{deleteFlag,jdbcType=TINYINT},
            </if>
        </set>
        WHERE
            user_id = #{userId,jdbcType=BIGINT} 
    </update>
​	示例代码中,user_id = #{userId,jdbcType=BIGINT}确保修改字段为空时,语句仍可以正常执行,此时表记录没有任何改动。	

6.4、批量修改记录

​	批量修改记录,方法名为updateItems,使用update标签。参数为Map<String,Object>类型,由于参数项的值的类型为Object,因此需表明JDBC数据类型;批量修改的字段和条件,由业务需求决定。调用前应确保至少有一个修改字段。
​	示例代码如下:	
    <update id="updateItems" parameterType="java.util.Map">
        UPDATE
            exa_users
        <set>
            <if test='orgId != null'>
                org_id = #{orgId,jdbcType=INTEGER},
            </if>
            <if test='userType != null'>
                user_type = #{userType,jdbcType=TINYINT},
            </if>
            <if test='deleteFlag != null'>
                delete_flag = #{deleteFlag,jdbcType=TINYINT},
            </if>
            <if test='operatorName != null'>
                operator_name = #{operatorName,jdbcType=VARCHAR},
            </if>
        </set>
        WHERE
            1 = 1
            <if test='userIdList != null and userIdList.size != 0'>
                AND user_id IN
                <foreach collection ="userIdList" item="userId" index= "index" open="(" close=")" separator =",">
                    #{userId}
                </foreach>
            </if>                
            <if test='userName != null'>
                AND user_name = #{userName,jdbcType=VARCHAR}
            </if>
            <if test='phoneNumber != null'>
                AND phone_number = #{phoneNumber,jdbcType=VARCHAR}
            </if>
    </update> 
​	updateItems方法,包含了条件不为key字段的修改方法以及批量修改记录方法。	

6.5、删除单条记录

​	删除单条记录,方法名为deleteItemByKey,使用delete标签。参数为对象的key值,由接口的@Param注解确定,由于数据类型已明确,因此无需表明JDBC数据类型。一般支持物理删除的关系表,需要删除方法,下例为用户角色表的记录删除方法。
​	示例代码如下:	
    <delete id="deleteItemByKey">
        DELETE FROM 
            exa_user_roles
        WHERE
            user_id = #{userId} AND
            role_id = #{roleId}
    </delete> 

6.6、批量删除记录

​	批量删除记录,方法名为deleteItems,使用delete标签。参数为Map<String,Object>类型,由于参数项的值的类型为Object,因此需表明JDBC数据类型;批量修改的字段和条件,由业务需求决定。调用前应确保至少有一个条件字段(否则为全表删除了)。一般支持物理删除的关系表,需要批量删除方法,下例为用户角色表的批量删除记录方法。
​	示例代码如下:	
    <delete id="deleteItems" parameterType="java.util.Map">
        DELETE FROM 
            exa_user_roles
        WHERE
            1 = 1
            <if test='roleId != null'>
                AND role_id = #{roleId,jdbcType=INTEGER}
            </if>
            <if test='userId != null'>
                AND user_id = #{userId,jdbcType=BIGINT}
            </if>
    </delete> 
​	上述示例代码,允许删除指定userId的所有用户角色数据,或指定roleId的所有用户角色数据。	

6.7、查询记录

​	查询记录,使用select标签。CRUD通用查询语句(不包括子查询、分组查询、UNION等特殊查询)包含4个部分:查询字段集、数据表及表的联结关系、查询条件、排序选项。
​	查询字段集,是实体类字段的一个子集或全集。
​	数据表及表的联结关系,包括主表和外键参照表,一般使用内联结(INNER JOIN),由于查询语句的执行次序优先执行ON的过滤,以避免庞大的笛卡尔乘积数,因此应使用JOIN..ON形式,而不是FROM table1,table2 WHERE形式。
​	如果外键数目较多,应考虑分步查询或结合缓存赋值。原则上,除主表外,最多联结3个表。
​	查询条件,根据5.3节的条件字段命名规范,确定查询条件。
​	排序选项,使用SortField列表sortList,理论上,所有字段都可以排序。考虑到SQL注入问题,排序选项的代码会显得冗长。

6.7.1、查询单条记录

​	查询单条记录,方法名为selectItemByKey,使用select标签。参数为对象的key值,由接口的@Param注解确定,由于数据类型已明确,因此无需表明JDBC数据类型。
​	示例代码如下:	
    <select id="selectItemByKey" resultType="com.abc.example.entity.User">
        SELECT 
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag,create_time,update_time
        FROM
            exa_users
        WHERE
            user_id = #{userId,jdbcType=BIGINT} 
    </select> 
​	上述示例代码,为单表查询。但是实体类User还需要对orgName外键引用字段进行赋值(往往前端页面需要),这可根据org_id字段查询exa_orgnizations表获取。由于所有查询都有类似外键引用字段问题需要处理,在下节selectItems方法时进一步讨论。

6.7.2、查询记录列表

​	查询记录列表,方法名为selectItems,使用select标签。参数为Map<String,Object>类型。
​	示例代码如下:	
    <select id="selectItems" resultType="com.abc.example.entity.User">
        SELECT 
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag,create_time,update_time
        FROM
            exa_users
        WHERE
            1 = 1
            <if test='userId != null'>
                AND user_id = #{userId,jdbcType=BIGINT}
            </if>
            <if test='userIdList != null and userIdList.size != 0'>
                AND user_id IN
                <foreach collection ="userIdList" item="userId" index= "index" open="(" close=")" separator =",">
                    #{userId}
                </foreach>
            </if>                
            <if test='userName != null'>
                AND user_name = #{userName,jdbcType=VARCHAR}
            </if>
            <if test='userNameLike != null'>
                AND user_name LIKE CONCAT('%',#{userNameLike,jdbcType=VARCHAR},'%')
            </if>
            <if test='userType != null'>
                AND user_type = #{userType,jdbcType=TINYINT}
            </if>
            <if test='userTypeList != null and userTypeList.size != 0'>
                AND user_type IN
                <foreach collection ="userTypeList" item="userType" index= "index" open="(" close=")" separator =",">
                    #{userType}
                </foreach>
            </if>                
            <if test='sex != null'>
                AND sex = #{sex,jdbcType=TINYINT}
            </if>
            <if test='deleteFlag != null'>
                AND delete_flag = #{deleteFlag,jdbcType=TINYINT}
            </if>
            <if test='phoneNumber != null'>
                AND phone_number = #{phoneNumber,jdbcType=VARCHAR}
            </if>
            <if test='phoneNumberLike != null'>
                AND phone_number LIKE CONCAT('%',#{phoneNumberLike,jdbcType=VARCHAR},'%')
            </if>
            <if test='realNameLike != null'>
                AND real_name LIKE CONCAT('%',#{realNameLike,jdbcType=VARCHAR},'%')
            </if>
            <if test='email != null'>
                AND email = #{email,jdbcType=VARCHAR}
            </if>
            <if test='emailLike != null'>
                AND email LIKE CONCAT('%',#{emailLike,jdbcType=VARCHAR},'%')
            </if>
            <if test='birthGte != null'>
                AND birth &gt;= #{birthGte,jdbcType=TIMESTAMP}
            </if>             
            <if test='birthLte != null'>
                AND birth &lt;= #{birthLte,jdbcType=TIMESTAMP}
            </if>             
            <if test='orgId != null'>
                AND org_id = #{orgId,jdbcType=INTEGER}
            </if>
            <if test='orgIdList != null and orgIdList.size != 0'>
                AND org_id IN
                <foreach collection ="orgIdList" item="orgId" index= "index" open="(" close=")" separator =",">
                    #{orgId}
                </foreach>
            </if>                
            <if test='openId != null'>
                AND open_id = #{openId,jdbcType=VARCHAR}
            </if>
            <if test='woaOpenid != null'>
                AND woa_openid = #{woaOpenid,jdbcType=VARCHAR}
            </if>
        <if test='sortList != null and sortList.size != 0'>
        ORDER BY
            <foreach collection ="sortList" item="sortItem" index= "index" open="" close="" separator =",">
                <choose>
                    <when test='sortItem.fieldName == "userId"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                user_id ASC
                            </when>
                            <otherwise>
                                user_id DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "userName"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                user_name ASC
                            </when>
                            <otherwise>
                                user_name DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "password"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                password ASC
                            </when>
                            <otherwise>
                                password DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "salt"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                salt ASC
                            </when>
                            <otherwise>
                                salt DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "userType"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                user_type ASC
                            </when>
                            <otherwise>
                                user_type DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "orgId"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                org_id ASC
                            </when>
                            <otherwise>
                                org_id DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "realName"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                real_name ASC
                            </when>
                            <otherwise>
                                real_name DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "email"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                email ASC
                            </when>
                            <otherwise>
                                email DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "phoneNumber"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                phone_number ASC
                            </when>
                            <otherwise>
                                phone_number DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "sex"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                sex ASC
                            </when>
                            <otherwise>
                                sex DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "birth"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                birth ASC
                            </when>
                            <otherwise>
                                birth DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "idNo"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                id_no ASC
                            </when>
                            <otherwise>
                                id_no DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "openId"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                open_id ASC
                            </when>
                            <otherwise>
                                open_id DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "woaOpenid"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                woa_openid ASC
                            </when>
                            <otherwise>
                                woa_openid DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "remark"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                remark ASC
                            </when>
                            <otherwise>
                                remark DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "operatorName"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                operator_name ASC
                            </when>
                            <otherwise>
                                operator_name DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "deleteFlag"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                delete_flag ASC
                            </when>
                            <otherwise>
                                delete_flag DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "createTime"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                create_time ASC
                            </when>
                            <otherwise>
                                create_time DESC
                            </otherwise>
                        </choose>                        
                    </when>
                    <when test='sortItem.fieldName == "updateTime"'>
                        <choose>
                            <when test='sortItem.sortOrder == 0'>
                                update_time ASC
                            </when>
                            <otherwise>
                                update_time DESC
                            </otherwise>
                        </choose>                        
                    </when>
                </choose>
            </foreach>            
        </if>                           
        <if test='rows != null'>
            <choose>
                <when test='offset != null'>
                    LIMIT #{offset,jdbcType=INTEGER},#{rows,jdbcType=INTEGER}
                </when>
                <otherwise>
                    LIMIT #{rows,jdbcType=INTEGER}
                </otherwise>
            </choose>
        </if>
    </select> 
​	上述示例代码,为单表查询。
​	查询条件,使用了5.3节规定的命名规则,从而允许同一个字段的不同查询条件选项。其中对于List条件,空集等于全集,这是由于全集往往是一个很大的数据集,使用空集表示可简化处理,并且sql语句不支持"in ()"的空集形式。列表的无效集(真正的空集),可以使用一个无效的数据,如[-1]来表示。
​	排序选项,根据sortList列表次序,确定排序字段和排序方法。由于避免使用会引入SQL注入的"${field_name}"形式,因此需要遍历所有可排序的字段。如果sortList列表为空或空集,则使用默认排序(主键字段排序)。
​	LIMIT选项参数(rows和offset),可用于分页查询。单元测试辅助服务类,使用此参数,获取一定数量的样本记录。

6.7.3、外键引用字段查询

​	实体类有一些外键引用字段,如User对象的orgName,其根据org_id,查询exa_orgnizations表获取。
​	有三种处理方法,方法1为联结表查询;方法2为分步查询;方法3为分步查询结合缓存赋值。假设用户表exa_users有10万条记录,组织表exa_orgnizations有1000条记录,下面比较各种处理方法的优劣。

​	方法1:联结表查询,即联结exa_users和exa_orgnizations表,获取org_name值。代码如下:
    <select id="selectItems" resultType="com.abc.example.entity.User">
        SELECT 
            t1.user_id,t1.user_name,t1.password,t1.salt,t1.user_type,t1.org_id,t1.real_name,t1.email,
			t1.phone_number,t1.sex,t1.birth,t1.id_no,t1.open_id,t1.woa_openid,t1.remark,
			t1.operator_name,t1.delete_flag,t1.create_time,t1.update_time,
			t2.org_name
        FROM
            exa_users t1
        INNER JOIN
            exa_orgnizations t2
        ON
            t1.org_id = t2.org_id
        WHERE
            ... 
    </select> 
​	方法1,联结查询,为了提升查询性能,此时主表应建立外键(如exa_users表的org_id字段)的索引。使用Explain对查询语句进行性能分析,优化查询语句。如果使用"t2.org_name like '%a%'"的查询条件,由于需要全表扫描联结后数据(10万条),大致需要1秒量级完成查询,性能不如方法2(分步单表查询)。
​	方法1的优点是处理代码比较简单;缺点是可能有性能问题。

​	方法2:分步查询,即将联结查询拆分为单表查询或更少联结表的查询。为了提升查询性能,此时主表仍应建立外键(如exa_users表的org_id字段)的索引。
​	如查询用户数据,分两种情况。
​	情况1:查询条件不包含exa_orgnizations表字段的查询条件,则先查询exa_users表,获取userList,然后获取不重复的orgIdList,再查询exa_orgnizations表,并建立字典(orgId到Orgnization对象的字典),然后逐个给userList的项设置orgName值。
​	情况2:查询条件如果包含exa_orgnizations表的查询条件,如"org_name like '%a%'",则先查询exa_orgnizations表,获取orgList,并建立字典(orgId到Orgnization对象的字典),然后获取不重复的orgIdList,再根据orgIdList条件和exa_users表的其它条件,查询User记录,获取userList。然后逐个给userList的项设置orgName值。
​	对于情况1,由于exa_orgnizations表的org_id字段为主键字段,因此orgIdList的查询条件对性能影响可以忽略不计,第二次查询时间消耗很少,主要是代码处理处理复杂度有所增加。由于需要使用外键引用字段,一般为前端,此时往往使用分页查询,假设每页50条记录,则处理orgName属性值的时间一般不超过50ms(在1000条记录中查询orgIdList,大致需要20ms,然后是内存处理)。
​	对于情况2,由于exa_users表的org_id字段为索引字段,因此orgIdList的查询条件对exa_users表的查询性能影响可以忽略不计。exa_orgnizations表的查询,由于like条件不支持索引,使用全表扫描,在一个较小的数据集中搜索(1000条记录),大致需要50ms,结合内存处理,总时间不超过100ms。
​	方法2的优点是没有性能问题;缺点是处理代码比较复杂,对于情况1,需要频繁查询exa_orgnizations表。

​	方法3:分步查询结合缓存赋值。exa_users表的查询,同方法2,也分两种情况,但orgName的赋值,不必每次都查询exa_orgnizations表,而是使用缓存。由于组织Orgnization对象数目相对有限,且被多个实体类对象引用,因此可以使用缓存(单机部署可以直接使用内存,集群部署可使用Redis),关于缓存对象管理,后面将详细讨论。
​	情况1:查询条件不包含exa_orgnizations表字段的查询条件,则先查询exa_users表,获取userList,然后遍历userList的项,根据orgId,从缓存(一般为orgId到Orgnization对象的字典)中获取Orgnization对象,然后设置orgName值。
​	情况2:查询条件如果包含exa_orgnizations表的查询条件,如"org_name like '%a%'",则先查询exa_orgnizations表,获取orgList,然后获取不重复的orgIdList,再根据orgIdList条件和exa_users表的其它条件,查询User记录,获取userList。然后逐个给userList的项设置orgName值。
​	方法3的优点是没有性能问题,相对于方法2,赋值处理得到简化,并且减少了对外键表的查询,处理性能更高;缺点是需要缓存管理,引入了缓存数据一致性的问题和生命周期管理。
​	缓存对象管理,需要系统层面进行统一设计。一般使用缓存的对象,是记录数相对较少(不到1万条)或增长非常缓慢的,且被多种实体类对象引用的对象。

​	在实际处理中,这3种方法都会使用。方法1,用于外键较少,且查询条件字段属于主表字段的情况;方法2,用于外键较多,且外表记录数较多,不适合建立缓存的的情况;方法3,用于外键对象使用缓存管理的。

6.7.4、查询记录数

​	查询记录数,方法名为selectCount,使用select标签。参数为Map<String,Object>类型,返回值为Integer或Long型。
​	示例代码如下:	
    <select id="selectCount" resultType="java.lang.Integer">
        SELECT 
            COUNT(1)
        FROM
            exa_users
        WHERE
            1 = 1
            <if test='userId != null'>
                AND user_id = #{userId,jdbcType=BIGINT}
            </if>
            <if test='userIdList != null and userIdList.size != 0'>
                AND user_id IN
                <foreach collection ="userIdList" item="userId" index= "index" open="(" close=")" separator =",">
                    #{userId}
                </foreach>
            </if>                
            <if test='userName != null'>
                AND user_name = #{userName,jdbcType=VARCHAR}
            </if>
            <if test='userNameLike != null'>
                AND user_name LIKE CONCAT('%',#{userNameLike,jdbcType=VARCHAR},'%')
            </if>
            <if test='userType != null'>
                AND user_type = #{userType,jdbcType=TINYINT}
            </if>
            <if test='userTypeList != null and userTypeList.size != 0'>
                AND user_type IN
                <foreach collection ="userTypeList" item="userType" index= "index" open="(" close=")" separator =",">
                    #{userType}
                </foreach>
            </if>                
            <if test='sex != null'>
                AND sex = #{sex,jdbcType=TINYINT}
            </if>
            <if test='deleteFlag != null'>
                AND delete_flag = #{deleteFlag,jdbcType=TINYINT}
            </if>
            <if test='phoneNumber != null'>
                AND phone_number = #{phoneNumber,jdbcType=VARCHAR}
            </if>
            <if test='phoneNumberLike != null'>
                AND phone_number LIKE CONCAT('%',#{phoneNumberLike,jdbcType=VARCHAR},'%')
            </if>
            <if test='realNameLike != null'>
                AND real_name LIKE CONCAT('%',#{realNameLike,jdbcType=VARCHAR},'%')
            </if>
            <if test='email != null'>
                AND email = #{email,jdbcType=VARCHAR}
            </if>
            <if test='emailLike != null'>
                AND email LIKE CONCAT('%',#{emailLike,jdbcType=VARCHAR},'%')
            </if>
            <if test='birthGte != null'>
                AND birth &gt;= #{birthGte,jdbcType=TIMESTAMP}
            </if>             
            <if test='birthLte != null'>
                AND birth &lt;= #{birthLte,jdbcType=TIMESTAMP}
            </if>             
            <if test='orgId != null'>
                AND org_id = #{orgId,jdbcType=INTEGER}
            </if>
            <if test='orgIdList != null and orgIdList.size != 0'>
                AND org_id IN
                <foreach collection ="orgIdList" item="orgId" index= "index" open="(" close=")" separator =",">
                    #{orgId}
                </foreach>
            </if>                
            <if test='openId != null'>
                AND open_id = #{openId,jdbcType=VARCHAR}
            </if>
            <if test='woaOpenid != null'>
                AND woa_openid = #{woaOpenid,jdbcType=VARCHAR}
            </if>
    </select>  

6.7.5、根据唯一键或准唯一键查询记录

​	根据唯一键或准唯一键查询记录,方法名为selectItemByXXX,使用select标签。参数为对象的key值,由接口的@Param注解确定,返回值为对象记录。
​	示例代码如下:	
    <select id="selectItemByUserName" resultType="com.abc.example.entity.User">
        SELECT 
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag,create_time,update_time
        FROM
            exa_users
        WHERE
            user_name = #{userName,jdbcType=VARCHAR} 
    </select>

    <select id="selectItemByPhoneNumber" resultType="com.abc.example.entity.User">
        SELECT 
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag,create_time,update_time
        FROM
            exa_users
        WHERE
            phone_number = #{phoneNumber,jdbcType=VARCHAR} 
    </select>

    <select id="selectItemByIdNo" resultType="com.abc.example.entity.User">
        SELECT 
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag,create_time,update_time
        FROM
            exa_users
        WHERE
            id_no = #{idNo,jdbcType=VARCHAR} 
    </select>

    <select id="selectItemByOpenId" resultType="com.abc.example.entity.User">
        SELECT 
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag,create_time,update_time
        FROM
            exa_users
        WHERE
            open_id = #{openId,jdbcType=VARCHAR} 
    </select>

    <select id="selectItemByWoaOpenid" resultType="com.abc.example.entity.User">
        SELECT 
            user_id,user_name,password,salt,user_type,org_id,real_name,email,phone_number,sex,
            birth,id_no,open_id,woa_openid,remark,operator_name,delete_flag,create_time,update_time
        FROM
            exa_users
        WHERE
            woa_openid = #{woaOpenid,jdbcType=VARCHAR} 
    </select>  
​	对于准唯一键,调用dao接口前,需排除例外情况(如手机号码允许为空串)。如果查询结果集超过一条记录,会抛出异常,因此调用时,需要用try/catch进行保护。	

(未完待续...)