327日目 PHP(laravel)が分かってきた社畜L_part2

## はじめに
### なんか便利なlaravelの機能
[前回の記事](https://qiita.com/QiiTaro_/items/b907f96be737638411e1)で話しましたが、laravelには開発に便利な機能がたくさんあります‼
~~まだlaravelでしか開発したことないので他のフレームワークと比べて〇〇なんてことは全く言えませんが...~~
便利な機能を知ることでよりスッキリしたコーディングが可能になったので紹介します‼

### 用語紹介
| 用語 | 解説 |
|-----------|------------|
| **リレーション** | テーブル同士を紐づける設定。<br>リレーションが行われると１つのデータ取得時に、紐づけられたテーブルのデータも取得される。 |
| **ソフトデリート**   | 物理的にはデータを削除せず、データは残したまま無効化し削除されたものとして扱う機能 |
| **メインテーブル** | メインとなる（データを直接問い合わせられる）テーブル　（※1） |
| **サブテーブル** | メインテーブルに紐づけられる形で、データを間接的に問い合わせられるテーブル。　（※1） |
| **中間テーブル** | メインテーブルとサブテーブルの紐づけの際に**必要があれば**登場するテーブル　（※1） |
| **メインモデルファイル**   | メインテーブルの設定を記述するモデルファイル　（※1） |
| **サブモデルファイル**   | サブテーブルの設定を記述するモデルファイル　（※1） |
| **中間モデルファイル**   | 中間テーブルの設定を記述するモデルファイル　（※1） |

（※1）いずれも公式名称ではありませんが、説明の都合で便宜上そう呼ぶことにします。
モデルファイルそのものについては[前回の記事](https://qiita.com/QiiTaro_/items/b907f96be737638411e1)を参考にしてください！

## 解説する機能
今回の説明は以下の機能になります。順に説明していきます。
- リレーション
- ソフトデリート（論理削除）

## :point_up:リレーション
#### :point_right:概要
リレーションとはテーブル同士の連携を指します。
（SQL文のテーブルJOINの概念を知っていると理解が速いと思います。）

例えば、県の情報を持つ**prefecturesテーブル**と市の情報を持つ**citiesテーブル**があるとします。

■prefecturesテーブル
| id | prefecture_name |
|:-:|:-:|
| 1 | 熊本県 |
| 2 | 山口県 |
| 3 | 青森県 |

■citiesテーブル
| id | city_name |
|:-:|:-:|
| 1 | 下関市 |
| 2 | 弘前市 |
| 3 | 八代市 |

ここからprefecturesテーブルにリレーションを示すカラム（この例では city_id）を追加することで２つのテーブルを紐づけます。
**⚠**テーブル名は複数形（~s）ですが、カラム名は単数形になります。cities_idとはしない！

■prefecturesテーブル

| id | prefecture_name | city_id |
|:-:|:-:|:-:|
| 1 | 熊本県 | 3 |
| 2 | 山口県 | 1 |
| 3 | 青森県 | 2 |

このリレーション設定があれば、メインとなるprefecturesテーブルを参照したときに
熊本県→八代市、山口県→下関市、青森県→弘前市というcitiesテーブルの情報も芋づる式に取得できるということです。

また、このリレーション設定は「/app/Models」配下の該当テーブルに対応するモデルファイルに記載することになります。

#### :point_right:種類
リレーションには大きく4種類あり、用途によって使い分けることが重要です。

| 種類 | 解説 | 用途の例 |
|-----------|------------|------------|
| **hasOne<br>1：1** | 1つのidに、<br>1レコードが対応 | 1テーブルだけだとカラム数が<br>膨大になるなどの都合で<br>複数テーブルにカラム分割している<br>場合に、<br>対応するレコードを取得する際に。 |
| **hasMany<br>1：n** | 1つのidに、<br>複数レコードが対応 | 県に紐づく複数の市の情報を<br>取得する際に。 |
| **belongsTo<br>n：1** | 複数idに、<br>1レコードが対応 | 複数の市に紐づく同じ県の情報を<br>取得する際に。 |
| **belongsToMany<br>n：n** | 中間テーブル経由で、<br>２テーブル間で<br>複数レコードが<br>双方向に対応 | 県ごとに紐づく複数の特産物の情報を<br>取得する際に。 |

では改めて種類ごとにイメージとモデルファイル/コントローラファイル設定を見ていきましょう。

#### :point_right:hasOne（1：1）
- イメージ
    メインテーブルとサブテーブルのレコードが1：1で対応します。
    **⚠**対応データが複数/対応データが無い場合は利用できません。

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766505/17bbd9cc-212f-4434-82d5-05db0256fc65.png)

    ■prefecturesテーブル（リレーション設定込み）
    | id | prefecture_name | population_id |
    |:-:|:-:|:-:|
    | 1 | 熊本県 | 1 |
    | 2 | 山口県 | 2 |
    | 3 | 青森県 | 3 |
    
    ■populationsテーブル
    | id | population[万人] |
    |:-:|:-:|
    | 1 | 160|
    | 2 | 130 |
    | 3 | 120 |
    
    ★取得されるデータ
    設定により以下のようなリレーションが成立し、指定したidに該当する行のデータが取得できます。
    | id | prefecture_name | population_id | population[万人] |
    |:-:|:-:|:-:|:-:|
    | 1 | 熊本県 | 1 | 160 |
    | 2 | 山口県 | 2 | 130 |
    | 3 | 青森県 | 3 | 120 |
<br>

- メインモデルファイル設定内容
    ```php
    // メインモデルファイルにてサブモデルファイルの利用を宣言
    use App\Models\Population;
    
    // 任意の名前でfunctionを作成
    public function relatePopulations()
    {
        // populationsテーブルとの紐づけを設定
        return $this->hasOne(Population::class);
    }
    ```
<br>

- コントローラファイル設定内容
    コントローラからモデルファイルを参照するためにこの設定が必要になります。
    ```php    
    // コントローラファイルにて、リレーション設定を記載したモデルファイルの利用を宣言
    use App\Models\Prefecture;

    $prefectureId = 3; //青森県
    
    // リレーション設定されたサブテーブルのデータを取得
    // 例）青森県のidをkeyに、人口が120万人というデータを取得
    $ans = Prefecture::find($prefectureId)->relatePopulations;
    ```
<br>

#### :point_right:hasMany（1：n）
- イメージ
    メインテーブルの1レコードがサブテーブルの複数レコードに紐づきます。

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766505/3aee8542-01c2-4339-90d0-830650058cb9.png)

    ■prefecturesテーブル
    | id | prefecture_name |
    |:-:|:-:|
    | 1 | 熊本県 |
    | 2 | 山口県 |
    
    ■citiesテーブル（リレーション設定込み）
    | id | city_name | prefecture_id |
    |:-:|:-:|:-:|
    | 1 | 熊本市 | 1 |
    | 2 | 八代市 | 1 |
    | 3 | 山口市 | 2 |
    | 4 | 下関市 | 2 |
    
    ★取得されるデータ
    設定により以下のようなリレーションが成立し、指定したidに該当する行のデータが取得できます。
    | id | prefecture_name | prefecture_id | city_name |
    |:-:|:-:|:-:|:-:|
    | 1 | 熊本県 | 1 | 熊本市 |
    | 1 | 熊本県 | 1 | 八代市 |
    | 2 | 山口県 | 2 | 山口市 |
    | 2 | 山口県 | 2 | 下関市 |
