Eloquentのグローバルスコープ解説: クエリ全体に一貫性を持たせる方法

:::note 
<details><summary>AI要約</summary>

- グローバルスコープは特定のモデルに対する全てのクエリに適用され、select、update、delete、countなどの操作に自動的に適用される
- 特定の条件（例：active = 1やdeleted_at IS NULL）が全てのクエリに一貫して適用され、条件を満たすデータだけが常に処理される
- ローカルスコープはクエリビルダ内でメソッドチェーンとして呼び出されるが、グローバルスコープは自動的に適用されるため、明示的に呼び出す必要がない
- グローバルスコープはスコープクラスを定義し、それをモデルに適用することで利用される
- グローバルスコープを無効にするには、withoutGlobalScopeメソッドを使用する必要がある
</details>
:::
## はじめに
以前にローカルスコープの記事を出しました。今回はグローバルスコープについて解説していこうと思います。グローバルというのもあり、責任範囲が広くなってしまうので注意が必要ですよね

## 概要
Eloquentは、Laravelの強力なORM（Object-Relational Mapping)で、データベース操作を簡単かつ直感的に行うことができます
グローバルスコープは、特定のモデルに対する全てのクエリに一貫した条件を適用するための機能です
これにより、コードの再利用性とクエリの一貫性が向上します

## メリット、デメリット
### メリット
- 一貫性のあるクエリ
    - 特定の条件を全てのクエリに自動的に適用することで、一貫性を保つことができます
- コードの再利用
    - 共通のクエリロジックを一箇所にまとめることで、コードの重複を避けることができます
- メンテナンス性の向上
    - クエリロジックが一箇所に集約されるため、変更が必要な場合でも、スコープの定義部分を変更するだけで済みます

### デメリット
- 柔軟性の欠如
    - 全てのクエリに対して同じ条件が適用されるため、特定のクエリで条件を変更するのが難しくなる場合があります
- デバッグの難しさ
    - グローバルスコープが適用されたクエリの動作を理解しづらく、デバッグが難しくなる場合があります

## ローカルスコープとの違い
- 影響範囲
    - ローカルスコープは特定のクエリにのみ適用されるが、グローバルスコープは特定のモデルに対するすべてのクエリに適用される
- 自動適用
    - グローバルスコープは自動的にすべてのデータベース操作（select, update, delete, countなど）に適用される
- 一貫性
    - グローバルスコープにより、特定の条件（例：active = 1, deleted_at IS NULL)がすべてのクエリに一貫して適用され、特定の条件を満たすデータだけが処理される
- 処理保証
    - グローバルスコープは、特定の条件を満たすデータが常に処理されることを保証する

## グローバルスコープの定義と適用
グローバルスコープは、スコープクラスを定義しそれをモデルに適用することで利用します
スコープクラスは`Scope`インターフェースを実装する必要があります

### 例：グローバルスコープの定義
```php
namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('active', 1);
    }
}
```

### 例：グローバルスコープの適用
```php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Scopes\ActiveScope;

class User extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new ActiveScope);
    }
}
```

### グローバルスコープの無効化
特定のクエリに対してグローバルスコープを無効にしたい場合は、
`withoutGlobalScope`メソッドを使用します

```php
$users = User::withoutGlobalScope(ActiveScope::class)->get();
```

## 実際の開発現場で使われる場面
### アクティブユーザーの自動フィルタリング
例えば、アクティブなユーザーのみを常に取得する必要がある場合、グローバルスコープを使用して全てのクエリに`active = 1`の条件を自動的に適用することができます

### ソフトデリート（論理削除）されたデータの自動除外
ソフトデリートを使用している場合、グローバルスコープを使用して`deleted_at`が`NULL`のデータのみを自動的に取得することができます
#### ソフトデリートスコープの定義
```php
namespace App\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class SoftDeletingScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $builder->whereNull('deleted_at');
    }
}
```

#### ソフトデリートスコープの適用
```php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Scopes\SoftDeletingScope;

class Post extends Model
{
    protected static function booted()
    {
        static::addGlobalScope(new SoftDeletingScope);
    }
}
```

### グローバルスコープの失敗例
グローバルスコープは特定のモデルに対する全てのクエリに適用されますが、モデルの外から呼び出すことはできません
また、グローバルスコープを無効にする方法を誤ると、期待通りの動作をしないことが動作をしないことがあります

### Postモデルでユーザーも取得する場合
```php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function getAllUsersPosts()
    {
        // グローバルスコープを無効にしない場合
        return User::with('posts')->get(); // ここではactive=1のユーザーのみ取得される
    }
}
```

もし、`active=1`ではないユーザーも含める場合は、期待通りの結果にならない

グローバルスコープを無効にするには以下のように`withoutGlobalScope`メソッドを使用する必要があります

```php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function getAllUsersPosts()
    {
        // グローバルスコープを無効にする
        return User::withoutGlobalScope(ActiveScope::class)->with('posts')->get(); // ここでは全ユーザーが取得される
    }
}
```

## まとめ
Eloquentのグローバルスコープは、特定のモデルに対する全てのクエリに一貫した条件を適用するための強力な機能です
これにより、クエリの一貫性が保たれ、コードの再利用性とメンテナンス性が向上します
ただし、柔軟性に欠ける場合やデバッグが難しくなることがあるため、適切な場面での利用が推奨されます。

## 参考リンク

https://readouble.com/laravel/9.x/ja/eloquent.html