Laravel Eloquent Advanced Techniques: Mastering Database Relations and Performance

Laravel's Eloquent ORM is powerful out of the box, but mastering its advanced features can transform how you handle complex database operations. From polymorphic relationships to custom query scopes, let's explore techniques that will elevate your Laravel applications to enterprise level.

Complex Relationship Patterns

Polymorphic Many-to-Many Relationships

One of Eloquent's most powerful features is polymorphic relationships. Let's build a comprehensive tagging system:

// Models setup
class Tag extends Model
{
    protected $fillable = ['name', 'slug', 'color'];

    public function posts()
    {
        return $this->morphedByMany(Post::class, 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany(Video::class, 'taggable');
    }

    public function products()
    {
        return $this->morphedByMany(Product::class, 'taggable');
    }

    // Get all tagged models regardless of type
    public function taggables()
    {
        return $this->hasMany(Taggable::class);
    }

    // Scope for finding tags by multiple types
    public function scopeForTypes($query, array $types)
    {
        return $query->whereHas('taggables', function ($q) use ($types) {
            $q->whereIn('taggable_type', $types);
        });
    }
}

class Post extends Model
{
    public function tags()
    {
        return $this->morphToMany(Tag::class, 'taggable');
    }

    public function syncTagsFromNames(array $tagNames)
    {
        $tags = collect($tagNames)->map(function ($name) {
            return Tag::firstOrCreate([
                'name' => $name,
                'slug' => Str::slug($name),
            ]);
        });

        $this->tags()->sync($tags->pluck('id'));
        
        return $this;
    }
}

// Usage examples
$post = Post::find(1);

// Attach tags by names
$post->syncTagsFromNames(['Laravel', 'PHP', 'Web Development']);

// Find all posts with specific tags
$posts = Post::whereHas('tags', function ($query) {
    $query->whereIn('slug', ['laravel', 'php']);
})->get();

// Get popular tags across all content types
$popularTags = Tag::withCount(['posts', 'videos', 'products'])
    ->having(DB::raw('posts_count + videos_count + products_count'), '>', 5)
    ->orderByDesc(DB::raw('posts_count + videos_count + products_count'))
    ->get();

Has-Many-Through with Intermediate Models

Handle complex relationships through multiple tables:

class Country extends Model
{
    public function users()
    {
        return $this->hasMany(User::class);
    }

    // Direct relationship to posts through users
    public function posts()
    {
        return $this->hasManyThrough(Post::class, User::class);
    }

    // More complex: get comments through posts through users
    public function comments()
    {
        return $this->through('posts')->has('comments');
    }

    // Custom has-many-through with additional constraints
    public function publishedPosts()
    {
        return $this->hasManyThrough(Post::class, User::class)
            ->whereNotNull('posts.published_at');
    }

    // Get statistics for dashboard
    public function getStatsAttribute()
    {
        return [
            'users_count' => $this->users()->count(),
            'posts_count' => $this->posts()->count(),
            'published_posts_count' => $this->publishedPosts()->count(),
            'comments_count' => $this->posts()->withCount('comments')->get()->sum('comments_count'),
        ];
    }
}

class User extends Model 
{
    public function country()
    {
        return $this->belongsTo(Country::class);
    }

    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

// Usage
$country = Country::with([
    'users.posts.comments',
    'posts.user'
])->find(1);

// Get all comments from a specific country
$comments = Comment::whereHas('post.user.country', function ($query) use ($countryId) {
    $query->where('id', $countryId);
})->get();

Advanced Query Builder Techniques

Custom Query Scopes and Builders

Create reusable, chainable query logic:

class PostQueryBuilder extends Builder
{
    public function published()
    {
        return $this->whereNotNull('published_at')
                   ->where('published_at', '<=', now());
    }

    public function byAuthor($authorId)
    {
        return $this->where('user_id', $authorId);
    }

    public function withCategory($category)
    {
        return $this->whereHas('categories', function ($query) use ($category) {
            $query->where('slug', $category);
        });
    }

