HATEOASディープリンクの問題

外部リンク(ディープリンク)-インターネットでは、これは、そのサイトのホーム(ホーム、開始)ページを指すのではなく、別のWebサイトのページを指すサイトにハイパーリンクを配置することです。 このようなリンクは、外部リンク(ディープリンク)と呼ばれます。

ウィキペディア
「ディープリンク」という用語は、英語の「ディープリンク」に最も近いものとして以下で使用されます。 この記事ではREST APIに焦点を当てるので、ディープリンクはHTTPリソースへのリンクを意味します。 たとえば、ディープリンクhabr.com/en/post/426691は 、habr.comの特定の記事を指します。



HATEOASはRESTアーキテクチャのコンポーネントであり、ハイパーメディアを介してクライアントにAPI情報を提供できます。 クライアントは、APIエントリポイントという唯一の固定アドレスを知っています。 サーバーから受信したリソースから、考えられるすべてのアクションを学習します。 リソースビューには、アクションまたは他のリソースへのリンクが含まれています。 クライアントはAPIと対話し、利用可能なリンクからアクションを動的に選択します。 HATEOASの詳細については、 ウィキペディアまたはHabréのこの素晴らしい記事をご覧ください。



HATEOASはREST APIの次のレベルです。 ハイパーメディアの使用のおかげで、彼はAPIの開発中に発生する多くの質問に答えています:サーバー側のアクションへのアクセスを制御する方法、クライアントとサーバー間の緊密な接続を取り除く方法、必要に応じてリソースのアドレスを変更する方法。 しかし、リソースへのディープリンクがどのように見えるかという質問には答えません。



「クラシック」REST実装では、クライアントはアドレス構造を知っており、REST APIで識別子によってリソースを取得する方法を知っています。 たとえば、ユーザーはオンラインストアの書籍ページへのディープリンクをたどります。 URLバーhttps://domain.test/books/1



ブラウザーのアドレスバーに表示されます。 クライアントは、「1」が書籍リソースの識別子であることを知っています。これを取得するには、REST API URL https://api.domain.test/api/books/{id}



この識別子を置き換える必要があります。 したがって、REST APIでのこの本のリソースへのディープリンクは、 https://api.domain.test/api/books/1



ようにhttps://api.domain.test/api/books/1



ます。



HATEOASでは、クライアントはリソース識別子またはアドレス構造を認識しません。 彼はハードコーディングせず、リンクを「発見」します。 さらに、URLの構造はクライアントの知識なしに変更される可能性があるため、HATEOASで許可されています。 これらの違いのため、ディープリンクは従来のREST APIと同じ方法で実装することはできません。 驚くべきことに、HATEOASでこのようなリンクを実装するためのレシピをインターネットで検索しても、多数の結果は得られませんでした。 したがって、いくつかの可能なオプションを検討し、最適なものを選択しようとします。



競合以外のゼロオプションは、ディープリンクを実装しないことです。 これは、内部リソースに直接切り替える機能を必要としない管理者やモバイルアプリケーションに適している場合があります。 クライアントは内部リソースに直接アクセスする方法を知らないため、これは完全にHATEOASの精神に沿ったものであり、ユーザーはエントリポイントから順番にのみページを開くことができます。 ただし、このオプションはWebアプリケーションには適していません。内部ページへのリンクにブックマークを付けることができ、ページを更新してもサイトのメインページに戻ることはありません。



したがって、最初のオプション:HATEOAS API URLハードコード。 クライアントは、ディープリンクが必要なリソースアドレスの構造を知っており、ルックアップのリソース識別子を取得する方法を知っています。 たとえば、サーバーはアドレスhttps://api.domain.test/api/books/1



を書籍リソースへの参照として返します。 クライアントは「1」が書籍の識別子であることを認識しており、ディープリンクをクリックすると、このURLを個別に生成できます。 これは確かに有効なオプションですが、HATEOASの原則に違反しています。 アドレス構造とリソース識別子は変更できなくなります。変更しないと、クライアントが破損し、強固な接続が確立されます。 これはHATEOASではありません。つまり、このオプションは私たちには適していないということです。



