web-dev-qa-db-ja.com

ServiceStack:RESTfulリソースのバージョン管理

メッセージベースのWebサービスの利点 の記事を読みましたが、ServiceStackでRestfulリソースをバージョン管理するための推奨されるスタイル/プラクティスがあるかどうか疑問に思っていますか?異なるバージョンは、異なる応答をレンダリングしたり、要求DTOで異なる入力パラメーターを持つ可能性があります。

私はURLタイプのバージョン管理(つまり/ v1/movies/{Id})に傾倒していますが、HTTPヘッダーでバージョンを設定する他のプラクティス(つまりContent-Type:application/vnd.company.myapp-v2)を見てきました。 )。

メタデータページで機能する方法を望んでいますが、ルートをレンダリングするときにフォルダ構造/名前空間を使用するだけで問題なく機能することに気付いたので、それほど要件はありません。

たとえば(これはメタデータページでは正しくレンダリングされませんが、直接ルート/ URLがわかっている場合は正しく実行されます)

  • / v1/movies/{id}
  • /v1.1/movies/ {id}

コード

namespace Samples.Movies.Operations.v1_1
{
    [Route("/v1.1/Movies", "GET")]
    public class Movies
    {
       ...
    } 
}
namespace Samples.Movies.Operations.v1
{
    [Route("/v1/Movies", "GET")]
    public class Movies
    {
       ...
    }   
}

および対応するサービス...

public class MovieService: ServiceBase<Samples.Movies.Operations.v1.Movies>
{
    protected override object Run(Samples.Movies.Operations.v1.Movies request)
    {
    ...
    }
}

public class MovieService: ServiceBase<Samples.Movies.Operations.v1_1.Movies>
    {
        protected override object Run(Samples.Movies.Operations.v1_1.Movies request)
        {
        ...
        }
    }
37
Steve Stevenson

既存のサービスを(再実装ではなく)進化させてみてください

バージョニングの場合、バージョンのエンドポイントごとに異なる静的タイプを維持しようとすると、傷ついた世界に陥ることになります。最初はこのルートを開始しましたが、最初のバージョンのサポートを開始するとすぐに、同じサービスの複数のバージョンを維持するための開発作業が爆発的に増加します。異なるタイプの手動マッピングを維持する必要があるため、複数のバージョンを維持する必要があります。それぞれが異なるバージョンタイプに結合された並列実装-DRYの大規模な違反。これは、同じモデルを異なるバージョンで簡単に再利用できる動的言語ではそれほど問題にはなりません。

シリアライザーに組み込まれているバージョン管理を利用する

明示的にバージョン管理するのではなく、シリアル化形式内のバージョン管理機能を利用することをお勧めします。

例: JSONおよびJSVシリアライザーははるかに復元力があります のバージョン管理機能として、通常、JSONクライアントでのバージョン管理について心配する必要はありません。

既存のサービスを防御的に強化する

XMLとDataContractを使用すると、重大な変更を加えることなく、フィールドを自由に追加および削除できます。応答DTOにIExtensibleDataObjectを追加すると、DTOで定義されていないデータにアクセスする可能性もあります。バージョニングへの私のアプローチは、重大な変更を導入しないように防御的にプログラムすることです。これが古いDTOを使用した統合テストの場合であることを確認できます。私が従ういくつかのヒントは次のとおりです。

  • 既存のプロパティのタイプを変更しないでください-別のタイプにする必要がある場合は、別のプロパティを追加し、古い/既存のプロパティを使用してバージョンを決定します
  • プログラムは、古いクライアントには存在しないプロパティを防御的に認識しているため、必須にしないでください。
  • 単一のグローバル名前空間を保持します(XML/SOAPエンドポイントにのみ関連します)

これを行うには、各DTOプロジェクトのAssemblyInfo.csの[Assembly]属性を使用します。

[Assembly: ContractNamespace("http://schemas.servicestack.net/types", 
    ClrNamespace = "MyServiceModel.DtoTypes")]

Assembly属性を使用すると、各DTOで明示的な名前空間を手動で指定する必要がなくなります。

namespace MyServiceModel.DtoTypes {
    [DataContract(Namespace="http://schemas.servicestack.net/types")]
    public class Foo { .. }
}

上記のデフォルトとは異なるXML名前空間を使用する場合は、以下に登録する必要があります。

SetConfig(new EndpointHostConfig {
    WsdlServiceNamespace = "http://schemas.my.org/types"
});

DTOへのバージョン管理の埋め込み

ほとんどの場合、防御的にプログラムし、サービスを適切に進化させる場合、入力されたデータから推測できるため、特定のクライアントが使用しているバージョンを正確に知る必要はありません。ただし、まれに、サービスがクライアントの特定のバージョンに基づいて動作を微調整する必要がある場合は、バージョン情報をDTOに埋め込むことができます。

