如何拆分Laravel Eloquent大型查询并实现逻辑复用?
Great question—let's break down how to split up this bulky query into reusable components, clean up that Blade-included logic, and make it easy to reuse across AJAX requests. Here's a practical, maintainable approach:
1. Extract Reusable Logic into Query Scopes
Laravel's local query scopes are perfect for wrapping repeatable query conditions directly on your models. This keeps your controller clean and centralizes logic where it belongs.
Add Scopes to Your Hotel Model
// app/Models/Hotel.php use Illuminate\Http\Request; class Hotel extends Model { // ... existing relationships ... // Scope for hotels with active, filtered rooms public function scopeWithActiveRooms($query, Request $request) { return $query->has('room') ->whereHas('room', fn($q) => $q->applyFilters($request)) ->with(['room' => fn($q) => $q->applyFilters($request) ->with('roomtype', 'floorroomcount', 'image', 'amenities') ->orderBy('price') ->where('astatus', 1) ->where('status', 0) ]); } // Scope for hotels with active clients public function scopeForActiveClients($query) { return $query->whereHas('client', fn($q) => $q->where('status', 1)); } // Scope for rating filtering public function scopeFilterByRating($query, $rating) { return $rating > 0 ? $query->where('star', $rating) : $query; } // Scope for location filtering public function scopeFilterByLocation($query, $locationId) { return $query->where('city', $locationId); } // Scope for area filtering (replaces the Blade include logic) public function scopeFilterByArea($query, Request $request) { if ($request->filled('type') && $request->type === 'Area' && $request->filled('location_id')) { // Cache this to avoid repeated DB hits for the same location $area = cache()->remember("hotel_area_{$request->location_id}", 3600, function () use ($request) { return Hotel::where('city', $request->location_id)->value('area'); }); if ($area) { $query->where('area', $area); } } return $query; } // Scope for active hotels public function scopeIsActive($query) { return $query->where('astatus', 1)->where('status', 0); } }
Add a Scope to Your Room Model
Since the room filtering logic repeats twice (in whereHas and with), extract it into the Room model to avoid duplication:
// app/Models/Room.php use Illuminate\Http\Request; class Room extends Model { // ... existing relationships ... public function scopeApplyFilters($query, Request $request) { // Handle amenities filter if ($request->filled('amenities')) { $amenities = $request->amenities; $count = count($amenities); $query->withCount(['amenities' => fn($q) => $q->whereIn('amenities_id', $amenities)]) ->having('amenities_count', $count); } // Handle guest count filter $memberCount = $request->filled('guestsCount') ? $request->guestsCount + $request->childCount : 1; return $query->where('capacity', '>=', $memberCount); } }
2. Create a Query Builder Service
For even better reusability across controllers or AJAX endpoints, wrap the query assembly in a dedicated service class. This keeps your controller thin and makes it trivial to reuse the exact query anywhere:
// app/Queries/HotelRoomQuery.php namespace App\Queries; use App\Models\Hotel; use Illuminate\Http\Request; class HotelRoomQuery { public static function build(Request $request) { return Hotel::query() ->withActiveRooms($request) ->forActiveClients() ->when($request->filled('rating'), fn($q) => $q->filterByRating($request->rating)) ->when($request->filled('location_id'), fn($q) => $q->filterByLocation($request->location_id)) ->filterByArea($request) ->isActive(); } }
3. Simplify Your Controller
Your controller method now becomes clean, readable, and easy to maintain:
// app/Http/Controllers/YourController.php use App\Queries\HotelRoomQuery; use Illuminate\Http\Request; class YourController extends Controller { public function loadMore(Request $request) { // Build the reusable query $hotelsQuery = HotelRoomQuery::build($request); // Execute and post-process $hotels = $hotelsQuery->simplePaginate(20); $hotels = $this->addRates($hotels, $request->checkin_date, $request->checkout_date); $hotels = $this->addAvailableCount($hotels, $request->checkin_date, $request->checkout_date); $hotels = $hotels->transform(function (Hotel $hotel) { $hotel->setRelation('room', $hotel->room->sortBy('price')->flatten()); return $hotel; }); return view('roomlist.loadmore', compact('hotels')); } }
4. Ditch the Blade Query Logic
Including query logic in Blade is an anti-practice—it mixes presentation and business logic, making it hard to debug and reuse. By moving that area-filtering logic into the Hotel model's scope, you can completely remove the @include('roomlist.area') line.
Bonus Optimizations
- Cache Frequently Used Data: As shown in the
FilterByAreascope, cache the area lookup to reduce unnecessary DB queries. - Validate Request Data: Use a
FormRequestclass to validate inputs likeguestsCount,amenities, andlocation_idbefore they reach your query logic. - Encapsulate Post-Processing: Move
addRatesandaddAvailableCountinto a service class or model method for even more reusability.
This approach makes your query logic modular, reusable across any AJAX request (or non-AJAX routes), and follows Laravel's best practices for clean, maintainable code.
内容的提问来源于stack exchange,提问作者arun