<br>

- メインモデルファイル設定内容
    ```php
    // メインモデルファイルにてサブモデルファイルの利用を宣言
    use App\Models\City;
    
    // 任意の名前でfunctionを作成
    public function relateCities()
    {
        // citiesテーブルとの紐づけを設定
        return $this->hasMany(City::class);
    }
    ```
<br>

- コントローラファイル設定内容
    ```php
    // コントローラファイルにて、リレーション設定を記載したモデルファイルの利用を宣言
    use App\Models\Prefecture;

    $prefectureId = 2; //山口県
    
    // リレーション設定されたサブテーブルのデータを取得
    // 例）山口県のidをkeyに、所属している市が山口市,下関市というデータを取得
    $ans = Prefecture::find($prefectureId)->relateCities;
    ```

#### :point_right:belongsTo（n：1）
- イメージ
    メインテーブルの複数レコードがサブテーブルの1レコードに紐づきます。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766505/ed66a930-da8b-4c05-8a6e-4ada12aef7c8.png)

     ■citiesテーブル（リレーション設定込み）
    | id | city_name | prefecture_id |
    |:-:|:-:|:-:|
    | 1 | 熊本市 | 1 |
    | 2 | 八代市 | 1 |
    | 3 | 山口市 | 2 |
    | 4 | 下関市 | 2 |
    
    ■prefecturesテーブル
    | id | prefecture_name |
    |:-:|:-:|
    | 1 | 熊本県 |
    | 2 | 山口県 |
    
    ★取得されるデータ
    設定により以下のようなリレーションが成立し、指定したidに該当する行のデータが取得できます。
    | id | city_name | prefecture_id | prefecture_name |
    |:-:|:-:|:-:|:-:|
    | 1 | 熊本市 | 1 | 熊本県 |
    | 2 | 八代市 | 1 | 熊本県 |
    | 3 | 山口市 | 2 | 山口県 |
    | 4 | 下関市 | 2 | 山口県 |
<br>
    