    public function popular($days = 30)
    {
        return $this->where('created_at', '>=', now()->subDays($days))
                   ->withCount(['views', 'likes', 'comments'])
                   ->orderByDesc(DB::raw('views_count + likes_count * 2 + comments_count * 3'));
    }

    public function trending()
    {
        return $this->select('posts.*')
                   ->selectRaw('
                       (
                           COALESCE(views_count, 0) * 1 +
                           COALESCE(likes_count, 0) * 3 +
                           COALESCE(comments_count, 0) * 5 +
                           (CASE 
                               WHEN created_at >= ? THEN 10 
                               WHEN created_at >= ? THEN 5 
                               ELSE 0 
                           END)
                       ) as trend_score
                   ', [now()->subDays(1), now()->subWeek()])
                   ->leftJoin('post_statistics', 'posts.id', '=', 'post_statistics.post_id')
                   ->orderByDesc('trend_score');
    }

    public function searchContent($term)
    {
        return $this->where(function ($query) use ($term) {
            $query->where('title', 'LIKE', "%{$term}%")
                  ->orWhere('content', 'LIKE', "%{$term}%")
                  ->orWhere('excerpt', 'LIKE', "%{$term}%")
                  ->orWhereHas('tags', function ($q) use ($term) {
                      $q->where('name', 'LIKE', "%{$term}%");
                  });
        });
    }

    public function withFullStats()
    {
        return $this->withCount([
            'views',
            'likes',
            'comments',
            'comments as recent_comments_count' => function ($query) {
                $query->where('created_at', '>=', now()->subDays(7));
            }
        ])->with([
            'user:id,name,avatar',
            'categories:id,name,slug',
            'tags:id,name,slug,color'
        ]);
    }
}

class Post extends Model
{
    public function newEloquentBuilder($query)
    {
        return new PostQueryBuilder($query);
    }

    // Local scopes for simple conditions
    public function scopeScheduled($query)
    {
        return $query->whereNotNull('published_at')
                    ->where('published_at', '>', now());
    }

    public function scopeByStatus($query, $status)
    {
        return match($status) {
            'published' => $query->published(),
            'scheduled' => $query->scheduled(),
            'draft' => $query->whereNull('published_at'),
            default => $query,
        };
    }

    // Complex scope with subqueries
    public function scopeWithPopularityScore($query)
    {
        return $query->addSelect([
            'popularity_score' => PostStatistic::selectRaw(
                'views_count * 1 + likes_count * 3 + comments_count * 5'
            )->whereColumn('post_id', 'posts.id')
        ]);
    }
}

// Usage examples
$posts = Post::published()
            ->byAuthor(1)
            ->withCategory('technology')
            ->popular()
            ->withFullStats()
            ->paginate(15);

$trendingPosts = Post::trending()
                   ->published()
                   ->take(10)
                   ->get();

$searchResults = Post::searchContent('Laravel')
                   ->published()
                   ->withPopularityScore()
                   ->orderByDesc('popularity_score')
                   ->paginate(20);

Advanced Aggregations and Window Functions

Leverage database features for complex analytics:

class AnalyticsService 
{
    public function getUserGrowthStats($months = 12)
    {
        return User::select(
            DB::raw('DATE_FORMAT(created_at, "%Y-%m") as month'),
            DB::raw('COUNT(*) as new_users'),
            DB::raw('SUM(COUNT(*)) OVER (ORDER BY DATE_FORMAT(created_at, "%Y-%m")) as cumulative_users')
        )
        ->where('created_at', '>=', now()->subMonths($months))
        ->groupBy(DB::raw('DATE_FORMAT(created_at, "%Y-%m")'))
        ->orderBy('month')
        ->get();
    }

