🐥

Prismaに入門して、MySQLと接続してCRUD処理を実装してみた

2023/01/14に公開約12,400字

概要

TypeScriptで実装されたORMであるPrismaに入門するために公式チュートリアルやりつつ、そこからCRUD処理を書きました。
公式ではPostgreSQLを使っていますが、私はMySQLの方が慣れているという理由でMySQLを使います。また、公式ではnpm/npxを使っていますが、こちらも本記事ではyarnを使います。

下準備(MySQL側の設定)

※MySQL自体のインストールや環境構築については割愛します。
PrismaからアクセスするためのMySQLユーザーを作成します。

mysql -uroot -p
mysql> create user prisma_tester@localhost identified by 'mysql';
mysql> create database prisma_crud_sample;
mysql> grant all on *.* to prisma_tester@localhost;

ここではユーザー名prisma_tester、パスワードmysqlで作成して、prisma_crud_sampleというDBを作成しています。最後にprisma_testerユーザーに対して権限を付与しています。PrismaはシャドウDBというものを自動で作るようでして、そのためにPrismaから接続するユーザーにはDBを新規作成する権限を与える必要があるようです。

まずは公式チュートリアルそのままやってみる

まず最初に公式チュートリアルをそのまま動かして、DBを操作できることを確かめていきます。
適当にディレクトリを作ります

mkdir prisma_test
cd prisma_test

下記の通りpackage.jsonを書きます。

package.json
{
  "dependencies": {
    "@prisma/client": "4.8.0",
    "@types/node": "^18.11.18",
    "prisma": "^4.8.0",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.4"
  }
}

必要なパッケージをインストールします。

yarn install

下記のコマンドを実行するとprismaディレクトリが作成され、その中にPrismaのDBスキームファイルであるschema.prismaが作成されます

yarn prisma init

schema.prismaは下記のような内容になっています。

schema.prisma
// 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 = "postgresql"
  url      = env("DATABASE_URL")
}

これを下記のように変更します。データベースにMySQLを用いるのでprovider = "mysql"に変更し、後は公式チュートリアルと同じようにUserPostProfileの3つのテーブルを定義しています。

schema.prisma
// 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 = "mysql"
  url      = env("DATABASE_URL")
}

model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  title     String   @db.VarChar(255)
  content   String?
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id])

  @@index([authorId], map: "Post_authorId_fkey")
}

model Profile {
  id     Int     @id @default(autoincrement())
  bio    String?
  userId Int     @unique
  user   User    @relation(fields: [userId], references: [id])
}

model User {
  id      Int      @id @default(autoincrement())
  email   String   @unique
  name    String?
  posts   Post[]
  profile Profile?
}

上記のurl = env("DATABASE_URL")という部分ですが、ここで参照しているDATABASE_URLという変数を.envファイルにて定義しておく必要があります。[DBの種類]://[ユーザー名]:[パスワード]@[ホスト名]:[ポート番号]/[DB名]という形式で指定します。

DATABASE_URL="mysql://prisma_tester:mysql@localhost:3306/prisma_crud_sample"

ここまできたら下記のコマンドでprisma clientを生成します。

yarn prisma generate

そして、下記のコマンドでschema.prismaの内容をDBに書き写します(マイグレーション)。

yarn prisma migrate dev --name init

ここでMySQLを確認すると、下記のようにschema.prismaの内容が反映されていることが確認できます。

mysql -uprisma_tester -p
mysql> use prisma_crud_sample;
mysql> show tables;
+------------------------------+
| Tables_in_prisma_crud_sample |
+------------------------------+
| _prisma_migrations           |
| Post                         |
| Profile                      |
| User                         |
+------------------------------+
4 rows in set (0.01 sec)

確認できましたら、チュートリアル通りにindex.tsを書いていきます。

index.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()
// const prisma = new PrismaClient({log: ['query']})

async function main() {
  await prisma.user.create({
    data: {
      name: 'Alice',
      email: 'alice@prisma.io',
      posts: {
        create: { title: 'Hello World' },
      },
      profile: {
        create: { bio: 'I like turtles' },
      },
    },
  })

  const allUsers = await prisma.user.findMany({
    include: {
      posts: true,
      profile: true,
    },
  })
  console.dir(allUsers, { depth: null })
}

main()
  .catch((e) => {
    throw e
  })
  .finally(async () => {
    await prisma.$disconnect()
  })

下記のコマンドで、index.tsに記述したDB操作を実行します。

yarn ts-node index.ts

エラーが出ずに実行が完了したら成功です。MySQLから確認すると、上記のコードで追加したAliceのデータが格納されていることが分かります。

