HAL - 如果链接在主体中,它是否违反HAL格式/标准?

时间:2017-06-22 15:07:53

标签: json rest hateoas hypermedia discoverability

根据 HAL标准(请参阅herehere),其他资源的链接应放在特定的嵌入式部分中。

所以例如这不是有效的HAL,我的理解是否正确?

{
  "movies": [
     {
       "id": "123",
       "title": "Movie title 1",
       "_links": {
           "subtitles": {
              "href": "/movies/123/subtitles"
           }
        }
     },{
       "id": "456",
       "title": "Movie title 2",
       "_links": {
           "subtitles": {
              "href": "/movies/456/subtitles"
           }
        }
     }
  ],
  "_links": {
      "self": {
         "href": "/movies"
      }
   }
}

上述JSON无效HAL的原因是链接应放置在链接到主要ID中的嵌入式部分(" _embedded" )中身体。所以正确的方法是:

   {
      "movies": [
         {
           "id": "123",
           "title": "Movie title 1",
         },{
           "id": "456",
           "title": "Movie title 2",
         }
      ],
      "_embedded": {
          "movies": [
             {
               "id": "123",
               "_links": {
                 "href": "movies/123/subtitles"
               }
             },
             {
               "id": "456",
               "_links": {
                 "href": "movies/456/subtitles"
               }
             }
          ]
      }
      "_links": {
          "self": {
             "href": "/movies"
          }
       }
    }

以上都是正确的吗?

由于

3 个答案:

答案 0 :(得分:3)

将这用作关于用hal重新设计的案例研究。 @darrel millers回答很好,但不是很好,有一些我认为应该清理的东西。这将是LONG。

最大的问题是什么是上下文... IE您返回的资源是什么。所有你得到的东西都是某种与电影有关的东西。就像假设这是一个搜索结果一样......拥有电影关系是错误的方法......因为电影与搜索结果相关"顶级资源"作为一个项目。所以它应该更像是

{
   title : "Results for search XXXXX"
   _links : {
      self : { href: "https://host.com/search/with/params/XXXXX"},
      item : [
         { href : "https://host.com/url/to/movie/result/one", title : "A great Movie"},
         { href : "https://host.com/url/to/movie/result/two",  title : "A Terrible Movie"},
      ]

   }  
}