2番目のオプションは、クライアントURLでREST API URLを置き換えることです。 本の例の場合、ディープリンクはhttps://domain.test/books?url=https://api.domain.test/api/books/1



ます。 ここでは、クライアントはサーバーから受信したリソースリンクを取得し、ページアドレスで完全に置き換えます。 これはHATEOASに似ており、クライアントは識別子とアドレスの構造を知らず、リンクを受信して​​そのまま使用します。 このような詳細なリンクをクリックすると、クライアントはurlパラメーターからREST APIリンクを介して目的のリソースを受け取ります。 解決策は機能しており、HATEOASの精神に沿っているように思われます。 ただし、そのようなリンクをブックマークに追加すると、今後APIでリソースのアドレスを変更できなくなります(または、常に新しいアドレスにリダイレクトする必要があります)。 もう一度、HATEOASの利点の1つが失われるため、このオプションも理想的ではありません。



したがって、パーマリンクが必要ですが、パーマリンクは変更される可能性があります。 このようなソリューションが存在し、インターネット上で広く使用されています-多くのサイトは、共有可能な内部ページへの短いリンクを提供しています。 簡潔さに加えて、サイトの利点はページの実際のアドレスを変更できることですが、そのようなリンクは壊れません。 たとえば、MicrosoftはWindowsリンクを使用して、 http://go.microsoft.com/fwlink/?LinkId=XXX



の形式のページを支援します。 長年にわたり、Microsoftサイトは何度か再設計されましたが、Windowsの古いバージョンのリンクは引き続き機能します。



このソリューションをHATEOASに適合させるだけです。 これが3番目のオプションです-REST APIで一意のディープリンク識別子を使用します。 これで、本のあるページのアドレスはhttps://domain.test/books?deepLinkId=3f0fd552-e564-42ed-86b6-a8e3055e2763



ます。 このような詳細なリンクをクリックすると、クライアントはサーバーに問い合わせる必要があります。どのリソースリンクがそのようなdeepLinkId



識別子に対応するのでしょうか。 サーバーはリンクhttps://api.domain.test/api/books/1



を返しhttps://api.domain.test/api/books/1



(まあ、またはすぐにリソースであるため、2回行かないようにします)。 REST APIのリソースアドレスが変更された場合、サーバーは単に別のリンクを返します。 参照識別子3f0fd552-e564-42ed-86b6-a8e3055e2763が書籍1のエンティティ識別子に対応するというレコードがデータベースに保存されます。



このため、リソースにはディープリンクの識別子を含むdeepLinkId



フィールドが含まれている必要があり、クライアントはそれらをページのアドレスで置き換える必要があります。 このアドレスを安全にブックマークして、友人に送信できます。 クライアントが特定の識別子と独立して機能することはあまり良くありませんが、これによりAPI全体のHATEOASの利点を維持することができます。





この記事は、実装例がなければ完成しません。 概念をテストするために、Spring Boot / KotlinのバックエンドとVue / JavaScriptのSPAフロントエンドを備えた架空のオンラインストアカタログサイトの例を考えてみましょう。 このストアでは本と鉛筆を販売していますが、サイトには2つのセクションがあり、製品のリストを確認してページを開くことができます。



セクション「書籍」:







1冊の本のページ:







商品の保管のために、Spring Data JPAエンティティが定義されています:



 enum class EntityType { PEN, BOOK } @Entity class Pen(val color: String) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() @OneToOne(cascade = [CascadeType.ALL]) val deepLink: DeepLink = DeepLink(EntityType.PEN, id) } @Entity class Book(val name: String) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() @OneToOne(cascade = [CascadeType.ALL]) val deepLink: DeepLink = DeepLink(EntityType.BOOK, id) } @Entity class DeepLink( @Enumerated(EnumType.STRING) val entityType: EntityType, @Column(columnDefinition = "uuid") val entityId: UUID ) { @Id @Column(columnDefinition = "uuid") val id: UUID = UUID.randomUUID() }
      
      





ディープリンクDeepLink



を作成して保存するには、 DeepLink



エンティティをDeepLink



