Laravelでページネーションを試したメモ

## はじめに
https://qiita.com/kenji123/items/a78e85ffc9309fad10bb
Laravelのコレクションに対し、ページネーションの使用を行っています。
今回のケースでは、データベースから直接ページネーションするのは断念し、Laravelのコレクションを使用してデータを処理し、その後でページネーションを適用しました。

## SQL vs コレクションベースのページネーション

通常、データを効率的にページネーションするためには、データベースクエリ自体で`paginate()`メソッドを使用するのが最も効率的です。これにより、必要なデータのみがデータベースから取得され、アプリケーションへの負荷が軽減されます。

しかし、データの加工が必要でクエリだけではページネーションが困難な場合は、データベースから全データを取得してからコレクションを使って加工し、`forPage()` メソッドや `slice()` メソッドなどを用いて手動でページネーションを行うことができます。

## コレクションベースでの対応

ID取得後、ページネーション適用に変更しました。
Laravelのコレクションを活用しています。

1. **フォローしているユーザーのIDと、その関係を取得**：
   - ユーザーID=10がフォローしているユーザーのIDリストを取得します。

2. **フォロワーのIDとその関係を取得**：
   - ユーザーID=10をフォローしているユーザーのIDリストを取得します。

3. **IDリストをユニークにする**：
   - フォローとフォロワーのIDリストを統合し、重複を排除してユニークなリストを作成します。

4. **関係の詳細を確認し、データを組み立てる**：
   - ユニークなIDリストに基づいて、各ユーザーとユーザーID=10との間の詳細な関係（相互フォロー、ブロック状態など）を設定します。

5. **ページネーションの適用**：
   - 最終的なコレクションに対して `LengthAwarePaginator` を使用してページネーションを行い、ビューにデータを渡します。

### 上記コードの疑似コード例

以下は上記プロセスを実装する擬似コードです：

```php
$userId = 10;
$followingIds = UserRelation::where('user_id', $userId)->get()->pluck('target_user_id');
$followersIds = UserRelation::where('target_user_id', $userId)->get()->pluck('user_id');

// IDリストをユニークにする
$uniqueIds = $followingIds->merge($followersIds)->unique();

$relationships = collect();
foreach ($uniqueIds as $id) {
    // このIDの詳細な関係を構築
    $isFollowing = $followingIds->contains($id);
    $isFollower = $followersIds->contains($id);
    $relationships->push([
        'id' => $id,
        'follow' => $isFollowing,
        'follower' => $isFollower,
        'mutual' => $isFollowing && $isFollower,
        // 他の関係のデータもここで加える
    ]);
}

// LengthAwarePaginatorを使用してページネーション
$perPage = 10;
$page = LengthAwarePaginator::resolveCurrentPage();
$paginatedItems = new LengthAwarePaginator(
    $relationships->forPage($page, $perPage),
    $relationships->count(),
    $perPage,
    $page,
    ['path' => request()->url(), 'query' => request()->query()]
);

return view('user_relations.index', ['relations' => $paginatedItems]);
```

このコードは、特定のビジネスロジックに基づいてユーザー関係を処理し、それをページネーションしてビューに表示する基本的な流れを示しています。この方法は、データベースからの直接的なページネーションが困難な場合や、特定の処理を行う必要がある場合に特に有用です。