- メインモデルファイル設定内容
    ```php
    // メインモデルファイルにてサブモデルファイルの利用を宣言
    use App\Models\Prefecture;
    
    // 任意の名前でfunctionを作成
    public function relatePrefectures()
    {
        // prefecturesテーブルとの紐づけを設定
        return $this->belongsTo(Prefecture::class);
    }
    ```
<br>

- コントローラファイル設定内容
    ```php
    // コントローラファイルにて、リレーション設定を記載したモデルファイルの利用を宣言
    use App\Models\Cities;

    $cityId = 1; //熊本市
    
    // リレーション設定されたサブテーブルのデータを取得
    // 例）熊本市のidをkeyに、所属する県が熊本県というデータを取得
    $ans = City::find($cityId)->relatePrefectures;
    ```
<br>

#### :point_right:belongsToMany（n：n）
- イメージ
    中間テーブル（prefecture_specialityテーブル）を経由して２つのテーブルを紐づけます。

    **⚠**中間テーブルには命名法則があります。
    1. ２つのテーブル名（単数形）をアルファベット順にアンダーバー「_」で繋げる
    2. カラムも左からアルファベット順

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766505/d4c49a66-f8f9-4085-a6c2-ad402eb9b9c1.png)

    ■prefecturesテーブル
    | id | prefecture_name |
    |:-:|:-:|
    | 1 | 熊本県 |
    | 2 | 山口県 |
    | 3 | 青森県 |
    
    ■prefecture_specialityテーブル
    | id | prefecture_id | speciality_id | created_at |
    |:-:|:-:|:-:|:-:|
    | 1 | 1 | 1 | 2025-02-17 01:11:11 |
    | 2 | 1 | 2 | 2025-02-17 02:11:11 |
    | 3 | 2 | 3 | 2025-02-17 03:11:11 |
    | 4 | 3 | 3 | 2025-02-17 04:11:11 |
    
    ■specialitiesテーブル
    | id | speciality_name |
    |:-:|:-:|
    | 1 | 馬肉 |
    | 2 | すいか |
    | 3 | りんご |
    
    ★取得されるデータ
    設定により以下のようなリレーションが成立し、指定したidに該当する行のデータが取得できます。
    - prefecturesテーブルをメインテーブルとするとき
        | id | prefecture_name | prefecture_id | speciality_id | speciality_name |
        |:-:|:-:|:-:|:-:|:-:|
        | 1 | 熊本県 | 1 | 1 | 馬肉 |
        | 1 | 熊本県 | 1 | 2 | すいか |
        | 2 | 山口県 | 2 | 3 | りんご |
        | 3 | 青森県 | 3 | 3 | りんご |
    <br>
    
    - specialityテーブルをメインテーブルとするとき
        | id | speciality_name | speciality_id | prefecture_id | prefecture_name |
        |:-:|:-:|:-:|:-:|:-:|
        | 1 | 馬肉 | 1 | 1 | 熊本県 |
        | 2 | すいか | 2 | 1 | 熊本県 |
        | 3 | りんご | 3 | 2 | 山口県 |
        | 3 | りんご | 3 | 3 | 青森県 |
<br>

- メインモデルファイル設定内容
    - prefecturesテーブルをメインテーブルとするとき）
        ```php
        // メインモデルファイルにて、サブモデルファイルの利用を宣言
        use App\Models\Speciality;
        
        // 任意の名前でfunctionを作成
        public function relateMiddles()
        {
            // middleテーブルを経由したspecialitiesテーブルとの紐づけを設定
            // belongsToMany([目的の紐づけ先]::class, '[中間テーブル名]', '[メインテーブルのid]', '[サブテーブルのid]')
            return $this->belongsToMany(Speciality::class, 'prefecture_speciality', 'prefecture_id', 'speciality_id')
        }
        ```
    <br>
    
    - specialityテーブルをメインテーブルとするとき
        ```php
        // メインモデルファイルにて、サブモデルファイルの利用を宣言
        use App\Models\Prefecture;
        
        // 任意の名前でfunctionを作成
        public function relateMiddles()
        {
            // middleテーブルを経由したprefecturesテーブルとの紐づけを設定
            return $this->belongsToMany(Prefecture::class, 'prefecture_speciality', 'prefecture_id', 'speciality_id')
        }
        ```
<br>

- コントローラファイル設定内容
    ```php
    // コントローラファイルにて、リレーション設定を記載したモデルファイルの利用を宣言
    use App\Models\Prefecture;
    use App\Models\Speciality;

    $prefectureId = 1; //熊本県
    $specialityId = 3; //りんご 
    
    // 中間テーブル経由でリレーション設定されたサブテーブルのデータを取得
    // 例）熊本県のidをkeyに、特産物が馬肉,すいかというデータを取得
    $ans = Prefecture::find($prefectureId)->relateMiddles;

    // 例）りんごのidをkeyに、それが特産物な山口県,青森県というデータを取得
    $ans = Speciality::find($specialityId)->relateMiddles;
    ```
