TypeORMでQueryBuilderを使わずにJOIN先のテーブルのカラムをWhere句で使用する

### 注意
この記事の公開から時間が経っていたり、`typeorm`のバージョンが異なる場合は、仕様が異なる場合があります。

```
動作確認: 2021/10/7
typeorm: v0.2.34
```

## TL;DR

### JOIN先の主キー(ID）で絞り込む場合

```typescript
const teenProgrammers = await getRepository(User).find({
  relations: ['role'], // なくてもOK
  where: {
    age: LessThanOrEqual(19),
    role: {
      id: programmer.id
    }
  }
})
```

### JOIN先の主キー(ID）以外で絞り込む場合

```typescript
const teenProgrammers = await getRepository(User).find({
  join: {
    alias: "user",
    innerJoin: { role: "user.role" },
  },
  where: (qb: SelectQueryBuilder<User>) => {
    qb.where({ age: LessThanOrEqual(19) }).andWhere(
      "role.name = :roleName",
      { roleName: "programmer" }
    );
  },
});
```


## 前提条件

### エンティティ

```typescript:entity/role.ts
@Entity()
export class Role {
    @PrimaryGeneratedColumn()
    id?: number;

    @Column()
    name: string;

    @OneToMany(() => User, user => user.id)
    users: User[];
}
```

```typescript:entity/user.ts
@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id?: number;

    @Column()
    name: string;

    @Column()
    age: number;

    @ManyToOne(() => Role)
    role?: Role
}
```

### メイン

```typescript:index.ts
createConnection().then(async connection => {
  const [programmer, boxer] = await getRepository(Role).save([
    {
      name: 'programmer'
    },
    {
      name: 'boxer'
    }
  ])

  await getRepository(User).save([
    {
      name: 'alice',
      age: 18,
      role: programmer
    },
    {
      name: 'bob',
      age: 55,
      role: boxer
    },
    {
      name: 'carol',
      age: 30,
      role: programmer
    }
  ])
})

}).catch(error => console.log(error))
```

### データベース内の状態

#### role

| id   | name       |
| ---- | ---------- |
| 1    | programmer |
| 2    | boxer      |


#### user

| id   | name  | age  | roleId |
| ---- | ----- | ---- | ------ |
| 1    | alice | 18   | 1      |
| 2    | bob   | 55   | 2      |
| 3    | carol | 30   | 1      |


## 他の方法

### クエリビルダを使う

表現力が高くSQLライクなのはいいですが、冗長です。

```typescript
const teenProgrammers = await getRepository(User)
  .createQueryBuilder("user")
  .innerJoinAndSelect("user.role", "role")
  .where("role.id = :roleId", { roleId: programmer.id })
  .andWhere("age <= 19")
  .getMany();
```

### できない例

SQLも問題なく生成されますが、パラメータにnullが渡されてしまうのでダメです。

```typescript
const teenProgrammers = await getRepository(User).find({
  relations: ["role"],
  where: {
    age: LessThanOrEqual(19),
    "role.id": programmer.id,
  },
});
```

発行されるSQL（抜粋）

```sql
LEFT JOIN "role" "User__role"
ON "User__role"."id"="User"."roleId"
WHERE "User"."age" <= ?
AND "User"."roleId" = ?
-- PARAMETERS: [19,null]
```

### ID以外をQBを使わずに指定するとダメ

この場合も発行されるSQLと渡されるパラメータは上記と同じで`roleId = null`になります。

```typescript
const teenProgrammers = await getRepository(User).find({
  relations: ["role"],
  where: {
    age: LessThanOrEqual(19),
    role: {
      name: "programmer",
    },
  },
});
```

### QBとrelationsを併用すると微妙

`role.name`と指定することはできません。

```typescript
const teenProgrammers = await getRepository(User).find({
  relations: ["role"],
  where: (qb: SelectQueryBuilder<User>) => {
    qb.where({ age: LessThanOrEqual(19) }).andWhere(
      "role.name = :roleName",
      { roleName: "programmer" }
    );
  },
});
```

```sql
FROM "user" "User"
LEFT JOIN "role" "User__role"
ON "User__role"."id"="User"."roleId"
WHERE "User"."age" <= ?
AND role.name = ? -- PARAMETERS: [19 "programmer"]
```

`User__role.name`と書けば冒頭のコードと同様に動作しますが、内部的に自動生成された（されるはずの）文字列を指定するのは微妙な気がします。

```typescript
const teenProgrammers = await getRepository(User).find({
  relations: ["role"],
  where: (qb: SelectQueryBuilder<User>) => {
    qb.where({ age: LessThanOrEqual(19) }).andWhere(
      "User__role.name = :roleName",
      { roleName: "programmer" }
    );
  },
});
```

## 参考
* [Finding entity with with relation condition #4396](https://github.com/typeorm/typeorm/issues/4396)