この記事は、TypeScript、PostgreSQL、Prisma を使用したバックエンド構築に関するライブストリームと記事のシリーズの一部です。最初のライブストリームを要約したこの記事では、データモデルの設計方法、CRUD操作の実行方法、Prismaを使用した集計クエリの方法について説明します。

はじめに
このシリーズの目的は、具体的な問題、すなわちオンラインコースの採点システムを解決することで、現代のバックエンドにおけるさまざまなパターン、問題、アーキテクチャを探求し、実証することです。これは、多様なリレーションタイプを備え、実際のユースケースを表現するのに十分な複雑さを持つため、良い例となります。
ライブストリームの録画は上記で利用でき、この記事と同じ内容をカバーしています。
このシリーズでカバーする内容
このシリーズでは、バックエンド開発のあらゆる側面におけるデータベースの役割に焦点を当て、以下の内容をカバーします。
- データモデリング
- CRUD
- 集計
- APIレイヤー
- バリデーション
- テスト
- 認証
- 認可
- 外部APIとの統合
- デプロイ
本日の学習内容
シリーズの最初の記事である本稿では、まず問題領域を提示し、バックエンドの以下の側面を開発することから始めます。
- データモデリング: 問題領域をデータベーススキーマにマッピングする
- CRUD: Prisma Clientを使用して、データベースに対するCreate、Read、Update、Deleteクエリを実装する
- 集計: Prismaを使用して、平均などを計算するための集計クエリを実装する
この記事の終わりには、Prismaスキーマ、Prisma Migrateによって作成された対応するデータベーススキーマ、そしてPrisma Clientを使用してCRUDおよび集計クエリを実行するシードスクリプトが完成します。
このシリーズの次のパートでは、リストの他の側面を詳しく説明します。
注: ガイド全体を通して、手順が正しく実行されたかどうかを確認できるさまざまなチェックポイントがあります。
前提条件
前提知識
このシリーズでは、TypeScript、Node.js、およびリレーショナルデータベースに関する基本的な知識があることを前提としています。JavaScriptの経験があるものの、TypeScriptを試したことがない方でも、問題なく読み進めることができます。このシリーズではPostgreSQLを使用しますが、ほとんどの概念はMySQLなどの他のリレーショナルデータベースにも適用されます。それ以外にPrismaの事前の知識は必要ありません。それらはこのシリーズでカバーされます。
開発環境
以下がインストールされている必要があります
Visual Studio Codeを使用している場合は、構文ハイライト、フォーマット、その他のヘルパーのためにPrisma拡張機能の使用をお勧めします。
注: Dockerを使用しない場合は、ローカルのPostgreSQLデータベースまたはHerokuでホストされているPostgreSQLデータベースを設定できます。
リポジトリをクローンする
シリーズのソースコードはGitHubで見つけることができます。
まず、リポジトリをクローンし、依存関係をインストールします。
注:
part-1
ブランチをチェックアウトすることで、この記事を同じ開始地点から読み進めることができます。
PostgreSQLを起動する
PostgreSQLを起動するには、real-world-grading-app
フォルダから以下のコマンドを実行します。
注: Dockerは
docker-compose.yml
ファイルを使用してPostgreSQLコンテナを起動します。
オンラインコースの採点システム用データモデル
問題領域とエンティティの定義
バックエンドを構築する際、最も重要な関心事の1つは、問題領域を適切に理解することです。問題領域(または問題空間)とは、問題を定義し、解決策を制約する(制約は問題の一部である)すべての情報を指す用語です。問題領域を理解することで、データモデルの形状と構造が明確になるはずです。
オンライン採点システムには以下のエンティティがあります。
- User: アカウントを持つ人物。ユーザーはコースとの関係を通じて、教師または生徒のいずれかになります。つまり、あるコースの教師であるユーザーが、別のコースの生徒になることも可能です。
- Course: 1人以上の教師と生徒、および1つ以上のテストを含む学習コース。例:「TypeScript入門」コースには、2人の教師と10人の生徒がいる場合があります。
- Test: コースには、生徒の理解度を評価するための多くのテストを含めることができます。テストには日付があり、コースに関連付けられます。
- Test result: 各テストには、生徒ごとに複数のテスト結果レコードを持つことができます。さらに、TestResultは、テストを採点した教師にも関連付けられます。
注: エンティティは、物理的なオブジェクトまたは無形の概念のいずれかを表します。例えば、ユーザーは人物を表しますが、コースは無形の概念です。
エンティティは、リレーショナルデータベース(この場合はPostgreSQL)でどのように表現されるかを示すために視覚化できます。以下の図は、各エンティティに関連するカラムと、エンティティ間の関係を記述する外部キーを追加しています。
この図について最初に注目すべき点は、すべてのエンティティがデータベーステーブルにマッピングされていることです。
この図には以下の関係があります。
- 1対多 (または
1-n
):Test
↔TestResult
Course
↔Test
User
↔TestResult
(graderId
経由)User
↔TestResult
(student
経由)
- 多対多 (または
m-n
)User
↔Course
(CourseEnrollment
リレーションテーブルを経由し、2つの外部キー:userId
とcourseId
を持つ)。多対多の関係は通常、追加のテーブルを必要とします。これは、採点システムが以下のプロパティを持つために必要です。- 単一のコースには、多くの関連ユーザー(生徒または教師として)を持つことができます。
- 単一のユーザーは、多くのコースに関連付けられることができます。
注: リレーションテーブル(JOINテーブルとも呼ばれる)は、2つ以上の他のテーブルを接続し、それらの間にリレーションを作成します。リレーションテーブルの作成は、異なるエンティティ間の関係を表すSQLにおける一般的なデータモデリングの実践です。本質的には、「1つのm-nリレーションは、データベースでは2つの1-nリレーションとしてモデル化される」ことを意味します。
Prismaスキーマを理解する
データベースにテーブルを作成するには、まずPrismaスキーマを定義する必要があります。Prismaスキーマは、データベーステーブルのための宣言的な設定であり、Prisma Migrateによってデータベースにテーブルを作成するために使用されます。上記のエンティティ図と同様に、データベーステーブル間のカラムとリレーションを定義します。
Prismaスキーマは、生成されたPrisma ClientおよびPrisma Migrateがデータベーススキーマを作成するための信頼できる情報源として使用されます。
プロジェクトのPrismaスキーマはprisma/schema.prisma
にあります。スキーマには、このステップで定義するスタブモデルとdatasource
ブロックが含まれています。datasource
ブロックは、接続するデータベースの種類と接続文字列を定義します。env("DATABASE_URL")
を使用すると、Prismaは環境変数からデータベース接続URLをロードします。
注: シークレットをコードベースから除外することは、ベストプラクティスとされています。このため、
env("DATABASE_URL")
はdatasourceブロックで定義されています。環境変数を設定することで、シークレットをコードベースの外に保つことができます。
モデルを定義する
Prismaスキーマの基本的な構成要素はmodel
です。すべてのモデルはデータベーステーブルにマッピングされます。
以下にモデルの基本的なシグネチャを示す例を示します。
ここでは、いくつかのフィールドを持つUser
モデルを定義します。各フィールドには、名前、型、およびオプションのフィールド属性が続きます。例えば、id
フィールドは次のように分解できます。
id
Int
スカラー-@id
(主キーを示す)と@default(autoincrement())
(デフォルトの自動インクリメント値を設定する)email
String
スカラー-@unique
firstName
String
スカラー--lastName
String
スカラー--social
Json
スカラー?
(オプショナル)-Prismaは、使用されるデータベースに応じてネイティブデータベースの型にマッピングされる一連のデータ型を定義しています。
Json
データ型を使用すると、自由形式のJSONを保存できます。これは、User
レコード間で一貫性がなく、バックエンドのコア機能に影響を与えることなく変更できる情報に役立ちます。上記のUser
モデルでは、Twitter、LinkedInなどのソーシャルリンクを保存するために使用されます。social
に新しいソーシャルプロファイルリンクを追加しても、データベースのマイグレーションは不要です。
問題領域を十分に理解し、Prismaでデータをモデリングすることで、prisma/schema.prisma
ファイルに以下のモデルを追加できるようになります。
各モデルには関連するすべてのフィールドがありますが、リレーションは無視されています(リレーションは次のステップで定義されます)。
リレーションを定義する
1対多
このステップでは、Test
とTestResult
の間に1対多のリレーションを定義します。
まず、前のステップで定義したTest
モデルとTestResult
モデルについて考えます。
2つのモデル間に1対多のリレーションを定義するには、以下の3つのフィールドを追加します。
- リレーションの「多」側(
TestResult
)に、型がInt
のtestId
フィールド(リレーションスカラー)を追加します。このフィールドは、基となるデータベーステーブルの外部キーを表します。 - 型が
Test
のtest
フィールド(リレーションフィールド)に、リレーションスカラーtestId
をTest
モデルのid
主キーにマッピングする@relation
属性を追加します。 - 型が
TestResult[]
のtestResults
フィールド(リレーションフィールド)
test
やtestResults
のようなリレーションフィールドは、その値型が別のモデル、例えばTest
やTestResult
を指していることで識別できます。それらの名前は、Prisma Clientでプログラム的にリレーションにアクセスする方法に影響を与えますが、実際のデータベースカラムを表すものではありません。
多対多リレーション
このステップでは、User
モデルとCourse
モデルの間に多対多のリレーションを定義します。
多対多リレーションは、Prismaスキーマでは暗黙的または明示的に定義できます。このパートでは、その2つの違いと、暗黙的または明示的などちらを選択すべきかを学びます。
まず、前のステップで定義したUser
モデルとCourse
モデルについて考えます。
暗黙的な多対多リレーションを作成するには、リレーションの両側にリストとしてリレーションフィールドを定義します。
これにより、Prismaはリレーションテーブルを作成し、採点システムが上記で定義されたプロパティを維持できるようにします。
- 単一のコースには、多くの関連ユーザーを持つことができます。
- 単一のユーザーは、多くのコースに関連付けられることができます。
しかし、採点システムの要件の1つは、ユーザーをコースに教師または生徒のいずれかのロールで関連付けることを許可することです。これは、リレーションに関する「メタ情報」をデータベースに保存する方法が必要であることを意味します。
これは、明示的な多対多リレーションを使用することで実現できます。User
とCourse
を接続するリレーションテーブルには、ユーザーがコースの教師か生徒かを示す追加のフィールドが必要です。明示的な多対多リレーションを使用すると、リレーションテーブルに追加のフィールドを定義できます。
そのためには、CourseEnrollment
という名前のリレーションテーブル用の新しいモデルを定義し、User
モデルのcourses
フィールドとCourse
モデルのmembers
フィールドの型を以下のようにCourseEnrollment[]
に更新します。
CourseEnrollment
モデルに関する注意事項
- ユーザーがコースの生徒か教師かを示すために、
UserRole
enumを使用します。 @@id[userId, courseId]
は、2つのフィールドからなる複数フィールド主キーを定義します。これにより、各User
がCourse
に、生徒として、または教師として、いずれか一方のロールでのみ関連付けられることが保証されます。
リレーションの詳細については、リレーションのドキュメントを確認してください。
完全なスキーマ
リレーションの定義方法を理解できたので、Prismaスキーマを以下のように更新します。
TestResult
がUser
モデルに対して2つのリレーション(student
とgradedBy
)を持つことに注意してください。これらは、テストを採点した教師とテストを受けた生徒の両方を表します。単一のモデルが同じモデルに対して複数のリレーションを持つ場合、リレーションを曖昧にしないために、@relation
属性のname
引数が必要です。
データベースをマイグレートする
Prismaスキーマが定義されたので、Prisma Migrateを使用してデータベースに実際のテーブルを作成します。
まず、Prismaがデータベースに接続できるように、DATABASE_URL
環境変数をローカルで設定します。
注: ローカルデータベースのユーザー名とパスワードは、両方とも
docker-compose.yml
でprisma
と定義されています。
Prisma Migrateを使用してマイグレーションを作成および実行するには、ターミナルで以下のコマンドを実行します。
このコマンドは2つのことを行います。
- マイグレーションを保存する: Prisma Migrateはスキーマのスナップショットを取得し、マイグレーションを実行するために必要なSQLを特定します。SQLを含むマイグレーションファイルは
prisma/migrations
に保存されます。 - マイグレーションを実行する: Prisma Migrateはマイグレーションファイル内のSQLを実行してマイグレーションを実行し、データベーススキーマを変更(または作成)します。
注: Prisma Migrateは現在プレビューモードです。これは、本番環境でPrisma Migrateを使用することは推奨されないことを意味します。
チェックポイント: 出力に以下のようなものが表示されるはずです。
おめでとうございます。データモデルの設計とデータベーススキーマの作成に成功しました。次のステップでは、Prisma Clientを使用してデータベースに対してCRUDおよび集計クエリを実行します。
Prisma Clientの生成
Prisma Clientは、データベーススキーマに合わせて自動生成されるデータベースクライアントです。Prismaスキーマを解析し、コードにインポートできるTypeScriptクライアントを生成することで機能します。
Prisma Clientの生成には、通常、3つのステップが必要です。
-
Prismaスキーマに以下の
generator
定義を追加します。 -
@prisma/client
npmパッケージをインストールします。 -
以下のコマンドでPrisma Clientを生成します。
チェックポイント: 出力に以下が表示されるはずです: ✔ Generated Prisma Client to ./node_modules/@prisma/client in 57ms
データベースのシード
このステップでは、Prisma Clientを使用して、データベースにサンプルデータを投入するためのシードスクリプトを作成します。
この文脈におけるシードスクリプトとは、Prisma Clientによる一連のCRUD操作(作成、読み取り、更新、削除)のことです。ネストされた書き込みも使用して、関連エンティティのデータベース行を単一の操作で作成します。
スケルトンファイルsrc/seed.ts
を開くと、Prisma Clientがインポートされ、Prisma Clientの関数呼び出しが2つあります。1つはPrisma Clientをインスタンス化するためのもので、もう1つはスクリプトの実行が終了したときに切断するためのものです。
ユーザーの作成
まず、main
関数で以下のようにユーザーを作成します。
この操作は、Userテーブルに1行を作成し、作成されたユーザー(作成されたid
を含む)を返します。特筆すべきは、user
が@prisma/client
で定義されている型User
を推論することです。
シードスクリプトを実行してUser
レコードを作成するには、package.json
内のseed
スクリプトを以下のように使用できます。
次のステップに進むにつれて、シードスクリプトを複数回実行することになります。一意制約エラーを避けるために、main
関数の冒頭でデータベースの内容を以下のように削除できます。
注: これらのコマンドは、各データベーステーブルのすべての行を削除します。慎重に使用し、本番環境では避けてください!
コースと関連するテストおよびユーザーの作成
このステップでは、コースを作成し、ネストされた書き込みを使用して関連するテストを作成します。
main
関数に以下を追加します。
これにより、Course
テーブルに1行、Tests
テーブルに関連する3行が作成されます(Course
とTests
は1対多の関係であるため、これが可能です)。
前のステップで作成したユーザーを、教師としてこのコースに関連付けたい場合はどうでしょうか?
User
とCourse
は明示的な多対多リレーションシップを持っています。つまり、CourseEnrollment
テーブルに行を作成し、User
をCourse
にリンクするために役割を割り当てる必要があります。
これは以下のように行うことができます(前のステップのクエリに追加します)。
注:
include
引数を使用すると、結果でリレーションを取得できます。これは後のステップでテスト結果とテストを関連付けるのに役立ちます。
ネストされた書き込み(members
とtests
の場合など)を使用する場合、2つのオプションがあります。
connect
: 既存の行とリレーションを作成するcreate
: 新しい行とリレーションを作成する
tests
の場合、作成されたコースにリンクされているオブジェクトの配列を渡しました。
members
の場合、create
とconnect
の両方が使用されました。これは、user
がすでに存在しているにもかかわらず、リレーションテーブル(members
によって参照されるCourseEnrollment
)に新しい行を作成する必要があり、その行がconnect
を使用して以前に作成されたユーザーとのリレーションを形成するためです。
ユーザーの作成とコースへの関連付け
前のステップでは、コース、関連するテストを作成し、コースに教師を割り当てました。このステップでは、さらにユーザーを作成し、それらを生徒としてコースに関連付けます。
以下のステートメントを追加します。
生徒のテスト結果の追加
TestResult
モデルを見ると、student
、gradedBy
、test
という3つのリレーションがあります。シャクンタラとデイビッドのテスト結果を追加するには、前のステップと同様にネストされた書き込みを使用します。
参考のために、再度TestResult
モデルを示します。
単一のテスト結果を追加すると、以下のようになります。
デイビッドとシャクンタラの両方の3つのテストそれぞれにテスト結果を追加するには、ループを作成できます。
おめでとうございます。この時点まで到達できたなら、データベースにユーザー、コース、テスト、テスト結果のサンプルデータを正常に作成できました。
データベースのデータを探索するには、Prisma Studioを実行できます。Prisma Studioは、データベースのビジュアルエディタです。Prisma Studioを実行するには、ターミナルで以下のコマンドを実行します。
Prisma Clientによるテスト結果の集計
Prisma Clientを使用すると、モデルの数値フィールド(Int
やFloat
など)に対して集計操作を実行できます。集計操作は、入力値のセット、つまりテーブル内の複数の行から単一の結果を計算します。例えば、TestResult
の行のセットにおけるresult
カラムの最小値、最大値、および平均値を計算するなどです。
このステップでは、2種類の集計操作を実行します。
-
コース内の各テストについて、すべての生徒を対象とした集計を行い、テストの難易度やクラスのテストトピックに対する理解度を表す集計結果を出します。
その結果は以下のようになります。
-
各生徒について、すべてのテストを対象とした集計を行い、コースにおける生徒の成績を表す集計結果を出します。
その結果は以下のターミナル出力のようになります。
まとめと次のステップ
この記事では、問題領域から始まり、データモデリング、Prismaスキーマ、Prisma Migrateによるデータベースマイグレーション、Prisma ClientによるCRUD、そして集計に至るまで、多くの内容を網羅しました。
コードに取り掛かる前に問題領域をマッピングすることは、バックエンドのあらゆる側面に影響を与えるデータモデルの設計に役立つため、一般的に良いアドバイスです。
Prismaはリレーショナルデータベースとの連携を容易にすることを目指していますが、基盤となるデータベースを深く理解していると役立つことがあります。
データベースの仕組み、適切なデータベースの選び方、そしてアプリケーションでデータベースを最大限に活用する方法について詳しく学ぶには、Prismaのデータガイドをご覧ください。
シリーズの次のパートでは、以下について詳しく学びます。
- APIレイヤー
- バリデーション
- テスト
- 認証
- 認可
- 外部APIとの統合
- デプロイ
8月12日CEST午後6時にYouTubeでライブ配信される次のライブストリームにご参加ください。
次の投稿をお見逃しなく!
Prismaニュースレターに登録する