公開するDTOの最初のリリースでは、バージョン管理を考えなくても、DTOを楽しく作成できます。

class Foo {
  string Name;
}

しかし、おそらく何らかの理由でフォーム/ UIが変更され、クライアントがあいまいなName変数を使用することを望まなくなり、特定のクライアントが使用していたバージョン:

class Foo {
  Foo() {
     Version = 1;
  }
  int Version;
  string Name;
  string DisplayName;
  int Age;
}

後でチームミーティングで議論されましたが、DisplayNameは十分ではなかったので、それらをさまざまなフィールドに分割する必要があります。

class Foo {
  Foo() {
     Version = 2;
  }
  int Version;
  string Name;
  string DisplayName;
  string FirstName;
  string LastName;  
  DateTime? DateOfBirth;
}

したがって、現在の状態では、3つの異なるクライアントバージョンがあり、既存の呼び出しは次のようになります。

v1リリース:

client.Post(new Foo { Name = "Foo Bar" });

v2リリース:

client.Post(new Foo { Name="Bar", DisplayName="Foo Bar", Age=18 });

v3リリース:

client.Post(new Foo { FirstName = "Foo", LastName = "Bar", 
   DateOfBirth = new DateTime(1994, 01, 01) });

同じ実装でこれらの異なるバージョンを引き続き処理できます(DTOの最新のv3バージョンを使用します)。例:

class FooService : Service {

    public object Post(Foo request) {
        //v1: 
        request.Version == 0 
        request.Name == "Foo"
        request.DisplayName == null
        request.Age = 0
        request.DateOfBirth = null

        //v2:
        request.Version == 2
        request.Name == null
        request.DisplayName == "Foo Bar"
        request.Age = 18
        request.DateOfBirth = null

        //v3:
        request.Version == 3
        request.Name == null
        request.DisplayName == null
        request.FirstName == "Foo"
        request.LastName == "Bar"
        request.Age = 0
        request.DateOfBirth = new DateTime(1994, 01, 01)
    }
}
61
mythz

問題のフレーミング

APIは、その式を公開するシステムの一部です。これは、ドメインでの通信の概念とセマンティクスを定義します。問題は、表現できるものや表現方法を変更したいときに発生します。

表現方法と表現内容の両方に違いがある可能性があります。最初の問題は、トークンの違い(名前ではなく姓名)である傾向があります。 2番目の問題は、さまざまなことを表現することです(自分の名前を変更する機能)。

長期的なバージョン管理ソリューションでは、これらの課題の両方を解決する必要があります。

APIの進化

リソースタイプを変更してサービスを進化させることは、暗黙的なバージョン管理の一種です。オブジェクトの構造を使用して動作を決定します。表現方法(名前など)にわずかな変更がある場合に最適に機能します。表現方法のより複雑な変更や表現力の変更の変更にはうまく機能しません。コードは全体に散らばる傾向があります。

特定のバージョン管理

変更がより複雑になると、各バージョンのロジックを個別に保つことが重要になります。神話の例でも、彼は各バージョンのコードを分離しました。ただし、コードは同じメソッドで混合されます。異なるバージョンのコードが互いに崩壊し始めるのは非常に簡単であり、それは広がる可能性があります。以前のバージョンのサポートを削除するのは難しい場合があります。

さらに、依存関係の変更に合わせて古いコードを同期させる必要があります。データベースが変更された場合、古いモデルをサポートするコードも変更する必要があります。

より良い方法

私が見つけた最善の方法は、式の問題に直接取り組むことです。 APIの新しいバージョンがリリースされるたびに、新しいレイヤーの上に実装されます。変更が小さいため、これは一般的に簡単です。

それは2つの点で本当に輝いています。1つはマッピングを処理するすべてのコードが1つの場所にあるため、後で理解または削除するのが簡単です。2つ目は、新しいAPIが開発されてもメンテナンスが不要です(ロシアの人形モデル)。

問題は、新しいAPIが古いAPIよりも表現力が低い場合です。これは、古いバージョンを維持するための解決策が何であれ、解決する必要がある問題です。問題があり、その問題の解決策が何であるかが明らかになります。

このスタイルのmythzの例の例は次のとおりです。

