Eloquent ORM - Praktyczny Przewodnik

Szczegółowa ściągawka dotycząca Eloquent ORM w Laravelu, z naciskiem na najlepsze praktyki, typowe pułapki i rozwiązania.

1. Podstawy Eloquent

Modele

  • Opis: Reprezentują tabele bazy danych, rozszerzają klasę Illuminate\Database\Eloquent\Model.
  • Konwencje:
    • Nazwa modelu w liczbie pojedynczej (User) mapuje się na nazwę tabeli w liczbie mnogiej (users).
    • Klucz główny id.
    • Pola created_at i updated_at automatycznie zarządzane.
  • Tworzenie modelu:
    php artisan make:model Post
  • Konfiguracja: W modelu możesz nadpisać domyślne konwencje:
    protected $table = 'my_posts';
    protected $primaryKey = 'post_id';
    public $incrementing = false;
    protected $keyType = 'string';
    public $timestamps = false; // Wyłączenie automatycznych timestampów
  • Masowe przypisanie (Mass Assignment):
    • Użyj $fillable, aby określić, które atrybuty mogą być masowo przypisywane.
    • Użyj $guarded, aby określić, które atrybuty NIE MOGĄ być masowo przypisywane (wszystkie inne mogą).
    • Dobra praktyka: Preferuj $fillable dla lepszej kontroli i bezpieczeństwa.
    // W modelu User
    protected $fillable = ['name', 'email', 'password'];
    // lub
    // protected $guarded = []; // Pozwala na masowe przypisywanie wszystkich pól (niezalecane!)

Podstawowe Operacje CRUD

  • Tworzenie:
    $post = Post::create(['title' => 'Mój Tytuł', 'content' => 'Treść posta']);
    // lub
    $post = new Post;
    $post->title = 'Nowy Tytuł';
    $post->content = 'Nowa Treść';
    $post->save();
  • Odczyt:
    $post = Post::find(1); // Po kluczu głównym
    $posts = Post::all(); // Wszystkie rekordy
    $activePosts = Post::where('active', true)->get();
    $firstPost = Post::where('slug', 'pierwszy-post')->first(); // Pierwszy pasujący
    $postOrFail = Post::findOrFail(100); // Rzuci wyjątek ModelNotFoundException jeśli nie znajdzie
  • Aktualizacja:
    $post = Post::find(1);
    $post->title = 'Zaktualizowany Tytuł';
    $post->save();
    // lub masowa aktualizacja
    Post::where('active', false)->update(['active' => true]);
  • Usuwanie:
    $post = Post::find(1);
    $post->delete();
    // lub masowe usuwanie
    Post::where('status', 'draft')->delete();
  • Soft Deletes:
    • Dodaj use SoftDeletes; do modelu i kolumnę deleted_at (timestamp) do tabeli.
    • Elementy są "usuwane" (deleted_at ustawiane), ale nadal istnieją w bazie.
    // W modelu
    use Illuminate\Database\Eloquent\SoftDeletes;
    class Post extends Model
    {
        use SoftDeletes;
    }
    
    // Użycie
    $post->delete(); // Ustawi deleted_at
    $trashedPosts = Post::onlyTrashed()->get(); // Pobierz tylko usunięte
    $allPosts = Post::withTrashed()->get(); // Pobierz wszystkie (włącznie z usuniętymi)
    $post->restore(); // Przywróć usunięty rekord
    $post->forceDelete(); // Trwale usuń z bazy

2. Relacje Eloquent

Typy Relacji

  • Jeden do Jednego (One-to-One): hasOne, belongsTo
    // User model
    public function phone() { return $this->hasOne(Phone::class); }
    
    // Phone model
    public function user() { return $this->belongsTo(User::class); }
  • Jeden do Wielu (One-to-Many): hasMany, belongsTo
    // Post model
    public function comments() { return $this->hasMany(Comment::class); }
    
    // Comment model
    public function post() { return $this->belongsTo(Post::class); }
  • Wiele do Wielu (Many-to-Many): belongsToMany (wymaga tabeli pośredniej)
    // User model
    public function roles() { return $this->belongsToMany(Role::class); }
    
    // Role model
    public function users() { return $this->belongsToMany(User::class); }
    • Operacje: attach(), detach(), sync(), syncWithoutDetaching().
    • $user->roles()->attach(1); // Dodaj rolę o ID 1
      $user->roles()->detach([1, 2]); // Usuń role o ID 1 i 2
      $user->roles()->sync([1, 3]); // Synchronizuj: dodaj 3, usuń wszystko oprócz 1 i 3
  • Polimorficzne (Polymorphic Relations): morphTo, morphMany, morphOne, morphToMany
    • Pozwalają na powiązanie modelu z wieloma innymi modelami na podstawie pojedynczej relacji.
    • Przydatne dla komentarzy, tagów, obrazów, gdzie jeden obiekt (np. Image) może należeć do różnych typów postów (Post, Video, Product).
    // Image model
    public function imageable() { return $this->morphTo(); }
    
    // Post model
    public function images() { return $this->morphMany(Image::class, 'imageable'); }
    
    // Video model
    public function images() { return $this->morphMany(Image::class, 'imageable'); }

