社交网络的RESTful设计

16

我正在努力理解RESTful设计,并希望用社交网络的术语来理解它。我已经阅读了 REST API Design Rulebook,查看了其他一些书籍并阅读了许多在线资源。然而,在将规则应用于实际问题时,我意识到我并没有完全理解所有内容。我想通过指定一些问题来确定我是否正确理解了REST:

层次结构与平面设计

假设我用某个特定用户来标识一个用户

/users/42

现在,该用户上传的照片将会出现在......
/users/42/photos

如果他/她在照片中标记了他/她的朋友,这些标记将出现在...
/users/42/photos/1337/tags

但是这种方法无法找到所有标记了特定用户的照片。我应该为此想出一个不同的层次结构吗?这似乎有点尴尬。我可以完全忽略层次结构,并提供与此类查询相结合的照片吗?
/photos?owner=42
/photos?tagged=42

当内容针对不同用户有所不同时的缓存处理

一个Web服务应该被设计成提供可缓存的数据(以便客户端可以决定使用本地副本,如果它认为没有任何更改),但这如何影响不同用户的隐私设置?两个已登录的用户可能拥有查看关于例如用户42的不同信息的权利。这是否意味着我需要为访问同一用户的配置文件信息的不同用户请求不同的URI,或者只要用户提供不同的凭据,缓存就不会成为问题?

同时从同一资源提供HTML和JSON/XML

我提到的书规定API应该在以api开头的子域名下访问,例如http://api.soccer.restapi.org。我原计划在同一个控制器上为用户访问和机器访问(例如移动应用程序)提供服务。控制器将通过HTTP请求标头中的Accept字段决定要提供哪个视图(text/htmlapplication/jsonapplication/xml)。我认为这可能是个坏主意(因为用户希望看到子域名www而不是api),但我不明白为什么会这样。请问wwwapi可以指向同一台服务器吗?或者我真的需要尝试将HTML视图移到另一个虚拟主机上吗?为什么?
我相信Ruby on Rails会(惯例优于配置)从同一个控制器提供HTML和JSON,因此共享了我的想法:HTML和JSON只是相同数据的不同表示形式。
换句话说,我的书指出某一资源应该只有一个URI,并且应根据“Accept”字段提供不同的表示形式。在不同的子域之间重定向用户会违反提供同一资源的规则,而复制信息(即将两个子域指向同一虚拟主机)会违反不为同一资源提供多个URI的规则。不提供“api”子域还违反了另一个设计规则。我如何在不违反任何规则的情况下解决这个问题?
限制返回的数据量
查询组件应用于分页,但是我可以拒绝没有搜索条件的列表请求并限制项目数量吗,而不违反REST?我既想减少数据库负载,又想避免有人映射整个用户目录。我希望…
/users

成为非法请求,同时
/users?name=leet+hacker

这个查询是有效的,但只会返回100个项目。

我也想知道是否可以合法地返回数据库列的子集,仅在使用查询明确请求更多/全部列时才返回它们。

控制器提供冗余数据

我认为提供像这样的控制器是合法的

/users/me

但它是否应该提供与文档URI完全相同的信息呢?

/users/42

还是应该重定向到它吗?

某些用户的扩展权限

提供额外功能的RESTful方法是什么,例如管理权限?我现在假设管理员(照片,用户组或整个站点的管理员)将能够查看有关特定对象的更多信息而其他用户则不能。这些信息是否应保留在完全相同的URI上并自动发送给管理员但不发送给其他任何人,是否应存储在不同位置,是否应使用某种管理员查询请求或以其他方式提供?

本地化和设置更新

虽然用户可见的大部分字符串应该由视图提供,但有些设计决策可能涉及API。最明显的是名称。社交网络有时允许用户输入不同的名称,在不同的语言环境下显示。某些语言中的名称,如俄语和阿拉伯语,不容易自动转录。在其他语言中,例如中文,本地和国际名称可能完全不同,没有任何相似之处或联系。如何以RESTful方式处理这个问题?我觉得答案会涉及到Accept-Language字段,但是有人认真考虑过在所属的社交网络上切换语言吗?所有这些信息每次都需要返回给调用者吗?还是可以依赖于设置?这样做是否适用于缓存?


2
也许将其分解为具体问题会更有用,这样人们遇到特定问题时就可以找到答案。 - Mark Dickinson
1
我曾经考虑过这个问题,但是由于我有很多疑问,我担心我完全误解了某些东西,而分开提问可能无法揭示我的误解。通过将所有内容保持在一起,我希望得到一个连贯的答案。因此,我尝试编写有意义的标题。你还认为我应该把它分开吗?谢谢! - Anders Sjöqvist
很多你的问题都是我们都会面临的选择。阅读《实践中的Rest》可能会帮助你考虑你正在公开的资源。在你的问题中,你问到如何通过标记用户查找照片,可以公开一个资源,photos/taggeduser/id/42。 - Mark Dickinson
1
我真的没有标准答案可以给你,目前我离RESTful还很远,但我认为如果你把问题分开,你会得到更多的赞。另外考虑使用Open Rasta来帮助你解决不同语言资源的问题。最后,我认为查询字符串是不可取的。祝你好运 :) - Mark Dickinson
很棒的问题和帖子,@AndersSjöqvist。顺便说一句,我喜欢看到所有这些问题聚集在一起。 - Aseem Kishore
2个回答

13

正如@Mark Dickinson所提到的,这里有很多问题,实际上应该分开处理,但我会尽力而为。

