Eloquent Model trong Laravel 13 cho người mới bắt đầu

Nguyễn Dương Thế Vĩ · Created: 10:01 09/09/2026 · Updated: 06:11 13/09/2026 · 14 Views

Hướng dẫn dùng **Model** (Eloquent ORM) từ số 0, qua ví dụ một model `Post` (bài viết blog). Bài này độc lập — bạn chỉ cần Laravel 13 + một bảng `posts` trong database là làm theo được.

1. Model là gì?

Model là một class PHP đại diện cho một bảng trong database. Mỗi dòng trong bảng = một object của model. Thay vì viết SQL tay, bạn thao tác bằng code PHP:

$post = Post::find(1);   // SELECT * FROM posts WHERE id = 1
echo $post->title;       // đọc cột title
$post->title = 'Mới';    // đổi giá trị
$post->save();           // UPDATE posts SET title = 'Mới' WHERE id = 1

Cơ chế này gọi là ORM (Object-Relational Mapping). Trong Laravel nó tên là Eloquent.

Quan trọng: Model không tạo ra bảng. Bảng do migration tạo. Model chỉ ánh xạ tới bảng đã tồn tại.


2. Tạo model

Dùng lệnh Artisan:

php artisan make:model Post

Muốn tạo kèm luôn migration (-m), factory (-f), seeder (-s), controller (-c):

php artisan make:model Post -mfsc

File tạo ra: app/Models/Post.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    //
}

Chỉ cần extends Model là đã có toàn bộ sức mạnh Eloquent.


3. Quy ước ngầm (convention) — hiểu để đỡ cấu hình

Laravel "đoán" nhiều thứ theo quy ước. Model Post mặc định:

Thứ Giá trị mặc định Ghi chú
Tên bảng posts số nhiều, snake_case của tên class
Khóa chính id kiểu bigint tự tăng
Timestamps created_at, updated_at Eloquent tự quản

Nếu bảng của bạn tên khác, khai báo tường minh:

class Post extends Model
{
    protected $table = 'blog_posts';   // bảng không phải "posts"
    protected $primaryKey = 'post_id'; // khóa chính không phải "id"
    public $timestamps = false;        // bảng không có created_at/updated_at
}

4. Mass Assignment — $fillable (BẮT BUỘC hiểu)

Khi bạn tạo/sửa nhiều cột cùng lúc từ một mảng (thường là dữ liệu form):

Post::create($request->all()); // gán hàng loạt

Laravel chặn việc này để tránh lỗ hổng bảo mật (kẻ xấu gửi thêm field lạ như is_admin=1). Bạn phải khai báo cột nào được phép gán hàng loạt:

class Post extends Model
{
    protected $fillable = ['title', 'slug', 'body', 'published'];
}

Nếu quên, bạn sẽ gặp lỗi MassAssignmentException.

  • $fillable = danh sách trắng (chỉ cột này được gán) — khuyên dùng.
  • $guarded = danh sách đen (chặn cột này, còn lại cho phép) — ngược lại.

💡 Nguyên tắc: những cột người dùng KHÔNG được tự set (vd views, role, user_id) thì đừng cho vào $fillable. Set chúng bằng code riêng.


5. Ép kiểu dữ liệu — casts()

Database lưu mọi thứ dạng chuỗi/số. casts() giúp Eloquent trả về đúng kiểu PHP:

class Post extends Model
{
    protected function casts(): array
    {
        return [
            'published'    => 'boolean',   // 0/1  -> true/false
            'published_at' => 'datetime',  // chuỗi -> Carbon object
            'meta'         => 'array',      // JSON -> mảng PHP (và ngược lại)
            'views'        => 'integer',
        ];
    }
}

Kết quả:

$post = Post::find(1);
$post->published;              // true (bool), không phải "1" (string)
$post->published_at->diffForHumans(); // "2 ngày trước" — vì là Carbon
$post->meta['seo_title'];      // truy cập như mảng

Laravel 11+ dùng method casts(). Code cũ dùng protected $casts = [...] (property) — vẫn chạy, nhưng method là cách mới, khuyến nghị.


6. Ẩn cột khi xuất JSON — $hidden

Khi model được trả về API (return $post;), Laravel tự chuyển sang JSON. Ẩn cột nhạy cảm:

protected $hidden = ['password', 'remember_token'];

7. CRUD — Tạo / Đọc / Sửa / Xóa (phần dùng nhiều nhất)

7.1. CREATE — tạo dòng mới

Cách 1 — create() (gán hàng loạt, cần $fillable):

$post = Post::create([
    'title'     => 'Bài đầu tiên',
    'slug'      => 'bai-dau-tien',
    'body'      => 'Nội dung...',
    'published' => true,
]);
// $post->id đã có ngay sau khi tạo

Cách 2 — tạo object rồi save():

$post = new Post();
$post->title = 'Bài thứ hai';
$post->slug  = 'bai-thu-hai';
$post->body  = 'Nội dung...';
$post->save(); // INSERT

firstOrCreate — tìm, không có thì tạo (tránh trùng):

