Eloquent ORM - 一对一关联

Eloquent 可以在 Model 之间建立关联查询,这样可以藉由这些关联快速查询出所需的资料。

举例来说,目前我们在拉 Todo 清单的时候都是整个资料库翻出来显示,如果可以建立 User 跟 Todo 之间的一对多关联,就能够只取出属於登入 User 的 Todo 清单。

关联有许多形式,常见的有一对一,一对多,多对多,也有多角关系的关联,这边会一一介绍。

一对一关联

这段目标是建立一个使用者的设定模型,一个使用者只会有一组设定资料,所以是一对一关联。

先来建模型

sail artisan make:model UserSetting -mcr

建立 UserSetting 模型的同时一并建立 Migration ,顺便加上 Controller。

首先,资料之间要关联需要额外的栏位作为查询的依据,所以在 Migration 里加上 user_id 栏位

/database/migrations/XXXX_XX_XX_XXXXXX_create_user_settings_table.php

@@ -16,6 +16,7 @@ class CreateUserSettingsTable extends Migration
         Schema::create('user_settings', function (Blueprint $table) {
             $table->id();
             $table->timestamps();
+            $table->unsignedBigInteger('user_id');
         });
     }

user_id 的型别要对应到 user 资料的 id 栏位,因为 id() 等於是 unsignedBigInteger 所以这边就建立 unsignedBigInteger 的栏位。

hasOne 关联

Eloquent 建立 Model 间关联的方式是在 Model 类别底下加入对应的函式,然後用像是 hasOne 这类方法建立 Query Builder。

/app/Models/User.php

@@ -8,13 +8,13 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
 use Illuminate\Notifications\Notifiable;
 use Laravel\Sanctum\HasApiTokens;
 use Illuminate\Database\Eloquent\SoftDeletes;
+use App\Models\UserSetting;

@@ -45,4 +45,9 @@ class User extends Authenticatable
     protected $casts = [
         'email_verified_at' => 'datetime',
     ];
+    
+    public function setting()
+    {
+        return $this->hasOne(UserSetting::class);
+    }

setting 这个函式的名称可以任意设定。

hasOne 方法第一个参数是目标 Model 的类别名称,UserSetting::class 会以字串的形式回传该类别包含 namespace 在内的名称,所以如果不用 use 引入类别的话也可以直接用字串

/app/Models/User.php

@@ -45,4 +45,9 @@ class User extends Authenticatable
     protected $casts = [
         'email_verified_at' => 'datetime',
     ];
+    
+    public function setting()
+    {
+        return $this->hasOne('App\Models\UserSetting');
+    }

如果没有传其他参数给 hasOne 的话, Eloquent 会以预设的栏位名称进行查询。
像这边是 User hasOne UserSetting ,就会在 UserSetting 资料表中的 user_id 栏位找对应 user->id 的资料。

如果不是用预设的栏位名称,也可以自行指定

举历来说像是改成在 UserSetting 中找 user_email 对应到 user 的 email 值的资料。

/app/Models/User.php

@@ -45,4 +45,9 @@ class User extends Authenticatable
     protected $casts = [
         'email_verified_at' => 'datetime',
     ];
+    
+    public function setting()
+    {
+        return $this->hasOne('App\Models\UserSetting','user_email','email');
+    }
hasOne(<模型名称>,<目标模型的外键名称>,<模型关联键名称>)

belongsTo 关联

像上面是 "User 拥有一个 UserSetting" 的关联,关联用的键值是建立在 UserSetting 这边,如果想要从 UserSetting 逆向找出拥有的 User 是谁,用的是 belongsTo 方法

/app/Models/UserSetting.php

@@ -4,8 +4,14 @@ namespace App\Models;
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use App\Models\User;
 
 class UserSetting extends Model
 {
     use HasFactory;
+
+    public function user()
+    {
+        return $this->belongsTo(User::class);
+    }
 }

用法跟 hasOne 差不多,不过如果要自订关联栏位的话顺序会反过来变成先写自己的栏位再写目标的栏位

 class UserSetting extends Model
 {
     use HasFactory;
+
+    public function user()
+    {
+        return $this->belongsTo(User::class,'user_id','id');
+    }
 }

写入关联资料

我们希望当新申请 User 的时候同时建立 UserSetting 资料,所以稍微改一下建立新 user 的流程。

/app/Http/Controllers/Auth/RegisteredUserController.php

@@ -46,6 +46,8 @@ class RegisteredUserController extends Controller
        $user = User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password),
        ]);
 
+        $user->setting()->create();
+
         event(new Registered($user));

