Thinkphp 第八章:模型關聯

模型的關聯操作是模型的最為強大,也是最為復雜的部分,通過模型關聯操作把數據表的關聯關系對象化,解決了大部分常用的關聯場景,封裝的關聯操作比起常規的數據庫聯表操作更加智能和高效,并且直觀,所以關聯也可以說是模型的一個殺手锏,一旦使用了就會越來越喜歡,本章學習的內容包括:

要掌握關聯,最關鍵是要掌握如何定義關聯(包括明確模型之間的關聯關系)以及如何進行關聯查詢,其它的關聯寫入操作基本了解即可,因為你可以選擇采用其它的替代方案完成區別并不大(對于多對多關聯,關聯寫入的優勢才能體現出來),也充分說明了關聯的優勢主要在查詢_

定義關聯

定義關聯最主要是要搞清楚模型之間的關聯關系是什么,然后才能“對癥下藥”調用相關的關聯方法。

我們先舉個簡單的例子來了解下關聯關系的概念,例如有一個多用戶博客系統,這個系統可能包括下面的一些數據表(當然實際上可能遠遠不止這些表,只是用來說明一些典型問題和僅供參考):城市表(city)、用戶表(user)、博客表(blog ,只記錄博客基礎信息)、內容表(content ,記錄博客的具體內容和擴展信息)、分類表(cate)、評論表(comment)、角色表(role)和用戶-角色表(auth)。

關聯關系通常有一個參照模型,這個參照模型我們一般稱為主模型(或者當前模型),關聯關系對應的模型就是關聯模型,關聯關系是指定義在主模型中的關聯,有些關聯關系還會設計到一個中間表的概念,但中間表不一定需要存在具體的模型。

主模型和關聯模型之間通常是通過某個外鍵進行關聯,而這個外鍵的命名系統會有一個約定規則,通常是主模型名稱+_id,盡量遵循這個約定會給關聯定義帶來很大簡化。

假設我們已經給這些數據表創建了各自的模型,這些模型之間存在一定的關聯關系,我們來分析下(注意關聯關系是相對某個參照模型的):

  • 博客和內容是一對一的,屬于hasOne關聯(以博客模型為參照),一般content表會有一個blog_id字段;
  • 反過來內容和博客之間就屬于belongsTo關聯(以內容模型為參照);
  • 博客一定屬于某個分類(這里設計為單個分類),就是belongsTo關聯(以博客模型為參照),一般blog表會有一個cate_id字段;
  • 而每個分類下面有多個博客,因此屬于hasMany關聯(以分類模型為參照);
  • 每個用戶會發布多個博客,所以用戶和博客之間屬于hasMany關聯(以用戶模型為參照),一般blog表會有一個user_id字段;
  • 每個博客會有多個評論,所以博客和評論之間屬于hasMany關聯(以博客模型為參照);
  • 每個用戶可以有多個角色,而每個角色也會有多個用戶,因此用戶和角色屬于belongsToMany關聯(多對多關聯無論以哪個模型為參照關聯不變),用戶和角色之間的中間表就是用戶權限表,這個中間表通常會設計user_idrole_id字段;
  • 每個城市有多個用戶,而每個用戶有多個博客,城市和博客之間并無直接關系,而是通過中間模型產生關聯,城市和博客之間就屬于hasManyThrough關聯(遠程一對多,以城市模型為參照),中間模型就是用戶;
  • 如果針對某個用戶和某個博客都能發表評論,那么用戶、博客和評論之間就形成了一種多態一對多的關聯關系,也就是說用戶會有多個評論(morphMany關聯,以用戶模型為參照),博客會有多個評論(morphMany關聯,以博客模型為參照),但評論表只有一個,評論表對于博客和用戶來說,不需要定義兩個關聯關系,而只需要定義一個morphTo關聯(以評論模型為參照)即可,評論表的設計就會被改造以滿足多態的設計,普遍的設計是會增加一個多態類型的字段來標識屬于某個類型(這里就是用戶或者博客類型);

大概了解了關聯關系的概念后,我們來看下關聯的表現方式是怎樣的。從面向對象的角度來看關聯的話,模型的關聯其實應該是模型的某個屬性,比如用戶的檔案關聯,就應該是下面的情況:

// 用戶的檔案
$user->profile;
// 用戶的檔案屬性中的手機資料
$user->profile->mobile;

$user本身是一個User模型的對象實例,而$user->profile則是一個Profile模型的對象實例,所以具備模型的所有特性而不是一個數組,包括進行Profile模型的CURD操作和業務邏輯執行,$user->profile->mobile則表示獲取Profile模型對象實例的mobile數據,包括下面的操作也是有效的。

// 對查詢出來的關聯模型進行數據更新
$user->profile->email = 'thinkphp@qq.com'
$user->profile->save();

這種關聯關系使用Db類是無法完成的,所以這個使命是由模型來完成的,模型的關聯用法很好的解決了關聯的對象化,支持大部分的關聯場景和需求。

為了更方便和靈活的定義模型的關聯關系,框架選擇了方法定義而不是屬性定義的方式,每個關聯屬性其實是對應了一個模型的關聯方法,這個關聯屬性和模型的數據一樣是動態的,并非模型類的實際屬性,下面我們會來解釋下原理。

例如上面的關聯屬性就是在User模型類中定義了一個profile方法:

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    public function profile()
    {
        return $this->hasOne('Profile');
    }
}

當我們訪問User模型對象實例的profile屬性的時候,其實就是調用了profile方法來完成關聯查詢。我們知道當獲取一個模型的屬性的時候會觸發模型的獲取器,而當獲取器在沒有檢測到模型有對應屬性的時候就會檢查是否存在關聯方法定義(對于關聯方法的判斷很簡單,關聯方法返回的是一個think\model\Relation對象),如果存在則調用對應關聯類的getRelation方法。

我們知道模型的方法名都是駝峰命名的,所以系統做了一個兼容處理,當我們定義了一個userProfile的關聯方法的時候,在獲取關聯屬性的時候,下面兩種方式都是有效的:

$user->userProfile;
$user->user_profile;

