Springboot如何优雅地实现更新Model

更新Model目前我有两种方式,但是感觉都不是很好:

方式一:

现有一个SpringBoot的WebApi项目,通过前端发起http请求来进行Model更新。
比如,要更新一个id为1的User{id,name,age,nickName}实体,我从前端只传了这样一个User实体json来更新nickName:{id:1,nickName:'young'},在后端接收到这样一个实体后我是这样更新的:

var user=userService.getById(model.id);
if(model.name!=null){
    user.name=model.name;
}
if(model.age!=null){
    user.age=model.age;
}
if(model.nickName!=null){
    user.nickName=model.nickName;
}
userService.update(user);
  • 缺点:从上面的例子中,显而易见,这种编码方式太糟糕了。如果User类有100个属性,我就要if判断100次!

方式二:

Mybatis中通过Generator工具可以生成一个updateBySelective()的方法,可以自动根据传入的模型的值来自行完成这样一个操作。
比如,如果你传入的实体中含有nickName属性,则自动帮你更新,否则不更新。这一切的操作都只需要你优雅地调用方法就行了:

userService.update(model);

但是这样的方法也会存在一个问题:
如果我的业务逻辑不允许修改age,但是当前端传入了age后就肯定会自动更新这个age,如果我要限制更新age,就只能这么写:

if(model.age!=null){
    model.age=null;
}
userService.update(model);

这样的代码也会显得丑陋无比……

  • 缺点:如果某些属性不允许更细的话,也需要不停加入if语句。

问题:

有没有更好的方式,可以解决上面两种方法的缺点,实现优雅地更新Model?

阅读 7.3k
3 个回答

前端接收参数使用 Payload 而不是实体:

@Data
class UserPaylaod {
    private String id;
    private String nickName;
    
    public User toUser() {
        // 将 Payload 转换为所需实体。
        // 这里进行一系列检查,包括但不限于:
        // id 对应实体是否存在
        // 参数是否符合要求
    }
}

好处是能强制忽略掉那些可能被恶意携带的额外参数。

至于对数据的校验问题,要么使用 Spring Boot 集成的 hibernate-validation,要么手写判断代码(也可以配合自定义注解完成一些常见的非空判断)。

我的流程一般是:

Payload 包含一个实体在多个场景(包括但不限于增删查改)下所需的字段,接收参数后调用 toModel 方法进行校验并转换为所需实体。

// CommonFields 是一些共有的字段,比如 id 啦之类的
@Data
public class UserPayload extends CommonFields implements ModelTransfer<User, Scene> {

   public enum Scene {
       Register, Login;
   }

   @NotBlank
   @Size(min = 5, max = 10)
   @Scene({Register, Login})
   // 标记这个字段在指定的场景中被要求存在(不存在则抛异常),否则忽略其值(从 JSON 反序列化后置字段空)
   private String nickName;

   @Override
   public User toModel(Scene scene) {
     // getBean 是我注入了 ApplicationContext 以方便校验时需要用到外部 service 之类的东西
     UserService userService = getBean(UserService.class);
     User user;
     switch (scene) {
         case Register:
             // TODO ...
             break;
         case Login:
             user = userService.findById(this.id);
             // 由于我搞了异常监听器,下面的情况我会直接抛异常截断流程
             Assert.notNull(user, "用户不存在!");
             // TODO ...
             // 此时不需要更新 nickName 则不 user.setNickName(nickName);
             // 其他场景需要时则调用,且由于工具的处理,不用担心此时 nickName 为空
             break;
         default:
             throw new IllegalSceneException("未知场景!");        
     }
     return user;
   }
}

@PostMapping("/user")
public ResponseData<User> register(@RequestBody userPayload) {
    User user = userPaylaod.toModel(Scene.Register);
    // TODO...
}

对于能用反射检查的就在 toModel 之前检查了(我写了个自定义参数处理器,因为有时要注入一些 session 属性值,同时会产生 payload 代理实例以方便我做其他处理),剩下的那些不同场景要求不一样的则手写校验规则了。

下一步打算支持特定场景的校验,比如: @Validate(scenes = { @SceneValidate( scene = Register, type = Validator.NotBlank, message = "不能为空!")}) 一类的写法。 (懒癌患者小声 bb)

上面的例子里,通过 hibernate-validation 和一些自己写的小工具解决你的第一个方法的问题,通过 payload 概念解决你的第二个方法的问题。

总的来说,这个事情框架能做到的有限,毕竟要考虑通用性,那么只好你自己定制一些小工具来做这些事了。

实体类有注解的,了解一下?比如@NotNull(message = "收货人地址id不能为空")

像1楼一样,前后端传值一般是定义一个Dto对象。使用hibernate-validation对Dto参数进行校验,BeanUtils.copyProperties()方法进行属性值替换。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题