### 今回のコード例
前回にページネーションを適用下もコードです。
```app/Http/Controllers/UserRelations.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\UserRelation;
use Illuminate\Support\Collection;
use Illuminate\Pagination\LengthAwarePaginator;

class UserRelationController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        $userId = 10; // 表示するユーザーIDを設定
    
        // フォローしているユーザーのデータを取得
        $following = UserRelation::where('user_id', $userId)
                            ->get(['target_user_id', 'is_following', 'is_blocking'])
                            ->keyBy('target_user_id');
    
        // フォロワーのデータを取得
        $followers = UserRelation::where('target_user_id', $userId)
                            ->get(['user_id', 'is_following', 'is_blocking'])
                            ->keyBy('user_id');
                            var_dump($followers);var_dump('aaaaaaaaaaaa');
        // 全ユーザーIDの一覧を取得
        $allUserIds = $following->keys()->merge($followers->keys())->unique();
        
        // 全データを統合
        $allRelations = $allUserIds->mapWithKeys(function ($id) use ($following, $followers) {
            $isFollowing = $following->has($id) && $following[$id]->is_following;
            $isFollower = $followers->has($id) && $followers[$id]->is_following;
            $isMutual = $isFollowing && $isFollower;
            $isBlocking = $following->has($id) ? $following[$id]->is_blocking : false;
            $isBlockedBy = $followers->has($id) ? $followers[$id]->is_blocking : false;
    
            return [$id => [
                'follow' => $isFollowing ? '○' : '',
                'follower' => $isFollower ? '○' : '',
                'mutual' => $isMutual ? '○' : '',
                'blocking' => $isBlocking ? '○' : '',
                'blocked_by' => $isBlockedBy ? '○' : ''
            ]];
        })->sortKeys();  // IDでソート
    
        // ページネーションの設定
        $perPage = 3;
        $currentPage = LengthAwarePaginator::resolveCurrentPage();
        $currentItems = $allRelations->slice(($currentPage - 1) * $perPage, $perPage)->all();
    
        // LengthAwarePaginatorを使ってページネーションを適用
        $paginatedItems = new LengthAwarePaginator(
            $currentItems, 
            $allRelations->count(), 
            $perPage, 
            $currentPage, [
            'path' => LengthAwarePaginator::resolveCurrentPath(),
            ]
        );
    
        return view('user_relations.index', ['relations' => $paginatedItems]);
    }
    
}

```
表示例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176603/f2730f91-ff6e-84a3-8a20-43588d787530.png)


```resources/views/user_relations/index.blade.html
<link rel="stylesheet" href="/css/style.css">

<table>
    <thead>
        <tr>
            <th>ID</th>
            <th>Follow</th>
            <th>Follower</th>
            <th>Mutual</th>
            <th>Blocking</th>
            <th>Blocked By</th>
        </tr>
    </thead>
    <tbody>
        @foreach ($relations as $id => $relation)
            <tr>
                <td>{{ $id }}</td>
                <td>{{ $relation['follow'] ? '○' : '' }}</td>
                <td>{{ $relation['follower'] ? '○' : '' }}</td>
                <td>{{ $relation['mutual'] ? '○' : '' }}</td>
                <td>{{ $relation['blocking'] ? '○' : '' }}</td>
                <td>{{ $relation['blocked_by'] ? '○' : '' }}</td>
            </tr>
        @endforeach
    </tbody>
</table>

{{ $relations->links() }}
```

```public/css/style.css

svg {
  width: 20px; /* SVGのサイズ調整 */
  height: 20px;
}


.relative.z-0.inline-flex.rtl\:flex-row-reverse.shadow-sm.rounded-md {
  justify-content: center; /* 中央揃えにする場合 */
  padding: 10px; /* パディングを追加 */
}

.relative.inline-flex.items-center.px-4.py-2.-ml-px.text-sm.font-medium {
  margin: 0 5px; /* 左右のマージンを調整 */
  border-radius: 4px; /* 角の丸みを調整 */
}
.hover\:text-gray-400, .focus\:ring-gray-300, .active\:bg-gray-100 {
  transition: all 0.3s ease; /* スムーズな遷移効果 */
}

```

上記styleで調整しているのは、< > の表示がおかしく一旦応急処置で対応。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176603/e1e50d4a-78c6-ff44-e0ab-b6455067620c.png)

## コレクションベースのページネーションの欠点