我們推薦關聯屬性統一使用后者,和數據表的字段命名規范一致,因此在很多時候系統自動獲取關聯屬性的時候采用的也是后者。

有興趣的可以去了解下Model類中getAttr方法的源碼,看看關聯屬性獲取的具體代碼實現。

看起來很普通的一個方法賦予了模型神奇的關聯特性,一個小小的hasOne方法背后是強大而復雜的關聯實現邏輯(后面會慢慢給你描述),ThinkPHP所說的讓開發更簡單就是因為有眾多這些簡單而又神奇的特性。

關聯方法的定義最關鍵是要搞清楚具體應該使用何種關聯關系,其次是掌握不同的關聯關系的定義方法和參數。

可以簡單的理解為關聯定義就是在模型類中添加一個方法(該方法注意不要和模型的對象屬性以及其它業務邏輯方法沖突),一般情況下無需任何參數,并在方法中指定一種關聯關系,比如上面的hasOne關聯關系(關聯的玄妙和復雜就在這個關聯方法的定義),5.0版本支持的關聯關系包括下面七種,后面會給大家陸續介紹:

模型方法 關聯類型
hasOne 一對一HAS ONE
belongsTo 一對一BELONGS TO
hasMany 一對多 HAS MANY
hasManyThrough 遠程一對多 HAS MANY THROUTH
belongsToMany 多對多 BELONGS TO MANY
morphMany 多態一對多 MORPH MANY
morphTo 多態 MORPH TO

關聯方法的第一個參數就是要關聯的模型名稱,也就是說當前模型的關聯模型必須也是已經定義的一個模型。

一般不需要使用命名空間,會自動使用當前模型的命名空間,如果不同請使用完整命名空間定義,例如:

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    public function profile()
    {
        // Profile模型和當前模型的命名空間不一致
        return $this->hasOne('app\model\Profile');
    }
}

兩個模型之間因為參照模型的不同就會產生相對的但不一定相同的關聯關系,并且相對的關聯關系只有在需要調用的時候才需要定義,下面是每個關聯類型的相對關聯關系對照:

類型 關聯關系 相對的關聯關系
一對一 hasOne belongsTo
一對多 hasMany belongsTo
多對多 belongsToMany belongsToMany
遠程一對多 hasManyThrough 不支持
多態一對多 morphMany morphTo

除此之外,關聯定義的幾個要點必須了解:

  • 關聯方法必須使用駝峰法命名;
  • 關聯方法一般無需定義任何參數;
  • 關聯調用的時候駝峰法和小寫+下劃線都支持;
  • 關聯字段設計盡可能按照規范可以簡化關聯定義;
  • 關聯方法定義可以添加額外查詢條件;

關聯方法定義參數說明:

下面先對七種關聯關系的定義方法及參數給出一個大致的說明。

hasOne關聯

用法:hasOne('關聯模型','外鍵','主鍵');

除了關聯模型外,其它參數都是可選。

  • 關聯模型(必須):模型名或者模型類名
  • 外鍵:默認的外鍵規則是當前模型名(不含命名空間,下同)+_id ,例如user_id
  • 主鍵:當前模型主鍵,一般會自動獲取也可以指定傳入

belongsTo關聯

用法:belongsTo('關聯模型','外鍵','關聯表主鍵');

除了關聯模型外,其它參數都是可選。

  • 關聯模型(必須):模型名或者模型類名
  • 外鍵:當前模型外鍵,默認的外鍵名規則是關聯模型名+_id
  • 關聯主鍵:關聯模型主鍵,一般會自動獲取也可以指定傳入

hasMany關聯

用法:hasMany('關聯模型','外鍵','主鍵');

除了關聯模型外,其它參數都是可選。

  • 關聯模型(必須):模型名或者模型類名
  • 外鍵:關聯模型外鍵,默認的外鍵名規則是當前模型名+_id
  • 主鍵:當前模型主鍵,一般會自動獲取也可以指定傳入

hasManyThrough

用法:hasManyThrough('關聯模型','中間模型','外鍵','中間表關聯鍵','主鍵');

  • 關聯模型(必須):模型名或者模型類名
  • 中間模型(必須):模型名或者模型類名
  • 外鍵:默認的外鍵名規則是當前模型名+_id
  • 中間表關聯鍵:默認的中間表關聯鍵名的規則是中間模型名+_id
  • 主鍵:當前模型主鍵,一般會自動獲取也可以指定傳入

belongsToMany關聯

用法:belongsToMany('關聯模型','中間表','外鍵','關聯鍵');

  • 關聯模型(必須):模型名或者模型類名
  • 中間表:默認規則是當前模型名+_+關聯模型名 (注意,在V5.0.8版本之前需要添加表前綴)
  • 外鍵:中間表的當前模型外鍵,默認的外鍵名規則是關聯模型名+_id
  • 關聯鍵:中間表的當前模型關聯鍵名,默認規則是當前模型名+_id

morphMany關聯

用法:morphMany('關聯模型','多態字段','多態類型');

  • 關聯模型(必須):模型名或者模型類名
  • 多態字段:多態字段信息定義包含兩種方式,字符串的話表示多態字段的前綴,數組則表示實際的多態字段
  • 多態類型:默認是當前模型名

數據表的多態字段一般包含兩個字段:多態類型和多態主鍵。

如果多態字段使用字符串例如morph,那么多態類型和多態主鍵字段分別對應morph_typemorph_id,如果用數組方式定義的話,就改為['morph_type','morph_id']即可。

morphTo關聯

用法:morphTo('多態字段','多態類型別名(數組)');

  • 多態字段:定義和morphMany一致
  • 多態類型別名:用于設置特殊的多態類型(比如用數字標識的多態類型)

基礎方法

關聯操作經常會涉及到幾個重要的方法,也是關聯操作的基礎,掌握了這幾個方法對于掌握關聯(尤其是關聯查詢)有很大的幫助,包括:

方法名 作用
relation 關聯查詢
with 關聯預載入
withCount 關聯統計(V5.0.5+
load 關聯延遲預載入(V5.0.5+
together 關聯自動寫入(V5.0.5+

我們對這些方法先有個基本的了解,暫時不用深究,首先要明白的是如何使用這些方法。load方法是數據集對象的方法,together方法是模型類提供的方法,其它幾個都是Query類提供的鏈式方法,在查詢方法之前調用。

relationwith方法的主要區別在于relation是單純的關聯查詢,比如你查詢一個用戶列表,然后需要關聯查詢用戶的檔案數據,使用relation方法的話就是,我先查詢用戶列表數據,然后每個每個用戶再單純查詢檔案數據。如果用戶列表數據有10個,那么就會產生11次查詢。如果使用with方法的話,雖然最終查詢出來的關聯數據是一樣的,但由于with查詢使用的是預載入查詢,因此實際只會產生2次查詢。而load方法則更先進,先查詢出用戶列表,然后在需要關聯數據的時候使用load方法獲取關聯數據,尤其適合動態關聯的情況,最終也是兩次查詢,因此稱為延遲預載入。

由于模型關聯的對象化封裝機制的優勢,其實relation方法基本上很少被用到,而是使用關聯惰性查詢及關聯方法的自定義查詢來替代了(會在下一節給你講解)。最常用的莫過于with方法,因為最常用因此被內置到模型類的getall方法的第二個參數了,我們后面對with方法的用法說明也均適用于getall方法的第二個參數。withCount用于在不獲取關聯數據的情況下提供關聯數據的統計,在查詢一對多或者多對多關聯的時候才需要使用。load方法則適用于在數據集的延遲預載入關聯查詢(對于默認的數據集查詢類型系統提供了一個load_relation助手函數,作用是等效的)。together方法用于一對一的關聯自動寫入操作(包括新增、更新和刪除),提供了更簡單的關聯寫入機制。

雖然作用不盡相同,但這幾個方法的使用方法都是類似的,這四個方法都只有一個參數,參數類型包括字符串和數組,并且數組方式還支持索引數組以方便完成關聯的自定義查詢。

下面以relation方法為例,來說明下上述關聯方法的基本用法(我們演示的是查詢用法,至于代碼示例中的具體關聯是怎么定義的你暫時不必關注或者自行按照前面講解的關聯定義進行測試定義),其它的幾個方法用法完全一樣,就不再一一重復,后面具體涉及到的某個方法的時候可能只會采用其中一種或者個別進行講解,請悉知。

最簡單的用法是:

// 查詢用戶的Profile關聯數據
$users = $user->relation('profile')->select();
// 查詢用戶的Book關聯數據
$users = $user->relation('books')->select();

關聯查詢的方法返回的依然是包含User對象實例的數據集,relation方法設定的關聯查詢結果只是數據集中的User模型對象實例的某個關聯屬性。

relation方法傳入的字符串就是關聯定義的方法名而不是關聯模型的名稱,由于模型方法名使用的都是駝峰法規范,假設定義了一個名為userBooks的關聯方法的話,relation方法可以使用兩種方式的關聯查詢:

// 駝峰法的關聯方法定義
$users = $user->relation('userBooks')->select();
// 或者使用下面的方式等效
$users = $user->relation('user_books')->select();

第一種傳入的是實際的駝峰法關聯方法名userBooks,第二種是傳入小寫和下劃線的轉化名稱user_books,兩種關聯查詢用法都會實際定位到關聯方法名稱userBooks,所以關聯方法定義必須使用駝峰法

對于上面的關聯查詢用法,在獲取關聯查詢數據的時候,同樣可以支持兩種方式:

foreach ($users as $user) {
    dump($user->userBooks);
}

或者

foreach ($users as $user) {
    dump($user->user_books);
}

默認情況下,關聯方法獲取的是滿足關聯條件的所有數據,如果需要自定義關聯查詢條件的話,可以使用

// 使用自定義關聯查詢
$user->relation(['books' => function ($query) {
    $query->where('title', 'like', '%thinkphp%');
}])->select();

表示查詢該用戶寫的標題中包含thinkphp的書籍,閉包中不僅僅可以使用查詢條件,還可以支持其它的鏈式方法,比如對關聯數據進行排序和指定字段:

// 使用自定義關聯查詢
$user->relation(['books' => function ($query) {
    $query
        ->field('id,name,title,pub_time,user_id')
        ->order('pub_time desc')
        ->whereTime('pub_time', 'year');
}])->select();

如果使用field方法指定查詢字段,務必包含你的當前模型的主鍵以及關聯模型的關鍵鍵,否則會導致關聯查詢失敗。

關聯方法可以同時指定多個關聯,即使是不同的關聯類型,使用:

// 查詢用戶的Profile和Book關聯數據
$users = $user->relation('profile,books')->select();

下面的數組方式是等效的

// 查詢用戶的Profile和Book關聯數據
$users = $user->relation(['profile','books'])->select();

一般使用數組的話,主要需要使用閉包進行自定義關聯查詢的情況,否則用逗號分割的字符串就可以了。

together方法不支持閉包,但可以支持數組方式定義多個關聯方法

關聯查詢

在熟悉了如何定義關聯方法和關聯方法的基礎用法之后,我們來具體了解如何進行實際的關聯查詢以及細節。

通常有兩種方式進行關聯的數據獲取:關聯預查詢和關聯延遲查詢。

關聯預查詢方式就是使用上節提到的relation方法,使用

// 指定User模型的profile關聯
$user = User::relation('profile')->find(1);
// profile關聯屬性也是一個模型對象實例
dump($user->profile);

relation方法中傳入關聯(方法)名稱即可(多個可以使用逗號分割的字符串或者數組)。這種方式,無論你是否最終獲取profile屬性,都會事先進行關聯查詢,因此稱為關聯預查詢。

如果關聯數據不存在,一對一關聯返回的是null,一對多關聯的話返回的是空數組或空數據集對象。

出于性能考慮,通常我們選擇關聯延遲查詢的方式。

// 不需要指定關聯
$user = User::get(1);
// 獲取profile屬性的時候自動進行關聯查詢
dump($user->profile);

這種方式下的關聯查詢是惰性的,只有在獲取關聯屬性的時候才會實際進行關聯查詢,因此稱之為關聯延遲查詢。

關聯屬性的名稱一般就是關聯(定義)方法的名稱,但同時也支持駝峰關聯方法的小寫+下劃線轉化名稱。

關聯自定義查詢

模型的關聯方法除了會自動在關聯獲取的時候自動調用外,仍然可以作為查詢構造器的鏈式操作來對待,以完成額外的附加條件或者其它自定義查詢(一對多的關聯關系時候比較多見類似場景),例如User模型定義了一個articleshasMany關聯:

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    public function articles()
    {
        return $this->hasMany('Article');
    }
}

普通的關聯查詢獲取的是全部的關聯數據,例如:

$user = User::get(1);
$articles = $user->articles;

articles返回的類型根據Article模型的數據集返回類型設定,如果Article模型返回的數據集類型是Collection,那么關聯數據集返回的也是Collection對象。

如果需要對關聯數據進行篩選,例如需要查詢用戶發表的標題里面包含think的文章,并且按照create_time倒序排序,則可以使用下面的方式:

$user     = User::get(1);
$articles = $user->articles()
    ->where('title', 'like', '%think%')
    ->order('create_time desc')
    ->select();

調用articles()關聯方法的動作有下面幾個:

  • 相當于切換當前模型到關聯模型對象(Article);
  • 并且會自動傳入關聯條件(user_id = 1);

如果是一對多或者多對多關聯,并且希望自主條件查詢關聯數據的話請參考該方式

如果你希望改變默認的關聯查詢條件而不是在外部查詢的時候指定,可以直接在定義關聯的時候添加額外條件,例如上面的查詢條件可以寫成:

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    public function articles()
    {
        return $this->hasMany('Article')
          ->where('title', 'like', '%think%')
          ->order('create_time desc');
    }
}

關聯方法里面的查詢條件會自動作為關聯查詢的條件帶入,下面的關聯查詢出來的數據就是包含額外條件的:

$user = User::get(1);
$articles = $user->articles;

如果需要你仍然可以在外部調用的時候追加額外條件,例如下面的關聯查詢就包含了關聯方法里面定義的和額外追加的條件:

$user     = User::get(1);
$articles = $user->articles()
    ->where('name', 'thinkphp')
    ->field('id,name,title')
    ->select();

如果你擔心基礎的關聯條件定義影響你的其它查詢,你可以像下面一樣單獨定義多個關聯關系,各自獨立使用互不影響。

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    public function articles()
    {
        return $this->hasMany('Article');
    }

    public function articlesLike($title)
    {
        return $this->hasMany('Article')
                    ->where('title', 'like', '%' . $title . '%')
                    ->field('id,name,title')
                    ->order('create_time desc');
    }    
}

