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

はじめに
このシリーズの目標は、具体的な問題であるオンラインコースの採点システムを解決することにより、最新のバックエンドのさまざまなパターン、問題、およびアーキテクチャを探求し、実証することです。これは、多様なリレーションタイプを備え、現実世界のユースケースを表現するのに十分な複雑さであるため、良い例となります。
ライブストリームの録画は上記で視聴可能であり、この記事と同じ内容を網羅しています。
このシリーズで扱う内容
このシリーズでは、バックエンド開発のあらゆる側面におけるデータベースの役割に焦点を当て、以下を網羅します。
- データモデリング
- CRUD
- 集計
- APIレイヤー
- バリデーション
- テスト
- 認証
- 認可
- 外部APIとの統合
- デプロイ
今日学ぶこと
このシリーズの最初の記事では、問題領域をレイアウトし、バックエンドの以下の側面を開発することから始めます。
- データモデリング:問題領域をデータベーススキーマにマッピングする
- CRUD:Prisma Clientを使用して、データベースに対する作成、読み取り、更新、および削除クエリを実装する
- 集計:Prismaを使用して平均などを計算するための集計クエリを実装する
この記事の終わりまでに、Prismaスキーマ、Prisma Migrateによって作成された対応するデータベーススキーマ、およびPrisma Clientを使用してCRUDおよび集計クエリを実行するシードスクリプトが完成します。
このシリーズの次のパートでは、リストにある他の側面について詳しく説明します。
注:このガイド全体を通して、手順が正しく実行されたかどうかを検証できるさまざまなチェックポイントがあります。
前提条件
前提知識
このシリーズでは、TypeScript、Node.js、およびリレーショナルデータベースの基本的な知識を前提としています。JavaScriptの経験はあるが、TypeScriptを試したことがない場合でも、問題なくついていくことができるはずです。このシリーズではPostgreSQLを使用しますが、ほとんどの概念はMySQLなどの他のリレーショナルデータベースにも適用できます。それ以外には、Prismaに関する事前の知識は必要ありません。Prismaについてはシリーズで取り上げます。
開発環境
以下がインストールされている必要があります。
Visual Studio Codeを使用している場合は、Prisma拡張機能を構文のハイライト、フォーマット、およびその他のヘルパー機能のために推奨します。
注:Dockerを使用したくない場合は、ローカルPostgreSQLデータベースまたはHerokuでホストされているPostgreSQLデータベースをセットアップできます。
リポジトリのクローン
このシリーズのソースコードはGitHubにあります。
開始するには、リポジトリをクローンして依存関係をインストールします。
注:
part-1
ブランチをチェックアウトすると、記事と同じ開始点から始めることができます。
PostgreSQLの起動
PostgreSQLを起動するには、real-world-grading-app
フォルダから次のコマンドを実行します。
注:Dockerは
docker-compose.yml
ファイルを使用してPostgreSQLコンテナを起動します。
オンラインコースの採点システムのデータモデル
問題領域とエンティティの定義
バックエンドを構築する際、最も重要な懸念事項の1つは、問題領域の適切な理解です。問題領域(または問題空間)とは、問題を定義し、ソリューションを制約するすべての情報(制約は問題の一部)を指す用語です。問題領域を理解することにより、データモデルの形状と構造が明確になるはずです。
オンライン採点システムには、次のエンティティがあります。
- ユーザー:アカウントを持つ人。ユーザーは、コースとの関係を通じて教師または生徒のいずれかになります。言い換えれば、あるコースの教師である同じユーザーが、別のコースの生徒になることができます。
- コース:1人以上の教師と生徒、および1つ以上のテストがある学習コース。たとえば、「TypeScript入門」コースには、2人の教師と10人の生徒がいる場合があります。
- テスト:コースには、生徒の理解度を評価するための多くのテストを含めることができます。テストには日付があり、コースに関連付けられています。
- テスト結果:各テストには、生徒ごとに複数のテスト結果レコードを含めることができます。さらに、TestResultは、テストを採点した教師にも関連付けられています。
注:エンティティは、物理的なオブジェクトまたは無形の概念のいずれかを表します。たとえば、ユーザーは人を表しますが、コースは無形の概念です。
エンティティを視覚化して、リレーショナルデータベース(この場合はPostgreSQL)でどのように表現されるかを示すことができます。以下の図は、各エンティティに関連する列と、エンティティ間の関係を記述する外部キーを追加しています。
図について最初に注意すべき点は、すべてのエンティティがデータベーステーブルにマッピングされることです。
図には次のリレーションがあります。
- 1対多(
1-n
としても知られています):Test
↔TestResult
Course
↔Test
User
↔TestResult
(graderId
経由)User
↔TestResult
(student
経由)
- 多対多(
m-n
としても知られています)User
↔Course
(2つの外部キー:userId
とcourseId
を持つCourseEnrollment
リレーションテーブル経由)。多対多リレーションには通常、追加のテーブルが必要です。これは、採点システムに次のプロパティを持たせるために必要です。- 単一のコースには、多くの関連ユーザー(生徒または教師として)を持つことができます。
- 単一のユーザーは、多くのコースに関連付けることができます。
注:リレーションテーブル(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
フィールドは次のように分解できます。
名前 | タイプ | スカラー vs リレーション | タイプ修飾子 | 属性 |
---|---|---|---|---|
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
フィールド(リレーションスカラー)。このフィールドは、基になるデータベーステーブルの外部キーを表します。 @relation
属性を使用してリレーションスカラーtestId
をTest
モデルのid
主キーにマッピングするTest
型のtest
フィールド(リレーションフィールド)。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
に1回だけ、生徒または教師として関連付けられますが、両方ではありません。
リレーションの詳細については、リレーションに関するドキュメントを確認してください。
完全なスキーマ
リレーションの定義方法を理解したので、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操作(作成、読み取り、更新、および削除)の集まりです。また、ネストされた書き込みを使用して、関連エンティティのデータベース行を1回の操作で作成します。
スケルトンのsrc/seed.ts
ファイルを開きます。ここでは、Prisma Clientがインポートされ、2つのPrisma Client関数呼び出しがあります。1つはPrisma Clientをインスタンス化するため、もう1つはスクリプトの実行が終了したら切断するためです。
ユーザーの作成
main
関数で次のようにユーザーを作成することから始めます。
この操作により、Userテーブルに行が作成され、作成されたユーザー(作成されたid
を含む)が返されます。user
は@prisma/client
で定義されているUser
型を推論することに注意してください。
シードスクリプトを実行してUser
レコードを作成するには、package.json
のseed
スクリプトを次のように使用できます。
次のステップに従うと、シードスクリプトを複数回実行することになります。一意制約エラーが発生するのを避けるために、main
関数の先頭でデータベースの内容を削除できます。
注:これらのコマンドは、各データベーステーブルのすべての行を削除します。注意して使用し、本番環境では避けてください!
コースと関連するテストおよびユーザーの作成
このステップでは、コースを作成し、ネストされた書き込みを使用して関連するテストを作成します。
main
関数に次を追加します。
これにより、Course
テーブルに行が作成され、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つのリレーションがあります。ShakuntalaとDavidのテスト結果を追加するには、前のステップと同様にネストされた書き込みを使用します。
参考までに、TestResult
モデルを再度示します。
単一のテスト結果を追加すると、次のようになります。
DavidとShakuntalaの両方の3つのテストそれぞれのテスト結果を追加するには、ループを作成できます。
おめでとうございます。この時点に到達した場合、ユーザー、コース、テスト、およびテスト結果のサンプルデータをデータベースに正常に作成しました。
データベース内のデータを探索するには、Prisma Studioを実行できます。Prisma Studioは、データベースのビジュアルエディタです。Prisma Studioを実行するには、ターミナルで次のコマンドを実行します。
Prisma Clientを使用したテスト結果の集計
Prisma Clientを使用すると、モデルの数値フィールド(Int
やFloat
など)に対して集計操作を実行できます。集計操作は、入力値のセット、つまりテーブル内の複数の行から単一の結果を計算します。たとえば、TestResult
行のセット全体でresult
列の最小値、最大値、および平均値を計算するなどです。
このステップでは、2種類の集計操作を実行します。
-
コース内のすべての生徒にわたる各テストについて、テストの難易度やクラスのテストトピックの理解度を表す集計を行います。
これにより、以下の結果が得られます。
-
すべてのテストにわたる各生徒について、コースにおける生徒の成績を表す集計を行います。
これにより、以下のターミナル出力が得られます。
まとめと次のステップ
この記事では、問題領域から始まり、データモデリング、Prisma Schema、Prisma Migrateを使用したデータベース移行、Prisma Clientを使用したCRUD、そして集計についてと、広範囲にわたって説明しました。
問題領域を明確にすることは、コードに飛び込む前に一般的に良いアドバイスです。なぜなら、それがデータモデルの設計に影響を与え、バックエンドのあらゆる側面に影響を与えるからです。
Prismaはリレーショナルデータベースの操作を容易にすることを目指していますが、基盤となるデータベースについてより深く理解していると役立つ場合があります。
データベースの仕組み、適切なデータベースの選び方、アプリケーションでデータベースを最大限に活用する方法について詳しく学ぶには、Prismaのデータガイドをご覧ください。
シリーズの次のパートでは、以下について詳しく学びます。
- APIレイヤー
- バリデーション
- テスト
- 認証
- 認可
- 外部APIとの統合
- デプロイ
次回のライブストリームにご参加ください。YouTubeで8月12日CEST午後6時00分にライブ配信されます。
次回の投稿をお見逃しなく!
Prismaニュースレターに登録してください