Django Rest Framework による API のバージョン管理

DRF
Google で「REST API バージョン管理」を簡単に検索すると、リクエストでバージョンを指定する方法について多くの議論が見つかります。 RESTful の原則と、URL にバージョン番号を埋め込むか、カスタム リクエスト ヘッダーを使用するか、既存の accept ヘッダーを利用するかが最善の方法であるかどうかについて、賛否両論については熱心な議論が絶えません。 残念ながらそれはありません 誰もが満足するたった一つの正解。 あなたが最終的にどのアプローチを採用するにせよ、一部の陣営はあなたのやり方は間違っていると主張するでしょう。 使用されるアプローチに関係なく、リクエストからバージョンが解析されると、異なるバージョン間で使用される異なるスキーマをサポートするために API コードをどのように管理するかという、より大きな別の疑問が生じます。 驚くべきことに、これを最適に達成する方法についてはほとんど議論されていません。
Rescale では、次の API を構築しました。 Django レスト フレームワーク。 今後の 3.1 リリースでは、API のバージョン管理のサポートが提供される予定です。 プロジェクト リーダーは、API ビルダーがリクエストでバージョンを指定するためのさまざまな戦略から選択できるフレームワークを提供することで、バージョン管理の議論を回避するという賢明な決定を下しました。 ただし、コード内でこのバージョンを処理するためのベスト プラクティスに関する公式のガイダンスはあまりありません。 実際、ドキュメントのほとんどは、「行動をどのように変えるかはあなた次第です」と述べて、この問題を冗談にしているだけです。
Stripe のエンジニアの XNUMX 人が素晴らしい投稿をしました 概要 Stripe が API との下位互換性をどのように扱うかについて説明します。 著者は、リクエストとレスポンスが通過する変換パイプラインについて説明します。 このアプローチの主な魅力の XNUMX つは、コア API ロジックが常に API の現在のバージョンを処理し、異なるバージョンに対応するための個別の「互換性」レイヤーがあることです。 クライアントからのリクエストは、コア API ロジックに渡される前に、「リクエスト互換性」レイヤーを通過して現在のスキーマに変換されます。 コア API ロジックからの応答は、「応答互換性」レイヤーを介して渡され、要求されたバージョンのスキーマ形式に応答がダウングレードされます。
この投稿では、DRF を使用してこのタイプの変換パイプラインをサポートするための潜在的なアプローチを検討したいと思います。 DRF に変換ロジックを挿入する自然な場所は、リクエスト データ (Serializer.to_internal_value) の検証と、APIView メソッドから返される応答データ (Serializer.to_representation) の準備の両方に関与するシリアライザー内です。
一般的な考え方は、データを現在のスキーマに変換するリクエストに適用される、順序付けられた一連の変換を作成することです。 同様に、応答データは逆の順序で変換を通過し、現在のスキーマのデータをクライアントが要求したバージョンに変換します。 これは、データベース移行における前方および後方の方法と非常によく似たものになります。
応答変換の使用方法の基本的な例として、メーリング リスト名と購読者の電子メールのリストを返す単純なシリアライザーを以下に示します。

class MailingListSerializer(serializers.ModelSerializer):subscribers =serializers.SerializerMethodField('_subscribers') def _subscribers(self, obj): return [obj.subscribers_set.all() の s.email] クラス メタ: model = models.MailingListフィールド = ('名前', '説明', '購読者')

このシリアライザーを使用するエンドポイントからのペイロードは、次のような形式の JSON を返す場合があります。

{'name': '猫に関する豆知識', 'description': '猫に関する面白い豆知識', '購読者': ['joe@email.com', 'jane@email.com']}

しばらくして、このエンドポイントは各加入者がメーリング リストにサインアップした日付も返す必要があると判断しました。 これは、サブスクライバ配列の各要素が単純な文字列ではなくオブジェクトになるため、元のバージョンの API を使用しているクライアントにとっては重大な変更になります。

{'name': '猫の豆知識', 'description': '猫に関する面白い豆知識', 'subscribers': [ {'email': 'joe@email.com', 'date_subscribed': '2015-01-15T00: 01:34Z'}, {'email': 'jane@email.com', 'date_subscribed': '2015-02-18T04:57:56Z'} ]}

この新しい形式でデータを返すにはシリアライザーを更新する必要があります。 API の元のバージョンとの下位互換性をサポートするには、VersioningMixin クラスから派生し、Transform クラスの場所を指定するように API を変更する必要もあります (これについては後で詳しく説明します)。

class MailingListSerializer(VersioningMixin,serializers.ModelSerializer):transform_base = 'api.transforms.mailinglist.MailingListSerializerTransform'subscribers =serializers.SerializerMethodField('_subscribers') def _subscribers(self, obj): return [{'email': s.email, 'date_subscribed': s.date_subscribed} for s in obj.subscribers_set.all()] クラス メタ: モデル = models.MailingList フィールド = ('名前', '購読者')

このシリアライザーに新しい API バージョンが導入されるたびに、新しい番号付き Transform クラスを api.transforms.mailinglist モジュールに追加する必要があります。 各 Transform は、応答データの辞書を変更することによって、バージョン N からバージョン N-1 へのダウングレードを処理します。

