json 具有多个唯一ID的REST API设计

vcirk6k6  于 2023-06-25  发布在  其他
关注(0)|答案(6)|浏览(115)

目前,我们正在为我们的系统开发API,并且有一些资源可能具有不同类型的标识符。例如,有一个名为orders的资源,它可能有一个唯一的订单号,也有一个唯一的id。目前,我们只有id的URL,这些URL是:

GET /api/orders/{id}
PUT /api/orders/{id}
DELETE /api/orders/{id}

但现在我们还需要使用订单号的可能性,这通常会导致:

GET /api/orders/{orderNumber}
PUT /api/orders/{orderNumber}
DELETE /api/orders/{orderNumber}

显然这是行不通的,因为id和orderNumber都是数字。
我知道有一些类似的问题,但它们并没有帮助我,因为答案并不真正适合,或者他们的方法并不是真正的休息或理解(对于我们和可能使用API的开发人员)。此外,问题和答案部分超过7年。
仅举几例:

    • 1.使用查询参数**

建议使用查询参数,例如

GET /api/orders/?orderNumber={orderNumber}

我认为,有很多问题。首先,这是一个对orders集合的过滤器,因此结果也应该是一个列表。但是,唯一的订单号只有一个订单,这有点令人困惑。其次,我们使用这样的过滤器来搜索/过滤订单的子集。此外,查询参数是某种二级参数,但在本例中应该是一级参数。这甚至是一个问题,如果对象不存在。通常get会返回404(未找到),但如果顺序1234不存在,则GET /api/orders/?orderNumber=1234将是一个空数组。

    • 2.使用前缀**

一些公共API使用某种类型的鉴别器来区分不同的类型,例如例如:

GET /api/orders/id_1234
GET /api/orders/ordernumber_367652

这适用于他们的方法,因为id_1234ordernumber_367652是他们真正的唯一标识符,也由其他资源返回。但是,这将导致如下所示的响应对象:

{
  "id": "id_1234",
  "ordernumber": "ordernumber_367652"
  //...
}

这不是很干净,因为类型(id或订单号)被建模了两次。除了更改所有标识符和响应对象的问题之外,这将是令人困惑的,如果您我想搜索所有大于67363的订单号(因此,也存在字符串/数字冲突)。如果响应没有添加类型作为前缀,用户必须为某些请求添加该类型,这也会非常混乱(有时您必须添加该类型,有时不添加...)

    • 3.使用动词**

这是什么,例如。Twitter:他们的URL以show.json结尾,所以你可以这样使用它:

GET /api/orders/show.json?id=1234 
GET /api/orders/show.json?number=367652

我认为,这是最糟糕的解决方案,因为它是不平静的。此外,它还有我在查询参数方法中提到的一些问题。

    • 4.使用子资源**

有些人建议将其建模为子资源,例如:

GET /api/orders/1234 
GET /api/orders/id/1234   //optional
GET /api/orders/ordernumber/367652

我喜欢这种方法的可读性,但我认为/api/orders/ordernumber/367652的含义应该是“获取(只是)订单号367652”,而不是订单。最后,这打破了一些最佳实践,例如使用复数和只使用真实资源。
最后,我的问题是:我们错过了什么吗?有没有其他的方法,因为我认为这不是一个不寻常的问题?

wvyml7n5

wvyml7n51#

对我来说,解决问题的最RESTful的方法是使用方法2,并稍作修改。
从理论上讲,你只需要有有效的 * 识别码 * 来识别你的订单。在设计过程的这一点上,识别码是id还是订单号并不重要。它是唯一标识您的订单的东西,这就足够了。
id和数字格式之间的不明确性是属于实现阶段的问题,而不是设计阶段的问题。
所以现在,我们所拥有的是:
GET /api/orders/{some_identification_code}
这是非常RESTful的。
当然,你仍然有解决你的模糊性的问题,所以我们可以继续进行实现阶段。不幸的是,您的订单identification_code集由两个共享格式的不同实体组成。它是微不足道的,它不能工作。但现在的问题在于这些实体格式的定义。
我的建议很简单:id将是整数,而number将是代码,例如N1234567。此方法将使您的资源表示可接受:

