larablog 建構日誌:專案資料表

larablog 建構日誌:專案資料表

larablog 專案使用的資料表

larablog 專案使用資料表及其關係圖

  上圖是專案使用的資料表項目以及關係示意圖,以下是各資料表簡介:

  • categories:分類資料表,儲存文章分類,透過「parent_id」項目指定上層分類。 關係設定:與 posts 資料表為一對多。
  • posts:文章資料表,儲存網站文章資料,透過「state(發佈狀態)」及「featured(是否為精選)」做為文章顯示開關。 關係設定:「category_id」為從屬於 categories 資料表關聯外鍵;「user_id」為從屬於 users 資料表關聯外鍵;與 tags 資料表透過 post_tag 資料表中介實現多對多關聯。
  • tags:標籤資料表,儲存文章會標記的標籤項目。 關係設定:與 posts 資料表透過 post_tag 資料表中介實現多對多關聯。
  • post_tag:作為文章(posts)與標籤(tags)的中介資料表,命名方式依照 Laravel 的命名慣例為:單數,英文字母排列。
  • comments:回應資料表,儲存網站訪客對文章的回應。 關係設定:「post_id」為從屬於 posts 資料表關聯外鍵。
  • users:會員資料表,儲存網站會員資料。(以本專案的情況僅儲存管理者) 關係設定:與 posts 資料表為一對多,與 user_social_profiles 的關係為一對一。
  • user_social_profiles:會員社群網站資料表,儲存文章詳細頁面中作者簡介、社群網站頁面資料。 關係設定:「user_id」為從屬於 users 資料表關聯外鍵。

建立 Model 的同時也建立 Migration

在建立 Model 的時候加上「-m」參數,會一併建立資料表的 Migration 檔案,以 Category 來說會是:

php artisan make:model Category -m

  在建立 Model 時同時產生的 Migration 檔案,在資料表名稱部分會以複數命名。學過英文的人就知道字尾為「y」的名詞,其複數會是「去 y 加上 ies」,所以在新增「Category」這個 Model 時加上「-m」參數,那麼連帶產生的 Migration 檔案所要建立的資料表名稱會是「categories」而不是「categorys」,這會影響稍後的外鍵設定。

中介資料表

  作為「posts」與「tags」中介的「post_tag」資料表,在命名規則部分與上述資料表不一樣:按哥布林老師的建議,作為多對多關係的中介資料表,資料庫名稱應為「單數,按英文字母順序排列」,建立 Migration 的指令如下:

php artisan make:migration create_post_tag_table

  在 Migration 檔案中需定義對應「posts」與「tags」資料表的外鍵,在命名上的建議是「post_id」與「tag_id」。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostTagTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('post_tag', function (Blueprint $table) {
            $table->id();
            $table->foreignId('post_id')->constrained();
            $table->foreignId('tag_id')->constrained();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('post_tag', function (Blueprint $table){
            $table->dropForeign(['post_id']);
            $table->dropForeign(['tag_id']);
            });
        Schema::dropIfExists('post_tag');
    }
}

  與會員有關的 Model 及相關的 Migration 檔案,在建立專案時就已經建立,之後再到 User Model 撰寫資料庫關聯內容。

資料表間有設定關聯時需注意建立順序

  從資料表關係圖可以看出「posts」的「category_id」欄位是關聯「categories」資料表「id」欄位的外鍵,在 Migration 檔案的建立順序上應為:categories 先,posts 後。如果沒有按照前述順序在執行 Migration 會發生錯誤,此時可透過更改 Migration 檔名的時間流水號解決。

以新方法設定資料表關聯,和注意事項

Laravel 透過 Migration 檔案進行資料表管理,以下是 posts 資料表的 Migration 檔案內容:

<?php
// database/migrations/2021_09_08_151439_create_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->bigInteger('category_id')->unsigned();
            $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
            $table->string('title');
            $table->string('slug')->unique();
            $table->string('cover_image');
            $table->string('introtext');
            $table->text('content');
            $table->bigInteger('user_id')->unsigned();
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->integer('sort')->default(0);
            $table->enum('status',['pending', 'published', 'unpublished']);
            $table->enum('featured',['yes','no']);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('post', function (Blueprint $table){
            $table->dropForeign(['category_id']);
            $table->dropForeign(['user_id']);
        });
        Schema::dropIfExists('posts');
    }
}

  在 Migration 檔案中我定義了建立資料表的「up」方法,以及滾回(rollback)時刪除資料表的「down」方法。

  在「up」方法中可以看到「category_id」欄位型態是「unsigned 的 bigInteger」,然後指定此欄位作為外鍵:與「categories」的「id」欄位做關聯,以同樣的格式定義「user_id」欄位。

  如果建立資料表時有按照 Laravel 的建議格式,那麼可以透過「constrained()」簡化敘述,也就是說你可以將

$table->bigInteger('category_id')->unsigned();
$table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');

  更改為

 $table->foreignId('category_id')->constrained(); //外鍵設定新寫法

  這樣 Laravel 會認為「category_id」欄位是做為與「categories」資料表的「id」欄位關聯的外鍵欄位,是不是比較簡單呢?

