PrismaからRDBのViewを参照する

1. はじめに

カラダノートの 堀内 です。 TypeScriptの次世代ORMである Prisma から、RDB側で作成したViewを参照する要件がありました。

せっかくなのでその手順をチュートリアルにします。

2. Viewを参照するチュートリアル

公式にPostgreSQLを例にしたドキュメントがあるので、そちらを参考にします。

www.prisma.io

RDB側の準備

まず RDB側に Viewを作成します。

公式のクイックスタート からサンプルプロジェクトをダウンロードします。

curl -L https://pris.ly/quickstart | tar -xz --strip=2 quickstart-master/typescript/starter

サンプルプロジェクトのprisma/dev.dbSQLiteのDBが以下の構造で同梱されています。

f:id:karadanote:20210416112450p:plain

PostとUserはPost.autherIdとUser.idでリレーションが構成されています。この2つのテーブルを結合した以下のようなViewを作成してみます。

f:id:karadanote:20210416112728p:plain

以下のコマンドからViewを作成します。

sqlite3 dev.db
CREATE VIEW view_posts AS SELECT Post.id, Post.title, Post.content, Post.published, Post.authorId, User.email, User.name from Post LEFT JOIN User on Post.authorId = User.id;

Viewが作成されたか確認してみます。

f:id:karadanote:20210416112825p:plain

Prisma側の準備

サンプルプロジェクトのprisma/schema.prismaに↑で作成したViewのモデル定義を記述します。

通常のテーブルであれば、npx prisma introspectRDBから自動的に検出してschema.prismaに記載してくれるのですが、現時点では以下の公式ドキュメントの記述どおり、Viewのintrospectionはサポートされていません。

You must manually add each view as a model to the Prisma schema right now. Introspection does not add views to the schema currently.

なので、自分で書きます。

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

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

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User?   @relation(fields: [authorId], references: [id])
  authorId  Int?
}

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

+ model view_posts {
+   id        Int     @unique
+   title     String
+   content   String?
+   published Boolean @default(false)
+   authorId  Int?
+   email String
+   name  String?
+ }

ここで2つポイントがあります。

  • モデル名はView名と同じにする
    • 同じにしないと does not exist in the current database. がでます
  • 何らかのカラムが必ずユニークになるようにする
    • @idや@uniqueを最低でも1つ指定します

コードからViewを呼ぶ

サンプルプロジェクトの script.ts から↑で作成したモデル定義を使ってViewを読んでみます。

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
  const posts = await prisma.view_posts.findMany()
  console.log(posts)
}

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

↑のコードを実行してみると、Viewのデータを参照できていることが確認できます。

eiichihoriuchi@EiichinoMacBook-Pro starter % npm run dev        

> script@ dev /Users/eiichihoriuchi/workspace/starter2/starter
> ts-node ./script.ts

[
  {
    id: 1,
    title: 'Hello World',
    content: null,
    published: false,
    authorId: 2,
    email: 'maria@prisma.io',
    name: 'Maria'
  }
]

3. おわりに

Viewを活用してコードから参照できれば、コードの中に複雑なSQLやデータの整形を書かずに済むので見通しが良くなりますよね。 GraphQLでもいいんですが、サブクエリや集計を利用するケースではRDBにもメリットがありますね。

TypeScript で 都道府県型を定義する

1. はじめに

カラダノートの 堀内 です。 前回 は電話番号型を定義してので、今回は都道府県を実装してみます。

2. ゴール

都道府県型を使って何を実現したいかというと、例えば、北海道と沖縄だけを対象とする処理を実装するときに、それ以外の都道府県だと型チェックでエラーを出したい、ということを目指します。

以下のようなイメージです。 ちなみに Prefecture<N> の Nは 都道府県コード に相当します。

  const hokkaido = Prefecture.from(1)   // Prefecture<1>
  const okinawa = Prefecture.from(47)  // Prefecture<47>
  const saitama = Prefecture.from(11)  // Prefecture<11>

  // 北海道と沖縄のみを対象とする処理
  const sendTo = (prefecture: Prefecture<1> | Prefecture<47>): void => {
    // do something ...
  }

  sendTo(hokkaido)
  sendTo(okinawa)
  sendTo(saitama)  // ここを型チェックでエラーを出したい

