(续前文)
Dao类提供操作访问数据库表的接口方法。常规的CRUD,将考虑下列接口方法: 1)插入单条对象记录; 2)批量插入对象记录; 3)修改单条对象记录; 4)批量修改对象记录; 5)删除单条对象记录; 6)批量删除对象记录; 7)查询多条对象记录; 8)查询指定key的对象记录; 9)查询记录条数; 10)根据唯一字段(或组合字段)查询单条对象记录。 Dao类使用@Mapper注解,类中代码应仅实现接口方法定义,不建议使用@Select,@Insert等注解直接在Dao类中实现接口(特殊情况除外),考虑的后期接口功能扩展的可能性,在Mybatis xml脚本文件中实现接口书写更容易规范,功能也更强大。
新增对象和批量新增对象的方法名和形式如下所示。其中如果支持自增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);
修改单个对象和批量修改对象的方法名和形式如下所示。
/** * @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);
涉及批量记录,往往与条件有关,包括后面的批量删除记录,查询记录等,都需要使用条件。抛开需要使用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层的紧耦合。
删除单个对象和批量删除对象的方法名和形式如下所示。
/** * @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);
查询是使用最频繁的功能,受业务需求驱动,查询的要求也多种多样。此处仅考虑通用的查询需求,不考虑分组(GROUP)查询和联合(UNION)查询及子查询,这些特殊查询属于定制需求。 建议一般情况使用单表查询,在主从表(Master-Detail)的情况下,子表可以关联主表查询。建议不要超过3个表的关联查询,否则由于搜索记录空间太过庞大(为笛卡尔乘积),即使有索引,性能也不会太好,何况有些查询条件会使得索引失效,如LIKE,大于等于等等。 那么如果一个表的外键比较多,如何处理呢?建议使用分步查询以及缓存来处理,这样可以获取较好的查询性能。 通用查询考虑提供下列4个方法:
/** * selectItems : 根据条件查询对象列表,此方法适用于API调用和内部调用 * selectItemByKey : 根据key值查询一个对象 * selectCount : 根据条件查询记录数 * selectItemByXXX : 根据唯一键(或组合唯一键)查询一个对象 */
查询对象列表的方法名和形式如下所示:
/** * @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与之取交集。 */
根据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);
查询对象记录数的方法名和形式如下所示:
/** * @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型足够了。
根据唯一键(或组合唯一键)查询对象的方法名和形式如下所示:
/** * @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);
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>
新增单条记录,方法名为insertItem,使用insert标签,参数为实体类对象。分自增ID和非自增ID。非自增ID,ID可以是全局ID,或用户指定ID,需要在调用dao接口前赋值。 由于数据类型可以通过实体类对象反射获取,因此无需表明JDBC数据类型;另外,由于实体类的属性都有默认值,因此,insert语句变得很简单。create_time和update_time字段,由数据库维护,无需新增。
示例代码如下:
<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>
示例代码如下:
<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>
批量单条记录,方法名为insertItems,使用insert标签,参数为实体类对象列表。分自增ID和非自增ID。非自增ID,需要在调用dao接口前赋值。 批量新增语句,使用foreach标签来实现对列表的遍历。
示例代码如下:
<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>
示例代码如下:
<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>
修改单条记录,方法名为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}确保修改字段为空时,语句仍可以正常执行,此时表记录没有任何改动。
批量修改记录,方法名为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字段的修改方法以及批量修改记录方法。
删除单条记录,方法名为deleteItemByKey,使用delete标签。参数为对象的key值,由接口的@Param注解确定,由于数据类型已明确,因此无需表明JDBC数据类型。一般支持物理删除的关系表,需要删除方法,下例为用户角色表的记录删除方法。 示例代码如下:
<delete id="deleteItemByKey"> DELETE FROM exa_user_roles WHERE user_id = #{userId} AND role_id = #{roleId} </delete>
批量删除记录,方法名为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的所有用户角色数据。
查询记录,使用select标签。CRUD通用查询语句(不包括子查询、分组查询、UNION等特殊查询)包含4个部分:查询字段集、数据表及表的联结关系、查询条件、排序选项。 查询字段集,是实体类字段的一个子集或全集。 数据表及表的联结关系,包括主表和外键参照表,一般使用内联结(INNER JOIN),由于查询语句的执行次序优先执行ON的过滤,以避免庞大的笛卡尔乘积数,因此应使用JOIN..ON形式,而不是FROM table1,table2 WHERE形式。 如果外键数目较多,应考虑分步查询或结合缓存赋值。原则上,除主表外,最多联结3个表。 查询条件,根据5.3节的条件字段命名规范,确定查询条件。 排序选项,使用SortField列表sortList,理论上,所有字段都可以排序。考虑到SQL注入问题,排序选项的代码会显得冗长。
查询单条记录,方法名为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方法时进一步讨论。
查询记录列表,方法名为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),可用于分页查询。单元测试辅助服务类,使用此参数,获取一定数量的样本记录。
实体类有一些外键引用字段,如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,用于外键对象使用缓存管理的。
查询记录数,方法名为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>
根据唯一键或准唯一键查询记录,方法名为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进行保护。 (未完待续...)