3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeORMで困ったところとその対処法

Last updated at Posted at 2021-11-20

はじめに

TypeScriptでのORM選択時の一大候補、TypeORM。
デコレーターでスキーマ定義、ActiveRecordスタイル、ダウンロード数。
そのどれもが魅力的である。

しかし、TypeORMにはいくつか落とし穴がある。
ここでは私が思い出せる限りの問題と、その対処法を並べていく。

Entity定義

問題

公式のサンプルのようにEntityを定義するとうまく行かない。

@Entity()
class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;
  @Column()
  name: string;
}

idもnameもコンストラクタで初期化されていないため、VSCodeが赤線で怒ってくる。

対処法

@Entit()
class User extends BaseEntity {
  @PrimaryGeneratedColumn()
- id: number;
+ id!: number;
  @Column()
- name: string;
+ name!: string;
}

!つけて回避できる。

create関数の引数型

問題

レコードを作るときの書き方は大体二通りだろう。

// newする。
const user = new User();
user.name = 'name';
await user.save();

// create関数を使用する。
await User.create({name: 'name'}).save();

注意すべきは、このどちらも、初期化漏れを許容することだ。
newする方は代入忘れを防ぐ方法がなく、create関数に渡す初期化子はPartialになっている。

それはつまり、

@Entity()
class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id!: number;
  @Column()
  name!: string;
+ @Column()
+ email!: string;
}

のように新しいカラムを足しても、型は警報を鳴らすことなくビルドが成功し、save()時にnot null制約に引っかかるということだ。

対処法

二通りある。

コンストラクタ

@Entity()
class User extends BaseEntity {
  constructor(name: string, email: string) {
    super();
    this.name = name;
    this.email = email;
  }

  @PrimaryGeneratedColumn()
  id!: number;
  @Column()
  name: string;
  @Column()
  email: string;
}

await new User('name', 'example@example.example').save();

これは上手くいくように見えるし、実際一定の条件下では上手くいく。
nameとemailの!も取れているため、コンストラクタでの初期化忘れにも対応している。

しかし、console.logを仕掛けてみるとわかるが、TypeORMはcreateConnection時になんと、一度引数無しで強引にインスタンスを作っているのだ。

それはつまり、上記のコードは一応動くが、厳密にはundefinedが引数として渡されることがある、ということであり、それは、

@Entity()
class User extends BaseEntity {
  constructor(props: {name: string, email: string}) {
    super();
    // ↓でアウト。
    this.name = props.name;
    this.email = props.email;
  }

  // ...
}

のように、オブジェクトを引数に取ると、createConnection時にpropsundefinedの状態で呼び出され、undefined.nameで死ぬということだ。

引数オブジェクトに対するアクセスは許されない。

これの対処法は、propsをoptionalにしてチェックをするということだが、これではコンストラクタでnameやemailを初期化していないケースが存在することになり、nameとemailに!が舞い戻ることになる。

@Entity()
class User extends BaseEntity {
  constructor(props?: {name: string, email: string}) {
    super();
    if (props == null) return;
    this.name = props.name;
    this.email = props.email;
  }


  @PrimaryGeneratedColumn()
  id!: number;
  @Column()
  name!: string;
  @Column()
  email!: string;
}

そしてoptionalにするということは、

await new User().save();

ができるようになるということだ。
ぐぬぬ。

build関数を自作

TypeORMのcreateが信用できなければ、ラッピングすれば良い。

@Entity()
class User extends BaseEntity {
  static build = (props: {name: string, email: string}) => User.create(props);

  // ...
}

await User.build({name: 'name', email: 'example@example.example'}).save();

当然だが、上手くいく。
定義が面倒だということ以外に目立った欠点はないだろう。
すべきだ。

関連

eager-and-lazy-relationsを使いさえすれば、この項目は無視できるかもしれない。
しかしパフォーマンスを気にしだすと途端に苦しくなる。

この項目ではUserがProfileを持っていると仮定する。
こんな具合だ。

@Entity()
class User extends BaseEntity {
  @OneToOne(() => Profile)
  readonly profile!: Profile;
}

型ずれ

問題

普通にfindした場合、

const user = await User.findOne()!;
user.profile;// type: Profile, real: undefined

関連は読み込まれないので、user.profileundefinedなのだが、型はProfileだ。
言うまでもなく危険である。

対処法

これを回避するためには、

@Entity()
class User extends BaseEntity {
  @OneToOne(() => Profile)
- readonly profile!: Profile;
+ readonly profile?: Profile;
}

のように、profileをoptionalにすれば良いのだが、それはそれで、

const user = await User.findOne({relations: ['profile']})!;
user.profile;// type: Profile | undefined, real: Profile
const userProfile = user.profile!;

のようになるため、読み込んだ後の使い勝手が悪いが、仕方ないだろう。

読み込み

問題

const user = await User.findOne({relations: ['profile']})!;

気をつけてほしい。
タイポは致命的だ。

relationsはstring[]だ。
relationsはstring[]なのだ。

image.png

それはつまり、こう書いてもコンパイルは通るということだ。

const users = await User.find({relations: ['プロフィール']});

ぐぬぬ。

対処法

わからない。

Webpack使用時の循環参照

問題

私はTypeORMをNext.jsと一緒に使っていたのだが、production環境の時だけエラーが出る事件があった。
結構な時間をかけて調査した結果、原因はこれだった。

TypeORMは関連を定義する時、関連先のEntityをimportする必要がある。

import {Profile} from './Profile';

@Entity()
class User extends BaseEntity {
  @OneToOne(() => Profile)
  readonly profile!: Profile;
}

それはUser.tsと同様に、Profile.tsでも必要だ。
そしてこれは循環importになり、使用しているビルドツールによってはサポートされていない。

理論上importimport typeに変更することで修正できるが、@OneToOne(() => Profile)の行があるので、スムーズにはいけない。

対処法

-import {Profile} from './Profile';
+import type {Profile} from './Profile';

@Entity()
class User extends BaseEntity {
- @OneToOne(() => Profile)
+ @OneToOne('Profile')
  readonly profile!: Profile;
}

@OneToOneに渡すものは文字列でも良いため、これで問題ない。
しかし、文字列だ。
文字列なのだ。
ぐぬぬ。

TypeORMを使用するときは用心すべし

ざっと思い出せる罠はこれくらいだが、とどのつまり、TypeORMの型情報はあまり信用できない。

const user = User.create({name: 'name'});
user.id; // Type: number, real: undefined
await user.save();
user.id; // Type: number, real: number

const user2 = User.find(user.id, {select: ['id']});
user2.name; // Type: string, real: undefined

create後のautogenerateカラムの型ずれは仕方がないにしても、selectはよほどのことがなければ使わないほうが身のためだろう。

最後に

これらの問題は私自身の力量不足や知識不足でTypeORMの力をフルに引き出せず、正しく使えていなかったから引き起こされたという説もある。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?