mysql> select * from User;
+----+-----------------+-------+
| id | email           | name  |
+----+-----------------+-------+
|  1 | alice@prisma.io | Alice |
+----+-----------------+-------+
1 row in set (0.00 sec)

CRUD処理を実装

次に、上記のサンプルを参考にCRUD処理を実装していきます。下記の通り、crud.tsというファイルを作成します。

crud.ts
import { PrismaClient, Prisma } from '@prisma/client'

const prisma = new PrismaClient()

// *******************************************
// CRUD functions
// *******************************************
// [A]ここにCRUD関数を書く


// *******************************************
// sample data
// *******************************************
const user01: Prisma.UserCreateInput = {
  name: 'Alice',
  email: 'alice@prisma.io',
  posts: {
    create: { title: 'Hello World' },
  },
  profile: {
    create: {bio: 'I like turtles' },
  },
}

const user02: Prisma.UserCreateInput = {
  name: 'Viola',
  email: 'viola@prisma.io',
  posts: {
    create: { title: 'Hello World!' },
  },
  profile: {
    create: { bio: 'I like rabbits' },
  },
}


// *******************************************
// execute CRUD
// *******************************************
const main = async () => {
  // [B]ここにCRUD関数の呼び出し処理を書く
}

main()

Create

最初にデータの登録処理を実装します。crud.tsの[A]の箇所に下記のコードを書きます。

crud.ts
const create = async (rec: Prisma.UserCreateInput) => {
  await prisma.user.create({ data: rec })
}

そして、[B]の箇所に下記のコードを書きます。

crud.ts
  console.log('=== Create ===')
  await create(user01)
  await create(user02)

これで、サンプルデータとして定義したAliceとViolaのデータがMySQLに書き込まれます。

Read

次にデータの読み込み処理を実装します。crud.tsの[A]の箇所に下記のコードを書き加えます。

crud.ts
const read = async (cond?: object): Promise<object[]> => {
  if (cond) {  // 検索条件あり
    const selectedUsers = await prisma.user.findMany({
      where: cond,
      include: {
        posts: true,
        profile: true,
      },
    })
    return selectedUsers
  } else {  // 検索条件なし
    const allUsers = await prisma.user.findMany({
      include: {
        posts: true,
        profile: true,
      },
    })
    return allUsers
  }
}

オプション引数condのところに条件が指定されていればその条件にあったデータを検索して返し、指定されていなければ全件取得して返します。
そして、[B]の箇所に下記のコードを書き加えます。

crud.ts
  console.log('=== Read ===')
  let recListAll = await read()
  console.dir(recListAll, { depth: null })  // -> AliceとViolaが表示される

  console.log('=== Read with condition ===')
  const recList: Array<any> = await read({email: 'viola@prisma.io'})  // any型を指定しておかないとrecList[0].idにアクセスできない
  console.dir(recList, { depth: null })  // -> 条件を満たしているViolaのみが表示される

この例では、条件を指定せずに全件取得した場合と、メールアドレスで絞り込みをした場合の2通りの処理を書いています。

Update

続いてデータの更新処理を実装します。crud.tsの[A]の箇所に下記のコードを書き加えます。

crud.ts
const update = async (id: number, data: object) => {
  const updateUser = await prisma.user.update({
    where: {
      id: id,
    },
    data: data
  })
  console.dir(updateUser, { depth: null })
}

idを指定して、該当するIDを持つデータを、dataで渡した内容で書き換えます。dataの中身は書き換えるフィールドだけを含んでいればよく、例えば{name: 'hoge'}のような形のオブジェクトを渡せば名前だけが更新されます。
そして、[B]の箇所に下記のコードを書き加えます。

crud.ts
  console.log('=== Update ===')
  const data = {
    name: 'Viola the Magnificent',
  }
  const selectedId = recList[0].id
  await update(selectedId, data)
  recListAll = await read()
  console.dir(recListAll, { depth: null }) // -> Violaの名前が変更されている

recListは先述のReadの検索条件付きの処理の際に取得したもので、selectedIdにViolaのIDを代入しています。そして、そのIDを指定してupdate関数を使ってViolaの名前を書き換えるという処理になっています。

Delete

最後にデータの削除処理を実装します。crud.tsの[A]の箇所に下記のコードを書き加えます。

crud.ts
const delete_ = async (id: number) => {
  const deletePosts = prisma.post.deleteMany({
    where: {
      authorId: id
    },
  })
  const deleteProfiles = prisma.profile.deleteMany({
    where: {
      userId: id
    },
  })
  
  const deleteUser = prisma.user.delete({
    where: {
      id: id
    },
  })
  
  await prisma.$transaction([deletePosts, deleteProfiles, deleteUser])
}

