ORM成分高めで帰ってきたPrisma 2

## はじめに

少し前に[Prisma 2のpreview](https://www.prisma.io/blog/announcing-prisma-2-zq1s745db8i5/)がアナウンスされたので、素振りがてら調べてみました。

**まだPreviewのため、実装やAPIは大きく変更される可能性があります**

## Prisma 1 v.s. Prisma 2

### Prisma 1 ってなんだっけ

そもそもPrisma is 何をおさらいしておくと、Prismaは元来graphcoolというGraphQL as a Serviceのバックエンド部分を切り出したフレームワークです。

![](https://cdn-images-1.medium.com/max/4000/1*MmTqUD1_jQtNRxsMhyQyDg@2x.png)

*https://www.prisma.io/blog/prisma-raises-4-5m-to-build-the-graphql-data-layer-for-all-databases-663484df0f60 より*

また、[prisma - 最速 GraphQL Server実装](https://qiita.com/mizchi/items/74fb8b1177e83fe10273)にも書いてある通り、

> - GraphQL の形をした ORM
>   - MySQL/Postgre への マイグレーションヘルパー付き
>   - モデル定義からインデックス自動生成
>   - CRUD自動生成

といった特徴を有しています。

Prismaでは、GraphQL SDLでデータモデルを定義し、

```graphql:datamodel.prisma
type User {
  id: ID! @id
  email: String @unique
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID! @id
  title: String!
  published: Boolean! @default(value: false)
  author: User @relation(link: INLINE)
}
```

```sh
$ prisma deploy
```

のコマンドを叩くと、上記のモデルのCRUDが自動生成された上でGraphQL APIとしても実行可能になる、という寸法です。

### Prisma 2で大きく変わった所
下図（Previewのannouncing blogより引用）が端的に1.xと2.xの違いを表しています。

![](https://i.imgur.com/l1Jg54Y.png)

Prisma 1では、GraphQL <-> RDBの変換を行うORM部分やマイグレーション機能が統合されたPrisma Serverというサーバーを用意する必要がありました[^managed]。

上図からもわかるとおり、Prisma 2ではPrisma Serverは撤廃され、アプリケーションのAPIサーバーが直接DBとやりとりするようになっています。

ちなみに、この「APIサーバー」というのは別にGraphQL APIである必要はありません。REST APIでもよいですし、gRPCやThriftなどでもよいです。また、後述しますが、Primsa 2を経由してDBとやりとりする際にGraphQLを使うこともありません。

つまるところ、**Prisma 2はもはやGraphQLとは完全に切り離されたORM**です。

[^managed]: 自分でサーバーを立てるもよし、[Prisma cloud](https://app.prisma.io/)というマネージドサービスを利用することもできます。

## PhotonとLift

さきほどの図でも登場していますが、Prisma 2は大きく2つのコンポーネントから構成されます。それがPhotonとLiftです。

- Photon: ORM本体。現状はMySQL/PostgreSQL/SQLiteのみに対応していますが、MongoDBへの対応も計画されているとのとこと
- Lift: マイグレーションエンジン。いわゆるRailsのマイグレーションとかと同じような位置づけ

Prisma 2でもSDLベースのモデル定義は健在です。下記のようなGraphQL SDLっぽい定義体を作成します[^psl]。コイツがすべての元となっていきます。

[^psl]: 正確にはPSL(Prisma Schema Language)と呼ぶらしい。

```graphql:schema.prisma
datasource db {
  provider = "sqlite"
  url      = "file:dev.db"
  default  = true
}

model User {
  id    String  @default(cuid()) @id @unique
  email String  @unique
  name  String?
  posts Post[]
}
```

PhotonのCLIはこの定義体をもとに、アプリケーションのソースコードからimport可能なDAOコードを出力してくれます。コード出力に関しては、現状はJavaScript / TypeScriptのみが実装されていますが、Go言語もマイルストンには含まれているようです。TypeScriptの場合、モデルの型情報も含まれてくるので、コードを書く際に補完やエラーチェックが行えるのは嬉しいですね。

Liftも.prismaファイルを元にマイグレーションを生成します。LiftのCLIを実行すると、migrationsディレクトリにタイムスタンプがプレフィクスとして付与されたディレクトリができていき、順次DBへマイグレーションを適用できるようになっています。
Railsのマイグレーションを触ったことがある人であればすんなりイメージ湧くと思います。

## GraphQLとの親和性

上述したとおり、Prisma 2はORMの層に徹するようになっています。
Prisma 1では、モデル定義からGraphQLのCRUDまで一気通貫で作成可能である点と比較すると、これは大きな違いです。

この件については、本家のFAQでも明言されており[^faq]、下記のように回答がなされています。

> With Prisma 2, Prisma's query engine doesn't expose a spec-compliant GraphQL endpoint any more, so usage of schema delegation and GraphQL binding with Prisma 2 is not officially supported. To build GraphQL servers with Prisma 2, be sure to check out GraphQL Nexus and its nexus-prisma integration. GraphQL Nexus provides a code-first and type-safe way to build GraphQL servers in a scalable way.

[^faq]: https://github.com/prisma/prisma2/blob/master/docs/faq.md#does-photon-support-graphql-schema-delegation-and-graphql-binding

「Prisma 2としてはGraphQL bindingなどはサポートしないけど、GraphQL Nexus（こいつもprisma organization配下のツール）とか使ったら割と簡単にできると思うよ」ということなので、どちらかというとこれらの別ツールチェインの話になりそう。

## 開発の流れ
折角なので手を動かした際のメモを書いて見たのですが、公式のサンプルなぞっただけ + α程度なので、読み飛ばしてもらっても大丈夫です。

### Prisma 2 CLIのインストール

```sh
$ npm i -g prisma2
```

### プロジェクトの作成

```sh
$ prisma2 init prisma2-nexus-example
```

[![asciicast](https://asciinema.org/a/261326.svg)](https://asciinema.org/a/261326)

- sqlite
- GraphQL APIのboilerplateを利用

無事プロジェクトの作成が完了すると、ボイラープレートによってセットアップされたモデルの定義ファイルを確認可能です。

```graphql:prisma/schema.prisma
datasource db {
  provider = "sqlite"
  url      = "file:dev.db"
  default  = true
}

generator photon {
  provider = "photonjs"
}

generator nexus_prisma {
  provider = "nexus-prisma"
}

model User {
  id    String  @default(cuid()) @id @unique
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        String   @default(cuid()) @id @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  published Boolean
  title     String
  content   String?
  author    User?
}
```

### DBのセットアップ
上記のモデル定義を元に、Liftでマイグレーションを実行し、DBを準備していきます。

まずは現在のモデル定義からCREATE TABLEなどを行うマイグレーションを作成する必要があります。これには次のコマンドを実行します。

```sh
$ prisma2 lift save
```

適当な名前（e.g. `init`）を入力すると、 下図のような感じで `migrations` の配下に `タイムスタンプ-名前` の形式で作成されます。

```txt
- prisma/                             
 |- migrations/                       
  |- 20190808024227-init/             
   |  datamodel.prisma                
   |  README.md                       
   |  steps.json                      
  |  lift.lock                        
 |  schema.prisma                     
```

README.mdからは、このマイグレーションが実行するであろうTDLや、元となったモデル定義の差分情報を確認可能です。

![Image from Gyazo](https://i.gyazo.com/7913839b6175ca94b7f6f4c075fb6203.png)

以下のコマンドで、作成されたマイグレーションを適用します。

```sh
$ prisma2 lift up
```

ちなみに、ロールバックは `prisma2 lift down` です。

### Photonの準備

次にPhoton側です。次のコマンドを実行することで、PhotonのTypeScriptクライアントが生成されます。

```sh
$ prisma2 generate
```

余談ですが、Photonのクライアントコードは `node_modules/@generated/photon` 以下に生成されます。
最初見たときに「なぜにnode_modules配下に。。。」と思ったのですが、src配下だと、生成したコードがtsserver上で展開されると、更新＆再生成した際にtsserver上のファイルは古いままの状態になり、補完やエラーチェックの都合が悪いからだとか[^node_modules]。
この事情もあって、`generate` コマンドはpackage.jsonでpostinstallに指定されています。

[^node_modules]: https://github.com/prisma/photonjs/issues/77#issuecomment-508162695 より。確かにtsserverでこれをちゃんと対応しようとすると、plugin作ってDocumentRegistoryに適切に更新を通知してScriptVersionCacheを破棄させる、などの対応が必要になるからわからんことは無いけども。。。

### Photonの実行

これでDBとPhotonクライアントの準備が整ったので、Photonを実行してみましょう。

今回選んだボイラープレートは、seed.tsにUser作成のサンプルが含まれているので、それを実行します。

```ts:prisma/seed.ts
import Photon from '@generated/photon'
const photon = new Photon()

async function main() {

  const user1 = await photon.users.create({
    data: {
      email: 'alice@prisma.io',
      name: 'Alice',
      posts: {
        create: {
          title: 'Join us for Prisma Day 2019 in Berlin',
          content: 'https://www.prisma.io/day/',
          published: true,
        },
      },
    },
  })
  const user2 = await photon.users.create({
    // 中略
  })

  console.log({ user1, user2 })
}

main()
  .catch(e => console.error(e))
  .finally(async () => {
    await photon.disconnect()
  })
```

`photon.users.create(...)` の部分がPhotonの実行ですね。TypeScriptで補完が効くのは嬉しいです。

```sh
$ yarn run ts-node prisma/seed.ts
```

DBを覗いてみると、PhotonによってUserテーブルへレコードが作成されていることが確認できるはずです。

```
$ sqlite3 prisma/dev.db
sqlite> select name from User;
Alice
Bob
```

### アプリケーションの実行

もはやPrisma 2とは関係ない部分なのですが、今回はGraphQL APIサーバーとしてのボイラープレートを選択しているので、この状態で `yarn start` するだけで実行可能です。

![Image from Gyazo](https://i.gyazo.com/6b61a0f07c2e69d4299e7e7d1415d874.png)

ちなみに、GraphQL関連のスタックは下記となっています。

- APIサーバー層: [graphql-yoga](https://github.com/prisma/graphql-yoga)
- Resolverフレームワーク層：[GraphQL Nexus](https://github.com/prisma/nexus)

いずれにせよ、GraphQL関連箇所は、最初から用意してもらったコードがただ動いているだけです。強いて言うなら、resolverからPhotonのクライアントコードが参照されている、という程度。

### モデルの編集とアプリケーションの実装変更

「テーブルに列を追加 + 当該列を設定するためのGraphQL Mutationを実装」というのをやってみました。

Postモデルに `category` というフィールドを追加し、

```graphql:prisma/schema.prisma
model Post {
  # ...略
  category  String?
}
```

Liftでマイグレーション作成＆実行します。

```sh
$ prisma2 lift save
$ prisma2 lift up
```

Photonクライアントも再生成します。

```sh
$ prisma2 generate
```

GraphQL Nexus側のコードを修正し、Post typeの定義を変更します。

```ts
export const Post = objectType({
  name: 'Post',
  definition(t) {
    t.model.id()
    t.model.createdAt()
    t.model.updatedAt()
    t.model.title()
    t.model.content()
    t.model.published()
    t.model.author()
    t.model.category()  // これが追加する部分
  },
})
```

上記における `t.model.category()` という箇所ですが、 `nexus-prisma` プラグインにより、Prisma 2によるPhotonクライアント生成時に、Photonのモデル情報をGraphQLのtype定義として利用するためのinterfaceも追加生成されており、その型をNexus側から参照することで、ちょっと楽ができるようになっていました。正直、Prisma 2とGraphQL Nexusの関係はまだよくわかってないです。。。

Mutation用のリゾルバはPhoton Clientとして書くだけです。こっちはわかりやすい。

```ts
    t.field('setCategory', {
      type: 'Post',
      args: {
        id: idArg(),
        category: stringArg(),
      },
      resolve: async (_, { id, category }, ctx) => {
        return await ctx.photon.posts.update({
          where: { id },
          data: {
            category,
          },
        })
      },
    })
```

なお、サンプルでの作業はGitHubの https://github.com/Quramy/prisma2-nexus-example にpushしてあるので、興味があればそちらもどうそ。[このコミット](https://github.com/Quramy/prisma2-nexus-example/commit/df7988c2b94894201d036b7ed20abc335d0a940f) が「モデル変更 + 実装修正」に相当する作業です。

## 所感

### ORMとして
ここまでにも何回か書いてきましたが、Prisma 2は基本的にただのORMです。

JavaScript / TypeScriptのORMという文脈だと、[TypeORM](https://github.com/typeorm/typeorm)あたりと比べてどうか、という話になるわけですよ。

書き味でいうとHibernate / Active Recordっぽさの強いTypeORMよりも、シンプルにObjectだけで完結するPrisma 2の方が好みです。

一方で、Prisma 2のコアはRustで実装されているというのもあり、何かあったときに自分でコード追えるのか？とかOSSとしてちゃんとメンテされ続けるのか？という不安は結構あります。

### GraphQLのツールとして

もともとPrisma 2のpreviewを読もうという気持ちになったのは、GraphQLの文脈だからこそなんですが、GraphQL APIサーバーのバックエンドとして捉えた場合に、いまいち使えるんだか使えないんだかよくわからん、という感じです。

疎結合化によって、Prisma 1.xのころより開発の手間が増えてるのは間違いないし、推奨されているNexusについてもprisma organization配下のレポジトリとは言え、どうもコアコミッタはPrismaの人ではないあたりに若干の不安があります。

また、NexusとPrisma 2を同時に触っていると、以下に挙げた哲学の差の違いがそこはかとなく気になりました。

- Prisma 2: PSL使った定義体ドリブンなフレームワーク
- Nexus: GraphQL type定義をコードベースで記述し、最後にGraphQL SDLが自動で出力されるフレームワーク

同じprisma系のツールなのであれば、[graphqlgen](https://github.com/prisma/graphqlgen)のようなSDLファーストなライブラリと組み合わせた方がしっくり来るかも？と思ってます。単純なCRUD程度だと定義相当冗長になるだろうけど（個人的に実装にannotateしていく類のフレームワークがそんなに好きじゃないというのもあります）。