Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】MedusaJS Dropped the Foreign Keys Between Its Modules: The defineLink Gamble

2026年09月23日に「Dev.to」が公開したITニュース「MedusaJS Dropped the Foreign Keys Between Its Modules: The defineLink Gamble」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

MedusaJS 2.0は、モジュール間のデータベース外部キーを削除した。これにより各モジュールの独立性が高まり、交換しやすくなる。しかし、データベースでのデータ整合性保証がなくなり、モジュールをまたぐ複雑なデータ検索・ソートは難しくなる。カスタマイズ重視の設計だ。

ITニュース解説

MedusaJS 2.0というコマースプラットフォームが、データベース設計において非常に大胆な変更を行った。通常、データベースでは複数のテーブル間の関連性を保証するために「外部キー」という仕組みを使う。これは、例えば「ある商品の価格情報」が「実際の商品データ」に確実に紐付いていることをデータベースが自動でチェックし、もし商品データが存在しないのにその価格情報だけが残るといった「データの矛盾」を防ぐ強力な機能だ。しかし、MedusaJS 2.0では、異なる機能モジュール(例えば「商品」モジュールと「価格」モジュール)のデータモデル間に設定されていたこの外部キーを意図的にすべて削除した。これは通常、データベース設計の基本とされており、経験の浅いエンジニアが外部キーを削除するプルリクエストを出せば、即座に却下されるような内容である。

なぜMedusaJSはこのような一見無謀な決定をしたのだろうか。その理由は「モジュールの徹底した隔離」という設計思想にある。MedusaJSは、商品管理、価格設定、カート、注文、在庫、配送といったコマースの様々なロジックを、それぞれが独立した「モジュール」として提供している。各モジュールは、自身のデータモデル、サービス、データベースの変更履歴(マイグレーション)を完全に独立して持っており、他のモジュールが持つリソース(サービスやデータモデル)については一切知らないという厳格なルールがある。例えば、商品のモジュールが価格のモジュールの内部に直接アクセスしたり、価格のテーブルが商品のテーブルにデータベースレベルで直接参照したりすることはない。

この隔離の最大のメリットは「移植性」と「交換可能性」だ。モジュールが隣接するモジュールについて何も知らないため、MedusaJSが提供する標準の価格モジュールを、ユーザー独自のカスタム価格モジュールに簡単に置き換えたり、特定のモジュールを別のデータストアに接続したり、あるいはMedusaJSから切り離して独立したサービスとして再利用したりすることが可能になる。もしデータベースの外部キーで複数のモジュールが密接に結合されていると、それらは「溶接」された状態になり、一方を動かせばもう一方も一緒に動かさなければならず、モジュールの独立した運用や交換が困難になる。MedusaJSは、コマースプラットフォームのユーザーが高度なカスタマイズを行うことを想定しており、そのニーズに応えるために、個々の部品が交換可能であることの価値が、モジュール間のデータベースレベルでの参照整合性よりも高いと判断したのである。

モジュール隔離と言っても、すべてのモジュールが別々のデータベースを持つわけではない。デフォルトでは、すべてのモジュールは単一のPostgreSQLデータベースを共有する。隔離はデータベースの物理的な分離ではなく、主にアプリケーションのコードとスキーマ設計のルールによって強制される。

では、外部キーを使わずに「この価格はこの商品に属する」といった関係性をどのように表現するのだろうか。MedusaJSではdefineLinkという仕組みを使う。これは、各モジュールが外部に公開している「リンク可能なハンドル」と呼ばれる設定を組み合わせて、モジュール間の「リンク」を定義するものだ。例えば、商品モジュールの商品と、ブログモジュールの記事をリンクさせると宣言すると、MedusaJSはこのリンク情報に基づいて、product_product_blog_postのような名前の新しい中間テーブルをデータベースに作成する。このリンクテーブルには、商品IDと記事IDという二つの列が作られるが、ここが重要な点だが、これらの列には外部キー制約が設定されない。

つまり、関係性は存在するが、その関係の正しさをデータベース自身は保証しないということになる。もし商品が削除されたとしても、このリンクテーブル内の商品IDは、もはや存在しない商品を指す「孤立した行」として残ってしまう。データベースが提供する「ON DELETE CASCADE」(親レコードが削除されたら関連する子レコードも自動で削除する機能)のような仕組みは、外部キーがないために利用できない。この結果、データの参照整合性を維持し、孤立行をクリーンアップする責任は、データベースからMedusaJSのアプリケーション層に移される。このトレードオフを受け入れるかどうかが、この設計の評価の分かれ目となる。

次に、リンクされたデータを実際にどのように読み出すかだが、MedusaJSはQueryというツールを提供する。これはモジュールをまたいだ読み出しを行うためのもので、GraphQLに似た記述形式で必要なデータを指定できる。例えば、あるブログ記事を取得し、その記事にリンクされている商品情報もまとめて取得したい場合、「記事のIDとタイトル、そしてリンクされている商品のすべてのフィールド」のように記述できる。Queryは、各モジュールのデータモデルとモジュール間のリンク情報を把握しているため、結果を自動的に組み立ててくれる。

