【SpringBoot】记一次多字段唯一匹配实体场景下的开发
前言
所谓多字段唯一匹配场景可能标题写的不太明确,其实背景就是可以通过多个字段唯一标识一个实体。比如用户表,可以分别通过user_id、user_name、card_id、mobile、email这些字段唯一表示一个用户。
这次对接了一波交付同学的定制化开放接口,如何实现多字段都可以匹配唯一一条数据的sql,记一下比较有意思的写法。由于组长说开放给TOB场景下交付同学的接口自由发挥能用就行,于是乎我直接放飞自我,记录一些有意思的思路。
方案基本都是基于动态sql拼接where条件,类似(where %s = %s)
接口模糊参数
这个方案是之前同事实现的,首先接口入参直接就使用HttpServletRequest,使用request.getParameterNames() 获取接口的入参放到paramsMap里
public List<UserInfo> getUserInfo(HttpServletRequest request) {
// 获取接口入参
Enumeration<String> parameterNames = request.getParameterNames();
while(parameterNames.hasMoreElements()){
String key = parameterNames.nextElement();
String value = request.getParameter(key);
paramsMap.put(key, value);
}
}
定义两个字段,一个field变量,一个value变量,接下来只需要根据唯一字段的优先级优先判断。
String field = "";
String value = "";
if (paramsMap.containsKey("user_id")) {
field = "user_id";
value = paramsMap.get("user_id");
} else if (paramsMap.containsKey("username")) {
field = "username";
value = paramsMap.get("username");
} else if (paramsMap.containsKey("card_id")) {
field = "card_id";
value = paramsMap.get("card_id");
} else if (paramsMap.containsKey("mobile")) {
field = "mobile";
value = paramsMap.get("mobile");
} else if (paramsMap.containsKey("email")) {
field = "email";
value = paramsMap.get("email");
}
由于ORM框架一直使用SpringJPA,在dao层定义静态sql并注入到对应的service中使用,对于我们这种动态sql使用起来有些许尴尬。我们可以根据直接注入entityManager
bean,动态sql直接用字符串拼起来。
String.format("SELECT * FROM `user` where `%s`='%s' and is_del=0", field, value);
拿到动态sql后用entityManager调用api即可实现
Query query = entityManager.createNativeQuery(sql, AccountBean.class);
accountBeans = query.getResultList();
在常规项目里还是考虑下其他方案,这样实现会有sql注入的风险,因为tob场景
且开给交付的接口
且是个边缘项目
所以偷个懒。正常使用手动设置参数的形式可以避免掉sql注入,由于这是其他同事开的先例我也懒得改了~
List results = sqlQuery.setParameter(field, "value").getResultList();
接口指定参数
使用request.getParameterNames
的方式可以完美解决根据唯一字段getuserInfo
的场景,就是写法上不是那么优雅。
这次的需求是开放出一个批量更新用户信息的接口,这个跟交付同学沟通后发现交付去调用开放接口的时候是知道哪些用户可以根据哪个唯一字段定义,作为一个显眼包那必须得用另一个方式实现。既然知道了唯一字段,我们在接口入参直接定义一个String uniqueField,用来指定唯一字段。
static String[] UNIQUE_FIELDS = {"user_id", "username", "card_id", "mobile", "email"};
void userSaveOrUpdate(List<UserForm.UpdateForm> userInfoForms, String uniqueField){
// 先判断是否在规定的唯一字段里
if (!Arrays.asList(UNIQUE_FIELDS).contains(uniqueField)) {
throw new UCException(StatusCode.PARAMETERS_INVALID, "用户唯一字段无效");
}
如果uniqueField是合法的唯一字段,field有了只需要找到value了,我们遍历一遍UpdateForm对象集合去获取value,用反射的方式去找field对应的value值。
public static final class UpdateForm {
@NotNull
@ApiModelProperty(value = "账号")
private String username;
@NotNull
@ApiModelProperty(value = "姓名")
private String name;
@ApiModelProperty(value = "身份证号")
private String cardId;
@ApiModelProperty(value = "手机号")
private String mobile;
@ApiModelProperty(value = "邮箱")
private String email;
}
遍历前先把uniqueField从下划线转为驼峰,代码里都是驼峰,数据库里都是下划线,这里做一层转换。
Map<String, UserForm.CreateForm> userNameFormMap = userInfoForms.stream().collect(
Collectors.toMap(
key -> {
Field groupField = null;
for (Field field : key.getClass().getDeclaredFields()) {
if (finalUniqueField.equals(field.getName())) {
groupField = field;
}
}
if (groupField == null) {
throw new UCException(StatusCode.PARAMETERS_INVALID, "用户唯一字段无效");
}
try {
groupField.setAccessible(true);
return groupField.get(key).toString();
} catch (IllegalAccessException e) {
log.error(e.getMessage(), e);
throw new UCException(StatusCode.PARAMETERS_INVALID, "用户唯一字段无效");
}
}, v -> v));
转为map后key中的值即为这些用户唯一字段对应的value集合,我们把value集合每个元素加上"'"和","
List<String> uniqueFieldValues = new ArrayList<>(userNameFormMap.keySet());
StringBuilder uniqueFieldValuesCond = new StringBuilder();
for (int i = 0; i < uniqueFieldValues.size(); i++) {
if (i != 0) {
uniqueFieldValuesCond.append(", ");
}
uniqueFieldValuesCond.append("'").append(uniqueFieldValues.get(i)).append("'");
}
最后配合select * from user where %s in (%s)
即可,搞完发现转了半圈还不如多写几个if else呢,算了还是用同事的代码吧,毕竟有他的git记录出了bug找他别找我。。。。。