【第26回】SQLをほとんど書かずに読み書きする|Prisma(嫁子が教えるTS講座)
今回のゴール
- スキーマを書いて、そこから型つきのコードが自動で作られる流れが分かる
findManyやcreateでデータを読み書きできる- マイグレーションが何をしているか分かる
前回(データベースの基礎)で決めた列の定義を、今回そのままコードにします。全体の流れは講座の目次へ。
嫁子: 今日の主役はSQLじゃなくてスキーマだよ。
1か所に形を書くと、そこからデータベースの定義もTypeScriptの型も両方が作られるの。第13回で「型が発注書になる」って言ったでしょ。あれのデータベース版だね。
1. Prismaが引き受けてくれること
Prismaを使うと、次の3つが1つのファイルから出てきます。
| 出てくるもの | 手で書くとしたら |
|---|---|
| テーブルを作るSQL | CREATE TABLE ... を自分で書く |
| 読み書きの関数 | SELECT や INSERT を文字列で組み立てる |
| TypeScriptの型 | テーブルに合わせて type を手で書き、ずれないよう管理する |
3つ目が大きいところです。列を1つ足すと、型も一緒に更新されます。手で書いた type とデータベースがずれる、という事故が起きません。
2. 準備する
第18回で作ったプロジェクトに入れます。
npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider sqlite
これで次のものができます(版によって構成が変わるので、実際に生成されたファイルを見てください)。
| ファイル | 役割 |
|---|---|
prisma/schema.prisma |
データの形を書く場所 |
prisma7.config.ts |
接続先などの設定 |
.env |
DATABASE_URL(接続先の文字列) |
.env はGitに含めないでください。第23回で書いた通り、接続情報はサーバー側だけに置くものです。
嫁子: この構成、版によってけっこう変わるところなの。
少し前までは接続先をスキーマの中に書いてたし、設定ファイルも無かった。ネットの記事どおりに進めて食い違ったら、たいていは世代の違いだよ。第18回・第22回でも同じ話をしたよね。
3. スキーマを書く
前回決めた形を、そのまま書きます。
generator client {
provider = "prisma-client"
output = "../app/generated/prisma"
}
datasource db {
provider = "sqlite"
}
model Item {
id Int @id @default(autoincrement())
name String
done Boolean @default(false)
createdAt DateTime @default(now())
}
model の中が、そのまま表の列になります。
| 書き方 | 意味 |
|---|---|
@id |
主キー(前回の話) |
@default(autoincrement()) |
自動で連番を振る |
@default(false) |
何も指定しなければ false |
@default(now()) |
登録時刻を自動で入れる |
String?(末尾に ?) |
空でもよい列 |
? の有無が、そのままTypeScriptの型に反映されます。第12回でやった memo?: string と同じ意味になります。
4. マイグレーションで反映する
スキーマを書いただけでは、データベースはまだ空です。次のコマンドで反映します。
npx prisma migrate dev --name init
これで3つのことが起きます。
- 変更内容のSQLファイルが作られる(
prisma/migrations/の中) - そのSQLがデータベースに適用される
- TypeScript用のコードが生成される
実際に作られたSQLはこうなっていました。
CREATE TABLE "Item" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"done" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
前回覚えた CREATE TABLE が、スキーマから自動で作られています。自分で書かなくても、何が起きたかは読めば分かります。
マイグレーションのファイルはGitに含めます。これがあると、別のパソコンでも同じ手順で同じ状態を再現できます。
嫁子: マイグレーションは「データベースの変更履歴」なの。第3回のGitがコードの履歴なら、こっちはテーブルの履歴だね。
列を足すたびにファイルが1つ増えていくよ。それを消さずに残しておくのが大事なの。履歴が飛ぶと、次に誰かが同じ状態を作れなくなるからね。
5. 読み書きする
準備ができたら、TypeScriptから触れます。接続部分は1か所にまとめておきます。
// app/lib/prisma.ts
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "@/app/generated/prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
function createClient() {
const adapter = new PrismaBetterSqlite3({
url: process.env.DATABASE_URL ?? "file:./dev.db",
});
return new PrismaClient({ adapter });
}
export const prisma = globalForPrisma.prisma ?? createClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
少し長いので、要点だけ。
adapter— どのデータベースにつなぐかを渡す部品。SQLiteなら専用のものを入れる(npm install @prisma/adapter-better-sqlite3)globalForPrisma— 開発中に接続が増え続けるのを防ぐ書き方。保存のたびに読み込み直されるため、何もしないと接続が溜まります
この2つは、意味が分からなくてもそのまま使って大丈夫です。毎回書くものなので、AIに「Prismaの接続ファイルを作って」と頼めば出てきます。
読む
const items = await prisma.item.findMany({
orderBy: { createdAt: "desc" },
});
足す
const item = await prisma.item.create({
data: { name: "牛乳" },
});
変える・消す
await prisma.item.update({
where: { id: 1 },
data: { done: true },
});
await prisma.item.delete({
where: { id: 1 },
});
where の指定が、前回の WHERE にあたります。Prismaでは update と delete に where が必須なので、前回書いた「全部消える事故」は起こしにくくなっています。
6. 型が自動でついてくる
ここがPrismaを使ういちばんの理由です。
const items = await prisma.item.findMany();
items[0].name; // string と分かっている
items[0].nmae; // ✕ その場でエラー
items[0].price; // ✕ そんな列は無い、とエラー
スキーマに書いていない列を使おうとすると、第11回で見たように実行前に止まります。
そして列を1つ足すと、npx prisma migrate dev を実行した時点で型も更新されます。手で type を直す作業が発生しません。
嫁子: データベースとコードの型がずれないことが保証されるの。これが手書きのSQLとの一番の違いだよ。
AIに「価格も保存して」と頼むと、スキーマとコードの両方を直してくる。片方だけ直して食い違う、が起きにくいの。
7. 画面につなぐ
第23回で書いた通り、サーバー側の画面からは直接呼べます。
// app/page.tsx
import { prisma } from "@/app/lib/prisma";
export default async function Page() {
const items = await prisma.item.findMany({
orderBy: { createdAt: "desc" },
});
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
登録は第24回の窓口経由になります。窓口の中身を、配列からPrismaに置き換えるだけです。
// app/api/items/route.ts
export async function POST(request: Request) {
const body = await request.json();
const name = String(body.name ?? "").trim();
if (name === "") {
return Response.json({ error: "名前が空です" }, { status: 400 });
}
const item = await prisma.item.create({ data: { name: name } });
return Response.json(item, { status: 201 });
}
ここまでで、ようやく「閉じても消えない」が実現します。第4部の最後に書いた宿題が、ここで解決しました。
つまずきポイント
スキーマを変えたら npx prisma migrate dev を忘れずに。書き換えただけでは、データベースにも型にも反映されません。
生成されたコードの置き場所は設定で決まります。output に書いたパスから読み込むので、import の行がネットの記事と違っていても慌てないでください。
接続先のファイルがどこにできるか確認してください。DATABASE_URL="file:./dev.db" の ./ がどこを指すかは環境によります。実際に生成された場所を見るのが確実です。
版によって手順が変わります。この回を書いた時点(2026年9月)で確認したのは次の組み合わせです。
| パッケージ | 版 |
|---|---|
| prisma / @prisma/client | 7.10.0 |
| @prisma/adapter-better-sqlite3 | 7.10.0 |
| next | 16.3.4 |
今回のまとめ
- スキーマ1か所から、SQL・読み書きの関数・TypeScriptの型が作られる
npx prisma migrate devで、変更履歴を残しながら反映する- 読み書きは
findMany/create/update/delete。whereは必須 - 列を足すと型も自動で更新される。手で
typeを直さなくていい - 接続部分は1か所にまとめる。意味が分からなくてもそのまま使ってよい
- 構成は版で変わる。ネットの記事と食い違ったら世代の違いを疑う
次にやること
次回は認証の入口です。ここまでで作ったデータは「誰のものか」を持っていません。人に使ってもらうなら、そこが必要になります。
今回の宿題:前回考えた「値段の列」を、スキーマに足して npx prisma migrate dev --name add-price を実行してください。新しいSQLファイルが1つ増えることと、型に price が現れることを確認するのが目的です。
嫁子: 1行足してコマンドを打つだけなんだけど、そのあと
item.priceが補完に出てくる瞬間がちょっと気持ちいいと思う。手で型を書いてないのに出てくるからね。そこがPrismaを使う理由そのものだよ。