articlesLike方法就作為自定義關聯查詢專用,并且需要傳入title參數,用法如下:

$user = User::get(1);
$articles = $user->articlesLike('think')
    ->select();

下面的用法則是錯誤的:

$user = User::get(1);
$articles = $user->articlesLike;

帶有參數的關聯定義方法不能直接用于關聯屬性獲取,只能用于鏈式關聯自定義查詢。

關聯約束

對于hasMany關聯關系,系統提供了根據關聯數據條件來查詢當前模型數據的關聯約束方法,包括hashasWhere兩個方法。

has方法主要用于查詢關聯數據的記錄數來作為當前模型的查詢依據,默認是存在一條數據即可。

// 查詢有評論數據的文章
$list = Article::has('comments')->select();

可以指定關聯數據的數量進行查詢,例如:

// 查詢評論超過3個的文章
$list = Article::has('comments', '>', 3)->select();

has方法的第二個參數支持>>=<<= 以及 =,第三個參數是一個整數。

如果需要復雜的關聯查詢約束條件的話,可以使用hasWhere方法,例如:

// 查詢評論狀態正常的文章
$list = Article::hasWhere('comments', ['status' => 1])->select();

或者直接使用閉包查詢,然后在閉包里面使用鏈式方法查詢:

// 查詢最近一周包含think字符的評論的文章
$list = Article::hasWhere('comments', function ($query) {
    $query
        ->whereTime('create_time', 'week')
        ->where('content', 'like', '%think%');
})->select();

使用閉包方式查詢的時候,需要注意一點,如果查詢的關聯模型字段可能同時存在當前模型和關聯模型的話,需要加上關聯模型的名稱作為別名。

// 查詢最近一周包含think字符的評論的文章
$list = Article::hasWhere('comments', function ($query) {
    $query
        ->whereTime('Comment.create_time', 'week')
        ->where('content', 'like', '%think%');
})->select();

V5.0.5+版本開始,has也支持hasWhere的所有用法。

關聯預載入

關聯查詢只是為了方便,但在實際的應用過程中,查詢多個數據的情況下如果數據較多,關聯查詢產生的性能開銷會較大(雖然這個很正常),比如查詢用戶的Profile關聯數據的話,如果有100個用戶數據,就會產生100+1次查詢,這就是N+1查詢問題,關聯預載入功能提供了更好的性能,但完成了一樣的關聯查詢效果。

關聯查詢的預查詢載入功能,主要解決了N+1次查詢的問題,例如下面的查詢如果有3個記錄,會執行4次查詢:

$list = User::all([1, 2, 3]);
foreach ($list as $user) {
    // 獲取用戶關聯的profile模型數據
    dump($user->profile);
}

如果使用關聯預查詢功能,對于一對一關聯來說,默認只有一次查詢,對于一對多關聯的話,就變成2次查詢,有效提高性能,關聯預載入使用with方法指定需要預載入的關聯(方法),用法和relation方法類似。

$list = User::with('profile')->select([1, 2, 3]);
foreach ($list as $user) {
    // 獲取用戶關聯的profile模型數據
    dump($user->profile);
}

關聯的預載入查詢不是惰性的,是連同數據查詢一起完成的,但由于封裝的合并查詢,性能方面遠遠優于普通的關聯惰性查詢,所以整體的查詢性能是非常樂觀的。