    public function getPostEngagementTrends()
    {
        return Post::select(
            'posts.*',
            DB::raw('ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as user_post_rank'),
            DB::raw('AVG(views_count) OVER (PARTITION BY user_id) as avg_user_views'),
            DB::raw('LAG(views_count) OVER (ORDER BY created_at) as prev_post_views')
        )
        ->join('post_statistics', 'posts.id', '=', 'post_statistics.post_id')
        ->where('posts.created_at', '>=', now()->subMonths(3))
        ->get();
    }

    public function getTopPerformingContent($limit = 10)
    {
        return Post::select(
            'posts.*',
            DB::raw('
                RANK() OVER (
                    ORDER BY (views_count * 1 + likes_count * 3 + comments_count * 5) DESC
                ) as performance_rank
            '),
            DB::raw('views_count * 1 + likes_count * 3 + comments_count * 5 as engagement_score'),
            DB::raw('
                PERCENT_RANK() OVER (
                    ORDER BY (views_count * 1 + likes_count * 3 + comments_count * 5)
                ) as percentile_rank
            ')
        )
        ->join('post_statistics', 'posts.id', '=', 'post_statistics.post_id')
        ->whereNotNull('published_at')
        ->orderByDesc('engagement_score')
        ->limit($limit)
        ->get();
    }

    public function getCategoryPerformance()
    {
        return Category::select(
            'categories.*',
            DB::raw('COUNT(posts.id) as posts_count'),
            DB::raw('AVG(post_statistics.views_count) as avg_views'),
            DB::raw('SUM(post_statistics.views_count) as total_views'),
            DB::raw('
                CASE 
                    WHEN AVG(post_statistics.views_count) > (
                        SELECT AVG(views_count) FROM post_statistics
                    ) THEN "Above Average"
                    ELSE "Below Average"
                END as performance_category
            ')
        )
        ->leftJoin('category_post', 'categories.id', '=', 'category_post.category_id')
        ->leftJoin('posts', 'category_post.post_id', '=', 'posts.id')
        ->leftJoin('post_statistics', 'posts.id', '=', 'post_statistics.post_id')
        ->groupBy('categories.id', 'categories.name', 'categories.slug')
        ->having('posts_count', '>', 0)
        ->orderByDesc('avg_views')
        ->get();
    }
}

Performance Optimization Strategies

Eager Loading Optimization

Prevent N+1 queries with sophisticated loading strategies:

class PostService
{
    public function getPostsForDashboard($perPage = 15)
    {
        return Post::with([
            // Basic relationships
            'user:id,name,email,avatar',
            'categories:id,name,slug,color',
            
            // Conditional loading
            'comments' => function ($query) {
                $query->latest()->limit(3)->with('user:id,name,avatar');
            },
            
            // Count relationships without loading
            'tags:id,name,slug',
        ])
        ->withCount([
            'views',
            'likes',
            'comments',
            'comments as recent_comments_count' => function ($query) {
                $query->where('created_at', '>=', now()->subDays(7));
            }
        ])
        ->withExists([
            'likes as user_liked' => function ($query) {
                $query->where('user_id', auth()->id());
            }
        ])
        ->published()
        ->latest()
        ->paginate($perPage);
    }

    public function getPostWithOptimizedComments(Post $post)
    {
        // Load post with paginated comments
        $post->load([
            'user:id,name,avatar',
            'categories:id,name,slug',
            'tags:id,name,slug,color'
        ]);

        // Separate query for comments with pagination
        $comments = $post->comments()
                         ->with(['user:id,name,avatar', 'replies.user:id,name,avatar'])
                         ->latest()
                         ->paginate(20);

        return compact('post', 'comments');
    }

    // Batch loading for multiple posts
    public function loadStatsForPosts($posts)
    {
        $postIds = $posts->pluck('id');

        // Single query to get all statistics
        $stats = DB::table('post_statistics')
                  ->whereIn('post_id', $postIds)
                  ->get()
                  ->keyBy('post_id');

        // Single query to get all recent activity
        $recentActivity = DB::table('post_views')
                           ->whereIn('post_id', $postIds)
                           ->where('created_at', '>=', now()->subDays(7))
                           ->groupBy('post_id')
                           ->selectRaw('post_id, COUNT(*) as recent_views')
                           ->get()
                           ->keyBy('post_id');

        // Attach data to posts
        $posts->each(function ($post) use ($stats, $recentActivity) {
            $post->statistics = $stats[$post->id] ?? null;
            $post->recent_views = $recentActivity[$post->id]->recent_views ?? 0;
        });

        return $posts;
    }
}

class OptimizedPostController extends Controller
{
    public function index(Request $request)
    {
        $posts = Post::when($request->category, function ($query, $category) {
                return $query->whereHas('categories', fn($q) => $q->where('slug', $category));
            })
            ->when($request->tag, function ($query, $tag) {
                return $query->whereHas('tags', fn($q) => $q->where('slug', $tag));
            })
            ->when($request->search, function ($query, $search) {
                return $query->searchContent($search);
            })
            ->with(['user:id,name,avatar', 'categories:id,name,slug'])
            ->withCount(['views', 'likes', 'comments'])
            ->published()
            ->latest()
            ->paginate(15);

        // Eager load additional data in batches
        if ($posts->count() > 0) {
            $this->loadAdditionalPostData($posts);
        }

        return response()->json([
            'posts' => $posts,
            'filters' => $request->only(['category', 'tag', 'search']),
        ]);
    }

    private function loadAdditionalPostData($posts)
    {
        $postIds = $posts->getCollection()->pluck('id');

        // Load user engagement in single query
        $userEngagement = DB::table('post_likes')
                           ->whereIn('post_id', $postIds)
                           ->where('user_id', auth()->id())
                           ->pluck('post_id')
                           ->flip();

        $posts->getCollection()->each(function ($post) use ($userEngagement) {
            $post->user_liked = isset($userEngagement[$post->id]);
        });
    }
}

Database Indexing and Query Optimization

// Migration with proper indexes
class CreateOptimizedPostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('excerpt')->nullable();
            $table->longText('content');
            $table->string('featured_image')->nullable();
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            // Compound indexes for common queries
            $table->index(['published_at', 'created_at']); // For published posts ordered by date
            $table->index(['user_id', 'published_at']); // For user's published posts
            $table->index(['created_at', 'user_id']); // For user's recent posts
            
            // Full-text search index
            $table->fullText(['title', 'content', 'excerpt']);
        });
    }
}

class Post extends Model
{
    // Use full-text search when available
    public function scopeFullTextSearch($query, $term)
    {
        return $query->whereFullText(['title', 'content', 'excerpt'], $term);
    }

