[CodeIgniter4 Tips] Model同士をJOINするModelを作った（useSoftDeleteを考慮したテーブルの結合方法）

### 2022年1月25日修正

RIGHT JOIN時にバグがあったので修正しました。

```
joinModel
- $cond .= ' AND ' . $model->table . '.' . $model->deletedField . ' IS NULL';
+ $this->where($model->table . '.' . $model->deletedField . ' IS NULL');

joinModelOnlyDeleted
- $cond .= ' AND ' . $model->table . '.' . $model->deletedField . ' IS NOT NULL';
+ $this->where($model->table . '.' . $model->deletedField . ' IS NOT NULL');
```

----

※先に、以下の記事を読んでいただくと理解が深まると思います

- [getCompiledSelect の罠、 useSoftDelete が抜け落ちる](https://qiita.com/bananacoffee/items/0aff3f5cd005a5f1bfc6)
- [getCompiledSelect で useSoftDelete が効くようにする](https://qiita.com/bananacoffee/items/1b9b56ce541174a3fb7d)

# Model で useSoftDelete を使いながらJOINをしようと思うと面倒くさい

[getCompiledSelect で useSoftDelete が効くようにする](https://qiita.com/bananacoffee/items/1b9b56ce541174a3fb7d) の解決部で触れているが、メンテナンス性を上げるために**Modelの機能を使いながらJOIN句を使おうとするとどうしてもサブクエリが発生してしまう**。

以下、[getCompiledSelect が useSoftDelete を認識してくれる場合](https://qiita.com/bananacoffee/items/1b9b56ce541174a3fb7d#getcompiledselect-%E3%81%8C-usesoftdelete-%E3%82%92%E8%AA%8D%E8%AD%98%E3%81%97%E3%81%A6%E3%81%8F%E3%82%8C%E3%82%8B%E5%A0%B4%E5%90%88) より

```php
$hogeModel = model('HogeModel');
$piyoModel = model('PiyoModel');
$hogePiyoList = $hogeModel
    ->join('(' . $piyoModel->getCompiledSelect() . ') piyo', 'hoge.id = piyo.hoge_id', 'inner')
    ->find();
// SELECT * 
// FROM hoge 
// INNER JOIN (SELECT * FROM piyo WHERE deleted_at IS NULL) piyo ON hoge.id = piyo.hoge_id  ← このサブクエリが憎い
// WHERE hoge.deleted_at IS NULL;
```

小規模な開発では問題にならないが、JOINが増えたり、レコード数が巨大になっていくほどサブクエリがボトルネックになっていくことは想像に難くない。

そこで、[getCompiledSelectを使わない（使えない）場合１](https://qiita.com/bananacoffee/items/1b9b56ce541174a3fb7d#getcompiledselect%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%84%E4%BD%BF%E3%81%88%E3%81%AA%E3%81%84%E5%A0%B4%E5%90%88%EF%BC%91) のように手動でJOINしたものが簡単に作成できるようにカスタムのModelを作成した。

# カスタムモデルの作成

[前回作成した MyModel.php] (https://qiita.com/bananacoffee/items/1b9b56ce541174a3fb7d#%E3%81%A4%E3%81%BE%E3%82%8Amodel%E3%81%AB-getcompiledselect-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%A6%E3%82%84%E3%82%8C%E3%81%B0%E3%82%88%E3%81%84) に追記する、

```app/Models/MyModel.php
/**
 * JOIN Model without delted
 *
 * モデル同士をJOINする（useSoftDeleteを利用する）
 *
 * @param object  $model
 * @param string  $cond   The join condition
 * @param string  $type   The type of join
 * @param boolean $escape Whether not to try to escape identifiers
 *
 * @return $this
 */
public function joinModel(object $model, string $cond, string $type = '', bool $escape = null) {
	if ($model->useSoftDeletes) {
		$this->where($model->table . '.' . $model->deletedField . ' IS NULL');
	}
	return parent::join($model->table, $cond, $type, $escape);
}

/**
 * JOIN Model include Deleted
 *
 * モデル同士をJOINする(deletedを含む)
 */
public function joinModelWithDeleted(object $model, string $cond, string $type = '', bool $escape = null) {
	return parent::join($model->table, $cond, $type, $escape);
}

/**
 * JOIN Model Only Deleted
 *
 * モデル同士をJOINする(deletedのみ)
 */
public function joinModelOnlyDeleted(object $model, string $cond, string $type = '', bool $escape = null) {
	if ($model->useSoftDeletes) {
		$this->where($model->table . '.' . $model->deletedField . ' IS NOT NULL');
	}
	return parent::join($model->table, $cond, $type, $escape);
}
```

見ての通り、`useSoftDeletes` が設定されていたら `$cond` に追記してビルダに渡しているだけである。

# 使い方

`join()` のテーブルの代わりにモデルを渡せば良い。
`MyModel`の継承の仕方は[前回の記事](https://qiita.com/bananacoffee/items/1b9b56ce541174a3fb7d#%E3%81%A4%E3%81%BE%E3%82%8Amodel%E3%81%AB-getcompiledselect-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%97%E3%81%A6%E3%82%84%E3%82%8C%E3%81%B0%E3%82%88%E3%81%84)を参照

```php
$hogeModel = model('HogeModel'); // MyModelを継承していること
$piyoModel = model('PiyoModel'); // MyModelは必要ないが継承しておいて損はないと思う
$hoteModel
	->joinModel($piyoModel, $hogeModel->table . '.id = ' . $piyoModel->table . '.hoge_id', 'inner')
	->find();
// SELECT * 
// FROM hoge 
// INNER JOIN piyo ON hoge.id = piyo.hoge_id
// WHERE hoge.deleted_at IS NULL AND piyo.deleted_at IS NULL;

$hoteModel
	->joinModelWithDeleted($piyoModel, $hogeModel->table . '.id = ' . $piyoModel->table . '.hoge_id', 'inner')
	->find();
// SELECT * 
// FROM hoge 
// INNER JOIN piyo ON hoge.id = piyo.hoge_id
// WHERE hoge.deleted_at IS NULL;

$hoteModel
	->joinModelOnlyDeleted($piyoModel, $hogeModel->table . '.id = ' . $piyoModel->table . '.hoge_id', 'inner')
	->find();
// SELECT * 
// FROM hoge 
// INNER JOIN piyo ON hoge.id = piyo.hoge_id
// WHERE hoge.deleted_at IS NULL AND piyo.deleted_at IS NOT NULL;
```

複雑なJoin句には対応できないかもしれないが、結構使えるはず。