// Tìm theo slug; nếu chưa có thì tạo với dữ liệu ở mảng thứ 2
$post = Post::firstOrCreate(
    ['slug' => 'bai-dau-tien'],          // điều kiện tìm
    ['title' => 'Bài đầu tiên', 'body' => '...'] // dữ liệu khi tạo mới
);

updateOrCreate — có thì cập nhật, không có thì tạo:

$post = Post::updateOrCreate(
    ['slug' => 'bai-dau-tien'],       // tìm theo
    ['title' => 'Tiêu đề đã sửa']     // giá trị cập nhật/tạo
);

7.2. READ — truy vấn dữ liệu

Lấy tất cả:

$posts = Post::all(); // Collection tất cả bài — CẨN THẬN nếu bảng lớn

Tìm theo khóa chính:

$post = Post::find(1);          // trả null nếu không thấy
$post = Post::findOrFail(1);    // ném lỗi 404 nếu không thấy (hay dùng ở controller)
$posts = Post::find([1, 2, 3]); // nhiều id -> Collection

Lọc theo điều kiện — where():

$posts = Post::where('published', true)->get();

// Nhiều điều kiện (AND)
$posts = Post::where('published', true)
             ->where('views', '>', 100)
             ->get();

// Toán tử tùy ý
Post::where('title', 'like', '%laravel%')->get();
Post::whereIn('id', [1, 2, 3])->get();
Post::whereNull('published_at')->get();
Post::whereBetween('views', [10, 100])->get();

Lấy 1 dòng:

$post = Post::where('slug', 'bai-dau-tien')->first();      // dòng đầu hoặc null
$post = Post::where('slug', 'bai-dau-tien')->firstOrFail(); // hoặc lỗi 404

Sắp xếp & giới hạn:

Post::orderBy('created_at', 'desc')->get(); // cũ -> mới ngược lại
Post::latest()->get();       // = orderBy('created_at','desc'), mới nhất trước
Post::oldest()->get();       // cũ nhất trước
Post::latest()->limit(5)->get(); // 5 bài mới nhất

Lấy riêng vài cột (nhẹ hơn):

Post::select('id', 'title')->get();
Post::pluck('title');          // Collection chỉ chứa title
Post::pluck('title', 'id');    // [id => title]
Post::where('id', 1)->value('title'); // đúng 1 giá trị của 1 cột

Phân trang — cực hay dùng:

$posts = Post::latest()->paginate(10); // 10 bài / trang, tự đọc ?page=

Trong Blade: {{ $posts->links() }} để render nút chuyển trang.


7.3. UPDATE — cập nhật

Sửa 1 object đã lấy:

$post = Post::findOrFail(1);
$post->title = 'Tiêu đề mới';
$post->save(); // chỉ UPDATE các cột thay đổi; tự cập nhật updated_at

Hoặc update() (gán mảng, cần $fillable):

$post->update(['title' => 'Tiêu đề mới', 'published' => true]);

Cập nhật hàng loạt (nhiều dòng cùng lúc):

Post::where('published', false)->update(['published' => true]);
// UPDATE posts SET published = 1 WHERE published = 0

Tăng/giảm số — increment() / decrement():

$post->increment('views');        // views = views + 1 (atomic, an toàn race)
$post->increment('views', 5);     // + 5
$post->decrement('stock', 2);     // - 2

💡 increment()/decrement() chạy thẳng ở DB (SET views = views + 1) nên an toàn khi nhiều request cùng lúc — khác với đọc rồi $post->views + 1 (dễ đếm sai). Lưu ý: 2 hàm này cũng tự cập nhật updated_at. Muốn không đụng timestamp: Post::withoutTimestamps(fn () => $post->increment('views'));


7.4. DELETE — xóa

$post = Post::find(1);
$post->delete();            // xóa 1 dòng

Post::destroy(1);           // xóa theo id
Post::destroy([1, 2, 3]);   // xóa nhiều id

Post::where('published', false)->delete(); // xóa hàng loạt theo điều kiện

Soft Delete (xóa mềm — chỉ đánh dấu, không mất hẳn):

use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
    use SoftDeletes; // cần cột deleted_at trong bảng
}

Khi đó ->delete() chỉ set deleted_at = now(). Khôi phục bằng ->restore(), xóa vĩnh viễn bằng ->forceDelete(), lấy cả bản đã xóa bằng ->withTrashed().


8. Aggregate — thống kê nhanh

Post::count();                       // đếm tổng số bài
Post::where('published', true)->count();
Post::sum('views');                  // tổng lượt xem
Post::avg('views');                  // trung bình
Post::max('views');                  // nhiều nhất
Post::where('slug', 'x')->exists();  // true/false — có tồn tại không
Post::where('slug', 'x')->doesntExist();

9. Quan hệ (Relationships) — sức mạnh thật sự của Eloquent

Giả sử: một User có nhiều Post, mỗi Post thuộc về một User (bảng posts có cột user_id).

Trong Post — thuộc về user:

use Illuminate\Database\Eloquent\Relations\BelongsTo;

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

Trong User — có nhiều post:

use Illuminate\Database\Eloquent\Relations\HasMany;

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

Cách dùng:

$post = Post::find(1);
echo $post->user->name;      // tên tác giả (truy cập như thuộc tính)

$user = User::find(1);
foreach ($user->posts as $post) {   // tất cả bài của user
    echo $post->title;
}

// Đếm bài của user không cần load hết
$user->posts()->count();
// Tạo bài gắn thẳng vào user (tự set user_id)
$user->posts()->create(['title' => 'Bài mới', 'body' => '...']);

9.1. Eager Loading — tránh lỗi N+1 (RẤT quan trọng)

Sai (mỗi vòng lặp query 1 lần → chậm):

$posts = Post::all();
foreach ($posts as $post) {
    echo $post->user->name; // +1 query mỗi bài!
}

Đúng — dùng with() nạp trước:

$posts = Post::with('user')->get(); // chỉ 2 query cho mọi bài
foreach ($posts as $post) {
    echo $post->user->name; // không query thêm
}

Lọc theo quan hệ:

Post::has('comments')->get();            // bài CÓ comment
Post::whereHas('user', fn ($q) =>
    $q->where('role', 'admin'))->get();  // bài của admin
Post::withCount('comments')->get();      // kèm $post->comments_count

10. Accessor & Mutator — biến đổi giá trị khi đọc/ghi

Accessor (biến đổi khi đọc) và Mutator (biến đổi khi ghi), gộp trong một Attribute:

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Str;

class Post extends Model
{
    // Tự tạo slug từ title khi GHI title
    protected function title(): Attribute
    {
        return Attribute::make(
            set: fn (string $value) => [
                'title' => $value,
                'slug'  => Str::slug($value),
            ],
        );
    }

    // Thuộc tính "ảo" excerpt khi ĐỌC (không có cột trong DB)
    protected function excerpt(): Attribute
    {
        return Attribute::make(
            get: fn () => Str::limit(strip_tags($this->body), 100),
        );
    }
}

Dùng:

$post->title = 'Xin Chào Laravel'; // slug tự thành "xin-chao-laravel"
echo $post->excerpt;               // 100 ký tự đầu, dù DB không có cột này

11. Query Scope — đóng gói điều kiện hay dùng lại

Thay vì lặp where('published', true) khắp nơi:

use Illuminate\Database\Eloquent\Builder;

class Post extends Model
{
    // Local scope: tên method bắt đầu bằng "scope"
    public function scopePublished(Builder $query): void
    {
        $query->where('published', true);
    }

    public function scopePopular(Builder $query, int $min = 100): void
    {
        $query->where('views', '>=', $min);
    }
}

Gọi (bỏ tiền tố scope, viết thường chữ đầu):

Post::published()->get();
Post::published()->popular(500)->latest()->get(); // xâu chuỗi thoải mái

12. Collection — kết quả trả về không phải mảng thường

get(), all(), quan hệ hasMany... trả về Collection (mảng "xịn" nhiều tiện ích):

$posts = Post::all();

$posts->count();                       // số phần tử
$posts->first();                       // phần tử đầu
$posts->where('published', true);      // lọc trong bộ nhớ
$posts->pluck('title');                // lấy list title
$posts->sortByDesc('views');           // sắp xếp
$posts->map(fn ($p) => $p->title);     // biến đổi
$posts->groupBy('user_id');            // gom nhóm
$titles = $posts->pluck('title')->implode(', '); // nối chuỗi

13. Bảng "cheat sheet" các hàm dùng nhiều nhất

Việc cần làm Câu lệnh
Tạo mới Post::create([...])
Tìm theo id Post::find($id) / findOrFail($id)
Lọc Post::where('col', $val)->get()
Lấy 1 dòng ->first() / ->firstOrFail()
Mới nhất Post::latest()->get()
Phân trang Post::paginate(10)
Cập nhật $post->update([...])
Tăng số $post->increment('views')
Xóa $post->delete() / Post::destroy($id)
Đếm Post::count()
Có tồn tại? Post::where(...)->exists()
Nạp quan hệ Post::with('user')->get()

14. Sai lầm thường gặp của người mới

  1. Quên $fillableMassAssignmentException. Thêm cột vào $fillable (trừ cột nhạy cảm).
  2. Lỗi N+1 → dùng with() khi lặp qua quan hệ.
  3. Post::all() trên bảng lớn → load hết RAM. Dùng paginate() hoặc chunk().
  4. Nhầm Model tạo bảng → không. Bảng do migration tạo; chạy php artisan migrate.
  5. Cho cột đếm (views) vào $fillable → nguy cơ mass-assignment. Tăng bằng increment() thay vì để người dùng gán.
  6. Đọc rồi cộng thủ công ($post->views + 1; save()) khi nhiều request → đếm sai. Dùng increment() (atomic).

Kết

Eloquent giúp bạn làm việc với database bằng PHP thuần, đọc như tiếng Anh. Ba thứ cần nắm chắc trước:

  1. $fillable — an toàn mass assignment.
  2. CRUD cơ bảncreate, find/where/get, update/increment, delete.
  3. Quan hệ + with() — nối bảng và tránh N+1.

Nắm 3 phần này là đã tự tin làm 80% công việc thực tế với model trong Laravel 13.