namespace APIv3 {
    class FooService : RestServiceBase<Foo> {
        public object OnPost(Foo request) {
            var data = repository.getData()
            request.FirstName == data.firstName
            request.LastName == data.lastName
            request.DateOfBirth = data.dateOfBirth
        }
    }
}
namespace APIv2 {
    class FooService : RestServiceBase<Foo> {
        public object OnPost(Foo request) {
            var v3Request = APIv3.FooService.OnPost(request)
            request.DisplayName == v3Request.FirstName + " " + v3Request.LastName
            request.Age = (new DateTime() - v3Request.DateOfBirth).years
        }
    }
}
namespace APIv1 {
    class FooService : RestServiceBase<Foo> {
        public object OnPost(Foo request) {
            var v2Request = APIv2.FooService.OnPost(request)
            request.Name == v2Request.DisplayName
        }
    }
}

露出した各オブジェクトはクリアです。同じマッピングコードを両方のスタイルで記述する必要がありますが、分離されたスタイルでは、タイプに関連するマッピングのみを記述する必要があります。適用されないコードを明示的にマップする必要はありません(これは、エラーのもう1つの潜在的な原因です)。以前のAPIの依存関係は、将来のAPIを追加したり、APIレイヤーの依存関係を変更したりしても静的です。たとえば、データソースが変更された場合、このスタイルで変更する必要があるのは最新のAPI(バージョン3)のみです。組み合わせたスタイルでは、サポートされている各APIの変更をコーディングする必要があります。

コメントの1つの懸念は、コードベースへの型の追加でした。これらのタイプは外部に公開されているため、これは問題ではありません。コードベースで型を明示的に提供すると、テストで型を簡単に見つけて分離できます。保守性が明確である方がはるかに優れています。もう1つの利点は、このメソッドが追加のロジックを生成せず、追加のタイプのみを追加することです。

2
No One

私もこれに対する解決策を考えていて、以下のようなことを考えていました。 (多くのグーグルとStackOverflowクエリに基づいているため、これは他の多くの人の肩の上に構築されています。)

まず、バージョンをURIに含めるか、リクエストヘッダーに含めるかについては議論したくありません。どちらのアプローチにも賛否両論があるので、私たち一人一人が自分たちの要件を最もよく満たすものを使用する必要があると思います。

これは、Javaメッセージオブジェクトとリソース実装クラスを設計/アーキテクチャ化する方法についてです。

それでは、始めましょう。

私はこれに2つのステップでアプローチします。マイナーな変更(例:1.0から1.1)およびメジャーな変更(例:1.1から2.0)

マイナーチェンジへのアプローチ

それで、@ mythzで使用されているのと同じサンプルクラスを使用するとします。

最初は

class Foo {   string Name; }

このリソースへのアクセスは/V1.0/fooresource/{id}として提供されます

私のユースケースでは、JAX-RSを使用しています。

@Path("/{versionid}/fooresource")
public class FooResource {

    @GET
    @Path( "/{id}" )
    public Foo getFoo (@PathParam("versionid") String versionid, (@PathParam("id") String fooId) 
    {
      Foo foo = new Foo();
     //setters, load data from persistence, handle business logic etc                   
     Return foo;
    }
}

ここで、Fooに2つのプロパティを追加するとします。

class Foo { 
    string Name;   
    string DisplayName;   
    int Age; 
}

この時点で私がしていることは、@ Versionアノテーションでプロパティにアノテーションを付けることです

class Foo { 
    @Version(“V1.0")string Name;   
    @Version(“V1.1")string DisplayName;   
    @Version(“V1.1")int Age; 
}

次に、要求されたバージョンに基づいて、そのバージョンに一致するプロパティのみをユーザーに返す応答フィルターがあります。便宜上、すべてのバージョンで返される必要のあるプロパティがある場合は、注釈を付けないでください。要求されたバージョンに関係なく、フィルターはそれを返します。

これは一種のメディエーションレイヤーのようなものです。私が説明したのは単純なバージョンであり、非常に複雑になる可能性がありますが、アイデアが得られることを願っています。

メジャーバージョンへのアプローチ

あるバージョンから別のバージョンに多くの変更が加えられると、これは非常に複雑になる可能性があります。そのとき、2番目のオプションに移動する必要があります。

オプション2は、基本的にコードベースから分岐し、そのコードベースで変更を行い、異なるコンテキストで両方のバージョンをホストすることです。この時点で、アプローチ1で導入されたバージョンメディエーションの複雑さを取り除くためにコードベースを少しリファクタリングする必要があるかもしれません(つまり、コードをよりクリーンにする)これは主にフィルターにある可能性があります。

これは私が考えているだけで、まだ実装していないので、これが良いアイデアかどうか疑問に思っていることに注意してください。

また、フィルターを使用せずにこのタイプの変換を実行できる優れたメディエーションエンジン/ ESBがあるかどうか疑問に思いましたが、フィルターを使用するほど単純なものは見たことがありません。たぶん私は十分に検索していません。

他の人の考えを知り、この解決策が元の質問に対処するかどうかに興味があります。

2
traviskds