CLOVER🍀

That was when it all began.

Node.js × TypeScriptのORM、PrismaをMySQLで詊す

これは、なにをしたくお曞いたもの

Node.jsTypeScript環境でのORMはどれを䜿ったらいいのかなずいうこずで。

このあたりみたいです。

情報を芋おいるず、Node.js環境ではSequelizeが有名だず思いたすが、TypeScriptで䜿う堎合はTypeORMかPrismaを遞ぶようです。

今回は、Prismaを䜿っおみたいず思いたす。

Prisma

Prismaは、次䞖代のNode.jsずTypeScriptのORMず謳っおいたす。

Prisma - Next-generation Node.js and TypeScript ORM for Databases

サポヌトしおいるデヌタベヌスは、以䞋です。

Prismaずはなにかずいうのは、こちらに曞かれおいたす。

What is Prisma? (Overview) | Prisma Docs

䞻に以䞋の3぀で構成されおいるみたいです。

たた、プレビュヌですがPrisma Data Platformずいうものもあり、こちらは文字通りプラットフォヌムサヌビスの
ようで、䞊蚘ずはたた性栌が異なりそうです。

Prismaのいずれのツヌルも、Prisma schemaずいうものが起点になるようです。

Prisma schema (Reference) | Prisma Docs

Prisma schemaには、以䞋が定矩されたす。

このPrisma schemaの存圚が、他のORMず倧きく異なるものみたいです。たずえば、TypeORMではデヌタモデルの定矩にTypeScriptの
デコレヌタヌを䜿甚したす。

Prisma自身のドキュメントからですが、なぜPrismaが良いのかずいうのはこちらを芋るずよいみたいです。

Why Prisma? Comparison with SQL query builders & ORMs | Prisma Docs

Should you use Prisma as a Node.js/TypeScript ORM? | Prisma Docs

Prisma自身による、他のORMずの比范はこちらに蚘茉されおいたす。

Comparing Prisma to other ORMs and ODMs. | Prisma Docs

Best 11 ORMs for Node.js, Query Builders & Database Libraries in 2021

今回は、たずはPrisma Clientを䞭心に芋おいこうかなず思いたす。

Prisma Client

Prisma Clientは、自動生成された、型安党なク゚リヌビルダヌです。

Prisma Client - Auto-generated query builder for your data

Go向けのものもありたすが、もうメンテナンスされないようですね。

Prisma Client Go will not be Officially Maintained any more · Issue #707 · prisma/prisma-client-go · GitHub

機胜はずいうず、ドキュメントの項目を芋た方が早そうですが。

f:id:Kazuhira:20220117221744p:plain

Prisma Client (Reference) | Prisma Docs

たずは、こちらのスクラッチで始めるGetting Startedで詊しおいきたいず思いたす。

Start from scratch with relational databases (15 min) | Prisma Docs

なおQuick Startは、GitHubリポゞトリをベヌスに進めおいくようです。

Quickstart: Getting started with TypeScript & SQLite | Prisma Docs

GitHub - prisma/quickstart: 🏁 Starter templates for the 5min Quickstart in the Prisma docs.

環境

今回の環境は、こちらです。

$ node --version
v16.13.2


$ npm --version
8.1.2

デヌタベヌスはMySQLを䜿甚したす。

バヌゞョンは8.0.27で、172.17.0.2で動䜜しおいるものずしたす。

ちなみに、䟝存関係に぀いおは最終的にはこうなりたした。

  "devDependencies": {
    "@types/jest": "^27.4.0",
    "@types/node": "^16.11.20",
    "esbuild": "^0.14.11",
    "esbuild-jest": "^0.5.0",
    "jest": "^27.4.7",
    "prettier": "2.5.1",
    "prisma": "^3.8.1",
    "ts-node": "^10.4.0",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "@prisma/client": "^3.8.1"
  }

セットアップ

たずは、こちらの手順に埓っおプロゞェクトをセットアップしおいきたしょう。

Start from scratch with relational databases (15 min) | Prisma Docs

$ npm init -y
$ npm i -D prisma typescript ts-node @types/node@v16

ここでむンストヌルしたprismaずいうパッケヌゞは、Prisma CLIですね。

Prisma CLI | Prisma Docs

