Project

General

Profile

Actions

文件 #3

open
JH

模組 2:資料庫與 Eloquent ORM(6–8 小時)

文件 #3: 模組 2:資料庫與 Eloquent ORM(6–8 小時)

Added by Jeffery Hsu about 6 hours ago. Updated about 6 hours ago.

Status:
已解決
Priority:
正常
Assignee:
-
Start date:
04/26/2026
Due date:
% Done:

0%

Estimated time:

Description

模組 2:資料庫與 Eloquent ORM(6–8 小時)

Migration、Seeder、Factory 進階使用。
Eloquent Model(Mass Assignment、Relationships、Scopes)。
Laravel 13 新增:PHP Attributes 取代傳統屬性($table、$fillable、$hidden 等)。
Query Builder 進階(JSON:API Resources 介紹)。
Pagination、Soft Deletes、Eloquent Events。
實作:建立多個 Model 與關聯(例如 User、Post、Category、Team)。

JH Updated by Jeffery Hsu about 6 hours ago Actions #1

進入模組 2!資料庫與 Eloquent ORM 是 Laravel 最強大且最優美的核心之一。在這個階段,我們將實作你提到的 UserPostCategoryTeam,並套用 Laravel 13 最新的 PHP Attributes 特性來取代傳統寫法。

為了讓你更好理解,我們先在腦海中(以及實際架構上)建立這些資料表的關聯:

  • Team (團隊):一個 Team 可以有多個 User (一對多)。
  • User (使用者):屬於一個 Team,並且可以發表多篇 Post (一對多)。
  • Category (分類):一個 Category 包含多篇 Post (一對多)。
  • Post (文章):屬於一個 User,也屬於一個 Category。

以下是逐步教學與驗證步驟:


第一步:建立 Migration、Factory 與 Model (包含 Soft Deletes)

我們可以使用 Artisan 指令一次性建立 Model、Migration、Factory 與 Seeder。-mfs 參數代表 (Migration, Factory, Seeder)。

php artisan make:model Team -mfs
php artisan make:model Category -mfs
php artisan make:model Post -mfs

(註:User Model 與 Migration 通常在 Laravel 預設安裝時就已存在,我們只需稍作修改。)

1. 撰寫 Migrations (定義資料表結構)
打開 database/migrations/ 下的新檔案,設定欄位。我們在這裡加入 Soft Deletes (軟刪除) 功能。

  • Teams Table:
    Schema::create('teams', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->timestamps();
    });
    
  • Users Table (修改預設):
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->foreignId('team_id')->nullable()->constrained(); // 關聯 Team
        $table->string('name');
        $table->string('email')->unique();
        $table->string('password');
        $table->timestamps();
    });
    
  • Categories Table:
    Schema::create('categories', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->timestamps();
    });
    
  • Posts Table:
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->foreignId('category_id')->constrained();
        $table->string('title');
        $table->text('content');
        $table->softDeletes(); // 啟用軟刪除
        $table->timestamps();
    });
    

執行 Migration:

php artisan migrate:fresh

第二步:Laravel 13 新特性 - PHP Attributes 與 Eloquent 關聯

Laravel 13 引入了原生的 PHP Attributes 來取代傳統的 $table, $fillable, $hidden 等陣列屬性。這讓程式碼更乾淨,且 IDE 支援度更好(Mass Assignment 防護依然有效)。

1. Team Model

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Attributes\Fillable;

#[Fillable(['name'])] // Laravel 13 新寫法:定義可批量賦值的欄位
class Team extends Model
{
    public function users(): HasMany
    {
        return $this->hasMany(User::class);
    }
}

2. User Model

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;

#[Fillable(['name', 'email', 'password', 'team_id'])]
#[Hidden(['password', 'remember_token'])] // Laravel 13 新寫法:隱藏欄位
class User extends Authenticatable
{
    public function team(): BelongsTo
    {
        return $this->belongsTo(Team::class);
    }

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

3. Post Model (包含 Scopes 與 Events)
我們在這裡加入 Local Scope (區域查詢作用域)Eloquent Events (模型事件)

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Table;

#[Table('posts')] // 若資料表名稱不符合慣例,可用此 Attribute 指定
#[Fillable(['title', 'content', 'user_id', 'category_id'])]
class Post extends Model
{
    use SoftDeletes; // 啟用軟刪除

