数据访问接口设计思考

数据访问接口设计思考

这题目有点大,但我也没想到一个更好的题目,先这么用着。老规矩,先说背景。本周遇到了一个难题,如下:某entity有add和update两个接口,add和update的参数基本一致(add时无id,update时有id,其他基本一致)。该entity有个字段A是Date类型,非必填项。如果add时传了这个字段,那么update的时候,没有办法把字段A给修改为NULL。为什么没办法呢?往下看。


接口设计现状

我们主要着眼在控制层的逻辑

基础类APIController

/**
 * 提供接口的基础服务如获取请求、鉴权、获取DTO
 */
public class APIController extends Controller {   
    ……
    /**
     * 获取请求的DTO
     * @param <T>
     * @return object
     */
    public static <T> T getDTO(Class<T> clazz) {
        JsonObject requestJsonData = (JsonObject) getDataOfJson();
        /**
         * 省略具体处理逻辑
         */
    }
    public static JsonElement getDataOfJson() {
        ……
    }

}

具体实体的Controller

/**
 * 事件实体的控制类,提供add和update方法
 */
public class Event extends APIController {
    ……
    public static void update() {
        ……    
        // 获取参数,只是为了验证是否有传递该参数,该字段对请求结果不影响,仅为了安全,防猜密钥之用途。
        @SuppressWarnings("unused")
        EventUpdateRequestDTO eventRequest = getDTO(EventUpdateRequestDTO.class);
        updateEvent(eventRequest);
    }
    ……
    private static void updateEvent(EventUpdateRequestDTO request) {
        ……
        // new Event
        Event event = EventService.getById(request.getEventId());
        if (event == null) {
            helper.returnError("5000005", "eventId does not exist.");
        }
        ……
        if (request.getPrvEventId() != null) {
            event.setPrvEventId(request.getPrvEventId());
        }
        /**
         * 省略具体字段的处理,对所有的字段都是在不为NULL的时候set
         */
        if (request.getSource() != null) {
            event.setSource(request.getSource());
        }
        ……
    }

}

从以上两段代码中可以看出,具体entity的Controller是通过父类的getDTO方法来拿到请求类的,由于父类封装了这个过程,在具体entity的Controller里,是不知道原始请求信息中是没传这个字段呢,还是传了但传的是NULL。update时,对所有的字段都统一粗暴地进行NULL判断,不为NULL时才set。于是本文开头的问题就很好理解了,对于一开始在add时就有值的字段,无法将其update成NULL。

为什么要跟NULL过不去

是的,我们在思考解决方案之前,要先回答这个问题。为什么我们要跟NULL过不去,为什么非要在update时将一个已有值的字段设置为NULL。我给出的答案如下:

  • 有业务场景需要,如本文例子中的event,该实体仅在特定状态下需要有endTime(Date类型)字段,其他状态下若保留endTime会造成干扰和误解。因此,有这样的业务需求在update时清空已有字段。
  • 即使目前没有业务场景需要,但接口功能还是得齐备。我可以不用,但你不能没有。万一我哪天就想用了呢?

    所以,还是得解决这个问题。

几种可能的解决方案

特殊处理法

实在是有这样的字段,那就特殊处理好了。比如可以这样:

    // 对endTime做特殊处理
    if (request.getEndTime() != null) {
        event.setEndTime(new Date(request.getEndTime() * 1000));
    } else {
        // 不传或传null的时候,将endTime设置为null(因为只有开测状态的大事件有结束时间,其他状态不需要结束时间)
        event.setEndTime(null);
    }

也可以这样:

    // 对endTime做特殊处理
    if (request.getEndTime() != null) {
        // 传特殊时间表示需要置为NULL
        if (request.getEndTime().longValue() == 0L) {
            event.setEndTime(null);
        } else {
            event.setEndTime(new Date(request.getEndTime() * 1000));    
        }            
    } 

and so on。总之,针对这个字段做一些特殊的约定和处理。这样解决的好处是改动小(未涉及接口框架的改动,也不牵连其他字段),坏处是埋坑。我们讨厌特殊处理,很大程度上是因为特殊处理需要人的特殊注意,容易出错。这段特殊处理的代码放在这里,其实就是埋了一个坑,指不定哪天就掉进去了。

修改接口协议

这里的接口协议指的是整体api的所有update接口协议。原本我们的协议是,update时仅传需要修改的字段。那我们改一下,改成和add类似的“全量”update,即update时需要传这个实体所有的字段值(即使有些字段没有修改)。这样的优点显而易见,不再存在无法清空某一字段的问题,想改成啥都行;缺点也一样显而易见,update接口交互的内容增多,很多不需要修改的值也传来传去的,浪费网络流量。再者,针对目前遇到的这个问题,这个方法不具备可行性,因为接口已经大规模铺开应用,修改接口协议不现实。如果是完全从头设计新接口,倒是可以考虑下这种方案。

修改基础类APIController

【写在前面的话】这个解决方案纯粹是我异想天开。所以这一小节的标题是“几种可能的解决方案”,仅仅只是可能。看完如果觉得搞笑,哈哈一笑就好,不要当真。嗯,因为我自己都觉得挺搞笑,或者说不现实。

我的想法是,既然是APIController的getDTO方法屏蔽了请求的细节(如参数是传了还是没传),那为什么不改写getDTO这个方法呢?哪里跌倒哪里爬起来,这个方法里就搞清楚请求原内容是什么,返回的DTO也要做相应修改。异想天开的地方来了:

public class EventUpdateRequestDTO {
    ……
    // 请求里有没有传endTime
    private boolean endTimeFlag;
    // endTime的内容
    private Long endTime;
    ……
}

getDTO的职责就是解析请求,根据实际情况设置每个字段的两个相关属性(传了没,如果传了值是啥)。

这种方法(虽然getDTO的逻辑我没有贴,还没时间写)可以解决本文开头提出的问题。优点就是,接口协议不用修改,不影响关联方,即使接口已经铺开使用了也没事。缺点是,如果一个entity有N个字段的话,那requestDTO里就会有2*N个属性,代码量大增,开发复杂度上来了。另外就是我觉得这个方案有点搞笑,冗余、不优雅。

总结

其实对于这个问题,我现在没有一个特别倾向的答案。由于项目时间的关系,目前我们采用的是特殊处理法(心不甘情不愿地埋了一个坑)。剩余两个解决方案也不太好。写这个文章,其实也是希望把问题摆出来,大家一起探讨怎么优雅地解决这个接口设计的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值