Chapter 02

データベースを扱う (Prisma vs ActiveRecord)

ykpythemind
ykpythemind
2022.09.28に更新

さっそくデータベースを扱っていきましょう。

T3 Stackでは Prisma をORMマッパーとして使用します。
環境構築時に打ったコマンド、 yarn prisma db push が、Railsの rails db:setup にあたるようなものです。

migration

ActiveRecordのmigrationが積み上げ型であるのに対し、PrismaはPrisma schemaを定義することで自動でSQL文を発行してくれます。[1]

schema.prisma ファイル

スキーマは prisma/schema.prisma ファイルに存在します。すでに create-t3-appがscaffoldingしてくれています。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "sqlite"
    // NOTE: When using postgresql, mysql or sqlserver, uncomment the @db.Text annotations in model Account below
    // Further reading: 
    // https://next-auth.js.org/adapters/prisma#create-the-prisma-schema
    // https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#string
    url      = env("DATABASE_URL")
}

model Example {
    id String @id @default(cuid())
}

// .... 略 ....

データベースの接続先情報と、modelの定義が書かれています。

初期化時には sqlite を開発用dbとして使うことになります。
datasourceディレクティブの中にある urlが接続先情報で、DATABASE_URL環境変数を接続先情報にしています。(これはRailsでもある挙動ですね。)

DATABASE_URL環境変数は 初期化時に.envファイルにすでに記入されているので ( 別チャプター:環境変数 で紹介します ) 特にデータベースを起動する必要なく開発に入ることができます。

modelの定義を書く

Prismaのドキュメント(Data model )を見ながら、
Railsチュートリアル第2章(Toyアプリケーション)と同様のモデル設計を書いていきます。

User has many Microposts の関連を作ります。Userモデルは初期化時に作成されているので、そこに追加する形で schema.prisma を編集します。

diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 3184c21..f2fc68b 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -53,6 +53,14 @@ model User {
     image         String?
     accounts      Account[]
     sessions      Session[]
+    microposts    Micropost[]
+}
+
+model Micropost {
+    id Int @id @default(autoincrement())
+    content String
+    userId String
+    user User @relation(fields: [userId], references: [id], onDelete: Cascade)
 }
 
 model VerificationToken {

model Micropostに対してmicropostsテーブルの内容がマッピングされることになります。
content String でTEXT型のカラムcontentを宣言しています。

フィールドは @id @relation などの属性を付与することができます。
@relation で宣言した User フィールドは、 userIdを外部キーとして User modelの内容を関連として引いてくるようになります。 ( Railsの belongs_to :user と同じ )

また、User model側からもMicropostが引けるよう、 model User内にも変更を加えます。( microposts Micropost[] の部分。これがないと2022/09/26現在migrationが通りませんでした。)

prisma db push

それではschema.prismaで定義したスキーマをデータベースに反映しましょう。Prismaは開発用の便利なコマンドとして db push を用意しています。

これは ドキュメントにもあるように プロトタイピングを目的とした開発用のコマンドのようです。

基本的には schema.prismaの内容をよしなにデータベースに反映してくれるようです。 [2]

クエリ

Prisma clientを用いてクエリを発行します。
その前に、適当にレコードを追加しておきます。

$ cat /tmp/sql
insert into user (id, name) values ('aaa', 'ykpythemind1');
insert into user (id, name) values ('bbb', 'ykpythemind2');
insert into user (id, name) values ('ccc', 'ykpythemind3');

insert into micropost (content, userId) values ("test post", "aaa");
insert into micropost (content, userId) values ("test post 2", "aaa");

$ sqlite3 prisma/db.sqlite < /tmp/sql

$ sqlite3 prisma/db.sqlite 'select * from user;'
aaa|ykpythemind1|||
bbb|ykpythemind2|||
ccc|ykpythemind3|||

$ sqlite3 prisma/db.sqlite 'select * from micropost;'
6|test post|aaa
7|test post 2|aaa

楽に検証できるようts-nodeをセットアップしておきます。

$ yarn add -D ts-node
diff --git a/tsconfig.json b/tsconfig.json
index 0608fd0..97ea0ad 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,4 +1,10 @@
 {
+  "ts-node": {
+    // these options are overrides used only by ts-node
+    "compilerOptions": {
+      "module": "commonjs"
+    }
+  },
   "compilerOptions": {
     "target": "es5",
     "lib": ["dom", "dom.iterable", "esnext"],

それではRailsでの User.allのようなクエリを実行してみます。

import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

async function main() {
  const allUsers = await prisma.user.findMany();

  console.debug(allUsers);
}

main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

上記のようなファイルを用意し、example.ts として保存し、実行しましょう。

$ yarn ts-node example.ts

登録されているuserレコードを3件取得することができました。

このチャプターの以降のスクリプトはすべて example.tsの main() 内を変更したものとしてください。

ActiveRecord::FinderMethods#find -> findUnique/findUniqueOrThrow

ユニークなカラムから検索して1つ返します。

  const user = await prisma.user.findUnique({ where: { id: "aaa" } });
  console.debug("user", user);
  
  const user2 = await prisma.user.findUniqueOrThrow({ where: { id: "not exist" } });
  console.debug("user2", user2);

findUniqueOrThrow の方が ActiveRecordのfind に近い挙動ですね。

ActiveRecord::FinderMethods#find_by -> findFirst

条件に当てはまるものを1つだけとってくるやつです。

  const user = await prisma.user.findFirst({ where: { name: "ykpythemind1" } });
  console.debug("user", user);

  const user2 = await prisma.user.findFirst({ where: { name: "nobody" } });
  console.debug("user2", user2);

ActiveRecord::QueryMethods#order -> orderBy オプション

  const users = await prisma.user.findMany({ orderBy: { name: "desc" } });
  console.debug(users.map((u) => u.name));

findManyやfindFirstなどの引数には orderByやselect, whereなどのオプションを渡すことができます。絞り込みに使うフィールドは型の補完がしっかり効きます。

関連を引く (has_many) -> includeオプション

Railsで以下のような関連を引く処理は、Prismaでは includeオプション を明示的に指定しておかないと引くことができません。( findFirst等で返却されるmodelはプリミティブな型のみで構成されるオブジェクトなので、メソッド呼び出しができません)

include: { microposts: true } をしておくことでmicropostsというプロパティが生えています。

  const user = await prisma.user.findUnique({
    where: { id: "aaa" },
    include: { microposts: true },
  });

  if (user) {
    console.debug("microposts", user.microposts);
  }

joins(xxx).where(xxx) -> where + some

  const users = await prisma.user.findMany({
    where: { microposts: { some: { content: "test post" } } },
  });

  if (users.length > 0) {
    console.debug("found users with test post");
  }

whereの中身にリレーション名を入れて、関連を絞り込んで検索することができます。
この場合は、 "test post" というMicropostを持っているユーザー一覧を取得します。

  • some ... 1件でも持っている場合 ( ユーザーが1件でも "test post" という Micropostを持っている場合 )
  • none ... 1件も持っていない場合 ( ユーザーのMicropostに "test post" という Micropostを所持していないユーザーを調べる )
  • every ... すべての関連レコードが条件を満たす場合 ( ユーザーのMicropostすべてが "test post" であるユーザーを調べる )

[3]

ActiveRecord::Persistence::ClassMethods#create -> create

  await prisma.user.create({
    data: { name: "taro" },
  });

nameカラムが "taro" の userレコードをインサートします。

以下のように関連付けも含めてインサートすることも可能です。

  await prisma.user.create({
    data: {
      name: "taro",
      microposts: { create: [{ content: "hello" }, { content: "good night" }] },
    },
  });

ActiveRecord::Persistence#update -> update

  await prisma.user.update({
    where: { id: "aaa" },
    data: {
      name: "Alice",
    },
  });

idが"aaa"のUserのnameをAliceに更新します。ここでのwhereの指定にはユニークなフィールドしか指定できません。(型エラーになります)

ActiveRecord::Relation#update_all -> updateMany

  await prisma.user.updateMany({
    where: { name: { startsWith: "ykpy" } },
    data: {
      name: "Alice",
    },
  });

nameカラムの内容が 'ykpy' から始まるものをすべて Aliceに更新します。

ActiveRecord::Persistence::ClassMethods#destroy -> delete

  await prisma.user.delete({
    where: { id: "aaa" },
  });

idが"aaa"のUserをdeleteします。ここでのwhereの指定にはユニークなフィールドしか指定できません。(型エラーになります)


基本的なCRUDのためのクエリは以上になります。詳細は https://www.prisma.io/docs/concepts/components/prisma-client を参照してください。

集計クエリや生SQLの実行方法などは別チャプターに書く予定です。

続きます。


その他

prisma format

prisma format でschema.prismaを自動整形することができます。

prisma studio

prisma studioPrisma Studio を起動できます。ブラウザでデータベースの内容を確認し、更新することができます。

Image from Gyazo

脚注
  1. ActiveRecordでいう ridgepole のような雰囲気です。 https://www.prisma.io/docs/concepts/components/prisma-migrate/migration-histories のように、差分でsqlファイルを吐いてそれを積み上げていく運用もできそうです。 未検証。 ↩︎

  2. 本番環境の運用は prisma db push ではなく prisma generate で行う想定のようです。これは別チャプターで解説したいと思います ↩︎

  3. 詳しくは https://www.prisma.io/docs/concepts/components/prisma-client/relation-queries#relation-filters にあります。微妙に慣れずミスってしまいそうなところではあります。 ↩︎