class MailingListSerializerTransform0001(Transform): def update_response_data(self, data, request, source): # 生の v2 入力辞書を v1 辞書に変換する data['subscribers'] = [s['email'] for s in data['subscribers ']]

Transform クラスはスキーマ移行に似ており、要求データと応答データを変換するメソッドが含まれています。 各 Transform クラス名には、接尾辞として数値が必要です。 VersioningMixin クラスはこれを使用して、リクエストまたは応答データに Transform を適用する順序を識別します。

class Transform(object): def update_request_data(self, data, request): """data は、クライアントからのリクエストを解析した出力であるネイティブ辞書です。 """ pass def update_response_data(self, data, request, source ): """data は、レンダラに渡されてクライアントに返されるネイティブ辞書です。source は、ネイティブ辞書に変換される元のオブジェクトです。""" パス

VerisoningMixin クラスは、リクエストを現在の API バージョンに変換したり、レスポンスを現在の API バージョンから要求されたバージョン。 次のコード スニペットでは、settings.API_VERSION は最新の現在の API バージョン番号を参照し、request.version フィールドはクライアントから要求された API バージョンに設定されます。

class VersioningMixin(object): def to_internal_value(self, data, *args, **kwargs): """現在の API バージョンに更新されるまで、指定されたリクエスト データに対して変換のパイプラインを適用します。""" の場合、データ: request = self.context['request'] for v in range(request.version, settings.API_VERSION): self._get_transform(v).update_request_data(data, request) return super(VersioningMixin, self).to_internal_value(data, * args, **kwargs) def to_representation(self, obj, *args, **kwargs): """必要な API バージョンにダウングレードされるまで、指定された応答データに対して変換のパイプラインを適用します。 """ data = super (VersioningMixin, self).to_representation(obj, *args, **kwargs) if obj: request = self.context['request'] for v in range(settings.API_VERSION, request.version, -1): t = self ._get_transform(v - 1) t.update_response_data(data, request, obj) 戻りデータ def _get_transform(self, version):transform_dict = {v: c for v, c in self._transform_classes()} TransformKlass = transform_dict.get( version, Transform) return TransformKlass() def _transform_classes(self): module,base = self.transform_base.rsplit('.', 1) mod = import_module(module) for name、Klass in Inspection.getmembers(mod): if name .startswith(base) および issubclass(Klass, Transform): m = re.search('\d+ このアプローチの主な利点は、APIView (通常、コア API ロジックを実装し、要求/応答処理にシリアライザーを使用するクラス) です。 ) 最新のスキーマ バージョンについてのみ考慮する必要があります。 さらに、Transform を作成するには、API の現在および以前のバージョンに関する知識のみが必要です。 特定の応答のバージョン 10 を作成する場合、v10 と v9 の間に作成する必要がある Transform は 7 つだけです。 v10 を要求するリクエストは、まず新しい変換によって v9 から v9 に変換されます。 残りは既存の v8 から v8 および v7 から v0 への変換で処理されます。 私たちは、これが今後発生するすべての下位互換性の問題に対する万能薬であるとは決して信じていません。 確かに、コストがかかる可能性がある一連の変換を通じてリクエストと応答を常に実行する必要がある場合、考慮すべきパフォーマンスの問題がいくつかあります。 さらに、データベース スキーマの変更に対して後方移行を作成することが不可能な場合があるのと同様に、このアプローチでは簡単に解決できない、より複雑で破壊的な API の変更が存在することは確かです。 ただし、基本的な API 変更の場合、これは懸念事項を分離し、バージョン管理の目的でコア API ロジック内に条件付き goop を埋め込むことを回避する良い方法であるように思えます。 , name) if m: yield (int(m.group(XNUMX)), Klass)

このアプローチの主な利点は、APIView (通常、コア API ロジックを実装し、リクエスト/レスポンス処理にシリアライザーを使用するクラス) は、最新のスキーマ バージョンのみを考慮する必要があることです。 さらに、Transform を作成するには、API の現在および以前のバージョンに関する知識のみが必要です。 特定の応答のバージョン 10 を作成する場合、v10 と v9 の間に作成する必要がある Transform は 7 つだけです。 v10 を要求するリクエストは、まず新しい変換によって v9 から v9 に変換されます。 残りは既存の v8 から v8 および v7 から vXNUMX への変換で処理されます。
私たちは、これが今後発生するすべての下位互換性の問題に対する万能薬であるとは決して信じていません。 確かに、コストがかかる可能性がある一連の変換を通じてリクエストと応答を常に実行する必要がある場合、考慮すべきパフォーマンスの問題がいくつかあります。 さらに、データベース スキーマの変更に対して後方移行を作成することが不可能な場合があるのと同様に、このアプローチでは簡単に解決できない、より複雑で破壊的な API の変更が存在することは確かです。 ただし、基本的な API の変更の場合、これは懸念事項を分離し、バージョン管理の目的でコア API ロジック内に条件付き goop を埋め込むのを避けるための良い方法であると思われます。

類似の投稿