はじめに
https://qiita.com/kenji123/items/a78e85ffc9309fad10bb
Laravelのコレクションに対し、ページネーションの使用を行っています。
今回のケースでは、データベースから直接ページネーションするのは断念し、Laravelのコレクションを使用してデータを処理し、その後でページネーションを適用しました。
SQL vs コレクションベースのページネーション
通常、データを効率的にページネーションするためには、データベースクエリ自体でpaginate()
メソッドを使用するのが最も効率的です。これにより、必要なデータのみがデータベースから取得され、アプリケーションへの負荷が軽減されます。
しかし、データの加工が必要でクエリだけではページネーションが困難な場合は、データベースから全データを取得してからコレクションを使って加工し、forPage()
メソッドや slice()
メソッドなどを用いて手動でページネーションを行うことができます。
コレクションベースでの対応
ID取得後、ページネーション適用に変更しました。
Laravelのコレクションを活用しています。
-
フォローしているユーザーのIDと、その関係を取得:
- ユーザーID=10がフォローしているユーザーのIDリストを取得します。
-
フォロワーのIDとその関係を取得:
- ユーザーID=10をフォローしているユーザーのIDリストを取得します。
-
IDリストをユニークにする:
- フォローとフォロワーのIDリストを統合し、重複を排除してユニークなリストを作成します。
-
関係の詳細を確認し、データを組み立てる:
- ユニークなIDリストに基づいて、各ユーザーとユーザーID=10との間の詳細な関係(相互フォロー、ブロック状態など)を設定します。
-
ページネーションの適用:
- 最終的なコレクションに対して
LengthAwarePaginator
を使用してページネーションを行い、ビューにデータを渡します。
- 最終的なコレクションに対して
上記コードの疑似コード例
以下は上記プロセスを実装する擬似コードです:
$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]);
このコードは、特定のビジネスロジックに基づいてユーザー関係を処理し、それをページネーションしてビューに表示する基本的な流れを示しています。この方法は、データベースからの直接的なページネーションが困難な場合や、特定の処理を行う必要がある場合に特に有用です。
今回のコード例
前回にページネーションを適用下もコードです。
<?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]);
}
}
<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() }}
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で調整しているのは、< > の表示がおかしく一旦応急処置で対応。
コレクションベースのページネーションの欠点
- メモリ使用量: 全データをアプリケーションメモリ上にロードするため、大量のデータを扱う場合はメモリオーバーヘッドが大きくなります。
- パフォーマンス: 特に大規模なデータセットの場合、データベースから全てのデータを取得し、アプリケーションレベルで処理を行うため、レスポンスタイムが長くなる可能性があります。
特に大きなデータセットに対して、メモリ使用量とパフォーマンスに関する懸念が生じます。以下の理由で問題が生じる可能性があります:
今回の場合の懸念です。
-
大量のデータの取得と保持:
get()
メソッドは、対象のレコードをデータベースから全てメモリにロードします。特にuser_id
やtarget_user_id
が多数の関連を持つ場合、これは非常にメモリ集約的になり得ます。 -
全ユーザーIDの計算:
keys()->merge()
とunique()
の使用は、大きなコレクションを生成し、これもまた大量のメモリを消費します。 -
全データの再加工:
mapWithKeys
を用いたデータの再加工は、すでにメモリにロードされたデータに対してさらに処理を行い、大規模なデータセットでは効率が悪くなります。 -
ページネーションの手動実装:
ここではLengthAwarePaginator
を使っていますが、これは元のデータセットを既に全てメモリ上にロードした後で適用されるため、理想的なデータベースクエリによるページネーションと比較して非効率的です。
複数のデータソースの統合
user_id
に基づいてフォローしているユーザーを取得し、target_user_id
に基づいてフォロワーを取得する必要があるため、これら二つのデータセットをうまく統合することが課題となっています。
-
Eloquentリレーションの最適化:
LaravelのEloquentリレーションを使用して、関連するデータを事前にwith()またはload()することで、各ユーザーに対してフォローとフォロワーの情報を効率的に取得します。これにより、複数のクエリを一つにまとめてパフォーマンスを向上させることが可能です。 -
中間テーブルの利用:
フォロー関係を管理する中間テーブルを作成し、フォローとフォロワーの情報を一つのテーブルに統合することで、クエリの複雑さを減らすことができます。これにより、ページネーション時に必要なデータのみを効率的に取得することが可能になります。 -
複合クエリの実行:
SQLのJOIN
クエリを使用して、user_id
とtarget_user_id
に基づく情報を一度に取得し、それに基づいて必要な情報を抽出します。これはデータベースレベルでの結合処理になるため、アプリケーションサーバーのメモリ負荷を減らすことができます。
具体的なコード例
例として、Eloquentリレーションを最適化して複数のデータソースを統合する方法を示します。以下の例では、User
モデルにfollowers
とfollowings
というリレーションを定義し、これを利用してデータを取得します。
// 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のコレクションによるページネーションは避けた方がいいかもしれません。