UserデータにはPostデータとProfileデータがリレーションで紐づいていますが、prismaではこうしたリレーションを持つデータについては、Userを削除する前に、そのデータに紐づくPost及びProfileを削除してからでないとエラーになります。上記の例ではまずdelateManyで該当するユーザーの投稿とプロフィールを全て削除してから、最後にユーザーのデータを削除しています。$transactionメソッドは、リストで渡された処理を順番にトランザクションとして実行します。(この場合ではPostの削除、Profileの削除、Userの削除という順番です。)
また、deleteはtypescriptやjavascriptでは予約語なので、関数名はdelete_としています。
そして、[B]の箇所に下記のコードを書き加えます。

crud.ts
  console.log('=== Delete ===')
  await delete_(selectedId)
  recListAll = await read()
  console.dir(recListAll, { depth: null }) // -> Violaが削除され、Aliceのみが表示される

selectedIdは先述のViolaのIDですので、この例ではViolaのデータが削除されます。

CRUD全体のコード

ここまでの処理を書き込んでいくと、crud.ts全体は下記のようになります。

crud.ts
import { PrismaClient, Prisma } from '@prisma/client'

const prisma = new PrismaClient()

// *******************************************
// CRUD functions
// *******************************************
const create = async (rec: Prisma.UserCreateInput) => {
  await prisma.user.create({ data: rec })
}

const read = async (cond?: object): Promise<object[]> => {
  if (cond) {  // 検索条件あり
    const selectedUsers = await prisma.user.findMany({
      where: cond,
      include: {
        posts: true,
        profile: true,
      },
    })
    return selectedUsers
  } else {  // 検索条件なし
    const allUsers = await prisma.user.findMany({
      include: {
        posts: true,
        profile: true,
      },
    })
    return allUsers
  }
}

const update = async (id: number, data: object) => {
  const updateUser = await prisma.user.update({
    where: {
      id: id,
    },
    data: data
  })
  console.dir(updateUser, { depth: null })
}

const delete_ = async (id: number) => {
  const deletePosts = prisma.post.deleteMany({
    where: {
      authorId: id
    },
  })
  const deleteProfiles = prisma.profile.deleteMany({
    where: {
      userId: id
    },
  })
  
  const deleteUser = prisma.user.delete({
    where: {
      id: id
    },
  })
  
  await prisma.$transaction([deletePosts, deleteProfiles, deleteUser])
}

const deleteAll = async () => {
  await prisma.post.deleteMany({})
  await prisma.profile.deleteMany({})
  await prisma.user.deleteMany({})
}


// *******************************************
// sample data
// *******************************************
const user01: Prisma.UserCreateInput = {
  name: 'Alice',
  email: 'alice@prisma.io',
  posts: {
    create: { title: 'Hello World' },
  },
  profile: {
    create: {bio: 'I like turtles' },
  },
}

const user02: Prisma.UserCreateInput = {
  name: 'Viola',
  email: 'viola@prisma.io',
  posts: {
    create: { title: 'Hello World!' },
  },
  profile: {
    create: { bio: 'I like rabbits' },
  },
}


// *******************************************
// execute CRUD
// *******************************************
const main = async () => {
  console.log('=== Create & Read ===')
  await create(user01)
  await create(user02)
  let recListAll = await read()
  console.dir(recListAll, { depth: null })  // -> AliceとViolaが表示される

  console.log('=== Read with condition ===')
  const recList: Array<any> = await read({email: 'viola@prisma.io'})  // any型を指定しておかないとrecList[0].idにアクセスできない
  const selectedId = recList[0].id
  console.dir(recList, { depth: null })  // -> 条件を満たしているViolaのみが表示される

  console.log('=== Update ===')
  const data = {
    name: 'Viola the Magnificent',
  }
  await update(selectedId, data)
  recListAll = await read()
  console.dir(recListAll, { depth: null }) // -> Violaの名前が変更されている

  console.log('=== Delete ===')
  await delete_(selectedId)
  recListAll = await read()
  console.dir(recListAll, { depth: null }) // -> Violaが削除され、Aliceのみが表示される

  // clear tables
  await deleteAll()
}

main()

deleteAll関数はデータベースにあるUserPostProfileのデータを全て削除する関数です。crud.tsを書き換えながら繰り返し実行してPrismaの動作を理解する目的で付け加えました。

下記のコマンドで、作成したCRUD処理を実行します。

yarn ts-node crud.ts

まとめ

PrismaとMySQLとの接続、公式チュートリアルの実装、そして基本的なCRUD処理の書き方を紹介しました。Prismaを使ってみての感想ですが、直感的に書けてコーディングしていてとても気持ちよく感じました。もう少し使ってみて自分の技術スタックに加えていきたいと思います。

Discussion

ログインするとコメントできます