但是这个结构对于客户端构建UI来说是昂贵的,因为它必须进行3次调用。遵循N + 1规则(结果集为1,每个结果为N)因此生成_embedded只是hal实现超文本预取模式(在http2中服务器实际上可以发送每个结果作为它自己的文档,客户端的缓存将预先填充这些结果,你不会& #39; t必然需要_embedded)。该结构看起来更像是这样:

{
   title : "Results for search XXXXX"
   _links : {
      self : { href: "https://host.com/search/with/params/XXXXX"},
      item : [
         { href : "https://host.com/url/to/movie/result/one", title : "A great Movie"},
         { href : "https://host.com/url/to/movie/result/two",  title : "A Terrible Movie"},
      ]

   },
   _embedded : {
     item : [
       {
         _links : {
           profile : {href : "https://host.com/result-movie"},
           canonical : {href : "https://host.com/url/to/movie/result/one"}
         },
         title : "a great movie",
         rating : "PG",

       },
       {
         _links : {
           profile : {href : "https://host.com/result-movie"},
           canonical : {href : "https://host.com/url/to/movie/result/two"}
         },
         title : "a terrbile movie"
         rating : "G",
       }
     ]
   }

}

获得3个资源的1个http请求非常棒。 1请求不是N + 1。谢谢你!

那么为什么是项目?搜索结果是否只包含电影......这种情况非常不可能......即使它今天也存在......你是否希望它明天只包含电影...它非常狭窄而这个结构是你必须永远维持的契约。但是您的UI确实希望将结果显示为电影。我已经添加的配置文件链接是...客户端使用配置文件链接来了解它当前正在处理的资源是什么......以及它可以用来构建UI的字段。处理集合时,一个体面的客户端会显示它可以使用的配置文件......并且只是忽略了它可以包含的内容(可能记录警告)。由客户端开发人员升级他们的应用程序以支持新的配置文件...不相信我?想一想网页浏览器如何解析它在html中无法理解的标签......在你的html文档中放置一个<thing-not-invented-yet></think-not-invented-yet>,看看一个好的客户端是如何工作的。

另外你应该注意的是我不使用自我链接但是规范......多年来我改变了我的立场。最近我默认为规范,我只使用自我来维护目标资源的版本,并且嵌入对象链接到嵌入的特定版本很重要。这在我的经历中非常罕见。我告诉客户跟随自己它的存在,在导航到嵌入项目时遵循规范。这使服务器可以完全控制它想要占用客户端的位置。顶级资源,在这种情况下结果应该仍然有一个自我......在这种情况下它是有道理的,因为soem随机搜索可能没有规范链接...除非它是一个非常常见的关键字搜索。 ..然后它可能应该像其他用户一样使用相同的URL。

让我们花点时间谈谈为什么item作为rel ...因为这非常重要。由于它是搜索结果..为什么rel不是result。答案非常简单.. result不是IANA链接注册管理机构https://www.iana.org/assignments/link-relations/link-relations.xhtml的成员,因此result完全无效......现在您可以&#34;命名空间& #34;您的扩展名为my:resultour:result(&#34;名称空间&#34;取决于您,这些只是示例)但如果IANA中已经存在一个非常好的扩展,为什么还要烦恼呢?注册表..它确实item

让我们谈谈items vs item(或x:movies vs x:movie)。好吧items在IANA也不是。所以它必须是x:items但是不要这样做,让我们考虑一下原因。如果我们的结果文档用HTML表示,它看起来像这样(忽略我遗漏的身体头部等,为了简洁而不是格式良好):

<html>
  <title>Results for search XXXXX</title>
  <a rel="item" href="https://host.com/url/to/movie/result/one" >A Great Movie</a>
  <a rel="item" href="https://host.com/url/to/movie/result/two" >A Great Movie</a>
</html>

这是第一个示例的SAME资源(没有嵌入子资源)。仅表示为text/html而不是application/hal+json。如果我在这里失去了你(这是大多数人真的很困惑的地方,我能提供的最好的就是在https://www.youtube.com/watch?v=u_pZBBELeEQ观看我的谈话)听听它清楚每个人的适当关系目标资源是单个项目而不是一组ITEMS。每个链接都针对一个项目(或一个单一的电影)。

有一个HAL的陷阱可以像JSON那样对待它,这会导致movies机器可读或更好的注释之类的语句。让我通过在用例中继续使用HTML表示来解释这是如何产生的。  当客户解析此文档以查找item个链接时,它必须解析每个a标记,并向下过滤到仅存在rel="item"属性的标记。这是一个&#34;全表扫描&#34; ..我们如何摆脱这些?我们创建一个索引。 JSON具有内置于其结构中的索引的概念。它是具有数组值的键。 index : [ {entry 1}, {entry 2} ]。 HAL的作者知道检索链接的最常见方式(在_links或_embedded中预取的链接)将是关系的......所以他构建了他的规范,使得rel被索引。所以当你看到:

   _links : {
      self : { href: "https://host.com/search/with/params/XXXXX"},
      item : [
         { href : "https://host.com/url/to/movie/result/one", title : "A great Movie"},
         { href : "https://host.com/url/to/movie/result/two",  title : "A Terrible Movie"},
      ]

   },

知道它真的是

   _links : {
      self : { rel: "self", href: "https://host.com/search/with/params/XXXXX"},
      item : [
         { rel:"item", href : "https://host.com/url/to/movie/result/one", title : "A great Movie"},
         { rel:"item", href : "https://host.com/url/to/movie/result/two",  title : "A Terrible Movie"},
      ]

   },

因为rel是LINK OBJECT而不是RESOURCE的属性。但是http上的字节是昂贵的(gzip会摆脱这个)并且开发人员不喜欢冗余(整个其他主题)所以当我们有hal时我们OMIT rel属性,因为HAL结构已经使rel显而易见。虽然当你的解析器遇到这个时,它并不是很明显:

{ href : "https://host.com/url/to/movie/result/one", title : "A great Movie"}
什么是rel?你必须从父节点传递它...这总是丑陋...反正所有这一切都表明在HAL中通常消除了冗余。一旦消除了这种冗余,就很有可能将该索引键改为复数形式items,但是知道这意味着你说你的链接(一旦裁员后退了)将是{rel: "items", href : "https://host.com/url/to/movie/result/one", title : "A great Movie"}并且这显然是错误的..这​​个链接不是很多项目......只有一个。

因此,在这种情况下删除冗余可能并不是最好的..但它带来好处的恶,HAL遵循_links和_embedded的模式,这就是我们要做的事情。使用我们的搜索结果...发送所有item链接都没有预先获取并且作为_embedded存在它不重要将它们保存在_links中。因此它应该是这样的:

{
   title : "Results for search XXXXX"
   _links : {
      self : { href: "https://host.com/search/with/params/XXXXX"}
   },
   _embedded : {
     item : [
       {
         _links : {
           profile : {href : "https://host.com/result-movie"},
           canonical : {href : "https://host.com/url/to/movie/result/one"}
         },
         title : "a great movie",
         rating : "PG",

       },
       {
         _links : {
           profile : {href : "https://host.com/result-movie"},
           canonical : {href : "https://host.com/url/to/movie/result/two"}
         },
         title : "a terrbile movie"
         rating : "G",
       }
     ]
   }

}

现在我们有一个非常好的搜索结果,其中包括2部电影(未来可以包含更多内容而不违反合同)。注意:如果你曾经使用JUST _links而没有_embedded ...你不能删除_links,因为有些客户端依赖于它们存在...所以最好早点想到这些东西.. 。当使用资源的HAL表示时,一个表现良好的客户端应该始终在_links之前检查_embedded ...所以你真的要知道你的客户是否表现良好。

好的,让我们转到x:movie是正确关系的情况......如果顶级资源是一个演员,那可能会很好。所以像:

{
   Name : "Paul Bettany"
   _links : {
      canonical : { href: "https://host.com/paul-bettany"},
      "x:movie" : [
         { href : "https://host.com/url/to/movie/result/one", title : "A great Movie"},
         { href : "https://host.com/url/to/movie/result/two",  title : "A Terrible Movie"},
      ],
      "x:spouse" : { href: "", title: "Jennifer Connely"}
   },
   _embedded : {
     "x:movie" : [
       {
         _links : {
           profile : {href : "https://host.com/result-movie"},
           canonical : {href : "https://host.com/url/to/movie/result/one"}
         },
         title : "a great movie",
         rating : "PG",

       },
       {
         _links : {
           profile : {href : "https://host.com/result-movie"},
           canonical : {href : "https://host.com/url/to/movie/result/two"}
         },
         title : "a terrbile movie"
         rating : "G",
       }
     ]
   }

}

注意:我在顶层使用了canoncial而不是self,因为actor是长寿资源......演员将永远存在......并且演员没有版本化。为了完整性,我在_links和_embedded中留下了x:movie,但实际上我没有_item中的那些。我还将它们保存在_links中以显示x:movie的原因,以便您可以将它与x:spouse区分开来(在我们开始的搜索结果案例中,语义差异没有意义)。最后,我注意到我嵌入了x:movie但不是x:spouse这很有用,这只是为了说明它不是一个或两个东西。您可以预先获取/嵌入您的用例所需的链接。事实上,我经常根据客户的身份嵌入东西。我知道iOS可以显示android无法显示的内容。

除了这些注释之外,我去这里的原因是我想明确表示你没有并且不应该拥有那些电影:你拥有的数据字段......只需依靠_embedded中的电影数据。你说soemthign喜欢将电影中的值与_links或_embedded中的值相匹配......你不应该这样做......这没有任何意义。电影是一种资源......使用电影的链接资源而不是某些数据字段。您需要尽早决定什么是资源以及什么是数据。我最好的建议是,如果一个东西有链接关系......那么它就是一个资源。在我的演讲中,我将通过更广泛的术语(超媒体控件)进一步了解更多细节,我不想进入这里。

最后一点注意事项..在超媒体应用程序中,如果你暴露内部id字段,你知道你做错了什么......就像你在这里做的那样。这应该是一个巨大的红旗,有些事情是错的。您所描述的id的用例是将数据字段电影与_embedded x:movie匹配。如上所述......你不应该这样做......并且id字段的存在应该会让你接受这种糟糕的做法。

我被要求在这里回答。所以我希望这有帮助。

答案 1 :(得分:1)

&#34; _Links&#34;属性必须位于资源对象的根目录中。 资源对象可能位于根目录,也可能位于 _embedded对象中。

我怀疑有些令人困惑的事情来自于#34; _embedded&#34;关键指向一个数组。仅当您要表示相关资源的多个实例时才会执行此操作。

在示例中,密钥为movies,推断您正在嵌入表示多个电影的资源对象。但是,该数组表示存在多个嵌入式资源对象。每个资源对象都是一部电影。

通过将键名更改为&#34; movie&#34;你明白了:

   {
      "movies": [
         {
           "id": "123",
           "title": "Movie title 1",
         },{
           "id": "456",
           "title": "Movie title 2",
         }
      ],
      "_embedded": {
          "movie": [
             {
               "id": "123",
               "_links": {
                 "href": "movies/123/subtitles"
               }
             },
             {
               "id": "456",
               "_links": {
                 "href": "movies/456/subtitles"
               }
             }
          ]
      }
      "_links": {
        "self": "https://example.org/movielist"
       }
    }

所以,现在你有了一个&#34;电影列表&#34;资源对象,你已经嵌入了一堆电影&#34; &#34; movie-list&#34;中每个项目的资源对象。每个资源对象由&#34;电影&#34;有一个&#34; _links&#34;财产相关信息。我假设&#34;字幕&#34;链接应该是一个自我链接。

答案 2 :(得分:0)

在您发布的specs中可以观察到,您可以拥有链接和/或嵌入资源:

enter image description here

资源的链接应该作为该资源的属性:

{
  "movies": [
     {
       "id": "123",
       "title": "Movie title 1",
       "_links": {
           "subtitles": {
              "href": "/movies/123/subtitles"
           }
        }
     }, {
       "id": "456",
       "title": "Movie title 2",
       "_links": {
           "subtitles": {
              "href": "/movies/456/subtitles"
           }
        }
     }
  ]
}

替代将直接嵌入电影的字幕资源:

{
    "movies" : [
       {
         "id" : "123",
         "title" : "Movie title 1",
         "_embedded" : {
            "subtitles" : [{
                "name" : "movie 1 subtitles"
                }
            ]
          }
        }, {
         "id" : "456",
         "title" : "Movie title 2",
         "_embedded" : {
            "subtitles" : [{
                "name" : "movie 2 subtitles"
                }
            ]
          }
        }
    ]
}

enter image description here