<br>

- （補足）中間テーブルへのデータ追加/削除する方法
    コントローラファイルに、以下を必要に応じて記述します。

    ```php
    $prefectureId = 1; //熊本県
    $specialityId1 = 1; //馬肉
    $specialityId2 = 2; //すいか

    
    // 1レコードだけの追加
    // 例）"県:熊本県-特産品:馬肉"という関連を追加
    $prefectureId->relateMiddles()->attach($specialityId1);

    // 複数レコードを追加
    // 例）"県:熊本県-特産品:馬肉", "県:熊本県-特産品:すいか"という関連を追加
    $prefecture->relateMiddles()->attach([
        $specialityId1 => ['created_at' => now()],
        $specialityId2 => ['created_at' => now()]
    ]);


    // 1レコードだけの削除
    // 例）"県:熊本県-特産品:馬肉"という関連を削除
    $prefectureId->relateMiddles()->detach($specialityId1);

    // 複数レコードを削除
    // 例）"県:熊本県-特産品:馬肉", "県:熊本県-特産品:すいか"という関連を削除
    $prefectureId->relateMiddles()->detach([$specialityId1, $specialityId2]);

    // 全ての関連付けを解除
    // 例）"県:熊本県"の関連をすべて削除
    $prefectureId->relateMiddles()->detach();
    ```

## :point_up:ソフトデリート（論理削除）
#### :point_right:概要
データ削除の際に物理的にレコード削除してしまうと、何かあった場合に復元できなくなってしまう。
そのような状況の回避方法として、レコードそのものは残したまま無効化することで**そのレコードは削除されたものとして扱う**ことができ、それをソフトデリート（論理削除）と呼びます。

#### :point_right:テーブルイメージ
ソフトデリートを行うためには**deleted_at**カラムを追加する必要があります。
deleted_atに日付が登録されたレコードがソフトデリートされたものとなります。
<br>

例：社畜Mと社畜♀をソフトデリートする。
■usersテーブル
| id | user_name | deleted_at |
|:-:|:-:|:-:|
| 1 | SYACHIKU_S | null |
| 2 | SYACHIKU_M | 2025-02-17 01:11:11 |
| 3 | SYACHIKU_L | null |
| 4 | SYACHIKU_♀ | 2025-02-18 02:22:22 |
| 5 | SYACHIKU_MM | null |

#### :point_right:モデルファイル設定内容
下記を追記することで、通常の物理削除がソフトデリートに切り替わります。
ソフトデリートされたデータは取得の際に無視されます（取得しない）。
```php
// ソフトデリート利用を宣言
use Illuminate\Database\Eloquent\SoftDeletes; 

class user extends Model
{
    // ソフトデリート利用を再宣言
    use SoftDeletes;
}
```


#### :v:おまけ：データの扱い方をいくつか紹介

- データ取得のパターン
    ```php
    $userId = 1;
    
    // 条件に合致するレコードがソフトデリートされていない場合に取得
    $ans = user::find($userId)->users
    
    // 条件に合致するレコードをソフトデリートされたかどうかを問わず取得
    $ans = user::withTrashed()->find($userId)->users;
    
    // 条件に合致するレコードがソフトデリートされている場合に取得
    $ans = user::onlyTrashed()->find($userId)->users;
    
    // ソフトデリートされていないレコードをすべて取得
    user::get();
    
    // ソフトデリートされたかどうかを問わずすべてのレコードを取得
    user::withTrashed()->get();
    
    // ソフトデリートされているレコードをすべて取得
    user::onlyTrashed()->get();
    ```
<br>

- ソフトデリート状態を解除する（restore）
    ```php
    // idをキーにソフトデリート状態を解除
    user::withTrashed()->where('id', 1)->restore();
    
    // 複数id（複数レコード）のソフトデリート状態を解除
    user::withTrashed()->whereIn('id', [1, 2, 3])->restore();
    
    // idが存在しなかった場合の参照エラーを回避するやり方
    $user = user::withTrashed()->find(1);
    $user->restore(); 
    ```
<br>

- データを物理削除する（forceDelete）
    ```php
    // idをキーに物理削除
    user::withTrashed()->where('id', 1)->forceDelete();
    
    // 複数id（複数レコード）を物理削除
    user::withTrashed()->whereIn('id', [1, 2, 3])->forceDelete();
    
    // idが存在しなかった場合の参照エラーを回避するやり方
    $user = user::withTrashed()->find(1);
    $user->forceDelete(); 
    ```

## まとめ
今回はlaravelの応用編ということで、個人的に便利だと思った機能のリレーションとソフトデリートについて解説しました‼
他にもlaravelには便利な書き方がたくさんあるので、laravelで開発をする際はぜひ活用してください‼