    // Fallback search method
    public function scopeBasicSearch($query, $term)
    {
        return $query->where(function ($q) use ($term) {
            $q->where('title', 'LIKE', "%{$term}%")
              ->orWhere('excerpt', 'LIKE', "%{$term}%");
        });
    }

    // Smart search that uses full-text when possible
    public function scopeSmartSearch($query, $term)
    {
        if (config('database.default') === 'mysql') {
            return $query->fullTextSearch($term);
        }
        
        return $query->basicSearch($term);
    }
}

Advanced Model Patterns

Custom Collections and Model Factories

class PostCollection extends Collection
{
    public function published()
    {
        return $this->filter(fn ($post) => $post->isPublished());
    }

    public function byAuthor($authorId)
    {
        return $this->filter(fn ($post) => $post->user_id == $authorId);
    }

    public function groupByMonth()
    {
        return $this->groupBy(fn ($post) => $post->created_at->format('Y-m'));
    }

    public function calculateEngagementMetrics()
    {
        return [
            'total_views' => $this->sum('views_count'),
            'total_likes' => $this->sum('likes_count'),
            'total_comments' => $this->sum('comments_count'),
            'average_engagement' => $this->avg(function ($post) {
                return $post->views_count + $post->likes_count * 3 + $post->comments_count * 5;
            }),
        ];
    }

