数据访问接口设计思考
这题目有点大,但我也没想到一个更好的题目,先这么用着。老规矩,先说背景。本周遇到了一个难题,如下:某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个属性,代码量大增,开发复杂度上来了。另外就是我觉得这个方案有点搞笑,冗余、不优雅。
总结
其实对于这个问题,我现在没有一个特别倾向的答案。由于项目时间的关系,目前我们采用的是特殊处理法(心不甘情不愿地埋了一个坑)。剩余两个解决方案也不太好。写这个文章,其实也是希望把问题摆出来,大家一起探讨怎么优雅地解决这个接口设计的问题。