Road To RDB 〜NoSQL から RDB へ〜 技術編
はじめに
こんにちは、株式会社キカガクの茂木です。私は社内で、法人向けプロダクトである「キカガク for Business」の開発チームに所属しています。
2024 年度の新卒ではありますが、今回こちらのプロジェクト紹介記事の担当をさせていただきました。
現在、弊社では 2023 年 10 月から継続して、NoSQL の Firestore から、RDB である PostgreSQL へ大規模なリアーキテクチャを進めています。
この記事では「Road To RDB」と題し、技術的なポイント 2 点に焦点を当ててご紹介します。
- システム構成
- プロジェクト進行における技術的な課題
システム構成
これまでの「キカガク for Business」は、フロントエンド (FE) とバックエンド (BE) にあたる Cloud Run functions(旧 Cloud Functions)と DB の Firestore のみで構成されていました。そのため、RDB へ移行するにあたっては、システム構成を大きく変更する必要があり、設計には多くの時間を費やしました。また、既存メンバー、新規メンバー含め、技術キャッチアップをしながらの実装は非常にチャレンジングでした。
システム構成の変容
RDB 移行前の弊社のシステム構成はこちらのイメージです。
- 言語:TypeScript
- フレームワーク:Next.js
- PaaS:Google App Engine
- 認証:Firebase Authentication
- DB:Firestore
- BE 処理:Cloud Run functions(旧 Cloud Functions)
導入企業様の中には、1 社で 1 万人規模のユーザーが登録されているケースもあり、この構成でこれまで複雑な処理を実現していたことに、今でも驚きを感じています。
また、必要なデータを表示するために複数回のリクエストが必要だったり、データを加工して表示するための重い処理が FE のコードに集中していました。
まさに「なんてこった。」という状態でした。
一方、 RDB 移行後のシステム構成は、こちらのイメージです。
- 言語:TypeScript
- 認証:Firebase Authentication
- FE フレームワーク:Next.js
- FE PaaS:Google App Engine
- BE フレームワーク:NestJS
- BE PaaS:Cloud Run
- DB:AlloyDB for PostgreSQL
- ORM:Prisma
- API アーキテクチャ:GraphQL
これまで FE (Next.js) と BE (Cloud Functions) のみでやってきたプロダクトに、BE (NestJS) を導入したことで、システムは一気にリッチとなりました。役割分担も明確になり、開発も快適に進められそうです。
そして、RDB 移行により、以下のようなメリットが得られました。
- 必要なデータを取るために、リクエストが 1 回で済む。
- 重い処理やドメイン知識を BE に集約することで、FE は表示に集中できる。
FE 側の処理の見通しが非常にクリアになりました。
なお、社内プロダクトで既に NestJS を利用していたことや TypeScript で開発できることも、技術的なキャッチアップを比較的用意にした要因でした。
しかし、これまで FE (Next.js) で担当していた重い処理やドメイン知識は BE (NestJS) に移動しただけでは根本的な解決にはなりません。そこで、BE (NestJS) に各種ロジックを見通し良く移行するために、BE (NestJS) のアーキテクチャを次のように設計しました。
BE のアーキテクチャ
弊社の BE (NestJS) では、ドメイン駆動設計 (DDD) を採用しており、アーキテクチャはオニオンアーキテクチャで実装しています。
- presentation 層
- FE からの GraphQL のリクエストエンドポイントを実装する層です。ここでは、リクエストのエンドポイントとしての役割のみを担当し、ビジネスロジックは基本的に実装しません。
- application-service 層
- ユースケースを実装する層です。これまで FE に存在していたロジックのほとんどが、この層に集約されます。
- infrastructure 層
- DB とのやりとりを実装する層です。Prisma を介して DB にアクセスし、application-service 層や domain 層にデータを提供します。
- domain 層
- ドメイン知識を表現、インターフェースを実装する層です。インターフェースをこの層で実装することで、依存関係が逆転 (DIP) し、application-service 層でもこの層でも DB のことを意識せずに済むようになります。
上記のような構成を実現する上で、やはり課題となるポイントはいくつかありました。
ここからは、実際に直面した課題についてご紹介します。
プロジェクト進行における技術的な課題
ディレクトリ構成の問題
弊社では、「キカガク for Business」の他にも toC サービスが存在します。そのため、DB は共有しつつ、NestJS 上でのファイルは明確に分けたいという状況でした。ここまでの要件をまとめると以下です。
- DDD で設計したい。
- オニオンアーキテクチャにしたい。
- プロダクトが複数存在しているため、共通のドメイン知識を共有できる構成にしたい。
これらの要件を満たすために、NestJSでサポートされているmonorepoモードに準拠したディレクトリ構成を採用しました。
.
├── apps
│ ├── business
│ │ ├── e2e-test # e2e テスト
│ │ └── src
│ │ ├── application-service # application-service 層
│ │ └── resolver (GraphQL 用の controller) # presentation 層
│ └── other product(割愛)
├── libs
│ ├── domain # domain 層
│ └── infrastructure # infrastructure 層
│ └── src
│ └── repository # DB 接続
└── (設定ファイル等)
この構成のポイントは、以下の通りです。
- company, user などの機能単位ではなく、domain, infrastructure といった層単位で分けることで、テーブルやカラムが追加されない限り libs 配下の domain や infrastructure に触れる必要が基本なくなり、新機能の実装は service と presentation に集中できる。
- presentation → application-service → (infrastructure) → domain という流れでコードを追うことで、ドメイン知識のキャッチアップを容易になる。
- apps を作成し、プロダクトごとにディレクトリを分けることで、互いの実装が干渉しないようになる。
このような形で実装することで、ドメイン知識のキャッチアップと実装を効率的にできるようにしました。
テストに関する問題
テストコードの作成は、多くの開発者が頭を悩ませる問題ではないでしょうか。
時間的な制約がある中で開発効率を向上させるためには、以下のような課題を解決する必要があるかと思います。
- 用意したモックデータは正しいのか?
- テストは網羅できているのか?
- 各階層でのテストの責務範囲は?
弊社でも例外なく、これらの課題に日々直面していました。そこで、これらの課題に対して、ルール化と生成 AI の活用を組み合わせて取り組んでいます。例えば、以下のような AI ツールを導入しています。
- Cursor
- ChatGPT for macOS の VSCode の拡張機能
- GitHub Copilot のカスタムインストラクション
特に今回は、GitHub Copilot のカスタムインストラクションについてご紹介します。
参考:https://dev.classmethod.jp/articles/custom-instructions-now-available-in-github-copilot-in-vs-code/
.github のディレクトリに、以下のような設定ファイルを追加します。
# 使用技術
- NestJS
# コーディングガイドライン
## 命名規則
### TypeScript
- ファイル名、ディレクトリ名: ケバブケース
- クラス名: パスカルケース
- 関数名: キャメルケース
- 変数名: キャメルケース
## テスト
- test ではなく it を使用する
- describe の説明文は日本語で記述する
このように設定することで、Copilot Chat に「xxx の test を生成して」と命令を出すと、設定したルールに従ってテストコードを生成してくれます。
さらに、独自のルールを追加することで、テストコードの大部分を GitHub Copilot に任せ、残りの部分を開発者が実装するという分担が可能になります。レビューや不具合調査も依頼すれば、実装をほぼ GitHub Copilot がサポートしてくれます。
まだ導入されてない方は、ぜひ試してみてください。
データ移行の課題
NoSQL から RDB への移行を検討している方であれば、必ず直面する課題がデータ移行です。
チームでは、以下の手順で実施しました。
- Firestore から CSV 形式でデータを抽出する。
- CSV データを PostgreSQL の COPY コマンドでインポートできる形式に変換する。
- 変換したデータを DB へ投入する。
この一連の作業はスクリプト化し、開発環境の DB に非同期でデータを投入できるようにしました。
データ移行で苦労した点
- 移行というよりそもそもの ER 設計
- 仕様把握の徹底 : Firestore は NoSQL データベースであるため、RDB への移行には、まず既存の全てのフィールドの仕様を詳細に把握する必要がありました。これは、既存機能を漏れなく再現するための重要なステップでした。
- 正規化とテーブル定義 : NoSQL の構造を正規化し、RDB のテーブル定義に落とし込む作業は、非常に複雑で時間を要しました。さらに、移行と並行して機能改善も進めていたため、設計はより複雑になりました。
- 膨大なヘッダー定義: 最終的なヘッダーの定義数は 1000 個以上にも及び、その検証も大変でした。
2. データの加工
- JSON 型との戦い : NoSQL の考え方が染み付いていたため、RDB への移行時に JSON 型以外の解決策がすぐには思い浮かびませんでした。そのため、一部のデータは JSON 型で保存することにしました。ちなみに、子テーブルに分割するか JSON のままにするかの判断基準は、データの CRUD 単位においていました。
- ファイルからの移行 : Firestore からのデータだけでなく、JSON ファイルなど異なる形式のデータも移行する必要がありました。特に手作業で作られた JSON ファイルはパースが大変でした。
3. セキュリティ面
- マルチテナント DB のセキュリティ: Firestore の場合は Cloud Firestore Security Rules でセキュリティを担保していました。弊社のシステムでは、マルチテナント DB を採用していることもあり、それに代わるセキュリティ対策が非常に重要でした。
- Row Level Securityの導入 : セキュリティを確保するために、Row Level Security(RLS)を導入しました。これにより、GRANT 文は複雑になったものの、CSVに権限を記述し、スクリプトで GRANT 文を生成する仕組みを構築することで、視認性とメンテナンス性を向上させました。
うまく対応できた点
データ移行において、特にうまく対応できたと感じている点は以下の通りです。
- 機能改修と並行して進める必要があったため、機能改修時の Firestore の設計では、新規テーブルやカラム追加で対応できるように配慮しました。
- ローカル開発環境を立ち上げると同時に移行データが投入されるように設定しました。これにより、データの入り方を参考にすることができ、技術キャッチアップ時に移行作業の理解を深めることができました。
- 今までに開発された機能の詳細を把握する必要があったため、スクリプトで未知のフィールドを検知する仕組みを実装していました。
また、Firestoreのデータ構造をドキュメントにまとめていたことが、移行設計の大枠をスムーズに考える上で非常に役立ちました。
今回のデータ移行は、計 20 ファイル、8000 行以上のスクリプトを実装するという大規模なものになりました。この作業をチームの担当者が一人でやり遂げてくれたことに、感謝しかありません。
おわりに
今回の記事では、「Road To RDB」でもプロジェクトにおける技術的な側面についてご紹介しました。
Firestore から PostgreSQL への移行は、単なるデータベースの変更ではなく、プロダクト全体のアーキテクチャを大きく見直す挑戦でした。フロントエンド中心だったシステムにバックエンドを導入し、複雑な処理をバックエンドに集約することで、フロントエンドは本来の役割である表示に集中できるようになりました。また、ディレクトリ構成、テスト、データ移行といった、開発において課題となりやすい部分についても、具体的な事例を示すことができたと感じています。
このブログが、これからデータベース移行を検討されている方々にとって、少しでも参考になれば幸いです。
キカガクでは、これからも技術的なチャレンジを続け、より良いプロダクトを開発していきます。
最後までお読みいただき、ありがとうございました!
参考
Documentation | NestJS - A progressive Node.js framework
Documentation | NestJS - A progressive Node.js framework
Documentation | NestJS - A progressive Node.js framework
How to install the Work with Apps Visual Studio Code extension | OpenAI Help Center
GitHub Copilot in VS Code でカスタムインストラクションを利用可能になりました | DevelopersIO
Discussion