    public function getPopularTags($limit = 10)
    {
        return $this->pluck('tags')
                   ->flatten()
                   ->countBy('name')
                   ->sortDesc()
                   ->take($limit);
    }
}

class Post extends Model
{
    public function newCollection(array $models = [])
    {
        return new PostCollection($models);
    }
    
    // Custom attributes
    public function getEngagementScoreAttribute()
    {
        return ($this->views_count ?? 0) + 
               ($this->likes_count ?? 0) * 3 + 
               ($this->comments_count ?? 0) * 5;
    }

    public function getReadingTimeAttribute()
    {
        $wordCount = str_word_count(strip_tags($this->content));
        return ceil($wordCount / 200); // Average reading speed
    }

    public function getExcerptOrContentAttribute()
    {
        if ($this->excerpt) {
            return $this->excerpt;
        }

        return Str::limit(strip_tags($this->content), 150);
    }

    // Model events for cache management
    protected static function booted()
    {
        static::saved(function ($post) {
            Cache::tags(['posts', "user:{$post->user_id}"])->flush();
        });

        static::deleted(function ($post) {
            Cache::tags(['posts', "user:{$post->user_id}"])->flush();
        });
    }
}

// Usage examples
$posts = Post::published()->get(); // Returns PostCollection
$metrics = $posts->calculateEngagementMetrics();
$popularTags = $posts->getPopularTags(5);
$monthlyPosts = $posts->groupByMonth();

Repository Pattern with Caching

interface PostRepositoryInterface
{
    public function findBySlug(string $slug): ?Post;
    public function getPublished(int $perPage = 15);
    public function getByAuthor(int $authorId, int $perPage = 15);
    public function search(string $term, int $perPage = 15);
    public function getPopular(int $days = 30, int $limit = 10);
}

class CachedPostRepository implements PostRepositoryInterface
{
    private $cacheTime = 3600; // 1 hour

    public function findBySlug(string $slug): ?Post
    {
        return Cache::tags(['posts'])->remember(
            "post.slug.{$slug}",
            $this->cacheTime,
            fn () => Post::with(['user', 'categories', 'tags'])->where('slug', $slug)->first()
        );
    }

    public function getPublished(int $perPage = 15)
    {
        $page = request('page', 1);
        $cacheKey = "posts.published.page.{$page}.perPage.{$perPage}";
        
        return Cache::tags(['posts'])->remember(
            $cacheKey,
            $this->cacheTime,
            fn () => Post::published()
                         ->with(['user', 'categories'])
                         ->withCount(['views', 'likes', 'comments'])
                         ->latest()
                         ->paginate($perPage)
        );
    }

    public function getByAuthor(int $authorId, int $perPage = 15)
    {
        $page = request('page', 1);
        $cacheKey = "posts.author.{$authorId}.page.{$page}";
        
        return Cache::tags(['posts', "user:{$authorId}"])->remember(
            $cacheKey,
            $this->cacheTime,
            fn () => Post::where('user_id', $authorId)
                         ->published()
                         ->with(['categories', 'tags'])
                         ->latest()
                         ->paginate($perPage)
        );
    }

    public function search(string $term, int $perPage = 15)
    {
        $page = request('page', 1);
        $cacheKey = "posts.search." . md5($term) . ".page.{$page}";
        
        return Cache::remember(
            $cacheKey,
            300, // 5 minutes for search results
            fn () => Post::smartSearch($term)
                         ->published()
                         ->with(['user', 'categories'])
                         ->paginate($perPage)
        );
    }

    public function getPopular(int $days = 30, int $limit = 10)
    {
        $cacheKey = "posts.popular.days.{$days}.limit.{$limit}";
        
        return Cache::tags(['posts'])->remember(
            $cacheKey,
            $this->cacheTime,
            fn () => Post::popular($days)
                         ->with(['user', 'categories'])
                         ->take($limit)
                         ->get()
        );
    }

