1 連絡・概要
1.1 連絡
後期中間試験、お疲れ様でした。後期中間成績は 小テスト➊~➌ を30%、課題1(Todoアプリ)を 70% の割合で総合して評価します (シラバス記載の評価割合です)。課題1は、README.md を5点、アプリ本体を5点の合計「10点満点」で評価しています (7.6点が標準評価になります)。
内容は優れているものの指示事項 (特に
README.md の先頭付近に「Todoアプリをホストしている GitHub Pages の URL」 を記載してください) に従っていないため、減点になっているものが目立ちました。皆さんの制作物 (課題1) の一覧はこちら です (学内ONLY)
次週は、OB講演を行ないます。
- 事前アンケートの回答が21件でとまっています。
1.2 案内
- Unity 1-Week GAME JAM
- https://unityroom.com/unity1weeks
- 次回の開催 12月22日(月) 0時 〜 12月28日(日) 20時 … お題「????」
- 2025年度 プログラミング1 (2年生) の ポートフォリオ の作品共有 (学内のみ)
1.3 就活・コンテスト応募に提出するリポジトリの「README」について
リポジトリの README について、次のチェックリストに「3個以上該当する」と、就活 (インターン) やコンテストの応募では「アプリを起動してもらえない」または「コードを見てもらえない」ということになりがちです。
アピールが弱いREADME、やっつけ仕事のREADME、生成AIで作成した雰囲気のREADME は、就活までにアップデートしておくことをお勧めします。
- このアプリやサービスを使うことで「ユーザは、どのような体験や価値が得られるのか」「ユーザのどのような困りごとを (どのように) 解決するのか」が書かれていない (伝わってこない)。
- このアプリやサービスについて「開発しようと思ったきっかけ(特定の機能を実装しようと考えた理由)や着想を得た体験や背景」が書かれていない (伝わってこない)。
- アプリの使い方、操作方法の説明が不足している。
- 使用した言語、フレームワーク、ライブラリが記載されていない。
- 技術面で注力したことや、チャレンジしたこと、目標としたことが明記されていない。
- Markdownの機能 (見出し、強調文字、箇条書き、リンク、コードブロックなど) が十分に活用されていない。
- スクリーンショットや GIF/APNG などの画像 (アニメーション) が少ない。
READMEは、選考の「プレゼン資料」であると位置づけて、より良いものになるように仕上げてください。また、生成AIを使って添削や校正を受けるようにしてください (そのうえで教員やアドバイザから助言をもらうと良いです)。
1.4 今後の授業の流れ
前回講義ではmicroCMSを使ってブログアプリの「バックエンド」を構築・構成しました。ここからの授業では、Next.js を使って自前でバックエンドを実装していきます。
具体的には、データ永続化、ウェブAPI、ユーザ認証 (外部サービス利用)、投稿記事やカテゴリのCRUD操作機能 (フロントエンドによるフォームの実装も含む) などの実装を進めていきます。まずは、リレーショナルデータベース (RDB) を使ったバックエンドサイドの「データの永続化」からはじめていきます。
ここから、また一段と難易度が高くなりますが、頑張っていきましょう🔥なお、繰り返しになりますが、本科目は学修単位科目であり、1回の授業あたり「4時間相当の授業時間外学習」をすることを前提 としたボリュームと進行速度となっています。1週間の間隔をあけての90分の取り組みでは理解・習得できるものではない ので、適宜、復習に努めてください。
「バックエンド」や「データの永続化」という概念は既に解説済みですが、もし、理解に自信がない場合は生成AIを使って再確認しておいてください。
(プロンプト例)
ウェブアプリ開発の文脈における「バックエンド」とは何ですか。
(プロンプト例)
ウェブアプリ開発の文脈における「データの永続化」とは何ですか。
リレーショナルデータベース (RDB) については、2年次の「情報2」の第13回と第14回でも取り上げています。講義資料と教科書 (キタミ式ITパスポート) の p.160 ~ p.191 もあわせて復習しておくことをお勧めします。データベースに関して理論を含めた専門的な内容については、4年生の後期開講の「データベース工学 (学修単位・2単位)」で学びます。
2 データの永続化
第04回講義と第05回講義のなかで取り組んだ「Todoアプリの開発」では、ウェブブラウザの機能であるローカルストレージ(Window.localStorage API) を使用して「データの永続化」を実装しました。つまり、フロントエンドでデータの永続化を行なっていました。
しかし、ローカルストレージは ブラウザが管理する領域にデータを保存する方式 であるため、それを利用したTodoアプリでは「PCから登録したタスクを、スマートフォンから確認する」といった使い方は で・き・ま・せ・ん でした 😭
また、同じ PC からアクセスしても「Chromeを使ったとき」と「Edgeを使ったとき」では、データを共有することができませんでした。本来「インターネットに接続可能な環境であればデバイスや場所を選ばずに利用できる」というのがウェブアプリの利点なのですが、ローカルストレージを使ったデータの永続化では、残念ながらこの利点を活かすことはできませんでした。
このようなことから、一般的なウェブアプリでは バックエンドに配置した「データベース」など使ってデータの永続化 が行なわれます。この方式であれば、先の問題の解決に加えて、次のようなメリットが得られます。
第一に、複数のユーザでデータを共有できるようになります。例えば、チームでタスク (Todo) を管理する場合、メンバー全員が同じタスクリストを参照し、更新内容をリアルタイムで共有することができます。
第二に、大量のデータを安全に保管できるようになります。ローカルストレージでは、その仕様上、保存容量に制限 (=文字列形式で約5MB) がありましたが、データベースを使えばギガバイト単位のデータも扱うことも可能で、なおかつ バックアップ などの堅牢な仕組みによってデータの消失を防ぐことも可能となります。
また、(リレーショナルデータベースとは別の仕組みになりますが) オブジェクトストレージ関連のウェブサービス (Amazon S3や Azure Blob Storage、GCP Cloud Storage) を利用すれば 画像、音声、動画など任意のバイナリデータを扱うことも可能 になります。
ここからの「ブログアプリ開発」では、以上のメリットを活かすために、バックエンドでリレーショナルデータベース (RDB) をデータを永続化する機能を実装していきます。
2.0.1 定着確認
- Window.localStorage API
は「ウェブサーバ」と「ウェブブラウザ」のどちらで提供される機能か答えよ。
- 答え: ウェブブラウザ
- Window.localStorage API は HTTP/HTTPS 経由で利用する API
である。この説明は「適切」か「不適切」か答えよ。
- 答え: 不適切
3 データベースについて
データベースのアーキテクチャ (内部の仕組み/データの整理方法) は、大きく分けると「RDB (リレーショナルデータベース)」と「NoSQL」の2つに分類されます。
ここで取り組む「ブログアプリの開発」には、リレーショナルデータベース を利用していきます。
3.1 NoSQL
NoSQL (ノーエスキューエル) の代表的なものとして「ドキュメント指向DB (ドキュメント型DB)」があります。ドキュメント指向DB は、ウェブブラウザのローカルストレージと同じように 固定的なスキーマを必要とせず 「JSON形式」や「XML形式」 でデータを自由に格納できるという特徴を持っています。
ここで「固定的なスキーマを必要としない」とは データのスキーマ (構造や型) が統一されていなくても良い ということを意味します。例えば、ドキュメント指向DBでは、以下のように 異なるスキーマを持った レコード (データ) を「同じ保存領域」に格納すること ができます。
{
"id": "user2",
"name": "萱島ウサギ",
"email": "abbit@example.com",
"skills": {
"jump": 3,
"speed": 2,
"stealth": 4
}
}1件目のレコードは age と hobbies
という属性を持ち、2件目のレコードは、それとは異なる email と skills
という属性を持っています。このようにドキュメント指向DBでは レコード (データ)
ごとに異なる項目や構造を持たせること が可能となっています。また 配列や階層構造 (ネスト構造) のデータ
が柔軟に格納できることも大きな特徴となっています。
さらに、ほとんどのドキュメント指向DBが JSON形式 (JavaScript Object Notation 形式) / BSON に対応しており、JavaScript / TypeScript との親和性が高く、ウェブアプリ開発で採用されることが増えています。その場合、フロントエンドからバックエンドまで一貫してJSON形式でデータをやり取りできるため、データの変換作業が最小限で済む というメリットがあります。
(プロンプト例)
ドキュメント指向データベースの文脈において「BSON」について解説してください。
一方で、後述の RDB (=リレーショナルデータベース) では、同じテーブル (表) に格納する全てのレコードは「共通のカラム (列)」 を持つことが要求されます。さらに、そのテーブルのスキーマ (カラムの名前、データ型、値の制約など) は レコードを登録する前に設計しておく必要がある という原則があります。
ドキュメント指向DBの代表的なプロダクトとしてはMongoDB(モンゴディービー) やCouchDB(カウチディービー) があります。また、ドキュメント指向DBの特性を活かした BaaS (= Backend as a Service) としてGCP FirebaseやAmazon DynamoDB、MongoDB Atlasなどのクラウドサービスがあり、様々なモバイルアプリやウェブサービスにおいて利用されています。
この他、NoSQLとしては「キーバリューストア型DB」や「グラフ型DB (ネットワーク型DB)」などのデータベースも存在します。興味がある学生は、生成AIやウェブ検索を利用して概要を把握しておいてください。
(プロンプト例)
ドキュメント指向DB、キーバリューストア型DB、グラフ型DBの概要を説明してください。
(プロンプト例)
ウェブアプリ開発の文脈で「BaaS」とは何ですか。専門用語でできるだけ避けて、初心者向けに機能や採用のメリットなどを解説してください。
3.1.1 定着確認
- 原則としてスキーマの事前定義が要求されるのは「RDB」と「ドキュメント指向DB」のどちらか答えよ。
- 答え: RDB
- RDBにおけるテーブルの「スキーマ」とは、どのようなものを指すか答えよ。
- 答え: カラム (列) の名前、データ型、制約など
- オブジェクト指向プログラミングにおける「オブジェクト」は、その属性 (プロパティ)
として「配列」や「階層構造」を持つことがある。これらのオブジェクトをより直接的に扱えるのは「ドキュメント指向DB」と「RDB」のどちらか。
- 答え: ドキュメント指向DB
- ドキュメント指向DBの代表的なプロダクト (≠クラウドサービス) を答えよ。
- 答え: MongoDB、CouchDB
- 一般的なドキュメント指向DBがサポートしている主要なドキュメント形式を2つ答えよ。
- 答え: JSON、XML、YAML
- NoSQLデータベースにはいくつかの種類が存在する。そのなかでも代表的な3つの型を答えよ。
- 答え: ドキュメント指向DB、キーバリュー型DB、グラフ型DB
- ウェブアプリ開発の文脈で「BaaS」とは何の略語か答えよ。
- 答え: Backend as a Service
3.2 RDB
RDB (Relational Database) は シンプルな「2次元の表形式」でデータを保持することが特徴 のDBです。近年では NoSQL の普及も進んでいますが、単に「DB」と言えば RDB を指すほど一般的であり、特に「銀行システム」や「ECサイト」などで広く採用されています。表形式によるデータ管理には多くのメリットがあるものの、可変長配列 や 階層構造のデータ を 直接的に扱えない という欠点があります。
(プロンプト例)
プログラム開発者の観点では、オブジェクトデータの永続化が直接的に可能なドキュメント指向DBのほうが、RDBと比較してストレージとしてのメリットが大きいように考えます。しかし、実際にはRDBが採用されることが多いのは、どのような理由か解説してください。
例えば、JavaScript/TypeScriptで、次のようなオブジェクトデータを扱っているとします。このデータでは、第7行目~第11行目のように「カテゴリ」に関する情報が 階層的な構造を持ち、その要素数が可変 (=例えば、2個のカテゴリをもつ場合もあれば、3個のカテゴリを持つ場合もある構造) となっています。
[
{
id: "p-001",
title: "JavaScriptだと動いたのに…な毎日",
content: "TypeScriptに移行してから…",
createdAt: "2024-03-19",
categories: [
{ id: "c-001", name: "プログラミング" },
{ id: "c-002", name: "TypeScript" },
{ id: "c-003", name: "JavaScript" },
],
},
{
id: "p-002",
title: "リストの[-1]ってマイナスなのになんで動くの?",
content: "Pythonのリストで…",
createdAt: "2024-03-24",
categories: [
{ id: "c-001", name: "プログラミング" },
{ id: "c-004", name: "Python" },
],
},
];このようなオブジェクトデータを RDBを用いて永続化するためには 正規化 という処理を施して、次のように 複数のテーブル (表) に分解して扱う必要 があります。
■ posts テーブル
| id | title | content | created_at |
|---|---|---|---|
| p-001 | JavaScriptだと… | TypeScriptに移行… | 2024-03-19 |
| p-002 | リストの[-1]って… | Pythonのリストで… | 2024-03-24 |
■ categories テーブル
| id | name |
|---|---|
| c-001 | プログラミング |
| c-002 | TypeScript |
| c-003 | JavaScript |
| c-004 | Python |
■ posts_categories テーブル
| post_id | category_id |
|---|---|
| p-001 | c-001 |
| p-001 | c-002 |
| p-001 | c-003 |
| p-002 | c-001 |
| p-002 | c-004 |
このテーブル設計において「posts_categoriesテーブル」は橋渡し的な役割を果たしています。このような複数のテーブルの「紐付け」を行なうテーブルを「中間テーブル」「Join Table」「Junction Table」と言います。1つの投稿記事が複数のカテゴリを持ち、なおかつ、1つのカテゴリが複数の投稿記事に属する という 多対多 (N:N) の関係性をRDBの形式で実現しています。このような変換をRDBでは データの正規化 (Normalization) と呼びます。正規化によって データの重複 を排除し、一貫性を保ちながら効率的なデータ管理 が可能になります。
RDBにおける「中間テーブル」や「多対多 (N:N)」の概念については「やさしい図解で学ぶ 中間テーブル 多対多 概念編」などを参照してください。詳しくは、4年生後期開講の「データベース工学」で学びます。
このように適切に正規化されたRDBでは、例えば、カテゴリ c-001
の「プログラミング」という名前を「コーディング」に変更したいとき、categoriesテーブルのなかの1件のレコードを更新するだけ
で完了するという大きなメリットがあります。
一方で、RDBを使用すると「プログラムからRDBにデータを保存するとき」と「RDBからプログラムにデータを読み込むとき」に煩雑な変換処理が必要となります。具体的には、データを保存するときには オブジェクトの内容を、各テーブルに記録するようなクエリ (SQL文) を作成して、それをRDBに発行する必要 があります。逆に、読み込む際には、各テーブルから必要なデータを抽出するためのクエリ (SQL文) を作成してRDBに発行し、その実行結果 (平坦化されたレコードの配列) を元の階層構造を持つオブジェクト形式に再構築する必要があります。
SQL (エス・キュー・エル: Structured Query Language) は、RDB に対する クエリ (検索、取得、挿入、更新、削除などの一操作指示) を記述するための言語です。
このような「オブジェクトデータ」と「リレーショナルデータ」を接続する際の困難さを O/Rインピーダンス・ミスマッチ と呼びます。
(プロンプト例)
O/Rインピーダンスミスマッチに関する質問です。現在、TypeScriptを使ってブログアプリ(ブログ記事とカテゴリがN:Nの関係)を開発しています。先生から、RDBから取得したデータは「平坦化されたレコードの配列」であるため、それを元の「階層構造を持つオブジェクト形式」に再構築する必要がある、と言われました。このことについて具体的なイメージがつかめないので解説してください。変換のプログラムを知りたいのではなく、それぞれのデータ構造と変換のイメージを把握したいです。
3.3 ORM
O/Rインピーダンス・ミスマッチを解決するための概念・技術として Object-Relational Mapping (ORM) というものがあります。これは、オブジェクト指向プログラミングにおける「オブジェクトモデル」と、RDBにおける「リレーショナルモデル」の間の「溝」を埋めるための (違いを吸収するための) 設計パターンとなります。
この Object-Relational Mapping を実装したツール/ライブラリは Object-Relational Mapper(ORM) と呼ばれ、開発者がRDBを直接意識することなく(つまり、SQL文でクエリを記述することなく)、プログラミング言語の ネイティブなオブジェクトとしてデータを扱うことを可能 にします。
TypeScript/JavaScript には Prisma (プリズマ) という ORM があり、広く使われています。このブログアプリ開発でも、Prisma を最大限に利用して RDB とのデータのやりとりを行ないます。
3.4 Prisma のインストール
プロジェクトに Prisma(O/R Mapper)
をインストールしていきます。VSCode でプロジェクト (next-blog-app)
をオープンして、Ctrl+J でターミナルを開き、以下のコマンドを実行してください
(1行づつ実行してください)。
npm i -D prisma
npm i @prisma/adapter-better-sqlite3 @prisma/client
(プロンプト例)
Next.js において、Prisma (v7) を ORM として用いて RDB (SQLite3) を利用しようと考えています。このためには
prisma、@prisma/adapter-better-sqlite3、@prisma/clientというライブラリが必要なことが分かったのですが、それぞれの役割の違いについて教えてください。また、-Dオプションをつけてインストールすべきライブラリはどれですか。Prisma のバージョンが最新の v7 であることに注意して回答してください。
また、データベースの「初期データ (Seed)」を作成する際に tsx を使用するので、こちらもプロジェクトのローカルにインストールしてください。
npm i -D tsx
ts-node は第01回講義でも解説したように TypeScriptを (JavaScriptに変換せずに) Node.js 上で 直接実行 するためのランタイム になります。
npmのアップデート
npmコマンドを実行した際に、次のように npm のアップデートを促すメッセージ が表示されたときは、適宜、指示に従ってアップデートしてください。
npm notice New minor version of npm available! 10.8.3 -> 10.9.1
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.9.1
npm notice To update run: npm install -g npm@10.9.1
具体的には、ターミナルから次のようなコマンドを実行してください。@
以降のバージョン番号は、適宜、読み替えてください。
npm install -g npm@10.9.1
なお、-g は グローバル環境を対象にインストール (アップデート)
することを指示するオプション になります。第01回講義でも触れたように、npm
は、通常、プロジェクトのローカル環境ではなく、グローバル環境にインストールします。
セキュリティ脆弱性に対する緊急対応
npmコマンドを実行した際に「XX high severity vulnerability」や「XX vulnerabilities (Y moderate, Z critical)」のように表示されたときは 既存のライブラリにセキュリティ脆弱性 が検出されたことを意味します。開発環境としてインストール済みのパッケージのなかに脆弱性を持つものが存在し、それに対してアップデート対応を促すメッセージとなっています。
「XX high severity vulnerability」のようなメッセージが表示されたときは、まずは以下のコマンドで詳細を確認してください。
npm audit
つづいて、次のコマンドで、当該パッケージをセキュティ対応したバージョンにアップデートします。成功すると「found 0 vulnerabilities」というメッセージが出力されます。
npm audit fix
なお、依存関係の問題などで npm audit fix では対応できない (解決できない)
こともあります。その場合は、個別に情報収集して対応したり、生成AIを使って解決してください。一般には、以下のコマンドで対応することが多いです。
npm audit fix --force
なお、npm audit fix --force (=破壊的変更を含む強制アップデート)
の実行後は、npm run build
コマンドで、プロジェクトが正常にビルドできることを確認してください。
(プロンプト例)
脆弱性が見つかったため、ライブラリをアップデートするように指示されました。
npm audit fixとnpm audit fix --forceの違いについて教えてください。
3.5 Prisma の初期化処理 (セットアップ処理)
次に Prisma を 使用するための準備 を行なっていきます。 この初期化処理は、プロジェクトのローカル環境に Prisma をインストール後、基本的に 初回のみに実行すればよい ものとなります。
VSCodeのターミナルから次のコマンドを実行してください。npm ではなく
npx であることに注意してください。--datasource-provider sqlite
のオプションにより、DB として SQLite (エスキューライト、スクライト)
を使用する設定にするように指示しています。
なお、Prisma は「SQLite」の他に MySQL、PostgreSQL など様々な DB に対応しています。
npx prisma init --datasource-provider sqlite
このコマンドを実行すると、
- プロジェクトフォルダの直下に
prismaというフォルダが作成され、さらに、そのなかにschema.prismaという 設定ファイル が作成されます。 - プロジェクトフォルダの直下に
prisma.config.tsという設定ファイルも作成されます。 - プロジェクトフォルダの直下の
.env(前回講義で 環境変数の設定 を記述したファイル) に環境変数DATABASE_URLが追記がされます。
3.6 .env の内容確認
.env
を開いて、Prismaの初期化処理によって追記された内容を確認してください。#
から開始する行は コメント なので編集・削除しても問題ありません。
ファイルの最後のほうに DATABASE_URL という環境変数設定が追加され、そこに
"file:./dev.db" がセット (代入) されていることが確認できると思います。
# microCMS
NEXT_PUBLIC_MICROCMS_BASE_EP=https://XXXXX.microcms.io/api/v1
NEXT_PUBLIC_MICROCMS_API_KEY=YYYYY
# prisma ▼▼ 追加されていることを確認 ▼▼
DATABASE_URL="file:./dev.db"これは Prisma がデータベース接続に際して参照する環境変数
で、この値によって、Prisma は dev.db
という「SQLiteのデータベースファイル
(現時点ではまだ作成されていません)」に接続するようになります。
3.7 prisma.config.ts の編集
prisma.config.ts は、Prisma 全体の挙動を制御する設定ファイル
となります。
以下のように、シーディング (seeding) に関する設定を
seed: "tsx prisma/seed.ts",
を追記してください。シーディングとは、データベースに初期データを投入するための処理を指します。
// This file was generated by Prisma and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "tsx prisma/seed.ts", // 追記
},
datasource: {
url: env("DATABASE_URL"),
},
});第10行目 では、シーディングコマンド
(npx prisma db seed) を実行したときに参照されるファイル
(prisma/seed.ts) を指定しています。現時点では、まだ prisma/seed.ts
は存在しません。
第13行目 では .env ファイルの DATABASE_URL から
DB接続情報
を読み取るように指示しています。現状では、DATABASE_URL に
file:./dev.db が設定されています。
3.8 schema.prisma の確認
prisma フォルダのなかの schema.prisma
を開いて、次のような内容になっていることを確認してください。なお、//
から開始する行は コメント なので編集・追加削除して問題ありません。
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "sqlite"
}第02行目 では、JavaScript (TypeScript) に対応した「Prisma Client」を生成することを指定しています。Prisma Client クラスから生成されるインスタンスを通して (SQLを記述することなく) TypeScript プログラムから DB のCURD操作 ができるようになります。
第03行目 では、次のセクションで schema.prisma
に追記する「DBのスキーマ定義」から自動生成する
型情報などを出力するフォルダ を指定しています。
第07行目 では、使用するDB (Prismaが接続するDB) が「SQLite」であることを指定しています。
また、schema.prisma は、データベースのスキーマ定義
(=テーブルやフィールドの構造定義)
を記述するファイルとしての役割も持ちます。次のセクションでは
ブログアプリに必要となる各テーブルのスキーマを定義していきます。
3.9 schema.prisma の編集
ブログアプリに必要な各種データ (投稿記事、カテゴリ) を保存するためのスキーマ を
schema.prisma に記述していきます。まず、schema.prisma
を次のように書き換えて、保存してください。
// このファイルを更新したら...
// 0. `npm run dev` や `npx prisma studio` を停止
// 1. dev.db を削除
// 2. npx prisma db push
// 3. npx prisma generate
// 4. npx prisma db seed
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "sqlite"
}
// 投稿記事テーブル
model Post {
id String @id @default(uuid())
title String
content String
coverImageURL String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
categories PostCategory[]
}
// カテゴリテーブル
model Category {
id String @id @default(uuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts PostCategory[]
}
// 投稿記事とカテゴリを紐づける中間テーブル
model PostCategory {
id String @id @default(uuid())
postId String
categoryId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
}第17行目 以降で「投稿記事テーブル Post」「カテゴリテーブル
Category」「投稿記事とカテゴリを紐づける中間テーブル
PostCategory」の 3つのテーブルのスキーマ (モデル) を定義
しています。この定義に基づき、Prisma によって DB にテーブルが作成され、また、それらのテーブルを操作するための「メソッド」 や
テーブルから取得される「データ型」 が TypeScript
で利用できるようになります。
投稿記事テーブル (Post) の定義の 第22行目
では、今後の展開の都合で「src/app/_types/CoverImage.ts で定義したような
url、height、width という属性を持った
coverImage」ではなく、coverImageURL
に変更している点に注意してください。
schema.prisma の詳細 (記述方法)
については、ウェブ記事や生成AIを利用して概要だけ把握しておいてください。特に
第44行目 と 第45行目
では特徴的な設定がされているので注意してください。
(プロンプト例)
TypeScript で Prisma を使用するにあたって、次のような
schema.prismaを受け取りました。「投稿記事テーブル」のスキーマ定義について、丁寧に解説してください。特にカラムの「型」や「制約」について説明してください。~ schema.prisma の内容を貼付け ~
つづいて「カテゴリテーブル」について同様に解説してください。
つづいて「中間テーブル」について同様に解説してください。
3.10 データベースの作成と確認
schema.prisma を編集・保存したら、次のコマンドを実行してください
(少し時間がかかりす)。
npx prisma db push
正常終了すると「Your database is now in sync with your Prisma
schema.」のような応答があります。このコマンドにより、schema.prisma に基づいて
実際にデータベースが新規作成され (現在の設定の場合は dev.db
というファイルがプロジェクトフォルダのルートに新規作成され)、その DB のなかに
「3つのテーブル」が作成 されます。
既にデータベースが存在する場合は
The database is already in sync with the Prisma schema. となります。
さらに、次のコマンドを実行してください。この操作により schema.prisma
に基づき、プロジェクトのなかで参照可能なメソッドや型情報などが
src/generated/prisma のなかに生成されます。
npx prisma generate
補足: 自動生成される「型」の参照
npx prisma generate を実行すると、プロジェクトのなかで
schema.prisma に記述したスキーマ (モデル)
に基づいて自動的に「型」が定義され、それを参照することができます。
例えば import type { Category } from "@/generated/prisma/client";
とすれば、以下のようにカテゴリの「型」を参照できるようになります。
つづいて、VSCode から、次のようにプロジェクトのルートに dev.db
(SQLiteのDBファイル) が作成されていることを確認してください。
Prismaによって作成されたDBの内容は「Prisma Studio」というツールを使って、ウェブブラウザから内容を確認することができます。Prisma Studio は、VSCodeのターミナルから、以下のコマンドで起動することができます (デフォルトでは 51212番ポート で起動します)。
npx prisma studio
Prisma Studio のトップ画面の左パネルには、テーブル一覧 (モデル一覧) が表示されます。いまは「投稿記事のテーブル」を確認したいので「Post」をクリックしてください。
画面が切り替わり、下図のように「投稿記事テーブル (Post)」の「カラム
(列)
の構成」を確認することができます。現時点では、まだ投稿記事のレコードが1件も存在しませんが、レコード追加後は、その内容について確認・編集することもできます。
- 「Insert row」のボタンを押下すると、この画面からレコードを追加することもできます (いまは追加しないでください)。
- レコードが存在するときは、その内容を編集することもできます。
- Prisma Studio では スキーマの変更 (カラムの追加や削除、名前変更など)
はできません。スキーマを変更するためには
schema.prismaの編集が必要になります。
- Prisma Studio では スキーマの変更 (カラムの追加や削除、名前変更など)
はできません。スキーマを変更するためには
VSCode のターミナルで Ctrl+C を入力すると Prisma Studio
を終了することができます。一旦、Prisma Studio を終了しておいてください。
3.11 DBの初期データ投入の準備 (src/lib/prisma.ts の新規作成)
Prisma を使って DB を操作するとき (たとえば、初期データの投入するとき) は、PrismaClient クラスからインスタンスを生成し、それを利用します。しかし、DB 操作の処理毎に PrismaClient のインスタンスを生成すると、DB 接続数が無駄に増えてリソースを圧迫するという問題 が生じます。
くわえて、Next.js を開発モード (npm run dev で起動)
では、ホットリロード (=ファイルの変更を検知して自動的にアプリケーションを再実行する機能)
のたびに PrismaClient が重複生成されて接続数が増えていくという問題があります。
そこで、アプリ全体で 1 つだけ PrismaClient インスタンスを共有する仕組み
が必要になります。以下の src/lib/prisma.ts は、そのための実装になります。
プロジェクトフォルダに src/lib/prisma.ts
というファイルを新規作成して、以下の内容を記述してください。
import { PrismaClient } from "@/generated/prisma/client";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
const globalForPrisma = global as unknown as {
prisma: PrismaClient;
};
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL || "file:./dev.db",
});
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
adapter,
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}3.12 DBの初期データの作成
Prisma Studio の画面から1件ずつ手作業でレコードを追加することも可能ですが、ここではプログラムを使って初期データを生成して、それを DB に挿入していきます。なお、データベースの初期データを作成して投入する作業を「シーディング (seeding)」といい、その際に使用する初期データを「シードデータ (Seed Data)」と呼びます。
prisma フォルダのなかに seed.ts
というファイルを新規作成して・・・
以下のコード (シーディングのためのTypeScriptプログラム) を貼付け、保存してください。
import { prisma } from "@/lib/prisma";
const main = async () => {
// 各テーブルから既存の全レコードを削除
await prisma.postCategory?.deleteMany();
await prisma.post?.deleteMany();
await prisma.category?.deleteMany();
// カテゴリデータの作成 (テーブルに対するレコードの挿入)
const c1 = await prisma.category.create({ data: { name: "カテゴリ1" } });
const c2 = await prisma.category.create({ data: { name: "カテゴリ2" } });
const c3 = await prisma.category.create({ data: { name: "カテゴリ3" } });
// 投稿記事データの作成 (テーブルに対するレコードの挿入)
const p1 = await prisma.post.create({
data: {
title: "投稿1",
content: "投稿1の本文。<br/>投稿1の本文。投稿1の本文。",
coverImageURL:
"https://w1980.blob.core.windows.net/pg3/cover-img-red.jpg",
categories: {
create: [{ categoryId: c1.id }, { categoryId: c2.id }], // ◀◀ 注目
},
},
});
const p2 = await prisma.post.create({
data: {
title: "投稿2",
content: "投稿2の本文。<br/>投稿2の本文。投稿2の本文。",
coverImageURL:
"https://w1980.blob.core.windows.net/pg3/cover-img-green.jpg",
categories: {
create: [{ categoryId: c2.id }, { categoryId: c3.id }], // ◀◀ 注目
},
},
});
console.log(JSON.stringify(p1, null, 2));
console.log(JSON.stringify(p2, null, 2));
};
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});上記のプログラム (prisma/seed.ts) は、Next.js
からは切り離された独立したプログラムになります。
- 第10行目 ~ 第12行目 : 「カテゴリテーブル」に 3件分 のレコードを追加しています。
- 先ほど編集した
schema.prismaの 第30行目~第34行目 で定義しているidやcreatedAt、updatedAt、postsについては 明示的に値を与えていないこと に注意してください。 - 詳しいことは省略しますが、
idには@default(uuid())設定により UUIDv4 が自動代入され、createdAtには@default(now())設定により現在時刻が自動代入されます。
- 先ほど編集した
- 第15行目 ~ 第37行目 : 「投稿記事テーブル」に 2件分 のレコードを追加しています。
- 投稿記事のカテゴリの設定 (第22行目と第34行目)
では、
c1.idやc2.idのように、カテゴリテーブルにレコードを追加したときの戻り値 (c1、c2、c3) からIDを参照して行なっている点に注意してください。
- 投稿記事のカテゴリの設定 (第22行目と第34行目)
では、
- 中間テーブルに対しては 明示的にレコードを追加していないこと を確認してください。
- DBに対する操作 (
deleteManyやcreate) は、すべて PrismaClient (=第01行目 でインポートした PrismaClient のインスタンスprisma) を通じて行なっていることを確認してください。- 「クラス」や「インスタンス」については、昨年度のプログラミング1の講義資料を参照してください。
3.12.1 シーディングコマンドの実行と結果の確認
VSCodeのターミナルから次のコマンドを打ち込んで、シーディング (DBの初期データの生成) を実行してください。
npx prisma db seed
なお、seed.ts の 第39行目 と 第40行目
では、生成した投稿記事の内容を console.log()
でログ出力していますが、このログは以下のように (ウェブブラウザの開発者ツールではなく)
VSCode のターミナルに出力されることに注意してください。
次に、Prisma Studio を起動 (npx prisma studio コマンドを実行) して、DB
の各テーブルにレコードが追加されていることを確認してください。
Prisma Studio では、日時が ISO8601形式
で表示されることを確認してください。末尾の +00:00 は
UTC(協定世界時) を表すことに注意してください。
prisma/seed.ts では、中間テーブル (PostCategory) に対して
レコードを追加する文を記述していませんが、実際には (以下のように)
当該にテーブルに適切なレコードが追加されていること に注意してください。
このような中間テーブルに対するレコードの挿入なども Prisma が提供する ORM の機能となります。
以上のように、Prisma Client を利用することで、SQL文を記述せずに、TypeScript プログラムから DB に対しての CURD操作 (Create、Update、Read、Delete) が可能になります。
3.12.2 演習 (15分)
初期データとして「カテゴリ4」を追加するように
seed.tsを編集し、その後、npx prisma db seedを再実行してください。そして、Prisma Client から「カテゴリ4」が追加されていることを確認してください。初期データとして「投稿3」を追加するように
seed.tsを編集し、実行結果を確認してください。「投稿3」には「カテゴリ1」「カテゴリ3」「カテゴリ4」を設定してください。初期データとして「投稿4」を追加するように
seed.tsを編集し、実行結果を確認してください。「投稿4」には、いずれのカテゴリも設定しないようにしてください。
どうしても実装できないときはこちらを参照してください。
3.13 スキーマに変更を加えたときは…
テーブルに 新しくカラムを追加 したり、カラムの名前を変更 するなどの
スキーマの定義に関わる変更 を schema.prisma
に行ったときは、以下の手続きが必要になります。これらを実行すると DBのすべての レコード と テーブル が完全消去される
ので注意してください。
- ターミナルで
npm run devやnpx prisma studioを実行中しているときは停止する。 - プロジェクトルートの
dev.dbを削除する。 - ターミナルから
npx prisma db pushを実行する。 - ターミナルから
npx prisma generateを実行する。- この処理によって「型」の情報が更新されるので、それが確実に反映されるようにVSCodeの再読み込みを (念のために) 実行する。
- ターミナルから
npx prisma db seedを実行する。
なお、prisma migrate というコマンドを使用すれば 既存のデータを保持したままスキーマの変更を適用することも可能
ですが、開発の初期段階 (消えて困るようなデータが DB に格納されていない状態)
では上記の手続きの方が確実です。
prisma migrate の使い方については公式リファレンスや生成AIなどで調べてください。
以上で、バックエンドで DB を機能させるための準備が完了しました。なお、ここでは、一時的に「SQLite」を使用しますが、最終的には Supabase というクラウドサービスの「PostgreSQL」に切り替えていきます。
4 バックエンド (ウェブAPI) の実装
ここからは、Next.js を使って、ブログアプリ関連の「ウェブAPI」の機能を実装していきます。つまり、前回講義で取り上げた「郵便番号検索API」や「microCMS」のように、特定のエンドポイント (URL) にHTTPリクエストを送ると、JSONフォーマットでデータが取得できる ような機能を Next.js の枠組みで実装していきます。
例えば http://localhost:3000/api/categories というエンドポイントに
HTTPリクエスト (GET) を送ると、以下のような HTTP レスポンス
(主にJSONデータ) が取得できるような機能を Next.js で実装していきます。
4.1 APIの設計
カテゴリに関して次のようにCRUD操作に対応する4個のウェブAPIを実装していきます。カテゴリの新規作成・変更・削除に関係するAPIについては 管理者としてログインしているときだけ利用可能な ようにしていきます。
| エンドポイント | HTTP Method |
処理 | 管理者 権限 |
リクエスト ボディ |
|---|---|---|---|---|
| /api/categories | GET | カテゴリの一覧を取得する | ||
| /api/admin/categories | POST | カテゴリを追加する | 必要 | 必要 |
| /api/admin/categories/[id] | PUT | カテゴリの名前を変更する | 必要 | 必要 |
| /api/admin/categories/[id] | DELETE | カテゴリを削除する | 必要 |
ただし、管理者だけが利用できるように制限する処理は ログイン機能を実装してから追加 します。今回講義のなかでは、誰でも利用可能なAPIとして実装 します。
なお、ここでは「カテゴリ」のCRUD操作について解説しますが、次回以降、(宿題で) 投稿記事に関するCRUD操作を各自で実装してもらう予定なので、しっかりと理解しながら読み進めるようにしてください。
4.2 カテゴリの一覧を取得するウェブAPIの実装
Next.js (App Router) では src/app/api の配下に route.ts
(≠page.tsx)
という名前のファイルを配置することで、ウェブAPIのエンドポイントを作成
することができます。
例えば、以下のように src/app/api/categories/route.ts
というファイルを作成して、そこに GET 関数を定義すると http://localhost:3000/api/categories
というURLで利用可能なエンドポイントが作成できます(GET Method の
HTTPリクエストを受付けて処理できるようになります)。
📂 src/
└─ 📂 app/
└─ 📂 api/
└─ 📂 categories/
└─ route.ts
上記のようにフォルダとファイル (route.ts)
を作成して、以下の内容を貼付けて保存してください。
import { prisma } from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
// [GET] /api/categories カテゴリ一覧の取得
export const GET = async (req: NextRequest) => {
try {
const categories = await prisma.category.findMany({
orderBy: {
createdAt: "desc", // 降順 (新しい順)
},
});
return NextResponse.json(categories);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "カテゴリの取得に失敗しました" },
{ status: 500 }, // 500: Internal Server Error
);
}
};まずは、開発モードでアプリを立ち上げて http://localhost:3000/api/categories
にアクセスして、以下のように 「カテゴリの一覧」が
JSON形式で得られること
を確認してください。なお、Chromeでは、画面上部の「プリティ
プリント」にチェックを入れないと下図のように整形表示されないので注意してください。
以下、route.ts のコードを解説していきます。
まず、第01行目では、前のセクションで作成した
src/app/lib/prisma.ts のなかで定義した PrismaClient (=O/R
Mapper) のインスタンスである prisma をインポートしています。基本的は、この
prisma を通じて DB とデータのやりとりをします。
第07行目 から 第11行目 では、DB のカテゴリテーブルから
レコード全件 を取得して、定数 categories
に格納しています。ここでは、第01行目 でインポートした prisma
が持つ findMany
というメソッドを使っていますが、このメソッドは「非同期メソッド」なので、await が必要になること
に注意してください。ここでは、findMany に対して引数で「createdAt
属性に基づいて降順にソート」するようにも指示しています。戻り値を代入している
categories ですが、下図のようにエディタ上でマウスカーソルを重ねると
オブジェクトの「配列」 になっていることが確認できます。
このように 第07行目 の categories の型は TypeScript
により「型推論」されますが、もし明示的に「型」を指定したい場合は、ファイルの冒頭に
import { Category } from "@prisma/client"; を追加したうえで
const categories: Category[] = ... のようにします (次の図を参照)。
第05行目では、アロー関数形式で GET
という関数を定義して処理を記述していますが、ここでの 名前の設定
は非常に重要です。GET という関数名を付けることで「HTTP GET
リクエスト」を受付けるようになります。これを Get や
get、GetCategories
などに変更すると機能しなくなるので注意してください。
GET 以外の HTTP Method (POST、PUT、DELETE) を受け付けるウェブAPIを実装するときは、そのメソッドにあわせて関数名を設定する必要があります。HTTP Method の概要が分からないときは、生成AIを使って概要を把握しておいてください。
route.ts のなかに記述する GET / POST / PUT / DELETE 関数は、基本的に
NextRequest を引数にして「HTTP
Request」の情報を読み取り、NextResponse で「HTTP
NextResponse」を生成するように構成します。
(プロンプト例)
Next.js (App Router) でウェブAPIを実装しようとしています。そのための前提知識として HTTP Method について解説してください。
4.2.1 定着確認
- CRUD操作に対応する4つの HTTP Method を答えよ。
- 答え: POST、GET、PUT、DELETE
- 新しいリソースを作成するために使用される HTTP Method を答えよ。
- 答え: POST
- 既存のリソースを更新するために使用される HTTP Method を答えよ。
- 答え: PUT
- リソースを取得するため使用される HTTP Method を答えよ。
- 答え: GET
- リソースを削除するために使用される HTTP Method を答えよ。
- 答え: DELETE
HTTPに関する文脈において「リソース」とは URLでアクセス可能なデータや情報、ファイル を指します。
4.3 httpYac を使ったAPIのテスト
前回講義のなかで導入した
httpYac (VSCodeの拡張機能) を使って、前のセクションで実装した
APIのエンドポイント /api/categories のテスト (動作検証)
を行ないます。予め開発モードでアプリを起動 (npm run dev) しておいてください。
devtools/httpyac/LocalApi.http というファイルを新規作成して、以下のように HTTP
Method として「GET」により、エンドポイントに
http://localhost:3000/api/categories
にリクエストを送るためのコードを記述してください。
つづいて、当該セクションをアクティブにして [Ctrl]+[Alt]+[R]
を押下するか、send を押下してリクエストを実行してください。
成功すると、次のような応答が返ってきます (あらかじめ npx run dev
で開発モードでアプリを起動しておく必要があります)。
レスポンスでは、ステータスコード (上記の「200 - OK」の箇所) が重要となります。基本的には200番台が「成功」、その他、400番台、500番台が「失敗」を表します。知能情報実験実習2の後期のテーマ (サーバ構築基礎実習) のなかでもステータスコードについて習っていると思います。概要を把握しておいてください。
(プロンプト例)
HTTPレスポンスのステータスコードについて、主要なものを解説してください。
4.3.1 定着確認
- HTTPレスポンスのステータス「OK」を表わす番号を答えよ。
- 答え: 200
- HTTPレスポンスのステータス「Unauthorized」を表わす番号を答えよ。
- 答え: 401
- HTTPレスポンスのステータス「Bad Request」を表わす番号を答えよ。
- 答え: 400
- HTTPレスポンスのステータス「Internal Server Error」を表わす番号を答えよ。
- 答え: 500
- HTTPレスポンスのステータス「Method Not Allowed」を表わす番号を答えよ。
- 答え: 405
4.3.2 演習 (5分)
バックエンドで処理される route.ts のなかに console.log()
を記述すると、そのログが VSCodeのターミナル
に出力されることを確認してください。
VSCode の補完機能により、意図せずに import console from "inspector"
などがファイルに追加されていることがあります。それらがインポートされていると
console.log() を実行しても
VSCodeのターミナルにログが出力されないこと
があるので注意してください。console に関して見慣れないインポートがあれば削除してください。
4.3.3 演習 (10分)
PrismaClient の各種メソッドのうち、findMany
のようにDBとデータのやりとりをするメソッドは基本的に「非同期処理」になっています。非同期メソッドは
await と組み合わせないと期待する動作をしません
(awaitの付け忘れは、初心者がハマるポイントです)。
- 同期処理と非同期処理 (=
awaitとasyncの基本的な使用法) については前回講義で学習済みです。理解に不安がある人は復習しておいてください。
以下のコードのように findMany メソッドに await
を付け忘れると、どのようになるか (=コンパイルエラーになるのか、ランタイムエラー
(実行時エラー) になるのか、エラーが発生しないのか) を実際に確認してみてください。
import { prisma } from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
// [GET] /api/categories カテゴリ一覧の取得
export const GET = async (req: NextRequest) => {
try {
const categories = prisma.category.findMany({ // ◀ 意図的にawait忘れ
orderBy: {
createdAt: "desc",
},
});
return NextResponse.json(categories);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "カテゴリの取得に失敗しました" },
{ status: 500 },
);
}
};コンパイルエラーも、実行時エラーも発生せず、非常に解決しずらいバグ🐛が誕生していること (=何が問題でレスポンスが空になっているか分からない) に注意してください。
一方で、次のように categories に
明示的に型を与えると、await を忘れているときに
コンパイルエラーが発生して注意喚起してくれること
(問題箇所が把握しやすいこと、問題を早期発見できること) を確認してください。
import { prisma } from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@/generated/prisma/client"; // ◀ 型をインポート
// [GET] /api/categories カテゴリ一覧の取得
export const GET = async (req: NextRequest) => {
try {
// ▼ 型を明示している
const categories: Category[] = prisma.category.findMany({
orderBy: {
createdAt: "desc", // 降順 (新しい順)
},
});
return NextResponse.json(categories);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "カテゴリの取得に失敗しました" },
{ status: 500 }, // 500: Internal Server Error
);
}
};この例のように 「型」を明示するような堅牢なコード を記述することで、結果的として効率的に開発を進めることできます。可読性を損なわない範囲で「型」を明示するようにしてください。
4.3.4 演習 (10分)
次のように意図的にエラーを発生させるコード (第12行目) を追加してください。そのうえで、Thunder Client から HTTPリクエスト を送信し、そのレスポンスがどのようになるか (ステータスコードやレスポンスのボディがどのようになるか) を確認してください。
また、VSCodeのターミナルのログも確認してください。
import { prisma } from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@/generated/prisma/client";
export const GET = async (req: NextRequest) => {
try {
const categories: Category[] = await prisma.category.findMany({
orderBy: {
createdAt: "desc",
},
});
throw new Error("意図的にエラーを発生させる!!!"); // ◀ 追加
return NextResponse.json(categories);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "カテゴリの取得に失敗しました" },
{ status: 500 } // 500: Internal Server Error
);
}
};- 例外処理
try ~ catchの概念についてはプログラミング1で学習済みです。理解に不安がある場合は、生成AIなどを利用して概要を理解してください。
(プロンプト例)
TypeScriptにおける例外処理について解説してください。特に
try、catch、throwのキーワードと使い方について解説してください。
4.3.5 演習 (15分)
api/categories/route.ts
について、HTTPリクエストを正常に処理できた場合の「戻り値
(=NextResponse)」を、次のように変更したとき、レスポンスの「ステータスコード」と「レスポンスのボディ」がどのように変化するか
(あるいは変化しないのか) を Thunder Client から確認してください。
また、各変更に「どのような意味があるか (あるいは意味がないか)」 についても、生成AIを利用して考察してみてください。
(プロンプト例)
Next.js (App Router) の API Routes の戻り値として
return NextResponse.json(categories);をreturn NextResponse.json(categories, { status: 200 });に書き換えることには、どのような意味や意図があると考えられますか。
変更1 : NextResponse.json
メソッドの第2引数に「ステータスコード「200」」を明示的に与える。
変更2 : NextResponse.json メソッドの 第1引数
を次のように変更する。
4.3.6 EX演習 (宿題: 20分)
prisma.category.findMany の 第1引数
に与えるオブジェクトを次のように変更すると、categories
に格納されるデータがどのように変わるかを確認してください
(ThunderClientでレスポンスのボディを確認してください)。
なお、ここでは categories: Category[] = ...
のように型を指定するとコンパイルエラーになるので注意してください。
const categories = await prisma.category.findMany({
select: {
id: true,
name: true,
},
orderBy: {
createdAt: "desc",
},
});const categories = await prisma.category.findMany({
select: {
id: true,
name: true,
posts: {
select: {
post: {
select: {
id: true,
title: true,
},
},
},
},
},
orderBy: {
createdAt: "desc",
},
});const categories = await prisma.category.findMany({
select: {
id: true,
name: true,
_count: {
select: {
posts: true,
},
},
},
orderBy: {
createdAt: "desc",
},
});(プロンプト例)
Next.js において Prisma を使ってバックエンド開発をしています。
schema.prismaは次のようになっています。
~ (ここにschema.prismaを貼付け) ~
次のように PrismaClient のfindManyメソッドを実行すると、categoriesにはどのような値が得られますか。特に、メソッド引数のselectと_countの意味について解説してください。
演習を完了したら…
演習に取り組んだ後は “/api/categories” の レスポンスボディが以下の形式になるように、一旦、戻しておいてください。次回講義では、この形式のレスポンスであることを前提にフロントエンドを実装します。
[
{
"id": "db450dc7-a73b-4311-8b70-3af652efd144",
"name": "カテゴリ4",
"createdAt": "2024-12-08T12:00:42.713Z",
"updatedAt": "2024-12-08T12:00:42.713Z"
},
{
"id": "412b3199-5ae1-45eb-a224-8f53ba3790d0",
"name": "カテゴリ3",
"createdAt": "2024-12-08T12:00:42.707Z",
"updatedAt": "2024-12-08T12:00:42.707Z"
},
// ...略...
{
"id": "ad28ee69-359f-4d66-aaea-1680c97347b8",
"name": "カテゴリ1",
"createdAt": "2024-12-08T12:00:42.695Z",
"updatedAt": "2024-12-08T12:00:42.695Z"
}
]カスタマイズしたコードは削除せずに コメントアウトして残しておくことをお勧めします。あとの授業のなかで、ブログアプリをカスタマイズ・拡張してもらう課題の出題があります。
4.4 カテゴリを追加するウェブAPIの実装
カテゴリを追加 (新規作成) するウェブAPIを実装します。
このAPIは、将来的に (次々回以降の講義で) 管理者でログインしている場合のみ受理される
ように拡張していくので、エンドポイントを /api/admin/categories
のように設計したいと思います。また、リソースの追加処理に相当するので HTTP Method の POST に対応させるように実装します。
次の位置に route.ts を新規作成してください。
📂 src/
└─ 📂 app/
└─ 📂 api/
├─ 📂 categories/
│ └─ route.ts ( GET 作成済み )
└─ 📂 admin/
└─ 📂 categories/
└─ route.ts ◀◀◀ 新規作成
次のプログラムを route.ts
に貼り付けてください。コードの解説は後回しにします。
import { prisma } from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@/generated/prisma/client";
type RequestBody = {
name: string;
};
export const POST = async (req: NextRequest) => {
try {
const { name }: RequestBody = await req.json();
const category: Category = await prisma.category.create({
data: {
name,
},
});
return NextResponse.json(category);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "カテゴリの作成に失敗しました" },
{ status: 500 },
);
}
};開発モード (npm run dev) でアプリを起動して、以下のように
httpYac からAPIをテストしてください。この APIは、リソース (カテゴリ)
を追加をするもの なので、HTTP Method
を「POST」にしていることに注意してください。また、エンドポイントのパスに
admin を含めている点にも注意してください。
さらに、HTTPリクエストのボディ (Body) に、新規作成するカテゴリの
名前 (name) をJSON形式で与えていることに注目してください。
# [Ctrl]+[Alt]+[R] で各セクションを実行
### カテゴリ一覧の取得
GET http://localhost:3000/api/categories
### カテゴリの作成
POST http://localhost:3000/api/admin/categories
Content-Type: application/json
{
"name": "カテゴリ5"
}なお、既存のカテゴリの名前 (=例えば、シーディングで挿入した「カテゴリ1」など)
をボディに設定してリクエストを送信すると Unique constraint failed on the
fields: (name) というエラーで失敗します
(その原理で、同じリクエストを2回送信したときにも失敗します)。これは
schema.prisma の 第31行目 で @unique (ユニーク制約
(値の重複を許可しない制約)) を与えているためです。
(プロンプト例)
Prisma の
schema.prismaでname String @uniqueのようにカラムを設定しています。このカラムに適用される制約について解説してください。
つづいて、Prisma Studio を起動して (npx prisma studioを実行して)
カテゴリテーブルにレコードが追加されていることを確認してください。あるいは
/api/categories に GETリクエスト
を送って、そのレスポンスから カテゴリが正常に追加されていること
を確認してください。
VSCode: ターミナルの追加と切り替え
VSCodeでは、下図のように「+」ボタンを押下するとターミナル (セッション)
を追加することができます。そして、1個目のターミナルで rpm run dev
を実行して、2個目のターミナルで npx prisma studio
を実行するような使い方ができます。
4.4.1 演習
- HTTPリクエストのボディを以下のように設定して POST したとき、レスポンスがどのようになるかを Thunder Client を利用して確認してください。
{
"title": "カテゴリ9"
}- HTTPリクエストのボディを以下のように設定して POST したとき、どのようなレスポンスになるかを Thunder Client を利用して確認してください。
{
"age": 20,
"name": "カテゴリ9",
}- HTTP Method を「POST」から「GET」に切り替えて
/api/admin/categoriesにリクエストすると、どのようなレスポンスになるか (特に、どのようなステータスコードになるか) を Thunder Client を利用して確認してください。
4.5 カテゴリを追加するウェブAPIの実装 (コードの解説)
解説のためにコードを再掲します。
import { prisma } from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@/generated/prisma/client";
type RequestBody = {
name: string;
};
export const POST = async (req: NextRequest) => {
try {
const { name }: RequestBody = await req.json();
const category: Category = await prisma.category.create({
data: {
name,
},
});
return NextResponse.json(category);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "カテゴリの作成に失敗しました" },
{ status: 500 },
);
}
};第05行目 から 第07行目 で、HTTPリクエストの
ボディ として受け取るオブジェクトの型を RequestBody
として定義しています。
そして、第11行目 で、実際にリクエストのボディから name
の値を取得しています。この取得に際しては、TypeScript (JavaScript) の 分割代入
というテクニックが使われています
(分割代入については生成AIを利用して理解しておいてください)。第11行目
の処理を分割代入を使わずに分解して書けば、以下のようになります。
なお、req.json メソッドも非同期なので await
を忘れないようにしてください。
(プロンプト例)
TypeScript (JavaScript) における
const { id, name } = userのような「分割代入」について解説してください。
TypeScript (JavaScript) の「分割代入」には、どのようなメリットとデメリットがありますか。
第12行目 から 第15行目 では、PrismaClient の
create というメソッドを使ってカテゴリを追加しています。create
メソッドでは、実際に追加されたレコード (単体のカテゴリ) が戻り値となります
(厳密には、create メソッドの戻り値を await
した戻り値がレコードになります)。そして、その値をそのまま 第17行目
でHTTPレスポンスのボディに設定しています。
4.6 カテゴリを削除するウェブAPIの実装
api/admin/categories/[id]
のようなエンドポイントに、カテゴリを削除するAPI
を実装します。このAPIも管理者だけが実行できるように拡張する予定なので、次のように
admin を含むパスに配置します。
📂 src/
└─ 📂 app/
└─ 📂 api/
├─ 📂 categories/
│ └─ route.ts ( GET 作成済み )
└─ 📂 admin/
└─ 📂 categories/
├─ route.ts ( POST 作成済み )
└─ 📂 [id]/
└─ route.ts ◀ 新規作成
前回講義で解説したように、Next.js
において [id] のような角括弧で囲んだ表記は
パラメータプレースホルダ
といい、そこには動的に値が入ること意味します。例えば、削除したいカテゴリの id が
24f932b8-....-569f07ba16a7
の場合、具体的なエンドポイントは次のようになります。
/api/admin/categories/24f932b8-...-569f07ba16a7
route.ts に次のプログラムを貼付けて保存してください。
import { prisma } from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@/generated/prisma/client";
type RouteParams = {
params: Promise<{
id: string;
}>;
};
export const DELETE = async (req: NextRequest, routeParams: RouteParams) => {
try {
const { id } = await routeParams.params;
const category: Category = await prisma.category.delete({ where: { id } });
return NextResponse.json({ msg: `「${category.name}」を削除しました。` });
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "カテゴリの削除に失敗しました" },
{ status: 500 },
);
}
};URLパスのパラメータプレースホルダ [id]
で与えられる値は、第05行の RouteParams
の型定義、第11行目 の DELETE
関数定義の「第2引数」、第13行目 の
const { id } = await routeParams.params
によって取得できます。例えば、/api/admin/categories/aaa
というエンドポイントにリクエストがあれば、第13行目 の id には
"aaa" という文字列が代入されます。
削除処理は 第14行目 にある prisma.category.delete()
メソッドにより実行されます。このメソッドは、指定された条件に一致する「単一のレコード」を削除する機能
を持っています。
deleteメソッドに対する条件の与え方
deleteメソッドには、引数を使って「削除対象を指定する条件」を与えます。例えば、"c-1234"
という id のレコード (カテゴリ)
を削除したいときは、次のように引数を指定します。
この処理は、id という変数を使って次のようにも書けます。
TypeScript (JavaScript)
では、オブジェクトの「プロパティ名」と「値の変数名」が同じ場合、省略記法を使うことができます。つまり、id: id
を、単に id と書くことができます
(省表記が利用できます)。これを適用すれば、先のコードは、次のように記述することができます。
慣れるまでは非常に分かりづらいと思いますが、実務では頻繁に利用される記法なので覚えておいてください。第14行目 は、この略表記を使って記述されています。
なお、このような記法を オブジェクトリテラルのプロパティ省略記法 (Property Shorthand) といいます。
なお、delete
メソッドは削除に成功したとき、当該のレコードを戻り値とします。第15行目
では、その戻り値を利用して、「カテゴリ5」を削除しました。 のような HTTP
レスポンスを返すようにしています。
ファイルの編集が完了したら開発モードでアプリを起動して、httpYac からテストを実行してください。HTTP Method を「DELETE」とします。
エンドポイントの [id] の部分 (=削除したいカテゴリのID @post_id)
については、Prisma Studio から調べて設定してください。
# [Ctrl]+[Alt]+[R] で各セクションを実行
### カテゴリ一覧の取得
GET http://localhost:3000/api/categories
### カテゴリの作成
POST http://localhost:3000/api/admin/categories
Content-Type: application/json
{
"name": "カテゴリ5"
}
### カテゴリの削除
@post_id = 46562fea-edf7-4d72-9ac3-8add07b38260
DELETE http://localhost:3000/api/admin/categories/{{post_id}}存在しない id
をエンドポイントに指定した場合、どのようなレスポンスとなるか確認しておいてください。
4.7 カテゴリを編集するウェブAPIの実装
カテゴリの 名前を変更 (更新) するためのウェブAPI を実装します。具体的には、次のようなエンドポイントに対して、新しい名前の情報を持ったボディを「PUTリクエスト」で送信することで、これを実現します。
/api/admin/categories/24f932b8-...-569f07ba16a7
既に気づいたかもしれませんが、このエンドポイントは DELETEメソッドと共通 になります。route.ts
には複数のメソッドを含めることが可能なので api/admin/categories/[id]/route.ts
を次のように編集してください。
import { prisma } from "@/lib/prisma";
import { NextResponse, NextRequest } from "next/server";
import { Category } from "@/generated/prisma/client";
type RouteParams = {
params: Promise<{
id: string;
}>;
};
// ▼▼▼ 追加: ここから ▼▼▼
type RequestBody = {
name: string;
};
export const PUT = async (req: NextRequest, routeParams: RouteParams) => {
try {
const { id } = await routeParams.params;
const { name }: RequestBody = await req.json();
const category: Category = await prisma.category.update({
where: { id },
data: { name },
});
return NextResponse.json(category);
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "カテゴリの名前変更に失敗しました" },
{ status: 500 },
);
}
};
// ▲▲▲ 追加: ここまで ▲▲▲
export const DELETE = async (req: NextRequest, routeParams: RouteParams) => {
try {
const { id } = await routeParams.params;
const category: Category = await prisma.category.delete({ where: { id } });
return NextResponse.json({ msg: `「${category.name}」を削除しました。` });
} catch (error) {
console.error(error);
return NextResponse.json(
{ error: "カテゴリの削除に失敗しました" },
{ status: 500 },
);
}
};第20行目 の update メソッドでは、引数に与えるオブジェクトの
where
プロパティで「更新対象のレコードを指定」し、data
プロパティで「更新する値 (上書きする値)」を指定しています。
ここでも、先と同じく {id: id} を { id }
のように略表記するテクニック、{ name: name } を { name }
のように略表記するテクニックが使用されています。
ファイルの編集が完了したら開発モードでアプリを起動して、httpYac でテストしてください。
# [Ctrl]+[Alt]+[R] で各セクションを実行
### カテゴリ一覧の取得
GET http://localhost:3000/api/categories
### カテゴリの作成
POST http://localhost:3000/api/admin/categories
Content-Type: application/json
{
"name": "カテゴリ6"
}
### カテゴリの削除
@post_id = 46562fea-edf7-4d72-9ac3-8add07b38260
DELETE http://localhost:3000/api/admin/categories/{{post_id}}
### カテゴリの名前を変更
@post_id = 05400979-05c1-4c14-a477-6ed54013a963
PUT http://localhost:3000/api/admin/categories/{{post_id}}
Content-Type: application/json
{
"name": "カテゴリA"
}5 次回の授業
次回の授業では、投稿記事関連のウェブAPIの設計と実装をおこないます。また、フロントエンドから、それらAPIを利用する画面の実装も行ないます。