文件 #3
open模組 2:資料庫與 Eloquent ORM(6–8 小時)
0%
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 5 hours ago
進入模組 2!資料庫與 Eloquent ORM 是 Laravel 最強大且最優美的核心之一。在這個階段,我們將實作你提到的 User、Post、Category 與 Team,並套用 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. 驗證作法 (實際在瀏覽器查看)
- 確認你的本地伺服器正在運行(如果在不同的終端機分頁,請確認
php artisan serve和npm run dev都在執行中)。 - 打開瀏覽器,前往:
http://localhost:8000/posts
你應該會看到以下成果,這代表驗證完全成功:
- 網頁上呈現了漂亮的卡片網格,每張卡片裡面都有文章標題、一段摘要。
- 卡片上方有藍色的分類標籤 (Category) 以及作者名稱 (User),這證明了 Eloquent 關聯 (Relationships) 抓取資料非常成功。
- 滑到頁面最下方,你會看到美觀的分頁按鈕 (Pagination)。點擊「下一頁」,網址會自動變成
?page=2,並且流暢地切換文章。 - 整個過程沒有發生任何資料庫效能卡頓,因為我們使用了
with(['user', 'category'])解決了 N+1 查詢問題。
到這裡,模組 2 的資料庫、ORM、關聯,直到畫面呈現的資料流,已經被你徹底打通了!準備好進入下一個模組了嗎?
JH Updated by Jeffery Hsu about 5 hours ago
- Status changed from 新建立 to 已解決
JH Updated by Jeffery Hsu about 4 hours ago
- Tracker changed from 功能 to 文件