    public function invalidateCache(Post $post = null)
    {
        if ($post) {
            Cache::tags(['posts', "user:{$post->user_id}"])->flush();
        } else {
            Cache::tags(['posts'])->flush();
        }
    }
}

class PostService
{
    public function __construct(
        private PostRepositoryInterface $postRepository
    ) {}

    public function getPostsForHomepage()
    {
        return [
            'featured' => $this->postRepository->getPopular(7, 3),
            'recent' => $this->postRepository->getPublished(8),
            'popular' => $this->postRepository->getPopular(30, 5),
        ];
    }

    public function createPost(array $data): Post
    {
        $post = Post::create([
            ...$data,
            'slug' => $this->generateUniqueSlug($data['title']),
            'user_id' => auth()->id(),
        ]);

        $this->postRepository->invalidateCache();

        return $post;
    }

    private function generateUniqueSlug(string $title): string
    {
        $slug = Str::slug($title);
        $count = Post::where('slug', 'like', "{$slug}%")->count();
        
        return $count ? "{$slug}-" . ($count + 1) : $slug;
    }
}

Testing Advanced Eloquent Features

class PostTest extends TestCase
{
    use RefreshDatabase;

    public function test_post_with_tags_relationship()
    {
        $post = Post::factory()->create();
        $tags = Tag::factory()->count(3)->create();
        
        $post->tags()->attach($tags);
        
        $this->assertCount(3, $post->fresh()->tags);
        
        foreach ($tags as $tag) {
            $this->assertTrue($post->tags->contains($tag));
        }
    }

    public function test_popular_scope()
    {
        // Create posts with different engagement levels
        $popularPost = Post::factory()->create(['created_at' => now()->subDays(1)]);
        $regularPost = Post::factory()->create(['created_at' => now()->subDays(1)]);
        $oldPost = Post::factory()->create(['created_at' => now()->subDays(60)]);

        // Create statistics
        PostStatistic::factory()->create([
            'post_id' => $popularPost->id,
            'views_count' => 1000,
            'likes_count' => 50,
            'comments_count' => 20,
        ]);

        PostStatistic::factory()->create([
            'post_id' => $regularPost->id,
            'views_count' => 100,
            'likes_count' => 5,
            'comments_count' => 2,
        ]);

        $popularPosts = Post::popular(30)->get();
        
        $this->assertTrue($popularPosts->first()->is($popularPost));
        $this->assertCount(2, $popularPosts); // oldPost excluded by date
    }

    public function test_custom_collection_methods()
    {
        $posts = Post::factory()->count(5)->create();
        
        // Test custom collection methods
        $collection = $posts->fresh(); // Get PostCollection
        $this->assertInstanceOf(PostCollection::class, $collection);
        
        $metrics = $collection->calculateEngagementMetrics();
        $this->assertArrayHasKey('total_views', $metrics);
    }

    public function test_repository_caching()
    {
        Cache::shouldReceive('tags')->with(['posts'])->andReturnSelf();
        Cache::shouldReceive('remember')->once()->andReturn(collect());
        
        $repository = new CachedPostRepository();
        $posts = $repository->getPublished();
        
        // Second call should use cache
        $cachedPosts = $repository->getPublished();
    }
}

Conclusion

Mastering advanced Eloquent techniques transforms your Laravel applications from simple CRUD operations to sophisticated, high-performance systems. The patterns covered in this article—from complex relationships to performance optimization strategies—are essential for building scalable enterprise applications.

Key takeaways:

  • Complex relationships enable rich data modeling
  • Custom query builders create reusable, chainable query logic
  • Advanced aggregations provide powerful analytics capabilities
  • Performance optimization ensures scalability as your data grows
  • Repository patterns with caching improve maintainability and speed
  • Custom collections add domain-specific behavior to your models

These techniques will help you build Laravel applications that not only work well but scale gracefully and perform efficiently under real-world conditions. Start implementing these patterns gradually in your projects—the investment in learning advanced Eloquent will pay dividends in cleaner, more maintainable code.