3. Zaawansowane Zapytania Eloquent

Eager Loading (Ładowanie z wyprzedzeniem)

  • Problem N+1: Wielokrotne zapytania do bazy danych w pętli.
  • Rozwiązanie: Użyj with() do załadowania powiązanych relacji.
  • // Problem N+1:
    // $posts = Post::all();
    // foreach ($posts as $post) { echo $post->user->name; } // N zapytań do userów
    
    // Rozwiązanie:
    $posts = Post::with('user')->get(); // Załaduje userów jednym zapytaniem
    foreach ($posts as $post) { echo $post->user->name; }
    
    // Ładowanie zagnieżdżonych relacji
    $posts = Post::with('user.profile')->get();
    
    // Ładowanie wielu relacji
    $posts = Post::with(['user', 'comments'])->get();
    
    // Ładowanie z warunkami na relacji
    $posts = Post::with(['comments' => function ($query) {
        $query->where('approved', true);
    }])->get();

Lazy Loading (Leniwe Ładowanie)

  • Relacje są ładowane tylko wtedy, gdy są potrzebne.
  • Może prowadzić do problemu N+1, jeśli nie jest używane świadomie.
  • Alternatywa: Użyj load() lub loadMissing() na istniejących kolekcjach/modelach.
    $post = Post::find(1);
    $post->load('user'); // Załaduje relację user dla pojedynczego posta
    
    $posts = Post::all();
    $posts->load('user'); // Załaduje relację user dla całej kolekcji

Scope'y

  • Definiowanie wspólnych zestawów warunków zapytania.
  • Dodaj metodę scopeNazwaScopa do modelu.
  • // W modelu Post
    public function scopePublished($query)
    {
        return $query->where('published', true)->where('published_at', '<=', now());
    }
    
    // Użycie
    $publishedPosts = Post::published()->get();
  • Global Scopes: Stosowane automatycznie do wszystkich zapytań danego modelu.
    // W modelu
    protected static function boot()
    {
        parent::boot();
        static::addGlobalScope(new YourGlobalScope);
    }

Mutatory i Akcesoria (Mutators & Accessors)

  • Akcesoria (Getters): Modyfikują atrybuty podczas pobierania.
    // W modelu User
    public function getFullNameAttribute()
    {
        return "{$this->first_name} {$this->last_name}";
    }
    // Użycie: $user->full_name;
  • Mutatory (Setters): Modyfikują atrybuty podczas ustawiania.
    // W modelu User
    public function setPasswordAttribute($value)
    {
        $this->attributes['password'] = bcrypt($value);
    }
    // Użycie: $user->password = 'noweHaslo';
  • Dobra praktyka: Używaj ich do formatowania danych, szyfrowania, itp. Nie do skomplikowanej logiki biznesowej.

Casting (Rzutowanie)

  • Automatyczne konwertowanie atrybutów na dany typ danych.
  • Użyj właściwości $casts w modelu.
  • // W modelu Product
    protected $casts = [
        'is_active' => 'boolean',
        'options' => 'array',
        'price' => 'float',
        'published_at' => 'datetime',
    ];
  • Obsługiwane typy: integer, real, float, double, string, boolean, object, array, collection, date, datetime, timestamp, encrypted, AsEnum.

4. Dobre Praktyki i Pułapki