3. 実装

もういきなりですが以下のとおり実装しました。

/* prefecture.ts */

// 都道府県コード一覧
const PREFECTURE_CODES = [
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
  11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
  21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
  31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
  41, 42, 43, 44, 45, 46, 47
] as const

// 都道府県名一覧
const PREFECTURE_NAMES = [
  '北海道', '青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県', '茨城県', '栃木県', '群馬県',
  '埼玉県', '千葉県', '東京都', '神奈川県', '新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県',
  '岐阜県', '静岡県', '愛知県', '三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県',
  '鳥取県', '島根県', '岡山県', '広島県', '山口県', '徳島県', '香川県', '愛媛県', '高知県', '福岡県',
  '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県'
] as const

// 都道府県コードのUnion型
export type PrefectureCode = typeof PREFECTURE_CODES[number]

// 都道府県名のUnion型
export type PrefectureName = typeof PREFECTURE_NAMES[number]

// 都道府県コードの検証
const validate = (value: PrefectureCode): boolean => {
  return PREFECTURE_CODES.includes(value)
}

// 都道府県型
export type Prefecture<Code extends PrefectureCode> = {
  readonly code: Code
  readonly name: PrefectureName
}

// 都道府県型の生成
export const Prefecture = {
  from<T extends PrefectureCode>(value: T): Prefecture<T> {
    if (!validate(value)) {
      throw new Error(`${value}は都道府県コードではありません`)
    }

    return {
      code: value, 
      name: PREFECTURE_NAMES[value - 1]
    }
  }
}

export default Prefecture

で、ゴールのところでで書いたコードで動きを確認してみると、埼玉が型チェックでエラーになっています。

f:id:karadanote:20210405131545p:plain

ドメイン知識をこんな感じで型定義にどんどん集めていきたい。 的なドメイン駆動設計をやりたい方をカラダノートでは募集しております。

herp.careers

herp.careers

TypeScript と Opaque型で電話番号型を定義する

1. はじめに

カラダノートの 堀内 です。

TypeScript の型定義ライブラリの1つである type-fest を使って、電話番号型 を作って実験したメモです。

github.com

2. Opaque型について

2.1 実装の種類

TypeScriptでは公式にOpaque型をサポートしていないので、色々ググってみると現状以下の呼び方と実装があるようです。

  • Nominal型(公称型)
  • Opaque型(不透明型)

今回は type-festのOpaque型を使いましたが、utility-types ライブラリのNominal型でも同じことが実現できます。

というわけでOpaque型で実装してみます。

2.2 電話番号型の実装

今回は以下のように実装しました。

import { Opaque } from 'type-fest'

// 電話番号になりうる基底型
type BaseType = string

// 電話番号型
type PhoneNumber = Opaque<BaseType, 'PhoneNumber'>

// 電話番号型になりうるかの検証
const validate = (value: BaseType): value is PhoneNumber => {
  // 半角数字1文字以上を電話番号とみなす
  return /^[0-9]+$/.test(value)
}

interface CreateValueObject<Base, T> {
  (value: Base): T
}

const PhoneNumber: CreateValueObject<BaseType, PhoneNumber> = (value) => {
  if (!validate(value)) {
    throw new Error(`${value}は電話番号ではありません`)
  }
  return value  // as PhoneNumber としなくても PhoneNumber型が返る
}

export default PhoneNumber

2.3 実装した電話番号型の使い方

以下のように使用します。

import PhoneNumber from './phoneNumber'

const phoneNumberA = PhoneNumber('0120123456')
const phoneNumberB = PhoneNumber('08098765432')

console.log(phoneNumberA == phoneNumberB)
// false

2.4 Classでの電話番号型の実装

同じことはClassを使っても実現できます。例えば以下のように実装します。

