paint-brush
API としてのテーブル? 幻想と現実@truewebber
新しい歴史

API としてのテーブル? 幻想と現実

Aleksei Kish9m2025/03/05
Read on Terminal Reader

長すぎる; 読むには

著者は、サービス間通信の手段として共有データベース テーブルを使用するのはアンチパターンであると主張しています。これは簡単な解決策のように見えるかもしれませんが、バージョン管理の煩わしさ、所有権の不明確さ、スケーラビリティとセキュリティの難しさにつながります。代わりに、この記事では、各サービスがインターフェースを正式に定義し、独自のデータの所有権を保持する「コントラクト ファースト」アプローチを推奨しています。この方法により、チーム間の説明責任が明確になり、進化がスムーズになり、統合が堅牢になります。
featured image - API としてのテーブル? 幻想と現実
Aleksei Kish HackerNoon profile picture
0-item
1-item

導入

サービスインタラクションの文脈における「契約」とは何でしょうか?

サービス モジュールの相互作用のコンテキストでは、次のような疑問が必然的に生じます。通信はどのようなルールで行われるのでしょうか。IT 製品では、「契約」はシステム間でどのようなデータが流れ、どのように送信されるかについての正式な理解を表します。これには、データ形式 (JSON、Protobuf など)、構造要素 (フィールド、データ型)、通信プロトコル (REST、gRPC、メッセージ キュー)、およびその他の仕様が含まれます。


契約により、オープン性 (誰もが受信および送信内容を把握)、予測可能性 (契約を更新してバージョンを維持できる)、信頼性 (適切に管理された変更を行った場合、システムが故障しない) が保証されます。

なぜ人々はデータベース内のテーブルを「契約」として選ぶ傾向があるのか。

実際には、誰もがマイクロサービスや「契約」、API について話しているにもかかわらず、「API を構築する代わりに、データベースに共有テーブルを作成してみませんか?」というアプローチを採用する人をよく見かけます。


  • 歴史的または組織的な習慣:すべてが 1 つの企業内の 1 つの DB システムに保存されている場合、なぜ車輪の再発明を行うのでしょうか?


  • 「クイックフィックス」の考え方:承認ルールを設定したり API 仕様を設計したりせずに、私たちが書き、あなたが読むだけです。


  • 「ビッグ データ」の議論:数十ギガバイトまたは数百ギガバイトのデータを扱う場合、共有テーブルへの直接転送はより簡単、高速、経済的であるように見えますが、実際には、スケーラビリティとパフォーマンスの問題、およびデータ所有権の問題が発生します。


したがって、データ交換に共有テーブルを使用すると、効率的で、迅速な結果を得るために最適化されているように見えますが、長期的にはさまざまな技術的および組織的な課題が発生します。ただし、チームがデータ交換に共有テーブルを選択すると、実装中にさまざまな問題に直面する可能性があります。

「データベース内のテーブル」が契約ではない理由 (そしてそれがアンチパターンである理由)。

明確に定義されたインターフェースの欠如

サービスが REST/gRPC/GraphQL 経由で通信する場合、OpenAPI (Swagger)、protobuf スキーマ、または GraphQL スキーマという正式な定義があります。これらは、使用可能なリソース (エンドポイント)、想定されるフィールド、そのタイプ、および要求/応答の形式を詳細に定義します。「共有テーブル」が契約として機能する場合、正式な説明はありません。契約の正式な説明はありません。テーブル スキーマ (DDL) のみが使用可能ですが、それも十分に文書化されていません。テーブル構造の小さな変更 (列の追加または削除、データ型の変更など) は、このテーブルを読み書きする他のチームに影響を与える可能性があります。

バージョン管理と進化の問題

API のバージョン管理は通常の方法です。バージョン 1、バージョン 2 などがあり、下位互換性を維持しながら、クライアントを徐々に新しいバージョンに移行できます。データベース テーブルの場合、特定の DB エンジンに密接に結合されており、移行を慎重に処理する必要がある DDL 操作 ( ALTER TABLEなど) のみがあります。


クエリの更新を必要とするスキーマの変更について消費者に警告を送信できる集中システムはありません。その結果、 「裏取引」が発生する可能性があります。誰かがチャットに「明日、列 X を Y に変更します」と投稿しても、全員が時間内に準備できる保証はありません。

明確な所有権がない

API が明確に定義されている場合、その所有者は誰であるかが明らかです。つまり、API 発行元として機能するサービスです。複数のチームが同じデータベース テーブルを使用すると、構造や保存するフィールド、それらの解釈方法を誰が決定するかについて混乱が生じます。その結果、テーブルは「誰の所有物でもない」ものになり、変更のたびに「他のチームが古い列を使用しているかどうかを確認する必要がある」という課題が生じます。

セキュリティとアクセス制御の問題

