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

## はじめに

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

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

## Entity定義
### 問題

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

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

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

### 対処法

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

`!`つけて回避できる。

## create関数の引数型
### 問題

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

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

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

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

それはつまり、

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

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

### 対処法

二通りある。

#### コンストラクタ

```ts
@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が引数として渡されることがある、ということであり、それは、

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

  // ...
}
```

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

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

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

```ts
@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にするということは、

```ts
await new User().save();
```

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

#### build関数を自作

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

```ts
@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](https://typeorm.io/#/eager-and-lazy-relations)を使いさえすれば、この項目は無視できるかもしれない。
しかしパフォーマンスを気にしだすと途端に苦しくなる。

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

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

### 型ずれ
#### 問題

普通にfindした場合、

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

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

#### 対処法

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

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

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


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

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

### 読み込み
#### 問題

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

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188872/1bdd5826-fca7-1ad0-cfec-6b824e05f32e.png)

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

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

ぐぬぬ。

#### 対処法

わからない。

### Webpack使用時の循環参照
#### 問題

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

https://github.com/typeorm/typeorm/issues/4526

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

```ts
import {Profile} from './Profile';

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

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

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

#### 対処法


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

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

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

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

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

```ts
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の力をフルに引き出せず、正しく使えていなかったから引き起こされたという説もある。