export default class PhoneNumberClass {
  constructor(readonly value: string) {
    if (!this.validate(value)) {
      throw new Error(`${value}は電話番号ではありません`)
    }
  }

  validate(value: string) {
    // 半角数字1文字以上を電話番号とみなす
    return /^[0-9]+$/.test(value)
  }

  equals(other: PhoneNumberClass) {
    return this.value == other.value
  }
}

3. Opaqueの電話番号型とClassの電話番号型の違い

ここからは2つの実装の違いについて、色々実験してみます。

3.1 JSON文字列への変換

たとえば以下のようにuserAとuserBを作ってJSON文字列にしてみます。

  • userA: Opaqueで実装した電話番号を使用
  • userB: Classで実装した電話番号を使用
const userA = {
  name: 'karada',
  phoneNumber: PhoneNumber('0123456789')
}

const userB = {
  name: 'note',
  phoneNumber: new PhoneNumberClass('0123456789')
}

console.log(JSON.stringify(userA))
console.log(JSON.stringify(userB))
{"name":"karada","phoneNumber":"0123456789"}
{"name":"note","phoneNumber":{"value":"0123456789"}}

上記のとおり、Classで実装したほうはそのまま使うと{ "value": "xxx" } になってしまいます。

3.2 Jsonオブジェクト型への設定

以下のようなJSONオブジェクト型を定義してプロパティに電話番号を設定してみます。

type Json =
  | string
  | number
  | boolean
  | null
  | { [property: string]: Json }
  | Json[]

const userA: Json = {
  name: 'karada',
  phoneNumber: PhoneNumber('0123456789')
}

const userB: Json = {
   name: 'note',
   phoneNumber: new PhoneNumberClass('0123456789')
}

すると、userBのほうは型チェックでコケます。Json型はプリミティブのみサポートする型定義をしているので。

f:id:karadanote:20210331085256p:plain

3.3 ORMを使ったDBへの登録

同様に以下のような schema.prisma で定義されているDBに対して、Prisma Client を使ってデータを登録してみます。

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

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

model User {
  id    Int     @id @default(autoincrement())
  name  String?
  phoneNumber   String?
}

f:id:karadanote:20210331085415p:plain

このケースも同様にコケます。Prismaが生成するテーブルのカラムの型定義もプリミティブになっているので。

まとめると

Opaqueで実装した電話番号型では、JSON化したりDBに登録したりするサービスを実装するときに、プリミティブ型と同じように扱える、ということです。

もちろん、そもそもの Opaque型の利点としては、PhoneNumberを単にstringのエイリアスと定義しないことで、以下のような型チェックの恩恵を受けられることにあります。

f:id:karadanote:20210331001651p:plain

f:id:karadanote:20210331001702p:plain

他にもOpaqueでできることがあれば試してみたいですね。

Appendix

他の実装パターンとして以下の実装も参考にしました。

また、参考図書として、こちらの4章 ~ 6章が特に参考になります。

www.oreilly.co.jp

Prisma Client で踏み台のEC2を経由してRDSに接続する

はじめに

カラダノートの堀内です。 最近弊社では TypeScript ✕ Prisma の導入を進めはじめました。

この記事は Prismaのintrospection を開発環境やステージング環境のDBに対して踏み台のEC2を経由して実行するときのメモです。

f:id:karadanote:20210329000741p:plain
やりたいこと

手順

1) まずSSH でトンネリングしておきます。

$ ssh -N -L 3307:rdsのホスト:3306 ec2のユーザ名@ec2のホスト -i ec2のpem

2) prismaの設定を以下のように変更しておきます。

prisma/schema.prisma

datasource db {
  provider = "mysql"
  ...

prisma/.env

DATABASE_URL="mysql://DBユーザ名:パスワード@127.0.01:3307/接続先のDB名"

3)これで introspection を実行すれば接続先のRDSのDBから schema.prisma を生成してくれます。

$ npx prisma introspect

参考元

本家リポジトリに同様のIssueが挙がっていたので参考にしました。

Database Connection Via SSH Tunneling