しかし、QueryはデータベースレベルでSQLのJOIN(複数のテーブルを結合してデータを取得する操作)を実行するわけではない点に注意が必要だ。Queryは内部的に、指定された各モジュールから個別にデータを取得し、その後、アプリケーションのメモリ上でそれらのデータを「縫い合わせる(stitch)」ことで最終的な結果を生成する。ドキュメントにも「Medusaは異なるモジュールから来るデータを集約して最終結果を作成する」と明記されている。

この仕組みは、単純なモジュール間での関連データ取得には非常に便利だ。しかし、その限界はすぐに明らかになる。例えば、「特定の販売チャネルに属する商品を、価格の安い順に並べて表示する」といったシナリオを考えてみよう。商品の情報は商品モジュールにあり、販売チャネルとの関連性はリンクで、価格情報は価格モジュールにある。通常のデータベースなら、二つのJOIN操作とORDER BY句で簡単に書けるクエリだ。だが、MedusaJSのQueryでは、このような複雑な操作は直接実行できない。なぜなら、Queryは各モジュールからデータを個別に取得し、後で集約するため、データをフィルタリングしたりソートしたりする時点で、まだリンクされたモジュールのデータが結合されておらず、単一の結果セットとして扱えないからだ。ドキュメントでも「このアプローチは、リンクされたモジュールによってデータをフィルタリングする能力を制限する」と明確に述べられている。これはバグではなく、外部キーを削除したことによる必然的な代償である。モジュール間のエンティティを横断して安価にフィルタリングする仕組みを取り除いた結果、それが高価になったのだ。

この問題に対処するための「逃げ道」として提供されているのが「Indexモジュール」だ。これは、MedusaJSが持つデータを一つの関係性のあるストアに集約し、最適化された読み取り専用のインデックスとして機能させるアイデアだ。MedusaJSは起動時やデータ変更時に、各モジュールからのデータをこの中央インデックスに取り込む。そして、このインデックスに対してクエリを実行することで、ようやくモジュール境界を越えたフィルタリングやソートが可能になる。APIはQuery.graphとほぼ同じだが、query.indexを使うことで、Query.graphでは不可能だったリンクされたモジュールのフィールドによるフィルタリング(例: sales_channels: { id: "sc_123" })が可能になる。

ただし、Indexモジュールは実験段階であり、まだ開発中だ。機能フラグの裏に隠されており、デフォルトではコアAPIやワークフローでは使われていない。着実に改善は進んでいるものの、現時点では「デフォルトの読み込みパス」として頼るべきではない。もし今すぐにでも複雑なモジュール間フィルタリングが必要な場合、実験的なモジュールを使うか、そのようなフィルタリングが不要になるようにデータモデルを工夫する必要がある。これが、外部キーを削除したことによる現状のコストである。

特定のレコード間のリンクを実際に作成・削除するには、Linkサービスをアプリケーションの実行時に利用する。例えば、link.createを使って特定の商品とブログ記事をリンクさせたり、link.dismissでそのリンクを解除したりする。これは、かつてデータベースの外部キーとカスケードが自動で処理していた「関連の管理とクリーンアップ」を、アプリケーション層が手動で行うことを意味する。

MedusaJSが採用したdefineLinkという設計は、良い面と悪い面を両方持っている。良い面は、モジュール間の厳密な境界を保つ「モジュラーモノリス」を構築できたことだ。通常のモノリスでは、コードレベルでモジュールが分かれていても、データベースレベルでJOINを多用してしまうと、結局は密結合になってしまう。MedusaJSはデータベースレベルで結合を不可能にすることで、設計図上のモジュールの分離が実際に運用上も維持されることを保証した。これは、マイクロサービスの持つ「クリーンなドメイン分離」という利点を、マイクロサービスのようなネットワーク通信のオーバーヘッドや個別のデプロイコストなしで実現したと言える。

しかし、その代償も明確だ。データベースが提供する参照整合性や、安価なモジュール間クエリという強力な機能を失った。孤立したリンクデータはアプリケーション開発者の責任で管理しなければならないし、管理画面やストアフロントで頻繁に必要となるであろう、モジュールをまたいだフィルタリングやソートは、現状では実験的なIndexモジュールを使うか、アプリケーション側でデータを取得してから処理するなどの工夫が必要になる。これはコマースプラットフォームにとって決して小さな負担ではない。

結論として、defineLinkの賭けは、モジュールを頻繁にカスタマイズ・交換する予定があり、厳密なモジュールの境界がデータベースの参照整合性よりも重要、かつ複雑なモジュール間フィルタリングが主な要件ではない場合に適切な選択と言える。もし、製品が大規模なモジュール間レポート機能を必要とし、データベースのJOINによる効率的なクエリが今すぐ不可欠であるならば、MedusaJSのこの設計は課題となる可能性がある。なぜなら、そのためのツールであるIndexモジュールはまだ発展途上にあるからだ。MedusaJSは、Shopifyでは物足りず、Magentoのカスタマイズ性に魅力を感じるようなチームにとって、移植性が外部キーよりも重要であると判断した。多くのチームにとって、これは正しい賭けかもしれないが、どちらの側面に立つかを理解しておくことが重要だ。

関連コンテンツ

関連IT用語