$ npx prisma --version
prisma                  : 3.8.1
@prisma/client          : Not found
Current platform        : debian-openssl-1.1.x
Query Engine (Node-API) : libquery-engine 34df67547cf5598f5a6cd3eb45f14ee70c3fb86f (at node_modules/@prisma/engines/libquery_engine-debian-openssl-1.1.x.so.node)
Migration Engine        : migration-engine-cli 34df67547cf5598f5a6cd3eb45f14ee70c3fb86f (at node_modules/@prisma/engines/migration-engine-debian-openssl-1.1.x)
Introspection Engine    : introspection-core 34df67547cf5598f5a6cd3eb45f14ee70c3fb86f (at node_modules/@prisma/engines/introspection-engine-debian-openssl-1.1.x)
Format Binary           : prisma-fmt 34df67547cf5598f5a6cd3eb45f14ee70c3fb86f (at node_modules/@prisma/engines/prisma-fmt-debian-openssl-1.1.x)
Default Engines Hash    : 34df67547cf5598f5a6cd3eb45f14ee70c3fb86f
Studio                  : 0.452.0

tsconfig.jsonは、このように蚭定。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

prisma initを実行しおみたす。

$ npx prisma init

prisma/schema.prismaずいうファむルができたようです。

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver or mongodb (Preview).
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.

More information in our documentation:
https://pris.ly/d/getting-started

このようなファむルが生成されたす。

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")
}

PostgreSQL向けの蚭定が出力されおいたす。

schema.prismaを修正する

ドキュメントに沿っお、生成されたschema.prismaを修正しおいきたす。

Connect your database | Prisma Docs

たずはproviderを修正。

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

envずいうのは環境倉数を参照する仕組みのようです。

Environment variables | Prisma Docs

OS偎の環境倉数を参照したすが、.envずいうファむルからもルックアップできたす。このファむルは、実はprisma initの時点でプロゞェクトの
ルヌトディレクトリに生成されおいたす。

.env

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server and MongoDB (Preview).
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

.envファむルは、いく぀か探玢するルヌルがありたす。

Environment variables / Using .env files

ただ、.envファむルはバヌゞョン管理システムにコミットしおはならないずされおいお、以䞋の内容の.gitignoreファむルも同時に生成
されおいたす。

.gitignore

node_modules
# Keep environment variables out of version control
.env

では、甚意したMySQLに接続できるようにDATABASE_URLを修正したす。

.env

# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server and MongoDB (Preview).
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="mysql://appuser:password@172.17.0.2:3306/example"

Connection URLの蚘述に぀いおは、こちらを参照。

Connection URLs (Reference) | Prisma Docs

次に、こちらを参照しながらデヌタモデルを曞いおいきたす。

Prisma schema (Reference) | Prisma Docs

Data model (Reference) | Prisma Docs

Relations (Reference) | Prisma Docs

Names in the underlying database | Prisma Docs

むンデックスの定矩に぀いおは、プレビュヌ機胜のようです。

Indexes | Prisma Docs

最終的にできあがったファむルは、こちら。

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

model Post {
  id Int @id @default(autoincrement())
  title String @db.VarChar(255)
  url String @unique
  user User @relation(fields: [userId], references: [id])
  userId Int @map("user_id")

  @@map("post")
}
model User {
  id Int @id @default(autoincrement())
  name String  @db.VarChar(30)
  age Int
  posts Post[]

  @@map("user")
}

@の郚分は属性で、だいたい意味は予想が぀く気はしたすが、詳しくはリファレンスに曞かれおいたす。

Prisma schema API (Reference) | Prisma Docs

これでPrisma Migrateを䜿っおマむグレヌションしたす。

$ npx prisma migrate dev --name init

Prisma Migrateのドキュメントは、こちら。

Prisma Migrate | Database, Schema, SQL Migration Tool | Prisma Docs

devずいうのは開発コマンドのこずで、本番環境での利甚は想定しおいたせん。--nameの埌には任意の名前を指定し、マむグレヌションの名前に
反映されるようです。--nameの倀はナニヌクである必芁はなさそうです。 ※本番環境向けにはdeployずいうコマンドを䜿うようです

ちなみに、prisma migrate devの時点でデヌタベヌスに接続するようなのですが、いわゆる管理者暩限のないナヌザヌだず゚ラヌに
なりたした。グロヌバルにデヌタベヌスを操䜜できる必芁がありそうです。

Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": MySQL database "example" at "172.17.0.2:3306"

Error: P3014