Dobre Praktyki

  • Używaj Eager Loading (with()): Zawsze, gdy spodziewasz się, że będziesz potrzebować relacji w pętli lub na kolekcjach. Minimalizuje problem N+1.
  • Stosuj Scopes: Do hermetyzacji złożonych warunków zapytań i ponownego ich użycia. Uczyni kod czystszym i bardziej czytelnym.
  • Waliduj dane przed zapisem: Zawsze waliduj dane przychodzące od użytkownika, zanim zapiszesz je do bazy, nawet jeśli używasz $fillable.
  • Korzystaj z Migracji: Do zarządzania schematem bazy danych. Zapewniają spójność i możliwość śledzenia zmian.
  • Testuj swoje modele i zapytania: Zapewnia to, że Twoja logika ORM działa poprawnie i efektywnie.
  • Używaj Transakcji: Dla operacji, które muszą być atomowe (wszystkie albo żadna).
    DB::transaction(function () {
        // Operacje bazy danych
        User::create([...]);
        Post::create([...]);
    });
  • Dependency Injection: Wstrzykuj instancje modeli do kontrolerów/serwisów, zamiast używać ich statycznie wewnątrz metod. Ułatwia testowanie i zwiększa elastyczność.
    // Dobra praktyka
    use App\Models\User;
    
    class UserController extends Controller
    {
        public function show(User $user)
        {
            return view('user.profile', compact('user'));
        }
    }
  • Unikaj zbyt wielu kolumn w tabeli: Rozważ normalizację bazy danych i podział na powiązane tabele, jeśli tabela staje się zbyt szeroka.
  • Indeksy: Dodawaj indeksy do kolumn używanych w warunkach WHERE, JOIN, ORDER BY.

Popularne Pułapki i Problemy

  • Problem N+1:
    • Objaw: Duża liczba zapytań do bazy danych (widać w Laravel Debugbar lub logach).
    • Rozwiązanie: Eager Loading (with()).
      // Zamiast:
      // $posts = Post::all();
      // foreach ($posts as $post) { $post->user->name; }
      
      // Użyj:
      $posts = Post::with('user')->get();
      foreach ($posts as $post) { $post->user->name; }
  • Mass Assignment Exception:
    • Objaw: Błąd podczas próby masowego przypisania atrybutów.
    • Rozwiązanie: Zdefiniuj $fillable w modelu lub użyj $guarded = [] (z ostrożnością!).
      // W modelu
      protected $fillable = ['name', 'email', 'password'];
  • Nieprawidłowe nazwy kolumn klucza obcego/tabel relacji:
    • Objaw: Relacje nie działają lub zwracają puste kolekcje.
    • Rozwiązanie: Jawnie określaj nazwy kluczy obcych i tabel w definicjach relacji, jeśli odbiegają od konwencji Laravela.
      // Jeśli klucz obcy to 'author_id' zamiast 'user_id'
      public function user() { return $this->belongsTo(User::class, 'author_id'); }
      
      // Jeśli tabela pośrednia ma inną nazwę
      public function roles() { return $this->belongsToMany(Role::class, 'user_roles'); }
  • Niespodziewane nadpisywanie danych przy updateOrCreate:
    • Objaw: Dane są aktualizowane, nawet gdy powinny być tworzone.
    • Rozwiązanie: Upewnij się, że pierwszy argument (warunki wyszukiwania) w updateOrCreate jest unikalny dla rekordu, a drugi (wartości do ustawienia/aktualizacji) zawiera tylko to, co ma być zmienione.
      $user = User::updateOrCreate(
          ['email' => 'john.doe@example.com'], // Warunki wyszukiwania
          ['name' => 'John Doe', 'password' => bcrypt('secret')] // Wartości do stworzenia/aktualizacji
      );
  • Problemy z UTC/Strefami czasowymi:
    • Objaw: Daty są niepoprawne po zapisie/odczycie.
    • Rozwiązanie: Laravel domyślnie przechowuje daty w UTC. Upewnij się, że strefy czasowe są poprawnie skonfigurowane w config/app.php (timezone) oraz na serwerze bazy danych. Używaj obiektu Carbon do manipulacji datami.
  • Zbyt wiele zapytań na dashboardach/listach (Pagination):
    • Objaw: Strony ładują się wolno przy dużych zbiorach danych.
    • Rozwiązanie: Używaj paginacji (paginate()) i Eager Loading.
      $posts = Post::with('user')->latest()->paginate(10);
  • Brak obsługi błędów:
    • Objaw: Aplikacja zawiesza się na błędach bazy danych (np. unikalne klucze, brak rekordu).
    • Rozwiązanie: Stosuj bloki try-catch lub metody firstOrFail(), findOrFail().

SŁOWNICZEK POJĘĆ