V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
px920906
V2EX  ›  Node.js

请教后端业务逻辑代码如何分离

  •  
  •   px920906 · 2021-11-20 23:41:40 +08:00 · 5005 次点击
    这是一个创建于 1128 天前的主题,其中的信息可能已经有所发展或是发生改变。

    node+koa ,有这样一个接口:

    router.post(
      '/order',
      {...}, 
      async ctx => {
        const { body } = ctx.request
        // 一些需要查询数据库的参数验证逻辑,比如:
        const item = await Item.findOne({ where: { id: body.id } })
        if (!item) {
          throw Error('item not existing')
        }
        // 后续逻辑
        const result = await createOrder(body)
        ctx.body = result.id
      }
    )
    

    createOrder这个函数因为其他接口 /逻辑也会用到所以单独抽出来了,于是产生个问题: 函数内部也有用到查询Item的地方,那这样的话,/order这一个接口得查询Item两次。例子是查一个,查列表的情况岂不是更浪费。

    抽象出来大概是:如何避免 通用业务逻辑 与 调用它的 route 之间重复相同数据库查询操作?

    求赐教!谢谢!

    第 1 条附言  ·  2021-11-22 00:31:55 +08:00
    总结下前 6 楼的方案:
    1. item 作为可选参数传到函数里,但感觉不太优雅
    2. 查询一次之后把 item 缓存到全局变量里供后续使用。但可能会有时效性的问题,比如在验证之后、执行 createOrder 之前,另一个接口调用导致 item 发生了更新 /删除。其实方案一也有这个问题。
    3. 对 Item.create 进行二次封装,加入验证逻辑。这样的话又有另一个麻烦的点:
    当函数内部创建 item 时验证参数(注意是 createOrder 函数接收的参数)失败,抛出一个错误,里面包含对应参数(形参的名称,以字符串形式给出)和错误原因,那 route 捕获到这个错误之后要抛出客户端错误,如何将函数参数和 request 里的参数对应?因为这两个不一定是对称的,比如传进接口的是 item['id'],但传给 createOrder 的形参是 itemId 。

    不知道还有没有更好的办法。
    9 条回复    2021-11-24 10:27:58 +08:00
    lscho
        1
    lscho  
       2021-11-20 23:46:53 +08:00
    createOrder 增加一个可空参数 item 呗,存在就不查数据库,不存在再查。
    eason1874
        2
    eason1874  
       2021-11-21 00:04:17 +08:00
    createOrder 传参 Item

    如果 Item 是必须查询的,类似 List 于列表页,Post 于文章页,那也可以查询出来放到一个全局变量 queried object ,后续逻辑要获取当前查询对象时读取这个变量就行
    towave
        3
    towave  
       2021-11-21 00:20:43 +08:00
    查还是要查的,你可以写成中间件的形式,把查数据库的验证逻辑写在中间件就不用重复这么多次了,应该是可行的
    ElmerZhang
        4
    ElmerZhang  
       2021-11-21 00:27:19 +08:00
    一种思路是如楼上所说,item 传参给 createOrder 。
    另外一种思路:item 必须存在是否也是 createOrder 成功的必要条件?如果是的话,这个判断可以放到 createOrder 内部去做,这样也能避免其他调用 createOrder 的地方也提前去查询 item 是否存在。
    zjsxwc
        5
    zjsxwc  
       2021-11-21 08:33:57 +08:00 via Android
    orm 托管的话,如果 findone 的 id 相同可以选择直接引用内存中的 item 对象,而不去数据库中查询
    OutOfMemery
        6
    OutOfMemery  
       2021-11-21 12:32:42 +08:00
    Item 里面封装校验逻辑,能被创建出来就是合法的,调用时只管用,这样就能将通用业务逻辑和其他业务逻辑分开
    libook
        7
    libook  
       2021-11-22 10:53:26 +08:00
    其实重复查两次的的问题还好,顶多是浪费一部分数据库性能而已,不会导致 bug ;目前的实现是有可能产生 bug 的,即在高并发场景下 createOrder 执行的时候不一定确保 item 一定存在,假设 item 有可能在“const item = await Item.findOne({ where: { id: body.id } })”执行后、“createOrder”执行前被其他进程的业务逻辑删除,那么此时 createOrder 就会在 item 不存在的情况下执行;若 createOrder 内部做了 item 的存在性验证,此时会抛出异常;若未做 item 的存在性验证,则会产生脏数据。同理 createOrder 内部的多项数据库操作如果不能确保事务性(原子性)的话,也会有同样的问题。

    针对题主的例子来说,item 的存在性验证仅为 createOrder 的顺利执行而服务,那么可以把这个过程并入 createOrder 内部,在 createOrder 内部查询 item 不存在则抛出异常,同时使用事务确保多表操作的原子性。
    px920906
        8
    px920906  
    OP
       2021-11-23 21:49:58 +08:00
    @libook 的确,后来我也想到了,但这样的话 createOrder 抛出的异常怎样比较好地转化到客户端异常呢?见 append 第三条
    libook
        9
    libook  
       2021-11-24 10:27:58 +08:00
    你 throw error 的时候是可以先 new 一个 Error 对象,然后给这个对象塞自己的自定义字段,然后 router 里 catch 到这个 error 之后拿你自定义字段看一眼,然后再根据当前 router 的情况决定如何返回 response 信息。这种做法会把 HTTP 协议层和业务逻辑层区分开,使得 createOrder 可以被多个 router 复用,每个 router 又能根据自己的情况来自定义返回。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   839 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 21:21 · PVG 05:21 · LAX 13:21 · JFK 16:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.