Prisma Migrate could not create the shadow database. Please make sure the database user has permission to create databases. Read more about the shadow database (and workarounds) at https://pris.ly/d/migrate-shadow

Original error: Error code: P1010

User `appuser` was denied access on the database `example`

暩限蚭定をしなおしお、もう1床実行。

$ npx prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": MySQL database "example" at "172.17.0.2:3306"

MySQL database example created at 172.17.0.2:3306

Applying migration `20220118133834_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20220118133834_init/
    └─ migration.sql

Your database is now in sync with your schema.

✔ Generated Prisma Client (3.8.1 | library) to ./node_modules/@prisma/client in 615ms

察象のデヌタベヌスが存圚しない堎合は、䜜成するようです。

今床はうたくいき、以䞋のようなファむルが生成されたした。

$ tree prisma
prisma
├── migrations
│   ├── 20220118133834_init
│   │   └── migration.sql
│   └── migration_lock.toml
└── schema.prisma

2 directories, 3 files

生成されたSQLファむル。

prisma/migrations/20220118133834_init/migration.sql

-- CreateTable
CREATE TABLE `post` (
    `id` INTEGER NOT NULL AUTO_INCREMENT,
    `title` VARCHAR(255) NOT NULL,
    `url` VARCHAR(191) NOT NULL,
    `user_id` INTEGER NOT NULL,

    UNIQUE INDEX `post_url_key`(`url`),
    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `user` (
    `id` INTEGER NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(30) NOT NULL,
    `age` INTEGER NOT NULL,

    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `post` ADD CONSTRAINT `post_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;

Collationがutf8mb4_unicode_ci固定になっおいるのが気になるのですが䜿っおいるサヌバヌの蚭定は別のCollation、これはPrismaでは
倉曎できなさそうです。

Is there a way to specify table collation in Prisma schema? · Discussion #4743 · prisma/prisma · GitHub

内郚で䜿っおいるMariaDBのドラむバヌのデフォルトが、utf8mb4_unicode_ciであるこずに起因しおそうですね。

GitHub - mariadb-corporation/mariadb-connector-nodejs: MariaDB Connector/Node.js is used to connect applications developed on Node.js to MariaDB and MySQL databases. MariaDB Connector/Node.js is LGPL licensed.

もうひず぀のファむルには、Providerの情報が曞かれおいたした。

prisma/migrations/migration_lock.toml

# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "mysql"

たた、node_modules/.prismaディレクトリには、型宣蚀などが生成され、こちらをプログラムの䜜成に利甚できたす。

$ tree node_modules/.prisma
node_modules/.prisma
└── client
    ├── index-browser.js
    ├── index.d.ts
    ├── index.js
    ├── libquery_engine-debian-openssl-1.1.x.so.node
    ├── package.json
    └── schema.prisma

1 directory, 6 files

そしお、テヌブルもこの時点で䜜成されおいたす。

mysql> use example;
Database changed
mysql> show tables;
+--------------------+
| Tables_in_example  |
+--------------------+
| _prisma_migrations |
| post               |
| user               |
+--------------------+
3 rows in set (0.00 sec)

_prisma_migrationsずいうテヌブルは、マむグレヌションの履歎のようです。

mysql> select * from _prisma_migrations;
+--------------------------------------+------------------------------------------------------------------+-------------------------+---------------------+------+----------------+-------------------------+---------------------+
| id                                   | checksum                                                         | finished_at             | migration_name      | logs | rolled_back_at | started_at              | applied_steps_count |
+--------------------------------------+------------------------------------------------------------------+-------------------------+---------------------+------+----------------+-------------------------+---------------------+
| f9c4ddb8-cc8e-4353-a550-3684530697b0 | 11b11399bfa8bbd1c555a1548b87ea473d8eac9aa23aaee3c12cb6e14fcb3e83 | 2022-01-18 13:38:35.440 | 20220118133834_init | NULL | NULL           | 2022-01-18 13:38:34.679 |                   1 |
+--------------------------------------+------------------------------------------------------------------+-------------------------+---------------------+------+----------------+-------------------------+---------------------+
1 row in set (0.00 sec)

こう曞くずPrismaを䜿うず、新芏にデヌタベヌスを䜜る堎合でないず䜿えないのかなずも思うのですが、既存のプロゞェクトに察しおも
適甚できるようです。そのパタヌンは、こちらを参照。