鑒于預載入查詢的重要性,模型的getall方法的第二個參數可以直接傳入預載入參數,例如下面的預載入查詢和前面是等效的:

$list = User::all([1, 2, 3], 'profile');
foreach ($list as $user) {
    // 獲取用戶關聯的profile模型數據
    dump($user->profile);
}

嵌套預載入

嵌套預載入指的是如果關聯模型本身還需要進行關聯預載入的話,可以在當前模型預載入查詢的時候直接指定,理論上嵌套是可以任意級別的(但實際上估計不會有這么復雜的關聯設計),假設Profile模型還關聯了一個名片模型(cards關聯方法),可以這樣進行嵌套預載入查詢。

$list = User::all([1, 2, 3], 'profile.cards');
foreach ($list as $user) {
    // 獲取用戶關聯數據
    dump($user->profile->cards);
}

一對一關聯的JOIN方式不支持嵌套預載入

預載入條件限制

可以在預載入的時候通過閉包指定額外的條件限制,但記住了,不要在閉包里面執行任何的查詢,例如:

$list = User::with(['articles' => function ($query) {
    $query->where('title', 'like', '%think%')
        ->field('id,name,title')
        ->order('create_time desc');
}])->select([1, 2, 3]);

foreach ($list as $user) {
    // 獲取用戶關聯的profile模型數據
    dump($user->profile);
}

如果是一對一預載入查詢的條件限制,注意field方法要改為withField方法,否則會產生字段混淆。

延遲預載入

有些情況下,需要根據查詢出來的數據來決定是否需要使用關聯預載入,當然關聯查詢本身就能解決這個問題,因為關聯查詢是惰性的,不過用預載入的理由也很明顯,性能具有優勢。

延遲預載入僅針對多個數據的查詢,因為單個數據的查詢用延遲預載入和關聯惰性查詢沒有任何區別,所以不需要使用延遲預載入。

如果你的數據集查詢返回的是數據集對象,可以使用調用數據集對象的load實現延遲預載入:

// 查詢數據集
$list = User::all([1, 2, 3]);
// 延遲預載入
$list->load('cards');
foreach ($list as $user) {
    // 獲取用戶關聯的card模型數據
    dump($user->cards);
}

如果你的數據集查詢返回的是數組,系統提供了一個load_relation助手函數可以完成同樣的功能。

// 查詢數據集
$list = User::all([1, 2, 3]);
// 延遲預載入
$list = load_relation($list, 'cards');
foreach ($list as $user) {
    // 獲取用戶關聯的card模型數據
    dump($user->cards);
}

關聯統計

有些時候,并不需要獲取關聯數據,而只是希望獲取關聯數據的統計(關聯統計僅針對一對多或者多對多的關聯關系),這個時候可以使用withCount方法進行制定關聯的統計。

$list = User::withCount('cards')->select([1, 2, 3]);
foreach ($list as $user) {
    // 獲取用戶關聯的card關聯統計
    echo $user->cards_count;
}

關聯統計功能會在模型的對象屬性中自動添加一個以“關聯方法名+_count”為名稱的動態屬性來保存相關的關聯統計數據。

如果需要對關聯統計進行條件過濾,可以使用

$list = User::withCount(['cards' => function ($query) {
    $query->where('status', 1);
}])->select([1, 2, 3]);
foreach ($list as $user) {
    // 獲取用戶關聯的card關聯統計
    echo $user->cards_count;
}

一對一關聯關系使用關聯統計是無效的,一般可以用exists查詢來判斷是否存在關聯數據。

關聯輸出

關聯屬性的輸出和模型的輸出轉換一樣,使用模型的toArray方法可以同時輸出關聯屬性(對象),例如:

$user = User::get(1,'profile');
$data = $user->toArray();
dump($data);
$data = $user->toJson();
dump($data);

對于使用了關聯預載入查詢和手動獲取了關聯屬性(延遲關聯查詢)的情況,toArraytoJson方法都會包含關聯數據。

可以調用visiblehidden方法對當前模型以及關聯模型的屬性進行輸出控制,下面來看一個例子:

$user = User::get(1, 'profile');
$data = $user->hidden(['name', 'profile.email'])->toArray();

上面的代碼返回的data數據中不會包含用戶模型的name屬性以及關聯profile模型的email屬性。

如果要隱藏多個關聯屬性的話,可以使用下面的方式:

$user = User::get(1, 'profile');
$data = $user->hidden(['name', 'profile' => ['email', 'address']])->toArray();

模型的visible方法(用于設置需要輸出的屬性)的用戶和hidden一致,在此不再多說,有一點必須強調下,同時調用visiblehidden方法的話,visible是優先的,所以下面的profile關聯屬性輸出會包含emailsex

$user = User::get(1, 'profile');
$data = $user->visible(['profile' => ['email', 'sex']])->hidden(['name', 'profile' => ['email', 'address']])->toArray();

在需要的時候,即使之前沒有進行任何的關聯查詢,你也可以在輸出的時候追加關聯屬性,例如:

$user = User::get(1);
$user->append(['profile'])->toArray();

該例子在調用toArray方法的時候才會進行profile關聯數據獲取并轉換輸出。

對于數據集查詢,如果返回類型是數據集對象仍然支持調用visiblehiddenappend方法,如果不是數據集對象的話可以先用collection助手函數轉換為數據集對象。

$users = User::all();
$data  = $users->hidden(['name', 'profile' => ['email', 'address']])
    ->toArray();

關聯實例

在學習完了關聯查詢、自定義條件查詢、關聯(及嵌套)預載入、延遲預載入、關聯約束和關聯統計后,我們已經基本上掌握了關聯的所有查詢操作,現在我們來通過一些實例來復習下關聯查詢操作, 以及了解下不同的關聯類型的新增、更新和刪除等操作,及其注意事項。

其實只要理解模型和對象的概念,關聯的新增、更新和刪除,甚至其它的業務邏輯操作的調用都是很容易掌握的。

本節涉及的關聯實例,各個模型對應的數據表結構如下(本示例僅僅演示關聯的用法,不打算重復強調模型本身的功能,因此對數據表結構做了必要的簡化以達到說明的效果):