因为在 Model 中建立了 setting 函式,所有 User 实例都会有 setting 这个方法,当呼叫的时候就会去搜寻 UserSetting 中关联的资料并建成 UserSetting 的实例,这样就能直接从这个实例上呼叫 create 方法写入资料,跟之前写入资料的方法相同。

因为 UserSetting 目前也没定义其他栏位,只有 id 跟 timeStamps ,所以 create 不用特地带入参数。

接着重新申请一个帐号,就会发现除了 users 外 user_settings 资料表也有新的资料。

读取关联资料

有了资料之後要建立对应的方法把它读出来,在 RegisteredUserController 建立新的函式

/app/Http/Controllers/Auth/RegisteredUserController.php

@@ -67,4 +69,13 @@ class RegisteredUserController extends Controller
 
         return redirect('/');
     }
+
+    public function setting(Request $request)
+    {
+        $user = Auth::user(); 
+
+        return $user->setting;
+    }
 }

跟上一篇一样用 Auth::user() 取得登入的 User。

接着 $user->setting 就可以直接取得对应的 UserSetting 资料了。

$user->setting 跟 $user->setting() 的用法可能会搞混,比较一下。

$user->setting() 会建立 Query Builder ,所以可以串接 create , update 这类方法做资料的更新。

如果要取得资料的可以串接 first 或 get,注意两者是有差别的, first 会取出单笔资料所以回传的是单一物件,但 get 是用来取得一组资料的所以回传会是阵列,然後里面只有一个物件。

至於 $user->setting ,可以视为 $user->setting()->first(); 的简易写法,因为用的是一对一关联的 hasOne 方法,所以回传会只有一个物件。

$user->setting 的回传

$user->setting()->get() 的回传,多包了一层阵列

可以多加一个路由来看这些回传值

/routes/auth.php b/routes/auth.php

@@ -66,3 +66,5 @@ Route::post('/logout', 
+                
+Route::get('/setting', [RegisteredUserController::class,'setting'])->middleware('auth');

登入之後在浏览器输入 localhost/setting

Eager Loading

目前上面的路由是用来只取出 UserSetting 资料的,不过一般都会是在取得 User 的同时,将 UserSetting 作为 User 资料的一部分取出。以 SQL 来说就是用 Join 一并载入关联资料。

在 Eloquent 中是以 with 方法来在模型上加载关联的资料。

with 方法是 Query Builder 的一种,所以後面要再加 first() 或 get() 才能取得资料。

@@ -73,8 +73,8 @@ class RegisteredUserController extends Controller
 
     public function setting(Request $request)
     {
-        $user = Auth::user(); 
+        $user = Auth::user()->with('setting')->first(); 
 
-        return $user->setting;
+        return $user;
     }
 }

这样就能看到 setting 的资料被加入到 User 的资料中回传了

资料库关联

上面建立的关联都是 ORM 层级的,在资料库上没有建成关联,如果要加上资料库层的关联限制的话,要用 migration。

/database/migrations/XXXX_XX_XX_XXXXXX_create_user_settings_table.php

@@ -16,7 +16,9 @@ class CreateUserSettingsTable extends Migration
         Schema::create('user_settings', function (Blueprint $table) {
             $table->id();
             $table->timestamps(); 
             $table->unsignedBigInteger('user_id');
+            
+            $table->foreign('user_id')->references('id')->on('users');
         });
     }

Eloquent ORM 的关联跟资料库的关联并没有直接的关系,只是一种方便的查询手段。所以就算资料库层没有建立关联,还是可以用 Model 的关联查资料,只是资料表要有对应的栏位。


<<:  Day 16 - Asynchronous 非同步进化顺序 - Async/Await

>>:  Day16 中断 Lua 的执行 - coroutine

Day24:检查登入人数

国庆连假中,假日只想耍废玩 game,不想进修QQ,但为了避免断赛,还是加减推一些东西,等明後天再来...

Day 10 - Web Storage API

LocalStorage 用来储存网页里要用来呈现在画面上的数据资讯 可存取时间:刷新或重开页面时都...

DAY20-EXCEL统计分析:单因子变异数分析实例

今天有一家饮料店想比较不同县市的分店销售量,想确认不同县市每天的销售量是否有明显的差异,於是随机抽选...

9.unity物件侦测(碰撞Collider2D)

碰撞器2D (参阅Collider 2D) 碰撞器可以让物体碰撞停下、设定障碍物;也可以达成捡金币、...

[前端暴龙机,Vue2.x 进化 Vue3 ] Day28.Vue3 小补充 Magic ~

下面来介绍一下,Vue 3 的一些小小魔法(个人觉得很 Magic ~ 哈哈), 有些是补充说明,...