HAL - 如果链接在主体中,是否违反了 HAL format/standard?

HAL - is it a violation to the HAL format/standard if links are in the main body?

根据 HAL 标准(参见 here and here),指向其他资源的链接应放在特定的嵌入部分。

例如,这不是有效的 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在主体中。所以正确的方法是:

   {
      "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"
          }
       }
    }

以上都正确吗?

谢谢

正如在您发布的 specs 中所观察到的,您可以链接 and/or 嵌入式资源:

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

{
  "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"
                }
            ]
          }
        }
    ]
}

“_Links”属性 必须位于 资源对象 的根目录中。 资源对象可能在根目录中,也可能在_embedded对象.

我怀疑一些令人困惑的地方来自于将“_embedded”键指向数组。仅当您想要表示相关资源的多个实例时才这样做。

在示例中,键是 movies,这表明您正在嵌入代表多部电影的资源对象。但是,该数组表示有多个嵌入的资源对象。每个资源对象是一部电影。

通过将密钥名称更改为 "movie",您将得到:

   {
      "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"
       }
    }

所以,现在您有一个 "movie-list" 资源对象的表示,并且您已经为 "movie-list" 中的每个项目嵌入了一堆 "movie" 资源对象。 "movie"指向的每个资源对象都有一个“_links”属性相关信息。我假设 "subtitles" link 应该是一个自我 link.

打算将其用作使用 hal 重新设计的案例研究。 @darrel millers 的回答很好,但不是很好,有些事情我认为应该澄清。这会很长。

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

{
   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 中,服务器实际上可以将每个结果作为它自己的文档发送,并且客户端的缓存将 pre-filled 包含这些结果,你会' 一定需要 _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 真的很想将结果显示为电影。我添加的配置文件 link 是为了......客户端使用配置文件 link 来了解它当前正在处理的资源是什么......以及它可以使用哪些字段来构建 UI。一个体面的客户端在处理 collection 时会显示它可以显示的配置文件......而只是忽略它不能显示的配置文件(可能会记录警告)。由客户端开发人员升级他们的应用程序以支持新配置文件……不相信我吗?想一想 Web 浏览器如何解析它在 html 中不理解的标签...在您的 html 文档中添加一个 <thing-not-invented-yet></think-not-invented-yet> 并查看一个好的客户端如何工作。

你应该注意的另一件事是我不使用 self links 但规范..多年来我改变了我对此的立场。最近我默认使用 canonical 并且我只在维护目标资源的版本时使用 self 并且将嵌入的 object link 嵌入到特定版本很重要。这在我的经验中是非常罕见的。我告诉客户在它存在时跟随自己,否则在导航到嵌入项目时遵循规范。这使服务器可以完全控制它想要将客户端带到哪里。顶级资源,在这种情况下,结果仍然应该有一个自我......在这种情况下,它是有意义的,因为 soem 随机搜索可能没有规范 link......除非它是一个非常常见的关键字搜索...那么它可能应该因为其他用户可以使用相同的 url.

让我们花点时间谈谈为什么 item 作为 rel...因为这真的很重要。既然是搜索结果..为什么不让 rel 为 result。答案很简单。result 不是 IANA link 注册表的成员 https://www.iana.org/assignments/link-relations/link-relations.xhtml,因此 result 完全无效...现在您可以 "namespace" 您的扩展名与 my:resultour:result 相关("namespace" 由您决定,这些只是示例)但是如果 IANA 注册表中已经存在一个非常好的扩展名,为什么还要费心呢。 .而且确实 item

我们来谈谈 itemsitem(或 x:moviesx:movie)。好吧 items 也不在 IANA 中..所以它必须是 x:items 但是让我们想想为什么不这样做。如果我们的结果文档以 HTML 表示,它看起来像这样(为简洁起见,忽略我丢失的 body 头部等而不是 well-formedness):

<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>

这是与第一个示例相同的资源(没有嵌入子资源)。仅表示为 text/html 而不是 application/hal+json。如果我在这里迷失了你(这是大多数人真正感到困惑的地方,我能提供的最好的办法就是在 https://www.youtube.com/watch?v=u_pZBBELeEQ 上观看我对此的演讲)很明显每个目标资源的适当关系是一个单一的项目而不是一组 ITEMS。每个 link 都针对一个项目(或一部单部电影)。

HAL 有一个陷阱,将其视为 JSON,这会导致像 movies 机器可读或更好的评论那样的陈述。让我继续用例中的 HTML 表示来解释这是如何发生的。 当客户端解析此文档以查找 item link 时,它必须解析每个 a 标记并过滤到仅存在 rel="item" 属性的标记。那是 "full table scan"..我们如何摆脱这些?我们创建一个索引。 JSON 的结构中内置了索引的概念。它是一个带有数组值的键。 index : [ {entry 1}, {entry 2} ]。 HAL 的作者知道检索 links(在 _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 的属性而不是资源。但是 http 上的字节很昂贵(gzip 会摆脱这个)并且开发人员不喜欢冗余(另一个主题)所以当我们有 hal 时我们省略 rel 属性,因为 HAL 结构已经使 rel apparent.虽然当你的解析器遇到这个时它并不是真正的 apparent:

{ href : "https://host.com/url/to/movie/result/one", title : "A great Movie"}

什么关系?您必须从 parent 节点传递它。这一直很丑陋...无论如何,所有这些都是为了表明 HAL 中通常消除了冗余。一旦消除了这种冗余,很容易将该索引键更改为复数形式 items 但要知道这意味着您说的是 link (一旦冗余被放回)将是 {rel: "items", href : "https://host.com/url/to/movie/result/one", title : "A great Movie"} 并且这显然是错误的..link 不是很多项目...只有一个。

所以在这种情况下删除冗余可能不是最好的..但它是有好处的,HAL 遵循 _links 和 _embedded 的模式,这就是我们要在搜索中做的事情结果..鉴于所有 item links 都没有 pre-fetched 并且作为 _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 是 long-lived 资源..那个 actor 将永远存在..并且 actor 没有版本。为了完整起见,我在 _links 和 _embedded 中都保留了 x:movie,但实际上我不会在 _item 中保留它们。我还将它们保留在 _links 中以显示 x:movie 的原因,以便您可以将其与 x:spouse 区分开来(在我们搜索结果的情况下,语义差异没有意义开始于)。最后,值得注意的是我嵌入了 x:movie 而不是 x:spouse 这只是为了说明它不是非此即彼的事情。您可以 pre-fetch/embed 您的用例所需的 link。事实上,我经常根据客户的身份嵌入内容。也就是说,我知道 iOS 可以显示 android 不能显示的内容。

抛开那些注释,我来这里的原因是我想明确表示您没有也不应该拥有该电影:您拥有的数据字段...仅依赖于 _embedded 中的电影数据。你说 soemthign 就像将电影中的值与 _links 或 _embedded 中的值相匹配......你不应该那样做......那没有任何意义。电影是一种资源...使用电影的 linked 资源而不是某些数据字段。您需要尽早决定什么是资源,什么是数据。我最好的提示是,如果某事物具有 link 关系..那么它就是一种资源。在我的演讲中,我使用更广泛的术语(超媒体控件)对此进行了更多详细介绍,但我不想在这里讨论。

最后一点..在超媒体应用程序中,如果您公开内部 ID 字段,您知道自己做错了什么..正如您在此处所做的那样。这应该是一个巨大的危险信号,表明出现了问题。您描述的 id 的用例是将数据字段电影与 _embedded x:movie 匹配。如前所述......你不应该这样做......并且 id 字段的存在应该让你陷入这种不良做法。

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