がんばれやってけ TypeORM
TypeScript アヨヨンヨアヨンヨー のやつ です。 アヨンヨーつくったひとに 「書いて〜😣」ていわれたので、がんばった。
概要
- TypeORM は EventListner とかと一緒に使わないと結構厳しい
- TypeScript の機能上仕方ない感もある
TypeORM とは
TypeScript で DB とよしなにやるための OR マッパーライブラリ。
デコレータを使って良い感じに DB の構造を指定してあげることができる。
例として、 OkWoman
なる以下のクラスのインスタンスの情報を DB に保存することを考える。
名前、年齢、誕生日、そして ok というプロパティを持っている。 ok が true だと 🙆 で false だと 🙅 という気持ち。
export class OkWoman { name: string; age: number; birthday: Date; ok: boolean; }
TypeORM ではデコレータを使うことで、 OkWoman のインスタンスを保存するテーブル名や、各プロパティを保存するカラム名、データの型などを指定してあげられる。 以下のようにする。
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm"; @Entity('ok_women') // テーブル名の指定 export class OkWoman { @PrimaryGeneratedColumn() // auto increment なカラムも指定できる id: number; @Column('varchar', { name: 'name' }) // カラムのデータ型とカラム名の指定 name: string; @Column('int', { name: 'age' }) age: number; @Column('date', { name: 'birthday' }) birthday: Date; @Column('tinyint', { name: 'ok' }) ok: boolean; }
こういう感じで Entity
を定義した後、 synchronize: true
をオプションに加えて DB に接続するか、 runMigrations
や synchronize
関数を使うと DB にテーブルが作成される。
DB への接続は下のようにする。
export const config: ConnectionOptions = { type: 'mysql', charset: 'utf8mb4', host: 'host', port: 3306, username: 'user', password: 'pass', database: 'database', entities: [ // このコネクションで用いる Entity を指定する。ここで指定したものを DB に保存したり DB から取り出したりできる。 OkWoman ], synchronize: true, logging: 'all', } await createConnection(config);
作成されるテーブルは以下の通り。
+----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +----------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | age | int(11) | NO | | NULL | | | birthday | date | NO | | NULL | | | ok | tinyint(4) | NO | | NULL | | | name | varchar(255) | NO | | NULL | | +----------+--------------+------+-----+---------+----------------+
const conn = createConnection(config)
として、 conn.getRepository(OkWoman)
とすると、 ok_woman
テーブルの操作を主に行なうためのレポジトリが得られる。
以下のようにして OkWoman
インスタンスを DB に保存したり DB から取り出したりする。
const repo = conn.getRepository(OkWoman); await repo.save({ name: 'popuko', age: 18, birthday: new Date('2017-12-09'), ok: false }); const record = await repo.findOne({ name: 'popuko' });
ここで、例えば repo.save({yeah: "yo"});
という風に、 OkWoman
のDeepPartial
でないものを OkWoman
のレポジトリで DB に保存しようとするとコンパイルエラーが出る。
DeepPartial<T>
は、下のリンク先で定義されている。T[P]
が DeepPartial<T[P]>
であるもの。Partial
の再帰版みたいなやつ。
typeorm/DeepPartial.ts at master · typeorm/typeorm · GitHub
こんな感じで TypeScript から DB 触れてしかも型も良い感じにチェックしてくれるから便利〜という代物である。
もっとがんばってくれ TypeORM
「こんな感じで TypeScript から DB 触れてしかも型も良い感じにチェックしてくれるから便利〜という代物である。」と言ったが、正直ムムっとなるところが多いので、そこについて話をする。 ムムっポイントは以下の2つ。
- DB から安全に
Entity
を取り出せていない - DB に安全に
Entity
を保存できていない
要するにランタイムエラーがすごいたくさん出てくる。かなしい😢
DB から安全に Entity
を取り出せていない
以下のように noOkWomanToInsert
なるものを DB に保存して取り出してみる。冷静に考えてかなりトチ狂った変数名な気がする。
const noOkWomanToInsert = { name: 'popuko', age: 18, birthday: new Date('2017/12/09'), ok: false, } const conn = await createConnection(config); console.log('connected mysql'); const repo = conn.getRepository(OkWoman); await repo.save(noOkWomanToInsert); const record = await repo.findOne({ name: 'popuko' });
このとき、 TypeORM の型定義によると record
の型は OkWoman | undefined
になっている。
DB に name
が 'popuko'
であるレコードが存在しない場合は undefined
が返ってくる。それはそう。
では、レコードが存在する場合は OkWoman
のインスタンスであるオブジェクトが record
に格納されているのかというと、実は全然そんなことない。
とりあえず中身を出力してみる。
すると、 Date
型と宣言した birthday
には string
型の値が、 boolean
型と宣言した ok
には number
型の値がセットされているのがわかる。
if (typeof record !== 'undefined') { console.log(record); // output: OkWoman { id: 1, name: 'popuko', age: 18, birthday: '2017-12-09', ok: 0 } // - birthday は string、ok は number となっている。 }
ok
については言うまでもなく、DB 内で tinyint
として保存しているのが原因である。
save
する際に true
を 1 、それ以外を 0 に変換してくれているようだ。(ソースコードの多分このあたり)
mysql では boolean
型を使えないので、 tinyint
で代用している(し、 typeorm のデフォルト設定もそうなっている)のだが、いざ取り出す時には 1 を true
、 0 を false
にはしてくれない。
これはかなり不便だ。
まず第一に、以下のように実際に取り出される値(true
でなく1、あるいは Date
でなく string
)を操作するようなコードはコンパイラエラーを起こす。
なぜなら record
は OkWoman
の型を持っており、 record.ok
は boolean
、 record.birthday
は Date
ということにされてしまっているので。
if (record.ok === 0) { console.log('she is not an ok_woman'); } // - Compile error: Operator '===' cannot be applied to types 'boolean' and '0'. if (record.birthday === '2017-12-09') { console.log(`Her birthday is ${record.birthday}`) } // - Compile error: Operator '===' cannot be applied to types 'Date' and 'string'.
逆に、次のようなコードはコンパイルエラーは起こさないもののランタイムエラーを起こす。
console.log(`Her birtday is ${record.birthday.toISOString()}`); // - TypeError: record.birthday.toISOString is not a function
正直かなり不愉快だ。
なんとかうまいこと TypeORM 側で良い感じに良いアレをして欲しいが、してくれてないので、私は現状 EntityListener
を使って騙し騙しやっている。
EntityListener は Entity
に Insert
やら Update
やら Load
やらのイベントが起きるタイミングで何らかの関数を実行するための機構である。
以下のような関数をデコレートして、 OkWoman
が DB から取り出されるタイミングで birthday
と ok
を型注釈に沿うような値に変換している。
@Entity('ok_women') export class OkWoman { @PrimaryGeneratedColumn() id: number; @Column('varchar', { name: 'name' }) name: string; @Column('int', { name: 'age' }) age: number; @Column('date', { name: 'birthday' }) birthday: Date; @Column('tinyint', { name: 'ok' }) ok: boolean; @AfterLoad() converter() { // DB から取り出される時に birthday と ok の値を変換する this.birthday = new Date(this.birthday); this.ok = (<any>this.ok === 0) ? false : true; } }
こうしてあげるとちゃんと birthday
に Date
、 ok
に boolean
の値がセットされる。が、正直ムムって感じだ😣
ちなみに、 birthday
に関してはカラムの型を date
でなく datetime
にするとちゃんと Date
型として取り出される。
datetime
はちゃんと Date
に変換するように、TypeORM の mysql ドライバに書いてあるからなのだが、ムムってなる。
DB に安全に Entity
を保存できていない
下のコードはコンパイルエラーを起こさないけどランタイムエラーは起きるよね、という話。
const repo = conn.getRepository(OkWoman); await repo.save({ name: 'sosogu' });
repo.save
が DeepPartial<OkWoman>
を受け取るようになっているのでこうなってしまう。
冷静に考えて DeepPartial<OkWoman>
型はある変数が DB に保存できることを全く保証しない。
TypeORM は Custom Repository を作れるので、ここでもう少し安全な saveHogeHoge
を作ってあげるのが良いと思う。
下みたいな感じ。
type ColumnsShouldInsert = | 'name' | 'age' | 'birthday' | 'ok' export type InsertableOkWoman = Pick<OkWoman, ColumnsShouldInsert>; @EntityRepository(OkWoman) export class OkWomanRepository extends Repository<OkWoman> { saveInsertable(entity: InsertableOkWoman) { return this.save(entity); } }
この Custom Repository を用いると、下のコードはちゃんとコンパイルエラーで弾かれる。
const conn = await createConnection(config); const repo = conn.getCustomRepository(OkWomanRepository); await repo.saveInsertable({ name: 'sosogu' }); // -- Compile error
ムムっ!😣
上の節で TypeORM の ムムっ!な部分とその対処法について述べた。 対処法は repository とか entity 自体をいじることになるので、結構微妙な気持ちになってしまう。 そんなに複雑でない DB をいじるときには knex.js なんかを使って自分で repository やら entity を 1 から実装した方が楽なんじゃないかと感じてしまう。 とはいうものの、(今回は面倒だから紹介してないが)テーブル間の関係なんかが増えてくると、やっぱり TypeORM(とかあるいは sequelize とか)を使う方が良いのだろう。
「DB から安全に Entity
を取り出せていない」「DB に安全に Entity
を保存できていない」も、現状の TypeScript の機能的にしょうがない気もする。
「Entity
を安全に保存・取り出すために Entity の型注釈読み込んで動的に save
や find
を生成しますッ!」とか言い出すと「お前それ JavaScript でも同じこと言えんの?」という感じになりそう。
JavaScript との互換性重視する以上しょうがないのかも。
でもなんかうまい感じのモデル生成言語で良い感じにアレして欲しいなあと思いつつ、色々騙し騙しなんとか TypeORM とつきあっています。
おしまい。
おまけ
ソースコードはこんな感じです。