    // 關聯
    public function user() { return $this->belongsTo(User::class); }
    public function category() { return $this->belongsTo(Category::class); }

    // Local Scope:只查詢特定使用者的文章
    public function scopeOfUser(Builder $query, $userId): Builder
    {
        return $query->where('user_id', $userId);
    }

    // Eloquent Events:在建立文章時自動執行的邏輯
    protected static function booted(): void
    {
        static::creating(function (Post $post) {
            // 例如:如果沒有標題,給予預設值 (實務上通常用 Validation 處理,此為展示 Event)
            if (empty($post->title)) {
                $post->title = 'Untitled Post';
            }
        });
    }
}

第三步:Factory 與 Seeder 進階使用

我們要產生大量假資料來測試 Pagination (分頁) 與 Query Builder。

1. 設定 Factories
database/factories/PostFactory.php 中:

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\User;
use App\Models\Category;

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => User::factory(),
            'category_id' => Category::factory(),
            'title' => fake()->sentence(),
            'content' => fake()->paragraphs(3, true),
        ];
    }
}

2. 撰寫 DatabaseSeeder
database/seeders/DatabaseSeeder.php 組合資料:

use App\Models\Team;
use App\Models\User;
use App\Models\Category;
use App\Models\Post;

public function run(): void
{
    // 建立 2 個團隊
    $teams = Team::factory(2)->create();

    // 建立 5 個分類
    $categories = Category::factory(5)->create();

    // 建立 10 個使用者,並隨機分配團隊
    $users = User::factory(10)->recycle($teams)->create();

    // 建立 50 篇文章,重複利用上述建立的 User 和 Category,避免產生多餘資料
    Post::factory(50)->recycle($users)->recycle($categories)->create();
}

執行 Seeder:

php artisan db:seed

第四步:Query Builder 進階與 JSON:API Resources

現在資料庫有資料了,我們可以透過 API Resources 將 Eloquent 模型轉換為優雅的 JSON 格式,這是前後端分離非常重要的一環。

1. 建立 API Resource

php artisan make:resource PostResource

2. 設定 PostResource
打開 app/Http/Resources/PostResource.php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            // 處理關聯資料,避免 N+1 問題
            'author' => $this->whenLoaded('user', fn() => $this->user->name),
            'category' => $this->whenLoaded('category', fn() => $this->category->name),
            'created_at' => $this->created_at->toIso8601String(),
        ];
    }
}

3. 在 Controller 中使用 (包含 Eager Loading 與 Pagination)

use App\Models\Post;
use App\Http\Resources\PostResource;

public function index()
{
    // Query Builder 進階:Eager Loading (with) 避免 N+1 查詢問題,並使用 Pagination 分頁
    $posts = Post::with(['user', 'category'])
                 ->latest()
                 ->paginate(10); // 每頁 10 筆

    return PostResource::collection($posts);
}

第五步:驗證作法 (使用 Tinker)

要驗證以上實作是否成功,最強大的工具就是 Laravel Tinker。

在終端機執行:

php artisan tinker

在 Tinker 環境中依序貼上以下指令來驗證:

// 1. 驗證關聯與 Eager Loading (應該會看到帶有 user 和 category 的文章集合)
$post = App\Models\Post::with(['user', 'category'])->first();
$post->user->name;
$post->category->name;

// 2. 驗證 Mass Assignment (Attribute 是否生效)
$newPost = App\Models\Post::create(['title' => 'Test', 'content' => 'Data', 'user_id' => 1, 'category_id' => 1]);

// 3. 驗證 Soft Deletes (軟刪除)
$newPost->delete();
App\Models\Post::find($newPost->id); // 會回傳 null
App\Models\Post::withTrashed()->find($newPost->id); // 加上 withTrashed() 才能找回

// 4. 驗證 Scope
App\Models\Post::ofUser(1)->get(); // 只撈出 user_id 為 1 的文章

只要這些 Tinker 指令都能順利執行並回傳正確結果,就代表你的資料庫結構、Eloquent 關聯、Laravel 13 的 Attributes 新寫法,以及進階查詢功能都已經完美建置成功了!

