Bỏ qua đến nội dung chính

Model trong Laravel là gì? Cách dùng Eloquent Model

Model là lớp đại diện cho một bảng dữ liệu, làm việc qua Eloquent ORM. Bài viết hướng dẫn tạo model, CRUD, quan hệ, xóa mềm, factory và seeder theo Laravel 13.

Tan Hien
Founder
Cập nhật 7 phút đọc
Chia sẻ:
Model trong Laravel là gì? Cách dùng Eloquent Model
Nội dung bài viết
  1. 1.Model nằm ở đâu trong mô hình MVC?
  2. 2.Tạo model và quy ước đặt tên
  3. 3.Khai báo model: fillable, casts, scope
  4. 4.Thao tác dữ liệu với Eloquent
  5. 4.1Đọc dữ liệu
  6. 4.2Thêm bản ghi: create() và save()
  7. 4.3Cập nhật bản ghi: update() và save()
  8. 4.4Xóa và xóa mềm
  9. 5.Quan hệ giữa các model
  10. 6.Migration, factory và seeder cho model
  11. 7.Kinh nghiệm từ Hozitech

Model trong Laravel là lớp PHP đại diện cho một bảng trong cơ sở dữ liệu, mỗi đối tượng model ứng với một dòng dữ liệu. Laravel dùng Eloquent ORM cho model: bạn đọc, thêm, sửa, xóa bản ghi bằng phương thức PHP như Post::find(1) hay $post->save() thay vì viết SQL. Model là chữ M trong mô hình MVC, nằm trong thư mục app/Models.

Bài viết hướng dẫn theo Laravel 13 (bản hiện hành, PHP 8.3+): tạo model, quy ước đặt tên, khai báo thuộc tính, các thao tác CRUD, quan hệ giữa các model, xóa mềm và cách tạo dữ liệu mẫu bằng factory, seeder.

Bảng tóm tắt các phương thức Eloquent Model trong Laravel: lấy danh sách, tìm, thêm, cập nhật, xóa, quan hệ
Các phương thức Eloquent hay dùng khi làm việc với Model

Model nằm ở đâu trong mô hình MVC?

Trong một request điển hình, route chuyển request tới controller, controller gọi model để lấy hoặc lưu dữ liệu, rồi truyền dữ liệu sang view để hiển thị. Model chỉ lo phần dữ liệu và quy tắc nghiệp vụ gắn với dữ liệu (quan hệ, ép kiểu, phạm vi truy vấn), không xử lý request hay HTML. Nếu chưa quen với cách chia này, hãy đọc trước bài mô hình MVC là gì.

Tạo model và quy ước đặt tên

php artisan make:model Post
# Tạo kèm migration, factory, seeder và controller
php artisan make:model Post -mfsc

Eloquent dựa vào quy ước để bớt cấu hình:

  • Tên bảng là dạng số nhiều, snake_case của tên class: Post → posts, OrderItem → order_items.
  • Khóa chính là cột id, kiểu số nguyên tự tăng.
  • Timestamps: model tự ghi created_at và updated_at khi thêm hoặc sửa.

Chỉ khi bảng không theo quy ước mới cần khai báo lại. Laravel 13 cho phép dùng attribute PHP; các phiên bản trước dùng thuộc tính class, cách này vẫn chạy ở Laravel 13:

// Laravel 13: attribute
use Illuminate\Database\Eloquent\Attributes\Table;
use Illuminate\Database\Eloquent\Model;
#[Table('tin_tuc')]
class Post extends Model
{
}
// Laravel 12 trở về trước (vẫn dùng được ở 13)
class Post extends Model
{
    protected $table = 'tin_tuc';
}

Khai báo model: fillable, casts, scope

Một model bài viết đầy đủ thường trông như sau:

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
#[Fillable(['title', 'slug', 'body', 'published_at'])]
class Post extends Model
{
    use HasFactory, SoftDeletes;
    protected function casts(): array
    {
        return [
            'published_at' => 'datetime',
        ];
    }
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
    #[Scope]
    protected function published(Builder $query): void
    {
        $query->whereNotNull('published_at');
    }
}
  • Fillable liệt kê các cột được phép gán hàng loạt qua create(), update(), fill(). Đây là lớp bảo vệ trước lỗ hổng mass assignment: nếu không giới hạn, người dùng có thể gửi thêm trường is_admin=1 trong form và tự nâng quyền. Ở Laravel 12 trở về trước, khai báo bằng protected $fillable = ['title', 'slug', 'body', 'published_at'];.
  • casts() ép kiểu khi đọc ra: chuỗi ngày thành đối tượng Carbon, JSON thành mảng, 'password' => 'hashed' tự băm mật khẩu.
  • Scope gom điều kiện truy vấn dùng lại nhiều lần. Gọi bằng Post::published()->get().

Thao tác dữ liệu với Eloquent

Đọc dữ liệu

use App\Models\Post;
$posts = Post::all();                                 // mọi bản ghi (cẩn thận với bảng lớn)
$posts = Post::published()->latest()->take(10)->get(); // 10 bài mới nhất đã đăng
$post = Post::find(1);                       // null nếu không có
$post = Post::findOrFail(1);                 // tự trả 404 nếu không có
$post = Post::where('slug', 'hoc-laravel')->first();
$count = Post::published()->count();

Kết quả nhiều bản ghi là một Collection, có sẵn các phương thức như map, filter, pluck; xem bài Laravel Collection. Với danh sách dài, dùng paginate() thay cho get() như hướng dẫn trong bài phân trang trong Laravel.