- **メモリ使用量**: 全データをアプリケーションメモリ上にロードするため、大量のデータを扱う場合はメモリオーバーヘッドが大きくなります。
- **パフォーマンス**: 特に大規模なデータセットの場合、データベースから全てのデータを取得し、アプリケーションレベルで処理を行うため、レスポンスタイムが長くなる可能性があります。

特に大きなデータセットに対して、メモリ使用量とパフォーマンスに関する懸念が生じます。以下の理由で問題が生じる可能性があります：
今回の場合の懸念です。

1. **大量のデータの取得と保持**:
   `get()`メソッドは、対象のレコードをデータベースから全てメモリにロードします。特に`user_id`や`target_user_id`が多数の関連を持つ場合、これは非常にメモリ集約的になり得ます。

2. **全ユーザーIDの計算**:
   `keys()->merge()`と`unique()`の使用は、大きなコレクションを生成し、これもまた大量のメモリを消費します。

3. **全データの再加工**:
   `mapWithKeys`を用いたデータの再加工は、すでにメモリにロードされたデータに対してさらに処理を行い、大規模なデータセットでは効率が悪くなります。

4. **ページネーションの手動実装**:
   ここでは`LengthAwarePaginator`を使っていますが、これは元のデータセットを既に全てメモリ上にロードした後で適用されるため、理想的なデータベースクエリによるページネーションと比較して非効率的です。


### 複数のデータソースの統合
`user_id`に基づいてフォローしているユーザーを取得し、`target_user_id`に基づいてフォロワーを取得する必要があるため、これら二つのデータセットをうまく統合することが課題となっています。

1. **Eloquentリレーションの最適化**:
   LaravelのEloquentリレーションを使用して、関連するデータを事前にwith()またはload()することで、各ユーザーに対してフォローとフォロワーの情報を効率的に取得します。これにより、複数のクエリを一つにまとめてパフォーマンスを向上させることが可能です。

2. **中間テーブルの利用**:
   フォロー関係を管理する中間テーブルを作成し、フォローとフォロワーの情報を一つのテーブルに統合することで、クエリの複雑さを減らすことができます。これにより、ページネーション時に必要なデータのみを効率的に取得することが可能になります。

3. **複合クエリの実行**:
   SQLの`JOIN`クエリを使用して、`user_id`と`target_user_id`に基づく情報を一度に取得し、それに基づいて必要な情報を抽出します。これはデータベースレベルでの結合処理になるため、アプリケーションサーバーのメモリ負荷を減らすことができます。

### 具体的なコード例

例として、Eloquentリレーションを最適化して複数のデータソースを統合する方法を示します。以下の例では、`User`モデルに`followers`と`followings`というリレーションを定義し、これを利用してデータを取得します。

```php
// User モデル
public function followers()
{
    return $this->belongsToMany(User::class, 'user_relations', 'target_user_id', 'user_id')
                ->withPivot('is_following', 'is_blocking');
}

public function followings()
{
    return $this->belongsToMany(User::class, 'user_relations', 'user_id', 'target_user_id')
                ->withPivot('is_following', 'is_blocking');
}

// コントローラー
public function index()
{
    $userId = 10;
    $perPage = 10;

    $user = User::with(['followers', 'followings'])->find($userId);

    // フォロワーとフォロー情報を取得
    $followers = $user->followers;
    $followings = $user->followings;

    // 必要に応じて追加の処理をここに記述

    return view('user_relations.index', [
        'followers' => $followers,
        'followings' => $followings
    ]);
}
```

このコードは、特定のユーザーのフォロワーとフォロー情報を効率的にロードし、それらをビューに渡す基本的な方法を示しています。さらに、これらのリレーションをページネーションすることも可能ですが、リレーションのページネーションは少し複雑になるため、必要に応じてさらにカスタマイズが必要になります。

## おわりに
今回はLaravelコレクションの使用理解を主にしています。
Laravelのコレクションによるページネーションは避けた方がいいかもしれません。