{
  "id": "1234",
  "ordernumber": "N367652"
  //...
}

此外,它在许多场景中是常见的,例如快递运输。

aamkag61

aamkag612#

这是我想出的另一个选择,我觉得稍微更可口。

GET /api/orders/1234
GET /api/orders/1234?idType=id //optional
GET /api/orders/367652?idType=ordernumber

原因是它使路径与REST标准保持一致,然后在服务中,如果他们确实传递了idType = orderNumber(id的idType是默认值),您可以从中获取。

ql3eal8s

ql3eal8s3#

我正在为同样的问题而挣扎,还没有找到完美的解决方案。我最终使用了这个格式:

GET /api/orders/{orderid}
GET /api/orders/bynumber/{orderNumber}

不是完美的,但它是可读的。

42fyovps

42fyovps4#

我也在纠结这个!在我的例子中,我只需要能够使用辅助ID进行GET,这使得这更容易一些。
我倾向于使用一个可选的前缀ID:

GET /api/orders/{id}
GET /api/orders/id:{id}
GET /api/orders/number:{orderNumber}

或者,这可能是使用URI规范中一个模糊的特性path parameters的机会,它允许您将参数附加到特定的路径元素:

GET /api/orders/{id}
GET /api/orders/{id};id_type=id
GET /api/orders/{orderNumber};id_type=number

使用非限定ID的URL是规范的URL。非规范URL的行为有两个选项:或者返回实体,或者重定向到规范URL。后者在理论上更纯粹,但可能会给用户带来不便。或者它可能对用户更有用,谁知道呢!
另一种方法是将订单号建模为它自己的东西:

GET /api/ordernumbers/{orderNumber}

这可以返回一个只有ID的小对象,用户可以使用该ID来检索实体。或者只是重定向到命令。
如果你也想要一个一般的搜索资源,那么也可以在这里使用:

GET /api/orders?number={orderNumber}

在我的情况下,我不想要这样的资源(还),我可能不舒服添加一个似乎是一般搜索资源,只支持一个字段。

slhcrj9b

slhcrj9b5#

因此,基本上,您希望将所有id和订单号视为订单记录的唯一标识符。唯一标识符的问题是,当然,它们必须是唯一的!但是你的ID和订单号都是数字的;它们的范围有重叠吗如果“1234”可以是id或订单号,那么显然/api/orders/1234不会引用唯一的订单。
如果范围 * 是 * 唯一的,那么您只需要在/api/orders/{id}的处理程序代码中使用鉴别器逻辑,它可以区分ID和订单号。这实际上是可行的,比如说,如果你的订单号比你的id有更多的数字。但我想如果你可以的话你早就这么做了。
如果范围可能重叠,则必须至少强制对它们的引用具有唯一的范围。最简单的方法是在提及订单号时添加前缀,例如前缀“N”。因此,如果ID为1234的订单的订单号为367652,则可以使用以下调用之一来检索它:

/api/orders/1234
/api/orders/N367652

但是,要么数据库必须更改为在所有订单号中包含“N”前缀(你说这是不可能的),要么处理程序代码必须在转换为int之前去掉“N”前缀。在这种情况下,“N”前缀应该 * 仅 * 在API调用中使用-面向用户的数据输入表单 * 不应该公开它 *!你不能有一个“按任何标识符查找”字段,用户可以在其中输入id或订单号(无论如何,这将有一个非唯一性的问题)。相反,你 * 必须 * 有单独的“按id查找”和“按订单号查找”选项。然后,您应该能够让订单号输入处理程序在提交给API之前自动添加“N”前缀。
从根本上讲,这是数据库设计的一个问题--如果这是一个要求(使用来自两个字段的值作为“唯一标识符”),那么在设计数据库字段时就应该考虑到这一点(即不重叠的范围)-如果您不能更改订单号格式,那么id格式应该有所不同。