Thêm bản ghi: create() và save()

// Cách 1: gán từng thuộc tính rồi save()
$post = new Post;
$post->title = 'Học Laravel từ đầu';
$post->slug = 'hoc-laravel-tu-dau';
$post->body = 'Nội dung...';
$post->save();
// Cách 2: create() — cần khai báo Fillable
$post = Post::create([
    'title' => 'Học Laravel từ đầu',
    'slug' => 'hoc-laravel-tu-dau',
    'body' => 'Nội dung...',
]);

Cập nhật bản ghi: update() và save()

$post = Post::findOrFail(1);
$post->title = 'Tiêu đề mới';
$post->save();
// hoặc
$post->update(['title' => 'Tiêu đề mới']);
// Cập nhật nếu có, tạo mới nếu chưa có
Post::updateOrCreate(
    ['slug' => 'hoc-laravel-tu-dau'],
    ['title' => 'Học Laravel từ đầu (cập nhật)'],
);

save() dùng được cho cả thêm mới và cập nhật: model mới tạo thì INSERT, model lấy từ CSDL thì UPDATE. Dữ liệu từ form nên được kiểm tra trước khi đưa vào create() hoặc update(), xem bài Laravel validation.

Xóa và xóa mềm

$post = Post::findOrFail(1);
$post->delete();
Post::destroy([1, 2, 3]);        // xóa theo khóa chính
// Khi model dùng SoftDeletes
$post->delete();                 // chỉ ghi thời điểm vào deleted_at
Post::withTrashed()->find(1);    // lấy cả bản ghi đã xóa mềm
$post->restore();                // khôi phục
$post->forceDelete();            // xóa hẳn

Với SoftDeletes, bảng cần cột deleted_at (thêm bằng $table->softDeletes() trong migration). Các truy vấn thông thường tự loại bản ghi đã xóa mềm.

Quan hệ giữa các model

Một người dùng có nhiều bài viết (hasMany), mỗi bài thuộc về một người (belongsTo):

// app/Models/User.php
use Illuminate\Database\Eloquent\Relations\HasMany;
public function posts(): HasMany
{
    return $this->hasMany(Post::class);
}
$user = User::findOrFail(1);
$all = $user->posts;                                        // thuộc tính: Collection đã nạp
$latest = $user->posts()->latest()->take(10)->get();        // phương thức: query builder, thêm điều kiện được
// Tránh lỗi N+1 khi hiển thị danh sách kèm tác giả
$posts = Post::with('user')->latest()->paginate(20);

Phân biệt $user->posts (thuộc tính, trả Collection) và $user->posts() (phương thức, trả query builder). Viết $user->posts->take(10)->get() là sai vì Collection không có get() kiểu truy vấn; cần dùng $user->posts()->take(10)->get().

Migration, factory và seeder cho model

Migration tạo bảng cho model:

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('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('title');
            $table->string('slug')->unique();
            $table->text('body');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Factory sinh dữ liệu mẫu, seeder gọi factory để nạp dữ liệu. Helper cũ factory(App\User::class, 10) và namespace App\User đã bị bỏ từ Laravel 8; cú pháp hiện tại là gọi factory() trên chính model (model cần trait HasFactory):

// database/factories/PostFactory.php
public function definition(): array
{
    return [
        'user_id' => User::factory(),
        'title' => fake()->sentence(),
        'slug' => fake()->unique()->slug(),
        'body' => fake()->paragraphs(3, true),
        'published_at' => now(),
    ];
}
// database/seeders/DatabaseSeeder.php
public function run(): void
{
    User::factory()
        ->count(5)
        ->has(Post::factory()->count(3))
        ->create();
}
php artisan migrate
php artisan db:seed
php artisan migrate:fresh --seed   # xóa sạch, tạo lại bảng và nạp dữ liệu (chỉ dùng ở môi trường dev)

Vị trí các thư mục database/migrations, database/factories, database/seeders được giải thích trong bài cấu trúc thư mục Laravel.

Kinh nghiệm từ Hozitech

Khi phát triển website bằng Laravel, Hozitech giữ model "mỏng nhưng đủ": khai báo Fillable, casts, quan hệ và scope dùng chung trong model; logic xử lý nhiều bước (tạo đơn hàng, gửi email) đặt ở lớp riêng thay vì nhồi vào controller hay model. Hai lỗi hay gặp nhất khi tiếp nhận mã nguồn cũ là mở toàn bộ mass assignment ($guarded = []) và truy vấn N+1 trong trang danh sách, chỉ cần thêm with() là số truy vấn giảm rõ rệt.

Nếu doanh nghiệp cần một website quản trị nội dung được xây dựng bài bản trên Laravel, có thể tham khảo dịch vụ thiết kế website của Hozitech.

Tan Hien

Về tác giả

Founder

Xin chào mình là Hiền hiện đang là Founder của Hozitech. Mình là kỹ sư phần mềm có hơn 9+ năm kinh nghiệm trong lĩnh vực thiết kế website, ứng dụng di động... Gần đây mình đang nghiên cứu thêm về lĩnh vực marketing. Mình rất vui khi được chia sẽ những kiến thức và kinh nghiệm của mình đến mọi người!

Câu hỏi thường gặp

Câu hỏi thường gặp

Sẵn sàng bắt đầu dự án của bạn?

Hãy để chúng tôi giúp bạn biến ý tưởng thành hiện thực. Đăng ký tư vấn miễn phí ngay hôm nay để nhận được giải pháp công nghệ tối ưu cho doanh nghiệp.