层次结构 vs. 扁平设计

在REST中并没有规定不能拥有多个并行的层次结构(尽管我理解在Rails中这样做很麻烦)。拥有/users/42/photos/owner/photos/owner/42包含相同集合是可以的。同样,/users/42/photos/tagged/photos/tagged/42也可以包含相同集合。但此时你不必担心URI。有一篇优秀的文章Three Easy Steps创建一个RESTful Hypermedia API描述了如何设计API,其中URI是最后一步决定的。

此外,通过使用HATEOAS约束,这些不同的URI应该通过客户端在运行时通过应用程序提供的链接和表单进行发现。

当内容因用户而异时缓存

如果您将从同一URL提供不同的内容,则无法缓存它。您可以将站点分成两种类型的内容:公共内容和个性化内容。所有人的公共内容应该是相同的,可以缓存。个性化内容对于每个用户都不同,这意味着您可以缓存它的量将大大减少(使用您在示例中使用的URL格式将减少至零)。

要在个性化内容上获得至少一小部分缓存,请通过用户对内容进行分区,以便为给定用户获取一些缓存命中率。例如,不要设置/users/42让所有人都可以访问,而是使用/<UID>/users/42,其中是请求用户的用户ID。例如,用户234将使用URI /234/users/42 访问用户42的个人资料页面。对于匿名用户,您可以删除/<UID部分或使用特定的用户ID,例如/public/users/42

从同一资源提供HTML和JSON/XML

使用Accept标头。这就是它存在的目的。

限制返回的数据

您不需要使/users成为非法请求。将其视为一个集合,并返回请求者有权查看的用户的分页列表。例如,对于匿名请求,您可以提供一个空列表(或204 No Content)。

<users/>

对于特定登录用户,您可能会提供他们的朋友。

<users>
    <user id="42" href="/users/42" name="John Doe"/>
    <user id="53" href="/users/53" name="Jane Doe"/>
    ...
    <next href="/users?page=2"/>
</users>
当用户 GET 请求 /users?page=2 时,您将提供下一页的结果。
<users>
    <user id="69" href="/users/69" name="John Smo"/>
    <user id="84" href="/users/84" name="Jane Smo"/>
    ...
    <next href="/users?page=3"/>
    <prev href="/users"/>
</users>

如果最后一页的结果没有提供next链接。要添加搜索功能,只需要在响应的一部分中添加适当的表单。

<users>
    <user id="69" href="/users/69" name="John Smo"/>
    <user id="84" href="/users/84" name="Jane Smo"/>
    ...
    <next href="/users?page=2"/>
    <prev href="/users"/>
    <search href="/users" method="get">
        <name cardinality="required" type="regex"/>
    </search>
</users>

搜索结果会分页显示,就像/users列表一样。例如,搜索leet hacker(假设您有权限查看系统中的许多leet hacker)将产生类似以下内容:

<users>
    <user id="234" href="/users/234" name="leet hacker"/>
    <user id="999" href="/users/999" name="leet hacker"/>
    ...
    <next href="/users?name=leet+hacker&page=2"/>
    <search href="/users" method="get">
        <name cardinality="required" type="regex"/>
    </search>
</users>

然而,您可能需要在用户元素中提供更多细节,以便区分精通黑客。

控制器提供冗余数据

两者都可以接受。但是如上所述(出于缓存原因),我会使用/<UID>/users/42,在这种情况下,您可能希望将/42/users/me重定向到/42/users/42

这也有助于从分析的角度来看,因为您可能想要单独跟踪用户访问自己的页面与用户访问其他用户页面的情况,以找出哪些用户很受欢迎,哪些用户很自恋。您甚至可能会发现两者之间存在关联 :)

某些用户的扩展权限

在相应的响应中提供管理员链接和表单。例如,访问他人图像的详细信息可能会提供

<image id="266" href="/photos/266" caption="leet hacker with computer"
       src="http://us.123rf.com/400wm/400/400/creatista/creatista0911/creatista091100003/5827629.jpg"/>
    <tagged-users>
        <user id="234" href="/users/234" name="leet hacker"/>
    </tagged-users>
    <owner id="234" href="/users/234" name="leet hacker"/>
</image>

但对于图片所有者来说,它可能会提供

<image id="266" href="/photos/266" caption="leet hacker with computer"
       src="http://us.123rf.com/400wm/400/400/creatista/creatista0911/creatista091100003/5827629.jpg"/>
    <tagged-users>
        <tagged-user id="234" href="/users/234" name="leet hacker">
            <delete href="/photos/266/tagged/234" method="delete"/> 
        </tagged-user>
        <add href="/photos/266" method="put">
            <user cardinality="required" type="user-id"/>
        </add>
    </tagged-users>
    <owner id="234" href="/users/234" name="leet hacker"/>
    <delete href="/photos/266" method="delete"/>
    <update href="/photos/266" method="put">
        <caption cardinality="optional" type="string"/>
    </update>
</image>

本地化和设置更新

使用Accept-Language作为默认语言,但允许用户在其设置中更改语言。


2
哇,这是一个很棒的答案!非常感谢您花费的时间!它几乎涵盖了我所需要的一切。它也符合我的想法,我开始明白REST并不总是能直接回答设计问题。 - Anders Sjöqvist

0
如果您正在使用WCF Web Api(即使您不是),也许值得您查看其中的OData支持。它确实解决了一些使事物更易搜索的问题。

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