不過用新式寫法建立外鍵關聯要注意:Laravel 透過欄位名稱去推測外鍵關聯的資料庫及其欄位,如果你的資料表或(及)關聯欄位命名方式不是按照建議規則,那麼還是按照舊式規則。

  在 Migration 檔案定義好要建立的資料表欄位及外鍵設定後透過以下指令執行:

php artisan migrate

在 Model 撰寫資料表關聯

  在 Laravel 8,Model 檔案會放在 /app/Models 資料夾,完成資料表建立作業後接著編輯 Model 檔案,加上編輯欄位與關聯設定。

/app/Models/Category.php(文章分類)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    use HasFactory;

    protected $table = 'categories';
    protected $fillable = [
        'parent_id',
        'name',
        'description',
        'slug',
        'status',
        'featured',

    ];

    public function parentCategory(){
        return $this->belongsTo('App\Models\Category');
    }

    public function posts(){
        return $this->hasMany('App\Models\Post');
    }
}

  在 Category 類別中我定義了存取的資料表名稱以及填入資料的欄位項目,接著定義「parentCategory」方法定義上一層分類;「posts」方法則定義與「posts」資料表的一對多(hasMany)關係。

/app/Models/Post.php(文章)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use TCG\Voyager\Traits\Translatable;

class Post extends Model
{
    use HasFactory, Translatable;

    protected $table = 'posts';
    protected $fillable = [
        'category_id',
        'title',
        'slug',
        'cover_image',
        'introtext',
        'content',
        'user_id',
        'sort',
        'status',
        'featured',
    ];

    public function user(){
        return $this->belongsTo('App\Models\User');
    }

    public function category(){
        return $this->belongsTo('App\Models\Category');
    }

    public function tags(){
        return $this->belongsToMany('App\Models\Tag')->withTimestamps();
    }

    public function comments(){
        return $this->hasMany('App\Models\Comment');
    }
}

  我在「user」方法定義從屬(belongsTo)於「users」的一對多反向關係:一個使用者會有多篇文章,「category」方法的情況也一樣。

  「tags」方法用來定義與「tags」資料表的多對多(belongsToMany)關係,加上「withTimestamps()」以在中介資料表新增資料時也填上建立時間。「comments」方法則定義與「comments」資料表的一對多(hasMany)關係:一篇文章會有多篇訪客回應。

/app/Models/Tag.php(標籤)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Cviebrock\EloquentSluggable\Sluggable;

class Tag extends Model
{
    use HasFactory;

    protected $table = 'tags';
    protected $fillable = [
        'name',
        'slug',
        'status',
        'featured',
    ];

    public function posts(){
        return $this->belongsToMany('App\Models\Post');
    }
}

  與「posts」 資料表的多對多(belongsToMany)關係寫在「posts」方法:一篇文章會有多個標籤,而一個標籤可能會被多個文章標記。

/app/Models/Comment.php(訪客回應)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    use HasFactory;

    protected $table = 'comments';
    protected $fillable = [
        'post_id',
        'name',
        'email',
        'website',
        'comment',
        'admin_reply',
    ];

    public function posts(){
        return $this->belongsTo('App\Models\Post')->withTimestamps();
    }
}

  「posts」方法定義與「posts」資料表的反向多對一關係。

/app/Models/User.php(會員)

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends \TCG\Voyager\Models\User
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var string[]
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function posts(){
        return $this->hasMany('App\Models\Post');
    }

    public function user_social_profile(){
        return $this->hasOne('App\Models\UserSocialProfile');
    }

}

  「posts」方法定義與「posts」資料表一對多關係;「user_social_profile」方法定義與「user_social_profiles」資料表的一對一(hasOne)關係:一個會員有一組社群網站資料

/app/Models/UserSocialProfile.php(會員社群網站資料)

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class UserSocialProfile extends Model
{
    use HasFactory;

    protected $table = 'user_social_profiles';
    protected $fillable = [
        'user_id',
        'avatar',
        'profile',
        'facebook',
        'instagram',
        'line',
        'twitter',
        'github',
    ];

    public function user(){
        return $this->belongsTo('App\Models\User');
    }
}

「user」方法定義與「users」資料表的反向一對一關係。

結語

  透過設定資料表間的關聯可以避免資料的重複性、減少輸入錯誤,連帶的還有批次變更的效果。在 Model 檔案中建立關係方法後可透過 Eloquent 語法存取關聯資料表間的資料欄位,讓專案的規模可以做得更大。

  瞭解並活用資料表關聯是我在這個專案中想實現的目標,在撰寫的當下其實還不夠熟練,要向哥布林老師多多請教,也請觀看此文章的朋友多多關注哥布林老師的 Laravel 百萬年薪訓練營 以及 Laravel Care 計畫 喔。

文章作者:A-Bo Lee
作者大頭照

居住在臺灣的 Joomler,期望以程式設計、開放原碼推廣活動收入養活一家老小。
35 歲後改姓李,id 作為曾為郭姓的證明。
FFXI:Abokuo@Sylph鯖、よろしくです。

看完文章有什麼想法嗎?利用下面表單告訴作者吧

請先閱讀
服務條款隱私權政策,送出回應意即同意前述文件。標記 * 欄位請務必填寫,電子郵件信箱僅作驗證使用,不會顯示在回應中。