在RESTful API中,我该如何处理对象层次结构?

76

我目前正在为一款现有的PHP应用程序设计API,为此正在研究REST作为一种合理的架构方法。

我相信我对关键概念有一个合理的掌握,但我很难找到任何处理对象层次结构和REST的人。

以下是问题所在...

在[应用程序]业务对象层次结构中,我们有:

Users 
 L which have one-to-many Channel objects
 L which have one-to-many Member objects
 
在我们的应用程序中,我们采用一种延迟加载的方法,根据需要用对象数组填充用户对象。在面向对象的术语中,我认为这是对象聚合,但我看到了各种不一致性的命名,不想开始关于精确命名约定的争论。

目前,考虑我有一些松散耦合的对象,根据应用程序需要可能会或可能不会进行填充。

从REST的角度来看,我正在尝试确定应该采取什么方法。就目前而言,以下是我的当前想法(仅考虑GET):

选项1-完全填充对象:

GET api.example.com/user/{user_id}

读取用户对象(资源),返回已经预加载和编码(JSON或XML)所有可能的频道和成员对象的用户对象。

优点:减少了对象数量,不需要遍历对象层次结构
缺点:对象必须完全填充(代价高昂)

选项2 - 填充主要对象并包含其他对象资源的链接:

GET api.example.com/user/{user_id}

读取用户对象(资源)并返回已填充用户数据的用户对象和两个列表。

每个列表都引用相应的(子)资源,例如:

api.example.com/channel/{channel_id}
api.example.com/member/{member_id}
    

我认为这很接近(或完全符合)超媒体的含义-客户端可以根据需要获取其他资源(只要我正确标记它们)。

优点:客户端可以选择加载下属资源或者不加载,更好地将对象分离为REST资源。
缺点:需要进一步旅行才能获取次要资源。

选项3-启用递归检索

GET api.example.com/user/{user_id}

读取用户对象并包含指向子对象列表的链接,例如:

api.example.com/user/{user_id}/channels
api.example.com/user/{user_id}/members

/channels调用将以上述形式返回频道资源列表:

api.example.com/channel/{channel_id}
    

优点:主要资源公开了如何获取下属的位置,但不会暴露它们是什么(更符合RESTful?),不需要一开始就获取下属,下属列表生成器(/channels 和 /members)提供了接口(类似方法),使响应更像服务。
缺点:现在需要三次调用才能完全填充对象。

选项4 - 重新考虑REST的对象设计

我正在重复使用[现有]应用程序对象层次结构,并尝试将其应用于REST - 或者更直接地为其提供API接口。

也许REST对象层次结构应该不同,或者新的RESTful思想正在暴露现有对象设计的局限性。

欢迎对以上内容进行思考。


在我的搜索过程中,我还发现了这一系列文章,我觉得非常有用和易于理解:http://www.infoq.com/minibooks/emag-03-2010-rest - paulkmoore
如果您正在寻找一种基于JSON的媒体类型来处理所有这些样式,请考虑Shoji:http://www.aminus.org/rbre/shoji/shoji-draft-02.txt - fumanchu
4个回答

45

毫无理由不将它们结合起来。

  • api.example.com/user/{user_id} - 返回用户表示
  • api.example.com/channel/{channel_id} - 返回频道表示
  • api.example.com/user/{user_id}/channels - 返回频道表示的列表
  • api.example.com/user/{user_id}/channel_list - 返回频道id列表(或使用上述链接链接到其完整表示的链接)

如果你有疑问,请考虑如何在没有“API”方面的情况下向人类用户显示数据:用户想要索引页面({user_id}/channel_list)和完整视图({user_id}/channels)。

一旦你拥有了这个,只需要支持JSON格式而不是(或除了)HTML作为表示格式,你就拥有了REST。


Piet - 非常感谢您,这非常有用。我想澄清关于从人类角度遍历数据的观点。我的想法是,如果我提供“下一个”资源列表而不是“完整视图”,那么我允许用户浏览他们想要浏览的内容,因此提供链接列表符合更好的RESTful(更符合HATEOS)。然而,出于性能考虑,提供填充列表也是有用的。我知道这在某种程度上取决于用例,但是我不确定是否最好暂时保持界面更简单(可能更慢)? - paulkmoore
3
你如何创建这样的层级对象?例如,假设我们有图书馆->书籍->章节->页面。一种方法是创建一个庞大的图书馆对象,但另一种选择是什么?你应该先创建图书馆,获取ID,然后再创建书籍等等。 - Pradeep Kumar Mishra
快速问题:第一个@Path或RequestMapping匹配会获取所有内容吗?如果是这样,第二个API将永远不会被执行,对吗?我应该在我的代码中最后放置更一般的API还是首先放置? - Steve Stilson

30

我能给出的最好建议是尽量避免将你的REST API视为暴露你的对象。你创建的资源应该支持你需要的用例。如果必要,你可以为所有三个选项创建资源:

api.example.com/completeuser/{id}
api.example.com/linkeduser/{id}
api.example.com/lightweightuser/{id}

显然我的名字有点滑稽,但你叫什么并不重要。关键是你要利用REST API以最合理的方式呈现数据,以适应特定的使用场景。如果有多个场景,请创建多个资源。我更喜欢将我的资源看作是UI模型而不是业务实体。


Darrel - 谢谢你提供这个非常有用的信息。我一直怀疑在对象映射方面可能存在一些微妙之处,现在我得出结论,虽然我可以使用现有的对象框架,但我需要仔细设计我的“资源视图”。谢谢。 - paulkmoore

11
我建议使用Restful Objects,这是一种暴露领域模型的RESTful标准。

Restful Objects的理念是为领域对象模型提供一个标准、通用的RESTful接口,使用JSON公开它们的结构表示,并使用HTTP GET、POST、PUT和DELETE与领域对象实例进行交互。

根据标准,URI将会是这样的:

  • api.example.com/object/user/31
  • api.example.com/object/user/31/properties/username
  • api.example.com/object/user/31/collections/channels
  • api.example.com/object/user/31/collections/members
  • api.example.com/object/user/31/actions/someFunction
  • api.example.com/object/user/31/actions/someFunction/invoke

还有其他资源:

  • api.example.com/services
  • api.example.com/domain-types

该规范定义了几个主要表示形式:

  • 对象 (代表任何领域对象或服务)
  • 列表 (链接到其他对象)
  • 属性
  • 集合
  • 操作
  • 操作结果(通常包含一个对象或列表, 或只是反馈信息)
  • 以及一些次要表示形式,如主页和用户

这很有趣,因为你会发现表示形式是完全自我描述的,如果需要,可以实现通用查看器。

或者,也可以直接由定制应用程序消费这些表示形式。


6
这是我经过多小时的搜索并得到回复者的意见后得出的结论:
如果我有一个实际上是多部分对象的对象,我需要将其视为单个资源。因此,如果我获取该对象,则所有下属应该都存在。这是为了使资源可缓存所必需的。如果我部分加载对象(并提供ETag标记),则其他请求者可能会收到部分对象,而不是完整对象。结论 - 如果要将对象作为资源提供,则应完全填充对象。 相关对象关系应作为链接提供给其他(主要)资源。通过这种方式,可以通过遍历API来发现对象。 此外,对于主应用程序站点有意义的对象层次结构可能看起来不符合RESTful方式,但更可能揭示现有层次结构存在问题。尽管如此,API可能需要比先前预期的更专业的用例,并且可能需要专门的资源。
希望能帮助到某些人。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接