city
    id - integer
    name - string

user
    id - integer
    name - integer
    email - string
    city_id - integer

role
    id - integer
    name - string

auth
    user_id - integer
    role_id - integer
    add_time - dateTime

blog
    id - integer
    name - string
    title - string
    cate_id - integer
    user_id - integer

content
    id - integer
    blog_id - integer
    data - text

cate
    id - integer
    name - string
    title - string

comment
    id - integer
    content - text
    commentable_id - integer
    commentable_type - string

模型類分別如下:

City模型

<?php

namespace app\index\model;

use think\Model;

class City extends Model
{
    /**
     * 獲取城市的用戶
     */    
    public function users()
    {
        return $this->hasMany('User');
    }    

    /**
     * 獲取城市的所有博客
     */    
    public function blog()
    {
        return $this->hasManyThrough('Blog', 'User');
    }    
}

User模型

<?php

namespace app\index\model;

use think\Model;

class User extends Model
{
    /**
     * 獲取用戶所屬的角色信息
     */
    public function roles()
    {
        return $this->belongsToMany('Role', 'auth');
    }

    /**
     * 獲取用戶發表的博客信息
     */    
    public function blogs()
    {
        return $this->hasMany('Blog');
    }    

    /**
     * 獲取所有針對用戶的評論
     */
    public function comments()
    {
        return $this->morphMany('Comment', 'commentable');
    }    
}

Role模型

<?php

namespace app\index\model;

use think\Model;

class Role extends Model
{
    /**
     * 獲取角色下面的用戶信息
     */
    public function users()
    {
        return $this->belongsToMany('User', 'auth');
    }
}

Blog模型

<?php

namespace app\index\model;

use think\Model;

class Blog extends Model
{
    /**
     * 獲取博客所屬的用戶
     */
    public function user()
    {
        return $this->belongsTo('User');
    }

    /**
     * 獲取博客的內容
     */    
    public function content()
    {
        return $this->hasOne('Content');
    }    

    /**
     * 獲取所有博客所屬的分類
     */    
    public function cate()
    {
        return $this->belongsTo('Cate');
    }    

    /**
     * 獲取所有針對文章的評論
     */
    public function comments()
    {
        return $this->morphMany('Comment', 'commentable');
    }    
}

Content模型

<?php

namespace app\index\model;

use think\Model;

class Content extends Model
{
    /**
     * 獲取內容所屬的博客信息
     */
    public function blog()
    {
        return $this->belongsTo('Blog');
    }
}

Cate模型

<?php

namespace app\index\model;

use think\Model;

class Cate extends Model
{
    /**
     * 獲取分類下的所有博客信息
     */
    public function blogs()
    {
        return $this->hasMany('Blog');
    }
}

Comment模型

<?php

namespace app\index\model;

use think\Model;

class Comment extends Model
{
    /**
     * 獲取評論對應的多態模型
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

關于不同關聯方法的參數說明請參考關聯定義部分,這里不再重復敘述。

auth數據表不需要創建模型,對于多對多關聯來說,中間表是不需要關注的。

一對一關聯

一對一關聯包含hasOnebelongsTo兩種關聯關系定義,系統對一對一關聯尤其是hasOne做了強化支持,這里用博客模型和內容模型之間的關聯為例說明。

先來說下普通情況的關聯操作。

[ 新增 ]

$blog        = new Blog;
$blog->name  = 'thinkphp';
$blog->title = 'ThinkPHP5關聯實例';
if ($blog->save()) {
    $content       = new Content;
    $content->data = '實例內容';
    $blog->content()->save($content);
}

當然,支持使用數組方式新增數據,例如:

$data = [
    'name'  => 'thinkphp',
    'title' => 'ThinkPHP5關聯實例',
];
$blog    = Blog::create($data);
$content = [
    'data' => '實例內容',
];
$blog->content()->save($content);

[ 查詢 ]

普通關聯查詢

$blog = Blog::get(1);
echo $blog->content->data;

預載入關聯查詢

$blog = Blog::get(1,'content');
echo $blog->content->data;

數據集查詢

$blogs = Blog::with('content')->select();
foreach ($blogs as $blog) {
    dump($blog->content->data);
}

默認一對一關聯查詢也是使用2次查詢,如果希望獲取更好的性能,可以修改關聯定義為:

    /**
     * 獲取博客的內容
     */    
    public function content()
    {
        // 修改關聯查詢方式為JOIN查詢方式
        return $this->hasOne('Content')->setEagerlyType(0);
    }   

修改后,關聯查詢從原來默認的IN查詢改為JOIN查詢,可以減少一次查詢,但有一個地方必須注意,指定的關聯表字段field方法必須改為withField方法。

[ 更新 ]

// 查詢
$blog = Blog::get(1);
// 更新當前模型
$blog->title = '更改標題';
$blog->save();
// 更新關聯模型
$blog->content->data = '更新內容';
$blog->content->save();

[ 刪除 ]

// 查詢
$blog = Blog::get(1);
// 刪除當前模型
$blog->delete();
// 刪除關聯模型
$blog->content->delete();

為了更簡單的使用一對一關聯的寫入操作,系統提供了關聯自動寫入功能(V5.0.5+版本開始支持),比較下面的代碼就會發現寫入操作和之前的寫法更簡潔了。

[ 新增 ]

$blog          = new Blog;
$blog->name    = 'thinkphp';
$blog->title   = 'ThinkPHP5關聯實例';
$blog->content = ['data' => '實例內容'];
$blog->together('content')->save();

當然,還可以更加對象化一些,例如:

$blog          = new Blog;
$blog->name    = 'thinkphp';
$blog->title   = 'ThinkPHP5關聯實例';
$content       = new Content;
$content->data = '實例內容';
$blog->content = $content;
$blog->together('content')->save();

甚至可以把關聯屬性合并到主模型進行賦值后寫入,只需要改成:

$blog        = new Blog;
$blog->name  = 'thinkphp';
$blog->title = 'ThinkPHP5關聯實例';
$blog->data  = '實例內容';
$blog->together(['content' => ['data']])->save();

如果不想這么麻煩每次調用together方法,也可以直接在模型類中定義relationWrite屬性,但必須是數組方式。不過考慮到模型的獨立操作的可能性,并不建議。

[ 查詢 ]

關聯查詢支持把關聯模型的屬性直接附加到當前模型

$blog = Blog::get(1);
$blog->appendRelationAttr('content', 'data');
echo $blog->data;

如果不想每次都附加操作的話,可以修改Blog模型的關聯定義如下:

    /**
     * 獲取博客的內容
     */    
    public function content()
    {
        return $this->hasOne('Content')->bind('data');
    }   

現在就可以直接使用

$blog = Blog::get(1, 'content');
echo $blog->data;

數據集的用法基本上類似。

[ 更新 ]

采用關聯自動更新的寫法如下:

// 查詢
$blog          = Blog::get(1);
$blog->title   = '更改標題';
$blog->content = ['data' => '更新內容'];
// 更新當前模型及關聯模型
$blog->together('content')->save();

更加對象化的寫法是:

// 查詢
$blog                = Blog::get(1);
$blog->title         = '更改標題';
$blog->content->data = '更新內容';
// 更新當前模型及關聯模型
$blog->together('content')->save();

一樣可以支持關聯屬性合并到主模型操作

// 查詢
$blog        = Blog::get(1);
$blog->title = '更改標題';
$blog->data  = '更新內容';
// 更新當前模型及關聯模型
$blog->together(['content' => 'data'])->save();

在關聯方法中使用bind方法把關聯屬性綁定到當前模型并不會影響關聯寫入,必須使用數組方式來明確告知當前模型哪些屬性是關聯的綁定屬性。

[ 刪除 ]

關聯自動刪除的操作很簡單

// 查詢
$blog = Blog::get(1);
// 刪除當前及關聯模型
$blog->together('content')->delete();

一對多關聯

一對多關聯包括hasManybelongsTo兩種關聯關系,我們以用戶和博客模型為例來說明,其實一對多關聯主要是查詢為主,關聯寫入比起單獨模型的操作并沒有任何優勢,所以建議一對多的關聯寫入仍然由各個獨立模型完成,請不要糾結。

可以查詢某個用戶的博客

$user = User::get(1);
// 獲取用戶的所有博客
dump($user->blogs);
// 也可以進行條件搜索
dump($user->blogs()->where('cate_id', 1)->select());

如果需要對關聯數據進行額外的條件查詢、更新和刪除操作就可以使用blogs方法。

反過來,如果需要查詢博客所屬的用戶信息,可以使用

$blog = Blog::get(1);
dump($blog->user->name);

遠程一對多

遠程一對多的作用是跨過一個中間模型操作查詢另外一個遠程模型的關聯數據,而這個遠程模型通常和當前模型是沒有任何關聯的,用前面的例子來說的話就是:

  • 一個用戶發表了多個博客;
  • 一個城市有多個用戶;
  • 假設城市和博客之間沒有直接關聯;

如果需要獲取某個城市下面的所有博客,利用已經掌握的關聯概念是可以實現的,只是需要通過兩次關聯操作來獲取,代碼看起來類似下面:

$city  = City::getByName('shanghai');
$blogs = [];
foreach ($city->users as $user) {
    $blogs[$user->id] = $user->blogs()->order('id desc')->limit(100)->select();
}
// 然后對博客數據進行額外組裝處理
// ...

雖然思路還是比較清晰,但略顯麻煩,另外還要對數據進行組裝,而且不便于統一排序和限制,例如希望一共取出100個博客數據就不好辦。

為了簡化這種操作,我們引入了遠程一對多的關聯關系來更好的解決,在City模型中已經定義了blogs關聯,實現方案修改如下:

$city  = City::getByName('shanghai');
$blogs = $city->blogs()
    ->order('id desc')
    ->limit(100)
    ->select();

看起來是不是直觀很多,而且對博客數據的自定義查詢也相當方便,無論是性能還是功能都更佳,因為我們不需要對用戶模型進行查詢操作。當然,很多朋友會說,直接在博客模型中添加城市id豈不是更簡單,這是架構設計的問題了,不屬于本次討論的范疇,本實例的假設前提是城市和博客模型之間沒有任何直接關聯。

但有一個結論是顯而易見的:架構的優化對于代碼的優化來說有時候更有效

多對多關聯

多對多關聯較前面兩種關聯來說復雜很多,但越是復雜越能體現出模型關聯的優勢,下面我們以用戶和角色模型來看下如何操作多對多關聯。

多對多關聯關系必然會有一個中間表,最少必須包含兩個字段,例如auth表就包含了user_idrole_id(建議對這兩個字段設置聯合唯一索引),但中間表仍然可以包含額外的數據。

中間表不需要創建任何模型(auth表沒有對應模型),多對多關聯關系會創建一個虛擬的中間表模型(也稱之為樞紐模型)Pivot,對中間表的所有操作只需要對該模型進行操作即可,事實上,一般情況下你根本無需關注中間表的存在就可以輕松完成多對多關聯操作。

多對多的關聯寫入操作一般有下列幾種方式:

  • 用戶和角色數據獨立寫入,然后通過關聯完成中間表的寫入;
  • 用戶數據獨立寫入,然后通過關聯完成角色數據和中間表數據寫入;
  • 角色數據獨立寫入,然后通過關聯完成用戶數據和中間表數據寫入(多對多關聯相互之間操作是等同的,因此本質上和上面是同一種方式);
  • 通過關聯單獨完成中間表數據更新及刪除;

多對多的關聯寫入操作主要需要掌握下面兩個方法,我們后面會詳細講解,除非模型獨立操作,一般不需要使用save方法。

方法 描述
attach 附加關聯的一個中間表數據
detach 解除關聯的一個或者多個中間表數據

首先完成第一種方式,僅僅操作中間表數據。

// 查詢用戶
$user = User::get(1);
// 查詢角色
$role = Role::getByName('admin');
// 增加用戶-角色數據
$user->roles()->attach($role->id);

如果中間表有額外數據需要寫入,可以使用:

// 查詢用戶
$user = User::get(1);
// 查詢角色
$role = Role::getByName('admin');
// 傳入中間表的額外屬性
$user->roles()->attach($role->id, ['add_time' => '2017-1-18']);

事實上,attach方法是一個很智能的方法,第一個參數能夠識別包括數字、字符串、數組和模型實例并做出不同的處理。

參數類型 作用描述
數字或字符串 要附加中間表的關聯模型主鍵
索引數組 首先寫入關聯模型,然后附加中間表
普通數組 附加多個關聯數據的主鍵
模型實例 附加關聯模型

如果要添加的角色尚未創建,則可以使用下面的方式添加用戶-角色數據:

// 查詢用戶
$user = User::get(1);
// 增加用戶-角色數據 并同時創建新的角色
$user->roles()->attach([
    // 添加一個編輯角色
    'name' => 'editor',
]);

如果需要獲取新增的角色表自增主鍵ID,最新版本的attach方法返回的是一個Pivot模型對象。

// 查詢用戶
$user = User::get(1);
// 增加用戶-角色數據 并同時創建新的角色
$pivot = $user->roles()->attach([
    // 添加一個編輯角色
    'name' => 'editor',
], ['add_time' => '2017-1-31']);
// 獲取中間表的數據
echo $pivot->role_id;
echo $pivot->user_id;
echo $pivot->add_time;

下面則表示給用戶添加多個角色授權:

// 查詢用戶
$user = User::get(1);
// 給用戶授權多個角色(根據角色主鍵)
$user->roles()->attach([1, 2, 3], ['add_time' => '2017-1-31']);

要解除一個用戶的角色,可以使用:

// 查詢用戶
$user = User::get(1);
// 查詢角色
$role = Role::getByName('admin');
// 刪除中間表數據
$user->roles()->detach($role->id);

可以同時解除用戶的多個角色權限

// 查詢用戶
$user = User::get(1);
// 刪除中間表數據
$user->roles()->detach([1, 2, 3]);

解除用戶的所有角色可以用

// 查詢用戶
$user = User::get(1);
// 刪除中間表數據
$user->roles()->detach();

如果需要解除用戶的權限同時刪除這個角色,可以使用:

// 查詢用戶
$user = User::get(1);
// 查詢角色
$role = Role::getByName('test');
// 刪除中間表數據以及關聯表數據
$user->roles()->detach($role->id,true);

多對多關聯的查詢和其它關聯類似(一樣支持關聯自定義查詢),區別在于每個關聯模型數據還有一個額外的樞紐模型數據,例如:

// 查詢用戶
$user = User::get(1);
// 獲取用戶的角色
$roles = $user->roles;
foreach ($roles as $role) {
    // 輸出用戶的角色名
    echo $role->name;
    // 獲取中間表模型
    dump($role->pivot);
}

多態一對多

多態關聯允許一個模型在單個關聯定義方法中從屬一個以上其它模型,例如用戶可以評論書和文章,但評論表通常都是同一個數據表的設計。多態一對多關聯關系,就是為了滿足類似的使用場景而設計。

多態一對多關聯主要涉及的是關聯查詢,關聯寫入本身不建議通過關聯操作完成,請確保用各自的模型獨立完成數據寫入。

多態一對多的多態表設計很重要,例如本例子中的評論表因為需要保存多個模型的評論數據,就可以設計成多態關聯。

要獲取博客的評論數據可以使用:

$blog = Blog::get(1);

foreach ($blog->comments as $comment) {
    dump($comment);
}

當然,一樣可以進行評論篩選過濾

$blog     = Blog::get(1);
$comments = $blog->comments()
    ->where('content', 'like', '%think%')
    ->order('id desc')
    ->limit(20)
    ->select();
foreach ($comments as $comment) {
    echo $comment->content;
}

對于評論模型來說,則可以這樣操作

$comment = Comment::get(1);
$commentable = $comment->commentable;

Comment 模型的 commentable 關聯會返回 BlogUser 模型的對象實例,這取決于評論所屬模型的類型。

如果你的多態類型字段保存的數據并非是模型名稱之類的,而是采用數字保存(提高存儲和查詢性能),比如1表示博客,2表示用戶。

關聯定義方法需要對應修改為:
Blog模型

    /**
     * 獲取所有針對文章的評論
     */
    public function comments()
    {
        return $this->morphMany('Comment', 'commentable', 1);
    }   

User模型

    /**
     * 獲取所有針對用戶的評論
     */
    public function comments()
    {
        return $this->morphMany('Comment', 'commentable', 2);
    }   

Comment模型

    /**
     * 獲取評論對應的多態模型
     */
    public function commentable()
    {
        return $this->morphTo(null, [
            '1' => 'Blog',
            '2' => 'User',
        ]);
    }

如果你的模型使用不同的命名空間,可以使用完整的命名空間方式定義:

    /**
     * 獲取評論對應的多態模型
     */
    public function commentable()
    {
        return $this->morphTo(null, [
            '1' => 'app\model\Blog',
            '2' => 'app\model\User',
        ]);
    }

總結

本章我們了解了模型關聯的概念,并著重學習了關聯的查詢,并針對不同的關聯類型給出了實際的關聯操作指引,下一章我們會來說下數據庫和模型操作的性能和安全方面的話題。

上一篇:第七章:模型高級用法
下一篇:第九章:性能和安全

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容

  • Eloquent: 關聯模型 簡介 數據庫中的表經常性的關聯其它的表。比如,一個博客文章可以有很多的評論,或者一個...
    Dearmadman閱讀 17,334評論 6 16
  • Swift1> Swift和OC的區別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,129評論 1 32
  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,074評論 6 13
  • 1.設計模式是什么? 你知道哪些設計模式,并簡要敘述?設計模式是一種編碼經驗,就是用比較成熟的邏輯去處理某一種類型...
    龍飝閱讀 2,176評論 0 12
  • 太陽落下了時候可真美呀! 可他卻把江山河水照的更美了,江山就像一件漂亮的衣裳,把河水照的就像一個 活娃娃水中劃...
    高詩涵閱讀 202評論 0 1