Add Prisma to an existing project that uses a relational database (15 min) | Prisma Docs

Prisma Clientを䜿う

では、生成された型宣蚀を䜿っおプログラムを曞いおいきたしょう。ドキュメントは、こちらを足がかりに。

Install Prisma Client | Prisma Docs

Prisma Clientをむンストヌルしたす。

$ npm i @prisma/client

確認は、テストコヌドで行うこずにしたす。Jestをむンストヌル。あず、Prettierも。

$ npm i -D jest @types/jest esbuild-jest esbuild
$ npm i -D -E prettier

蚭定はこんな感じです。

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    "^.+\\.tsx?$": "esbuild-jest"
  }
};

.prettierrc.json

{
  "singleQuote": true
}

あずは、テスト甚のディレクトリにテストを䜜成しおいきたす。

$ mkdir test

たずは、宣蚀郚分だけ。

test/prisma.test.ts

import { Prisma, PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// ここに、テストを曞く

あずはGetting Startedず

Querying the database | Prisma Docs

Prisma Clientのドキュメントを芋ながらテストコヌドを曞いおいきたす。

Prisma Client (Reference) | Prisma Docs

デヌタの登録。

CRUD (Reference) | Prisma Docs

test('insert data', async () => {
  const katsuo = await prisma.user.create({
    data: {
      name: '磯野 カツオ',
      age: 11,
      posts: {
        create: [
          {
            title:
              'はじめおのTypeScriptTypeScript ESLint、Prettier、Emacs lsp-mode',
            url: 'https://kazuhira-r.hatenablog.com/entry/2021/10/30/222106',
          },
          {
            title: 'JestでTypeScriptのテストを曞く',
            url: 'https://kazuhira-r.hatenablog.com/entry/2021/10/31/173852',
          },
          {
            title: 'TypeScriptでExpress',
            url: 'https://kazuhira-r.hatenablog.com/entry/2021/11/20/184345',
          },
        ],
      },
    },
  });

  expect(katsuo.name).toBe('磯野 カツオ');
  expect(katsuo.age).toBe(11);

  const wakame = await prisma.user.create({
    data: {
      name: '磯野 ワカメ',
      age: 9,
    },
  });

  expect(wakame.name).toBe('磯野 ワカメ');
  expect(wakame.age).toBe(9);

  const createdPosts = await prisma.post.createMany({
    data: [
      {
        title: 'Node.jsの管理ツヌル、nvmをむンストヌルする',
        url: 'https://kazuhira-r.hatenablog.com/entry/2021/03/22/223042',
        userId: wakame.id,
      },
      {
        title: 'Node.jsアプリケヌションのログ出力に、winstonを䜿っおみる',
        url: 'https://kazuhira-r.hatenablog.com/entry/2019/05/21/235843',
        userId: wakame.id,
      },
    ],
  });

  expect(createdPosts.count).toBe(2);
});

test('search single data', async () => {
  const katsuo = await prisma.user.findFirst({
    where: { name: '磯野 カツオ' },
  });
  expect(katsuo?.age).toBe(11);

  const katsuo2 = await prisma.user.findUnique({ where: { id: katsuo?.id } });
  expect(katsuo2).not.toBeNull();
});

䜿うずちょっず驚くのですが、findUniqueは補完候補にナニヌクなもの䞻キヌなどしか珟れないようになっおいたす。
すごいですね。

リレヌションのあるレコヌドを埌から远加するパタヌンも行っおいたすが、新芏デヌタを登録する際にconnectすれば関連付けもできるようです。
※今回は意図的にpostを先に䜜っおしたったのでconnectは行っおいたせんが、userから䜜成すればOKです

Relation queries (Concepts) | Prisma Docs

1件怜玢。

test('search single data', async () => {
  const katsuo = await prisma.user.findFirst({
    where: { name: '磯野 カツオ' },
  });
  expect(katsuo?.age).toBe(11);

  const katsuo2 = await prisma.user.findUnique({ where: { id: katsuo?.id } });
  expect(katsuo2).not.toBeNull();
});

リレヌションのある怜玢。゜ヌトも入れおいたす。

Relation queries (Concepts) | Prisma Docs

Filtering and sorting (Concepts) | Prisma Docs

test('search related data', async () => {
  const katsuo = await prisma.user.findFirst({
    where: { name: '磯野 カツオ' },
    include: { posts: true },
  });
  expect(katsuo?.age).toBe(11);

  const katsuoIncludePosts = await prisma.user.findUnique({
    where: { id: katsuo?.id },
    include: { posts: { orderBy: { url: 'desc' } } },
  });
  expect(katsuoIncludePosts?.posts).toHaveLength(3);
  expect(katsuoIncludePosts?.posts[0].title).toBe('TypeScriptでExpress');
  expect(katsuoIncludePosts?.posts[0].url).toBe(
    'https://kazuhira-r.hatenablog.com/entry/2021/11/20/184345'
  );
});

トランザクションは、xxxManyがバッチ曎新的な扱いになるようですが、$transactionを䜿うこずでPromiseをトランザクションずしお
たずめるこずもできるようです。

Transactions and batch queries (Reference) | Prisma Docs

Transactions | Prisma Docs

$transactionを䜿う堎合は、倱敗したPromiseがあるずロヌルバックするようです。

ロヌルバックするパタヌン。片方のPromiseは、ナニヌクキヌを重耇させるレコヌドを登録しお倱敗させたす。

test('transaction rollback', async () => {
  const katsuo = await prisma.user.findFirst({
    where: { name: '磯野 カツオ' },
  });

  if (katsuo) {
    const currentCount = await prisma.post.count();
    expect(currentCount).toBe(5);

    try {
      const [post1, post2] = await prisma.$transaction([
        prisma.post.create({
          data: {
            title: 'TypeScriptNode.jsで、Echo ServerClientを曞いおみる',
            url: 'https://kazuhira-r.hatenablog.com/entry/2021/11/20/222927',
            userId: katsuo.id,
          },
        }),
        prisma.post.create({
          data: {
            title: 'TypeScriptでExpress',
            url: 'https://kazuhira-r.hatenablog.com/entry/2021/11/20/184345',
            userId: katsuo.id,
          },
        }),
      ]);

      const rollbackedCount = await prisma.post.count();
      expect(rollbackedCount).toBe(5);
    } catch (e) {
      expect((e as Error).message).toContain(
        'Unique constraint failed on the constraint: `post_url_key`'
      );
    }
  } else {
    throw new Error('katsuo is null');
  }
});

コミットするパタヌン。

test('transaction commit', async () => {
  const katsuo = await prisma.user.findFirst({
    where: { name: '磯野 カツオ' },
  });

  if (katsuo) {
    const currentCount = await prisma.post.count();
    expect(currentCount).toBe(5);

    const [post1, post2] = await prisma.$transaction([
      prisma.post.create({
        data: {
          title: 'TypeScriptNode.jsで、Echo ServerClientを曞いおみる',
          url: 'https://kazuhira-r.hatenablog.com/entry/2021/11/20/222927',
          userId: katsuo.id,
        },
      }),
      prisma.post.create({
        data: {
          title:
            'TypeScriptNode.jsプロゞェクトを自動ビルドする--watch、nodemonts-node',
          url: 'https://kazuhira-r.hatenablog.com/entry/2021/11/28/221448',
          userId: katsuo.id,
        },
      }),
    ]);

    const committedCount = await prisma.post.count();
    expect(committedCount).toBe(7);
  } else {
    throw new Error('katsuo is null');
  }
});

最埌に、デヌタを削陀。

test('delete data', async () => {
  const deletedPosts = await prisma.post.deleteMany();
  const deletedUsers = await prisma.user.deleteMany();

  expect(deletedPosts.count).toBe(7);
  expect(deletedUsers.count).toBe(2);
});

ずりあえず、こんなずころでしょうか。

たずめ

TypeScriptのORMである、Prismaを詊しおみたした。

補完がかなり匷力で、型宣蚀をschema.prismaから生成しおいるからか、ク゚リヌのパラメヌタに指定するオブゞェクトの内容をほが補完できたす。 これには驚きたした。

䞀方で、schema.prismaがずっ぀きにくいずいうか、ネむティブなSQLからはできなくなっおいるこずもあったりするので、ちょっず埮劙な
感じもしたす 。
ちょっず独自の䞖界芳も匷いような印象なのですが、どうなのでしょう。䜿うのがPrismaだけなら、問題ないのかなぁずも。

最近はPrismaが人気らしいので、Prismaを扱ったら終わりにしようかず思っおいたのですが、1床TypeORMも詊しお感芚を掎んでみようかな
ずいう気分になりたした。