【第26回】SQLをほとんど書かずに読み書きする|Prisma(嫁子が教えるTS講座)

今回のゴール

前回(データベースの基礎)で決めた列の定義を、今回そのままコードにします。全体の流れは講座の目次へ。


嫁子: 今日の主役は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つのことが起きます。

  1. 変更内容のSQLファイルが作られる(prisma/migrations/ の中)
  2. そのSQLがデータベースに適用される
  3. 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;
}

少し長いので、要点だけ。

この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

今回のまとめ


次にやること

次回は認証の入口です。ここまでで作ったデータは「誰のものか」を持っていません。人に使ってもらうなら、そこが必要になります。

今回の宿題:前回考えた「値段の列」を、スキーマに足して npx prisma migrate dev --name add-price を実行してください。新しいSQLファイルが1つ増えることと、型に price が現れることを確認するのが目的です。

嫁子: 1行足してコマンドを打つだけなんだけど、そのあと item.price が補完に出てくる瞬間がちょっと気持ちいいと思う。

手で型を書いてないのに出てくるからね。そこがPrismaを使う理由そのものだよ。