前面我們在第五步透過 Tinker 在終端機裡驗證了資料庫的運作,但對於完整的 Web 開發來說,真正的「收尾」是將這些資料實際展示在畫面上。

因此,我們的第六步:將 Eloquent 資料庫與 Flux UI 前端介面整合 (實戰收尾)。這一步會把我們在模組 2 建立的資料,結合模組 1 的介面,做一個完整的串接。


第六步:將 Eloquent 資料庫與 Flux UI 前端介面整合

我們現在要把剛剛透過 Factory 產生出來的 50 篇文章,搭配分頁功能 (Pagination),漂亮地顯示在網頁上。

1. 設定路由 (Route)
打開 routes/web.php,加入文章列表的路由:

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

// 將 /posts 網址指派給 PostController 的 index 方法
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');

2. 完善控制器 (Controller)
打開 app/Http/Controllers/PostController.php。我們稍早有寫 API Resource 版,現在我們改寫成回傳 Blade 視圖的版本(實務上如果是純 Web 專案,回傳 View 最直接):

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        // 使用 Eager Loading 載入關聯,並使用分頁(每頁 9 筆,方便排版成 3x3 網格)
        $posts = Post::with(['user', 'category'])
                     ->latest() // 依照建立時間反序排列
                     ->paginate(9);

        // 將資料傳遞給視圖
        return view('posts.index', ['posts' => $posts]);
    }
}

3. 建立 Blade 視圖並套用 Flux UI
建立一個新檔案:resources/views/posts/index.blade.php
這裡我們將利用 Tailwind CSS 的網格系統 (Grid) 搭配 Flux UI 的卡片 (Card) 和標籤 (Badge) 元件來呈現資料:

<x-layouts.app>
    <x-slot:title>
        文章列表
    </x-slot>

    <div class="max-w-7xl mx-auto p-6">
        <div class="flex justify-between items-center mb-6">
            <flux:heading size="xl">最新文章</flux:heading>
            <flux:button variant="primary">新增文章</flux:button>
        </div>

        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            @foreach ($posts as $post)
                <flux:card class="flex flex-col h-full">
                    <div class="flex justify-between items-start mb-2">
                        <flux:badge color="blue" size="sm">{{ $post->category->name }}</flux:badge>
                        <span class="text-sm text-gray-500">{{ $post->user->name }}</span>
                    </div>
                    
                    <flux:heading size="lg" class="mb-2">{{ $post->title }}</flux:heading>
                    
                    <p class="text-gray-600 flex-grow">
                        {{ Str::limit($post->content, 100) }}
                    </p>

                    <div class="mt-4 pt-4 border-t border-gray-100 text-xs text-gray-400">
                        發布於 {{ $post->created_at->format('Y-m-d H:i') }}
                    </div>
                </flux:card>
            @endforeach
        </div>

        <div class="mt-8">
            {{ $posts->links() }}
        </div>
    </div>
</x-layouts.app>

4. 驗證作法 (實際在瀏覽器查看)

  1. 確認你的本地伺服器正在運行(如果在不同的終端機分頁,請確認 php artisan servenpm run dev 都在執行中)。
  2. 打開瀏覽器,前往:http://localhost:8000/posts

你應該會看到以下成果,這代表驗證完全成功:

  • 網頁上呈現了漂亮的卡片網格,每張卡片裡面都有文章標題、一段摘要。
  • 卡片上方有藍色的分類標籤 (Category) 以及作者名稱 (User),這證明了 Eloquent 關聯 (Relationships) 抓取資料非常成功。
  • 滑到頁面最下方,你會看到美觀的分頁按鈕 (Pagination)。點擊「下一頁」,網址會自動變成 ?page=2,並且流暢地切換文章。
  • 整個過程沒有發生任何資料庫效能卡頓,因為我們使用了 with(['user', 'category']) 解決了 N+1 查詢問題

到這裡,模組 2 的資料庫、ORM、關聯,直到畫面呈現的資料流,已經被你徹底打通了!準備好進入下一個模組了嗎?

JH Updated by Jeffery Hsu about 6 hours ago Actions #2

  • Status changed from 新建立 to 已解決

JH Updated by Jeffery Hsu about 6 hours ago Actions #3

  • Tracker changed from 功能 to 文件
Actions

Also available in: PDF Atom