多くのチームが DB にアクセスできる場合、誰がテーブルを読み書きできるかを追跡するのは困難です。意図していないデータであっても、権限のないサービスがデータにアクセスできる可能性があります。API を使用すると、このような問題の管理が簡単になります。アクセス権 (どのメソッドを誰が呼び出せるか) を制御し、認証と承認を使用し、誰が何を呼び出したかを監視できます。テーブルの場合は、はるかに複雑になります。

内部構造への依存

データに対する内部変更 (インデックスの再編成、テーブルのパーティション分割、DB の変更) は、グローバルな問題になります。テーブルがパブリック インターフェイスとして機能する場合、所有者はすべての外部のリーダーとライターを危険にさらすことなく内部変更を行うことはできません。

実際の問題点と典型的な問題

変更の調整

これは最も面倒な点です。次の日にスキーマが変更されることを他のチームにどうやって通知するか?

  • テーブル バージョンの更新の成功シナリオ:所有者は、古いテーブルと並行して、更新されたスキーマを持つ新しいテーブルを作成します。古いバージョンは現在の消費者が引き続きアクセス可能であり、所有者は「新しい構造が利用可能です。ドキュメントと期限を確認してください。両方のバージョンが存在する間に移行してください。」というメッセージを消費者に送信します。


  • ただし、OLAP シナリオや大量のデータがある場合、2 つの並列テーブルを維持するのは簡単な作業ではありません。また、古いスキーマから新しいスキーマにデータを移動する方法も決定する必要があります。これには、計画的なダウンタイムや非常に高度なインフラストラクチャが必要になる場合があります。このプロセスでは、必然的にリスクと余分な作業の両方が発生します。

データ整合性の問題

複数のチームが共有テーブルを使用して重要なデータを選択および更新すると、そのテーブルは簡単に「戦場」になります。その結果、ビジネス ロジックがさまざまなサービスに分散され、データの整合性を一元的に制御できなくなります。特定のフィールドが特定の方法で保存されている理由、誰が更新できるか、空白のままにすると何が起こるかを把握することは非常に困難になります。

デバッグと監視の課題

たとえば、テーブルが壊れたとします。つまり、不正なデータがあったり、誰かが重要な行をロックしたとします。問題の原因を特定するには、多くの場合、DB アクセス権を持つすべてのチームに、どのクエリが問題を引き起こしたのかを問い合わせる必要があります。原因は明らかではないことがよくあります。つまり、あるチームのクエリがデータベースをロックし、別のチームのクエリが目に見えるエラーを生成している可能性があります。

単一ノードの障害は全員の足を引っ張ります。

共有データベースは単一障害点です。データベースがダウンすると、多くのサービスもダウンします。1 つのサービスのクエリが集中したためにデータベースのパフォーマンスに問題が発生すると、すべてのサービスに問題が発生します。明確な API とデータ所有権を備えたモデルでは、各チームがサービスの可用性とパフォーマンスを管理できるため、1 つのコンポーネントの障害が他のコンポーネントに波及することはありません。

読み取り専用のレプリカを別途用意しても問題は解決しません。

よくある妥協案は、「メイン データベースに影響を与えずにクエリを実行できるように、読み取り専用のレプリカを提供します」というものです。最初はこれで負荷の問題は解決されるかもしれませんが、次のような問題があります。

  • バージョン管理の問題は残っています。主な問題は、メイン テーブルの構造が変更されると、レプリカの構造も多少の遅延を伴って変更されることです。


  • レプリケーションの遅延により、特に大規模なデータセットではデータの状態が予測不能になる可能性があります。


  • 所有権はまだ不明確です。フォーマット、構造、使用ルールを誰が定義するのでしょうか? レプリカは依然として、他の誰かのデータベースの「一部」です。

サービスインタラクションを適切に設計する方法(契約を優先)

明示的な契約定義。

最新の設計手法 (「API ファースト」や「コントラクト ファースト」など) は、正式なインターフェース定義から始まります。OpenAPI/Swagger、protobuf、または GraphQL スキーマが使用されます。これにより、人間とマシンの両方が、どのエンドポイントが利用可能か、どのフィールドが必須か、どのデータ型が使用されているかを把握できます。

データ所有者としてのサービス

マイクロサービス (またはモジュラー) アーキテクチャでは、各サービスがデータを完全に所有することが前提となっています。構造、ストレージ、ビジネス ロジックを定義し、その API へのすべての外部アクセス用の API を提供します。誰も「他人の」データベースに触れることはできません。触れることができるのは公式のエンドポイントまたはイベントだけです。これにより、変更が問題になるたびに作業が楽になり、誰が責任を負うのか常に明確になります。

実装例

  • REST/HTTP: サービスはGET /itemsPOST /itemsなどのエンドポイントを公開し、クライアントは明確に定義されたデータ スキーマ (DTO) を使用してリクエストを行います。


  • gRPC / バイナリ プロトコル: gRPC/protobuf では、サービスとメッセージは .proto ファイルで正式に定義され、メソッド、リクエスト、レスポンスが定義されている .proto ファイルに変更を加えるだけです。


  • イベント駆動型: データ所有サービスは Kafka や RabbitMQ などのブローカーにイベントを発行し、サブスクライバーはそれを消費します。ここでの契約はイベント形式です。構造の変更は、バージョン管理されたトピックまたはメッセージを通じて行われます。