pu3pd22g

pu3pd22g6#

对我来说,你提出的解决方案(1)是最好的方法,那就是:

GET /api/orders/?orderNumber={orderNumber}

为什么我最喜欢它?因为我可以围绕它构建一致的、直观的叙述,而不会破坏REST风格,也不会吸引一些非标准的方法。是不是很理想而且你不能说“但是”?不是。但是它很简单,这本身就是一个价值,并且足够好地解决你提到的问题。
那我建议怎样叙述呢
作为一个基础的先验位置,让我们声明orders具有“order number”属性,其值“是唯一的”,但我们选择将这个属性本身视为API语义的无意义细节(仅在那里,而不是一般情况下-请原谅我)。当纯粹考虑REST API语义时,它只是数据集中值分布的有趣特征,您独立地碰巧知道存在,并且在可预见的未来不会改变,* 但是 * 我们不是直接从API本身获取这些知识。
从API的Angular 来看,orders上有一个 * 特殊的唯一属性 *,您的API * 既 * 保证是唯一的,又表示为 * 键 *,通过它您可以通过其身份 * 获取对象。所有其他字段:

  • 都具有它们不是特殊的唯一属性的性质,
  • 在API的任何给定版本中,所有这些都可以被声明为唯一的,也可以不被声明为唯一的,
  • 原则上,在系统开发的整个生命周期中,可能会来来去去。

换句话说,orderNumber只是当前唯一的字段的一个示例,并且当前您说的是这个属性不会改变。现在或将来可能会有更多这样的领域。
现在,来谈谈你反对这种方法的论点。
首先,这是一个对orders集合的过滤器,因此结果也应该是一个列表。但是,唯一的订单号只有一个订单,这有点令人困惑。
可以这样想-我们总是可以使用一些过滤条件得到一个单一的项目列表,与订单号无关。
这里唯一的特殊情况是当我们通过orderNumber过滤时,同时,我们“想要或需要记住它的唯一性”,无论出于何种原因。
在这种情况下,假设我们“知道”它是唯一的,我们只会像对待任何其他意外响应一样对待列表中包含多个项目的响应,例如:响应有效载荷中的JSON无效,响应中的模式无效等。我将在下一点对此进行详细阐述。
其次,我们使用这样的过滤器来搜索/过滤订单的子集。
很好。正是我们想要的-我们寻找订单的子集,作为额外的洞察力,我们知道如果我们使用orderNumber过滤,我们应该期望这些子集的大小为0或1。任何更大的我们对待就像我们从服务器那里得到了胡言乱语。
具体来说,这种处理方式取决于应用程序,但作为第一近似,我们将这种情况视为具有完全混乱的有效载荷的响应。
只有当我们对如何使用特定错误的信息有了一些实际的了解(列表中的项目太多,而不是,比如说响应中不可解析的JSON),然后我们为此添加一些特殊的逻辑。
您的应用当前是否在API响应中因无效JSON而崩溃?在返回的多个项目上也会崩溃!它在做更聪明的事情吗?在这里做同样的事情。你能从日志中得到一个值吗?比如说,你得到了这个特定的问题吗?实施专用处理代码。
此外,查询参数是某种二级参数,但在本例中应该是一级参数。
我的建议是,从API语义的Angular 来看,不要将orderNumber视为一等公民 *(而不是从其他Angular 来看,例如:面向用户的UI,其中它必须保持其状态),以便简化关于它的推理。
这甚至是一个问题,如果对象不存在。通常一个get会返回一个404(未找到),但是一个GET/api/orders/?如果order 1234不存在,则orderNumber = 1234将是空数组。
到现在这个问题已经变成了完全预期的行为--我们达到了开悟的状态。

相关问题