ます。そのインスタンスは各ドメインオブジェクトで作成されます。 識別子自体は、エンティティが作成されたときのUUID標準に従って生成されます。 そのテーブルには、ディープリンクの識別子、リンク先のエンティティの識別子とタイプが含まれます。



サーバーのREST APIはHATEOASの概念に従って編成されています。APIエントリポイントには、製品コレクションへのリンクと、識別子を置き換えることでディープリンクを形成する#deepLink



リンクが含まれています。



 GET http://localhost:8080/api { "_links": { "pens": { "href": "http://localhost:8080/api/pens" }, "books": { "href": "http://localhost:8080/api/books" }, "deepLink": { "href": "http://localhost:8080/api/links/{id}", "templated": true } } }
      
      





クライアントは、「Books」セクションを開くときに、エントリポイントの#books



リンクでリソースのコレクションを要求します。



 GET http://localhost:8080/api/books ... { "name": "Harry Potter", "deepLinkId": "4bda3c65-e5f7-4e9b-a8ec-42d16488276f", "_links": { "self": { "href": "http://localhost:8080/api/books/1272e287-07a5-4ebc-9170-2588b9cf4e20" } } }, { "name": "Cryptonomicon", "deepLinkId": "a23d92c2-0b7f-48d5-88bc-18f45df02345", "_links": { "self": { "href": "http://localhost:8080/api/books/5d04a6d0-5bbc-463e-a951-a9ff8405cc70" } } } ...
      
      





SPAはVueルーターを使用します。ブックページ{ path: '/books/:deepLinkId', name: 'book', component: Book, props: true }



{ path: '/books/:deepLinkId', name: 'book', component: Book, props: true }



で定義され、ブックリストのリンクは次のようになります: <router-link :to="{name: 'book', params: {link: book._links.self.href, deepLinkId: book.deepLinkId}}">{{ book.name }}</router-link>







つまり、特定の本のページを開くと、 Book



コンポーネントが呼び出され、2つのパラメーターを受け取ります: link



(REST APIの本リソースへのリンク、 #self



リンクのhref



フィールドの値)およびリソースからのdeepLinkId



)。



 const Book = { template: `<div>{{ 'Book: ' + book.name }}</div>`, props: { link: null, deepLinkId: null }, data() { return { book: { name: "" } } }, mounted() { let url = this.link == null ? '/api/links/' + this.deepLinkId : this.link; fetch(url).then((response) => { return response.json().then((json) => { this.book = json }) }) } }
      
      





Vueルーターは、 deepLinkId



の値をpage /books/:deepLinkId



のアドレスに設定し、コンポーネントはlink



プロパティからの直接リンクによってリソースを要求します。 ページが強制的に更新されると、Vue RouterはコンポーネントプロパティdeepLinkId



設定し、ページアドレスから取得します。 link



プロパティはnull



ままです。 コンポーネントがチェックします。コレクションから取得した直接リンクがある場合、リソースが要求されます。 deepLinkId



識別子のみがdeepLinkId



場合、エントリポイントから#deepLink



リンクに置き換えられ、ディープリンクによってリソースを取得します。



バックエンドでは、ディープリンクのコントローラーメソッドは次のようになります。



 @GetMapping("/links/{id}") fun deepLink(@PathVariable id: UUID?, response: HttpServletResponse?): ResponseEntity<Any> { id!!; response!! val deepLink = deepLinkRepo.getOne(id) val path: String = when (deepLink.entityType) { EntityType.PEN -> linkTo(methodOn(MainController::class.java).getPen(deepLink.entityId)) EntityType.BOOK -> linkTo(methodOn(MainController::class.java).getBook(deepLink.entityId)) }.toUri().path response.sendRedirect(path) return ResponseEntity.notFound().build() }
      
      





識別子によって、ディープリンクの本質が決まります。 アプリケーションエンティティのタイプに応じて、コントローラメソッドへのリンクが形成され、エンティティメソッドによってリソースが返されentityId



。 要求はこのアドレスにリダイレクトされます。 したがって、将来エンティティコントローラーへのリンクが変更された場合、 deepLink



メソッドでリンク形成のロジックを単純に変更することができます。



サンプルの完全なソースコードはGithubで入手できます。



All Articles