バージョン管理

どのモデルであっても、インターフェースにバージョン管理を実装することは可能であり、また不可欠です。例:

  • REST では、/api/v1/… と /api/v2/ があります。


  • gRPC/protobuf には、後方互換性と前方互換性のための強力なメカニズムがあり、他のフィールド、メッセージ、メソッドを非推奨としてマークしながら、古いクライアントを壊すことなく新しいフィールド、メッセージ、メソッドを追加できます。


  • イベント駆動型アーキテクチャでは、すべてのコンシューマーが移行するまで、古いイベント形式と新しいイベント形式を並行して公開できます。

分散責任

基本的な原則は、データを所有するチームがデータの保存方法と管理方法を決定しますが、他のサービスに直接書き込みアクセス権を与えてはならないということです。他のユーザーは、外部データを編集するのではなく、API を経由する必要があります。これにより、責任の分散が明確になります。サービス A が故障した場合、それを修復するのはサービス A の責任であり、隣接するサービスではありません。

サービスインタラクションの例

単一チーム内

一見すると、すべてが 1 つのチームにあるのに、なぜ API で物事を複雑にするのでしょうか。実際には、単一の製品をモジュールに分割している場合でも、共有テーブルによって同じ問題が発生する可能性があります。


  • たとえば、`orders` テーブルを所有する「ファサード」または「マイクロサービス」を作成し、他のモジュール (分析など) がこのファサード/サービスを呼び出す方が適切です。


  • これにより、契約の原則が明示的に保持され、デバッグが簡素化されます。


たとえば、Orders サービスは注文テーブルの所有者であり、Billing サービスはそのテーブルに直接アクセスしません。注文の詳細を取得したり、注文を支払済みとしてマークしたりするために、Orders サービスのエンドポイントを呼び出します。

2つのチーム間

より高いレベルでは、2 つ以上のチームが異なる領域を担当する場合でも、原則は同じです。たとえば、次のようになります。

  • チーム A は、各アイテムに関する情報 (価格、在庫状況、属性) を含む製品カタログ サービスを担当します。


  • チーム B はショッピング カート サービスを担当します。


チーム B がチーム A に属する「カタログ」テーブルを直接クエリすると、チーム A での内部スキーマ変更 (フィールドの追加、構造の変更など) がチーム B に影響を及ぼす可能性があります。


適切なアプローチは API を使用することです。チーム A はGET /catalog/itemsGET /catalog/items/{id}などのエンドポイントを提供し、チーム B はそれらのメソッドを使用します。チーム A が古いバージョンと新しいバージョンをサポートできる場合は、/v2 をリリースして、B に移行する時間を与えることができます。

組織的側面と利点

透明なコミュニケーション

正式な契約があれば、Swagger/OpenAPI、.proto ファイル、イベント ドキュメントなど、すべての変更が可視化されます。更新は事前に話し合い、適切にテストし、必要に応じて下位互換性戦略を使用してスケジュールすることができます。

より速い開発

1 つのサービスの変更が他のサービスに与える影響は少なくなります。チームは、新しいフィールドやエンドポイント、古いフィールドやエンドポイントを適切に管理していれば、他の誰かに「影響を与える」ことを心配する必要がなく、スムーズな移行が保証されます。

アクセスとセキュリティ管理

API ゲートウェイ、認証、承認 (JWT、OAuth) はサービスでは標準ですが、共有テーブルではほぼ不可能です。アクセスを微調整 (誰がどのメソッドを呼び出せるか)、ログの保存、使用統計の追跡、クォータの適用が簡単になります。これにより、システムの安全性と予測可能性が高まります。

結論

データベース内の共有テーブルは、サービス間の合意ではなく実装の詳細であるため、契約とは見なされません。多くの問題 (複雑なバージョン管理、無秩序な変更、不明確な所有権、セキュリティ、パフォーマンスのリスク) があるため、このアプローチは長期的には実行不可能です。


正しいアプローチは、 Contract Firstです。これは、正式な設計を通じてインタラクションを定義し、各サービスがデータの所有者であり続けるという原則に従うことを意味します。これにより、技術的負債が軽減されるだけでなく、透明性が向上し、製品開発がスピードアップし、データベース スキーマの消火活動に追われることなく安全な変更が可能になります。


これは技術的な問題 (設計と統合の方法) と組織的な問題 (チームがどのようにコミュニケーションを取り、変更を管理するか) の両方です。データベース スキーマに関する無限の緊急事態に対処せずに製品を成長させたいのであれば、データベースへの直接アクセスではなく、契約の観点から考え始める必要があります。