Spring Boot实现高质量的CRUD-2
(续前文)
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
* ------------------------------------------------------------------------------
* 2022/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 >= #{birthGte,jdbcType=TIMESTAMP}
</if>
<if test='birthLte != null'>
AND birth <= #{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 >= #{birthGte,jdbcType=TIMESTAMP}
</if>
<if test='birthLte != null'>
AND birth <= #{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进行保护。
(未完待续...)