Eloquent ORM: Patterns und Optimierungen für Laravel
Eloquent ORM mit fortgeschrittenen Patterns und Optimierungstechniken meistern. Eager Loading, Query Scopes, Accessors, Mutatoren und Performance für Laravel-Anwendungen.

Eloquent ORM verwandelt Datenbankinteraktionen in flüssige und ausdrucksstarke Operationen. Über die elegante Syntax hinaus entscheidet die Beherrschung fortgeschrittener Patterns und Optimierungstechniken über die Performance von Laravel-Anwendungen in der Produktion.
Das N+1-Problem ist die häufigste Ursache für Langsamkeit in Eloquent-Anwendungen. Jede nicht optimierte Beziehung erzeugt eine zusätzliche SQL-Abfrage pro Datensatz.
Das N+1-Problem mit Eager Loading lösen
Das N+1-Problem entsteht, wenn jede Iteration über eine Sammlung eine zusätzliche Abfrage zum Laden der Beziehungen auslöst. Bei 100 Artikeln und ihren Autoren ergeben sich 101 Abfragen statt einer einzigen optimierten.
Eager Loading lädt alle Beziehungen in maximal einer oder zwei Abfragen und reduziert so die Antwortzeit drastisch.
// Demonstration of N+1 problem and its solution
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
// ❌ N+1 problem: 1 articles query + N author queries
public function indexWithProblem()
{
$articles = Article::all(); // 1 query
foreach ($articles as $article) {
echo $article->author->name; // N additional queries
}
}
// ✅ Eager loading: 2 queries maximum
public function indexOptimized()
{
$articles = Article::with('author')->get(); // 2 queries total
foreach ($articles as $article) {
echo $article->author->name; // No additional queries
}
}
// ✅ Nested eager loading for multiple relationships
public function indexWithNestedRelations()
{
// Loads articles → authors → profiles + articles → comments → users
$articles = Article::with([
'author.profile',
'comments.user'
])->get();
return view('articles.index', compact('articles'));
}
}Eager Loading mit with() antizipiert die Bedürfnisse und lädt die Daten im Voraus. Der Performance-Unterschied wird bei großen Sammlungen spektakulär.
Bedingtes und eingeschränktes Eager Loading
Umfangreiche Beziehungen erfordern manchmal ein partielles Laden. Einschränkungen beim Eager Loading begrenzen die abgerufenen Daten und vermeiden gleichzeitig N+1.
// Eager loading with constraints to optimize queries
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
class UserController extends Controller
{
public function showWithRecentOrders(int $id)
{
// Load only the 5 most recent paid orders
$user = User::with(['orders' => function (Builder $query) {
$query->where('status', 'paid')
->orderByDesc('created_at')
->limit(5);
}])->findOrFail($id);
return view('users.show', compact('user'));
}
public function indexActiveWithStats()
{
// Conditional eager loading with withCount
$users = User::query()
->where('active', true)
->with(['profile', 'subscription'])
->withCount(['orders', 'reviews']) // Adds orders_count and reviews_count
->withSum('orders', 'total') // Adds orders_sum_total
->get();
return view('users.index', compact('users'));
}
public function showWithConditionalRelation(int $id)
{
// Load relationship only if user is premium
$user = User::findOrFail($id);
$user->loadMissing(
$user->isPremium() ? ['premiumFeatures', 'analytics'] : []
);
return view('users.show', compact('user'));
}
}Die Methoden withCount() und withSum() fügen Aggregationen hinzu, ohne vollständige Sammlungen zu laden, was sich ideal für Dashboard-Statistiken eignet.
Query Scopes für wiederverwendbare Abfragen
Query Scopes kapseln die Filterlogik innerhalb des Modells. Lokale Scopes bieten Flexibilität, während globale Scopes automatisch auf alle Abfragen angewendet werden.
Lokale Scopes verwenden den Präfix scope im Modell, werden aber ohne diesen aufgerufen: scopeActive() wird zu User::active().
// Local and global scopes to encapsulate business logic
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
#[ScopedBy([PublishedScope::class])] // Global scope via PHP 8 attribute
class Article extends Model
{
// Simple local scope: Article::published()
public function scopePublished(Builder $query): Builder
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now());
}
// Local scope with parameter: Article::byCategory('tech')
public function scopeByCategory(Builder $query, string $category): Builder
{
return $query->where('category', $category);
}
// Local scope with optional parameter
public function scopePopular(Builder $query, int $minViews = 1000): Builder
{
return $query->where('views_count', '>=', $minViews)
->orderByDesc('views_count');
}
// Dynamic scope for flexible search
public function scopeSearch(Builder $query, ?string $term): Builder
{
if (empty($term)) {
return $query;
}
return $query->where(function (Builder $q) use ($term) {
$q->where('title', 'like', "%{$term}%")
->orWhere('content', 'like', "%{$term}%")
->orWhereHas('tags', fn($t) => $t->where('name', 'like', "%{$term}%"));
});
}
}// Reusable global scope across models
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
// Automatically applied to all Article queries
$builder->whereNotNull('published_at')
->where('published_at', '<=', now());
}
}// Using scopes in a controller
$articles = Article::query()
->byCategory('technology')
->popular(500)
->search($request->input('q'))
->with('author')
->paginate(20);
// Disable a global scope temporarily
$allArticles = Article::withoutGlobalScope(PublishedScope::class)->get();Die Verkettung von Scopes liefert lesbare und wartbare Abfragen, indem sie die Geschäftslogik im Modell zentralisiert.
Bereit für deine Laravel-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Accessoren und Mutatoren mit Attribute
Laravel 9+ führt eine einheitliche Syntax für Accessoren und Mutatoren über die Klasse Attribute ein. Dieser moderne Ansatz ersetzt die Methoden get*Attribute und set*Attribute.
// Modern accessors and mutators with the Attribute class
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class User extends Model
{
protected $fillable = ['first_name', 'last_name', 'email', 'password'];
// Accessor: generates a virtual attribute from other fields
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}",
);
}
// Mutator: transforms value before saving
protected function password(): Attribute
{
return Attribute::make(
set: fn (string $value) => Hash::make($value),
);
}
// Combined accessor + mutator
protected function email(): Attribute
{
return Attribute::make(
get: fn (string $value) => Str::lower($value),
set: fn (string $value) => Str::lower(trim($value)),
);
}
// Cached accessor to avoid recalculations
protected function initials(): Attribute
{
return Attribute::make(
get: fn () => Str::upper(
Str::substr($this->first_name, 0, 1) .
Str::substr($this->last_name, 0, 1)
),
)->shouldCache(); // Caches result during request
}
// Computed attribute based on a relationship
protected function ordersTotal(): Attribute
{
return Attribute::make(
get: fn () => $this->orders->sum('total'),
);
}
}// Transparent usage of accessors and mutators
$user = new User();
$user->first_name = 'John';
$user->last_name = 'Doe';
$user->email = ' JOHN@EXAMPLE.COM '; // Automatically normalized
$user->password = 'secret123'; // Automatically hashed
$user->save();
echo $user->full_name; // "John Doe"
echo $user->initials; // "JD"
echo $user->email; // "john@example.com"Die Methode shouldCache() optimiert teure Accessoren, indem sie mehrfache Neuberechnungen am gleichen Modell vermeidet.
Eigene Casts für komplexe Typen
Casts wandeln Werte automatisch zwischen PHP und der Datenbank um. Eigene Casts kapseln die Serialisierungslogik für Geschäftstypen.
// Custom cast for handling monetary amounts
namespace App\Casts;
use App\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
class MoneyCast implements CastsAttributes
{
public function __construct(
protected string $currency = 'USD'
) {}
// DB → PHP conversion: cents to Money object
public function get(Model $model, string $key, mixed $value, array $attributes): ?Money
{
if ($value === null) {
return null;
}
return new Money(
amount: (int) $value,
currency: $this->currency
);
}
// PHP → DB conversion: Money object to cents
public function set(Model $model, string $key, mixed $value, array $attributes): ?int
{
if ($value === null) {
return null;
}
if ($value instanceof Money) {
return $value->getAmountInCents();
}
if (is_numeric($value)) {
return (int) ($value * 100);
}
throw new InvalidArgumentException('Value must be Money instance or numeric');
}
}// Immutable Value Object representing amounts
namespace App\ValueObjects;
use JsonSerializable;
final readonly class Money implements JsonSerializable
{
public function __construct(
private int $amount, // Stored in cents
private string $currency
) {}
public function getAmountInCents(): int
{
return $this->amount;
}
public function getAmountInUnits(): float
{
return $this->amount / 100;
}
public function format(): string
{
return number_format($this->getAmountInUnits(), 2) . ' ' . $this->currency;
}
public function add(Money $other): self
{
return new self($this->amount + $other->amount, $this->currency);
}
public function jsonSerialize(): array
{
return [
'amount' => $this->getAmountInUnits(),
'currency' => $this->currency,
];
}
}// Using the custom cast
namespace App\Models;
use App\Casts\MoneyCast;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
protected function casts(): array
{
return [
'total' => MoneyCast::class, // USD by default
'shipping_cost' => MoneyCast::class . ':USD',
'tax_amount' => MoneyCast::class . ':USD',
'paid_at' => 'datetime',
'metadata' => 'array',
];
}
}// Natural usage with the cast
$order = Order::find(1);
echo $order->total->format(); // "149.99 USD"
echo $order->total->getAmountInCents(); // 14999
$order->total = 199.99; // Automatically converted
$order->save(); // Stored as 19999 in DBValue Objects in Kombination mit eigenen Casts gewährleisten die Integrität der Geschäftsdaten und behalten dabei eine elegante API bei.
Großvolumige Abfragen optimieren
Operationen über Millionen Datensätze erfordern spezifische Techniken, um den Speicher nicht zu erschöpfen. Chunking und Cursor verarbeiten die Daten in Stapeln.
Model::all() lädt alle Datensätze in den Speicher. Bei einer Tabelle mit 100.000 Zeilen kann das mehrere Gigabyte RAM verbrauchen und die Anwendung abstürzen lassen.
// Batch processing techniques for large tables
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ProcessUsersCommand extends Command
{
protected $signature = 'users:process';
public function handle(): int
{
// ✅ Chunk: processes in batches of 1000, reloads from DB
User::query()
->where('needs_processing', true)
->chunk(1000, function ($users) {
foreach ($users as $user) {
$user->processAccount();
}
});
// ✅ Chunk with updates: avoids infinite loop during modifications
User::query()
->where('status', 'pending')
->chunkById(1000, function ($users) {
foreach ($users as $user) {
$user->update(['status' => 'processed']);
}
});
// ✅ Lazy collection: single record in memory at a time
foreach (User::lazy(1000) as $user) {
$user->sendNewsletter();
}
// ✅ Cursor: for read-only operations, minimal memory
foreach (User::cursor() as $user) {
$this->info("Processing: {$user->email}");
}
// ✅ Mass update without Eloquent: maximum performance
User::query()
->where('last_login_at', '<', now()->subYear())
->update(['status' => 'inactive']);
// ✅ Optimized mass deletion
User::query()
->where('deleted_at', '<', now()->subMonths(6))
->forceDelete();
return self::SUCCESS;
}
}Die Wahl zwischen chunk(), lazy() und cursor() hängt vom Anwendungsfall ab: chunk() für Änderungen, lazy() für Zwischenoperationen und cursor() für einfaches Lesen mit minimalem Speicherverbrauch.
Fortgeschrittene polymorphe Beziehungen
Polymorphe Beziehungen ermöglichen es einem Modell, über eine einzige Beziehung zu mehreren unterschiedlichen Modelltypen zu gehören. Diese Flexibilität eignet sich ideal für Kommentare, Tags oder angehängte Dateien.
// Model with inverse polymorphic relationship
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
protected $fillable = ['body', 'user_id'];
// A comment can belong to Article, Video, or any other model
public function commentable(): MorphTo
{
return $this->morphTo();
}
public function user()
{
return $this->belongsTo(User::class);
}
}// Parent model with polymorphic relationship
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Article extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}// Another parent model using the same relationship
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Video extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}// Migration for polymorphic comments table
// database/migrations/2026_01_15_create_comments_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->foreignId('user_id')->constrained();
$table->morphs('commentable'); // Creates commentable_type and commentable_id
$table->timestamps();
// Composite index for polymorphic queries
$table->index(['commentable_type', 'commentable_id']);
});
}
};// Using polymorphic relationships
$article = Article::find(1);
$article->comments()->create([
'body' => 'Excellent article!',
'user_id' => auth()->id(),
]);
$video = Video::find(1);
$video->comments()->create([
'body' => 'Very instructive video',
'user_id' => auth()->id(),
]);
// Retrieve parent from comment
$comment = Comment::with('commentable')->find(1);
echo get_class($comment->commentable); // App\Models\Article or App\Models\VideoPolymorphe Beziehungen vermeiden die Duplizierung von Tabellen und zentralisieren die Logik für übergreifende Funktionen.
Bereit für deine Laravel-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Traits und Observer für wiederverwendbare Logik
Traits kapseln wiederverwendbare Verhaltensweisen über Modelle hinweg. Observer zentralisieren Hooks auf Lebenszyklusereignisse.
// Trait for automatic slug generation
namespace App\Models\Concerns;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
trait HasSlug
{
public static function bootHasSlug(): void
{
static::creating(function (Model $model) {
if (empty($model->slug)) {
$model->slug = $model->generateUniqueSlug();
}
});
}
protected function generateUniqueSlug(): string
{
$slug = Str::slug($this->getSlugSource());
$originalSlug = $slug;
$counter = 1;
// Check uniqueness and add suffix if needed
while (static::where('slug', $slug)->exists()) {
$slug = "{$originalSlug}-{$counter}";
$counter++;
}
return $slug;
}
// Can be overridden in the model
protected function getSlugSource(): string
{
return $this->title ?? $this->name;
}
}// Trait for using UUIDs as primary key
namespace App\Models\Concerns;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
trait HasUuid
{
public static function bootHasUuid(): void
{
static::creating(function (Model $model) {
if (empty($model->{$model->getKeyName()})) {
$model->{$model->getKeyName()} = (string) Str::uuid();
}
});
}
public function getIncrementing(): bool
{
return false;
}
public function getKeyType(): string
{
return 'string';
}
}// Observer to centralize hooks on Article model
namespace App\Observers;
use App\Models\Article;
use App\Jobs\NotifySubscribersJob;
use App\Services\SearchIndexService;
use Illuminate\Support\Facades\Cache;
class ArticleObserver
{
public function __construct(
private SearchIndexService $searchIndex
) {}
public function created(Article $article): void
{
// Invalidate recent articles cache
Cache::tags(['articles', 'recent'])->flush();
// Index for search
$this->searchIndex->index($article);
}
public function updated(Article $article): void
{
// Update search index
$this->searchIndex->update($article);
// Notify subscribers if article was just published
if ($article->wasChanged('published_at') && $article->published_at !== null) {
NotifySubscribersJob::dispatch($article);
}
Cache::tags(['articles'])->flush();
}
public function deleted(Article $article): void
{
$this->searchIndex->remove($article);
Cache::tags(['articles'])->flush();
}
// Prevent deletion if article has comments
public function deleting(Article $article): bool
{
if ($article->comments()->exists()) {
return false; // Cancel deletion
}
return true;
}
}// Model using traits and observer
namespace App\Models;
use App\Models\Concerns\HasSlug;
use App\Models\Concerns\HasUuid;
use App\Observers\ArticleObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy(ArticleObserver::class)]
class Article extends Model
{
use HasSlug, HasUuid;
protected $fillable = ['title', 'content', 'published_at'];
}Die boot*-Methoden der Traits werden während der Modellinitialisierung automatisch ausgeführt und ermöglichen so eine transparente Integration.
Fazit
Das Beherrschen von Eloquent ORM beruht auf dem Verständnis der zugrunde liegenden Mechanismen und der Anwendung passender Patterns. Die vorgestellten Techniken verwandeln naive Abfragen in performanten und wartbaren Code.
Eloquent-Optimierungs-Checkliste:
✅ Systematisch with() für angezeigte Beziehungen verwenden
✅ withCount() einsetzen, statt Sammlungen zum Zählen zu laden
✅ Filterlogik in Query Scopes kapseln
✅ Accessoren wiederholten Berechnungen in den Views vorziehen
✅ Eigene Casts für geschäftliche Value Objects implementieren
✅ chunk() oder lazy() für Operationen auf großen Tabellen verwenden
✅ Seiteneffekte in Observern zentralisieren
✅ Gemeinsame Verhaltensweisen in Traits auslagern
Die Werkzeuge php artisan telescope oder laravel-debugbar machen die generierten SQL-Abfragen sichtbar und decken fehlende Optimierungen auf.
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
Verwandte Artikel

Laravel und PHP Interviewfragen: Die Top 25 in 2026
Die 25 haeufigsten Laravel- und PHP-Interviewfragen. Eloquent ORM, Middleware, Artisan, Queues, Tests und Architektur mit ausfuehrlichen Antworten und Codebeispielen.

Laravel 11: Eine vollständige Anwendung von Grund auf erstellen
Umfassender Leitfaden zu Laravel 11: Installation, Eloquent-Modelle, Authentifizierung mit Breeze, Controller, Routen, Autorisierung, REST-API mit Sanctum, Tests mit Pest und Produktions-Deployment.

Laravel Middleware im Detail: Authentifizierung, Rate Limiting und eigene Middleware
Umfassender Leitfaden zu Laravel Middleware mit praktischen Beispielen zu Authentifizierung, Rate Limiting, eigener Middleware-Erstellung und fortgeschrittenen Produktionsmustern.