胖胖的Eloquent
Eloquent采用了ActiveRecord的模式葛虐,這也讓Eloquent招致了好多批評(píng)棉钧,讓我們?nèi)タ船F(xiàn)在Eloquent/Model.php
文件涕蚤, 該文件已經(jīng)有3500多行万栅,此時(shí)的Model集成了太多的功能了烦粒,一個(gè)新人很難短時(shí)去理解Model并去很好的使用了代赁,目前Eloquent/Model中主要混合了4個(gè)功能:
- Domain Model(包括了data model和領(lǐng)域邏輯)
- Row Data Gateway(例如save,delete等數(shù)據(jù)持久化操作)
- Table data gateway(各種find方法)
- Factory(新建model)
上面介紹的幾種ORM設(shè)計(jì)模式徒役,可以去之前的文章查看:orm 系列 之 常用設(shè)計(jì)模式
我們可以看到Model中混合了各種模式忧勿,這就要求使用者在使用的時(shí)候清楚的知道怎么使用鸳吸,這里的清楚知道怎么用是指根據(jù)SOILD原則,優(yōu)雅的使用Model速勇,本文的目的就是幫助Model的使用者達(dá)成優(yōu)雅的目標(biāo)。
那理想的Model使用是什么樣子的呢养匈?我們希望Model的使用不是ActiveRecord个初,而是較為清晰的DataMapper模式猴蹂,能夠讓domain model和database解耦,然后由DataMapper來完成映射工作磅轻,更具體點(diǎn)聋溜,我們希望的是像clean architecture中定義的架構(gòu)一樣,內(nèi)層是Domain Model漱病,外面是Domain Services,Domain Services又可以具體分為:
-
Repositories
服務(wù)領(lǐng)域?qū)ο蟮拇嫒±齑绻蠖耸菙?shù)據(jù)庫晃危,就是負(fù)責(zé)將數(shù)據(jù)從數(shù)據(jù)庫中取出老客,將對(duì)象存入數(shù)據(jù)庫胧砰。
-
Factories
負(fù)責(zé)對(duì)象的創(chuàng)建。
-
Services
具體的業(yè)務(wù)邏輯权纤,通過調(diào)用多個(gè)對(duì)象和其他服務(wù)來完成一個(gè)業(yè)務(wù)目標(biāo)乌妒。
由此實(shí)現(xiàn)很好的解耦和關(guān)注點(diǎn)分離撤蚊,更具體的關(guān)注clean architecture可以查看簡(jiǎn)書專題:clean architecture.
Eloquent拆解
講述了一些方法論后,我們來動(dòng)手實(shí)作一下
talk is cheap, show me the code
第一步槽唾,我們定義一個(gè)member表
php artisan make:migration MemberCreate
第二步庞萍,編寫表定義
Schema::create('members',function(Blueprint $table){
$table->increments('id');
$table->string('login_name');
$table->string('display_name');
$table->integer('posts')->unsigned();
});
第三步忘闻,執(zhí)行migration
php artisan migrate
第四步齐佳,生成model文件
php make:model Member
下面開始定義一些接口
The Member Model Interface
interface MemberInterface
{
public function getID();
public function getLoginName();
public function getDisplayName();
public function getPostCount();
public function incrementPostCount();
public function decrementPostCount();
}
The Eloquent Member Model Implementation
class Member extends Model implements MemberInterface
{
const ATTR_DISPLAY_NAME = ‘display_name’;
const ATTR_LOGIN_NAME = ‘login_name’;
const ATTR_POST_COUNT = ‘posts’;
protected $fillable = [
self::ATTR_LOGIN_NAME,
self::ATTR_DISPLAY_NAME
];
public function getID()
{
return $this->getKey()
}
public function getLoginName()
{
return $this->{self::ATTR_LOGIN_NAME};
}
public function getDisplayName()
{
return $this->{self::ATTR_DISPLAY_NAME};
}
public function getPostCount()
{
return $this->{self::ATTR_POST_COUNT};
}
public function incrementPostCount()
{
$this->{self::ATTR_POST_COUNT}++;
}
public function decrementPostCount()
{
if ($this->getPostCount() > 0) {
$this->{self::ATTR_POST_COUNT}--;
}
}
}
The Member Repository Interface
interface MemberRepositoryInterface
{
public function find($id);
public function findTopPosters($count = 10);
public function save(MemberInterface $member);
}
The Eloquent Member Repository Implementation
class EloquentMemberRepository implements MemberRepositoryInterface {
/** @var Member */
protected $model;
/**
* EloquentMemberRepository constructor.
*
* @param \App\Member $model
*/
public function __construct( Member $model )
{
$this->model = $model;
}
public function find( $id )
{
return $this->model->find($id);
}
/**
* @param int $count
*
* @return \Illuminate\Support\Collection
*/
public function findTopPosters( $count = 10 )
{
return $this->model
->orderBy(($this->model)::ATTR_POST_COUNT, 'DESC')
->take($count)
->get();
}
public function save( MemberInterface $member )
{
$member->save();
/*
注意:此處我們接口聲明上是 MemberInterface 本鸣,這個(gè)是普適的規(guī)則
但是此處的 Eloquent 實(shí)現(xiàn)是基于 Eloquent Model的荣德,因此假設(shè) 傳入的
MemberInterface 實(shí)現(xiàn)了 save 方法
*/
}
}
基本使用
DemonstrationController
{
public function createPost(MemberRepositoryInterface $repository)
{
// validate request, create the post, and...
$member = Auth::user()->member();
$member->incrementPostCount();
$respository->save($member);
}
public function deletePost(MemberRepositoryInterface $repository)
{
// validate request, delete the post, and...
$member = Auth::user()->member();
$member->decrementPostCount();
$respository->save($member);
}
public function dashboard(MemberRespositoryInterface $repository)
{
$members = $repository->findTopPosters(20);
return view('dashboard', compact('members'));
}
}
使用的時(shí)候我們看到了好的方式,那如果我們沒有定義repository和interface曹傀,會(huì)怎么樣呢皆愉?
DemonstrationController
{
public function createPost()
{
// validate request, create the post, and...
$member = Auth::user()->getMember();
$member->posts++;
$member->save();
}
public function deletePost()
{
// validate request, delete the post, and...
$member = Auth::user()->getMember();
if ($member->posts > 0) {
$member->posts--;
$member->save();
}
}
public function dashboard()
{
$members = Member::orderBy('posts', 'DESC')->take(20)->get();
return view('dashboard', compact('members'));
}
}
上面簡(jiǎn)單的業(yè)務(wù)邏輯posts不能小于0,都沒有很好的封裝异剥,如果上面我們一些增加和減少的功能和save封裝到一起呢冤寿?
DemonstrationController
{
public function createPost()
{
// validate request, create the post, and...
$member = Auth::user()->member();
$member->incrementPostCount();
}
public function deletePost()
{
// validate request, delete the post, and...
$member = Auth::user()->member();
$member->decrementPostCount();
}
}
class Member extends Model implements MemberInterface
{
//...
public function incrementPostCount()
{
$this->{self::ATTR_POST_COUNT}++;
$this->save();
}
public function decrementPostCount()
{
if ($this->getPostCount() > 0) {
$this->{self::ATTR_POST_COUNT}--;
$this->save();
}
}
// ...
}
這樣做主要有兩個(gè)問題
- 如果我們將Model的實(shí)現(xiàn)由Eloquen轉(zhuǎn)換為其他呢?這將會(huì)使應(yīng)用出錯(cuò)
- 我們每個(gè)更改都是執(zhí)行一個(gè)sql語句号杠,嚴(yán)重浪費(fèi)姨蟋,我們完全可以做完更改后,統(tǒng)一一次update
通過上面的對(duì)比堂飞,我們更能發(fā)現(xiàn)使用Repository和Interface的好處酝静,能讓我們更好的實(shí)現(xiàn)關(guān)注點(diǎn)分離,下面我們會(huì)更深入的討論一些問題:包括Collections, Relations, Eager Loading, 和 Schema Changes稼稿。
Eloquent進(jìn)階
首先介紹collection的問題敞恋,看代碼
class FooController
{
public function bar(PostRepositoryInterface $repository)
{
$posts = $repository->findActivePosts();
$posts->load('author');
}
}
上面的代碼中硬猫,雖然我們使用的type hint表明使用的repository是PostRepositoryInterface,但是方法findActivePosts
返回的collection顯然是跟Eloquent耦合的Eloquent\Collection衬横,那怎么解決這個(gè)問題呢蜂林?有以下幾個(gè)方案
- 讓
findActivePosts
返回?Collection,而不是Eloquent\Collection构眯,避免在Repository之外使用Eloquent相關(guān)的功能 - 通過custom collections方法惫霸,返回自定義的collection
下面介紹第二個(gè)議題Eager Loading
還是看代碼
class FooController
{
public function bar(PostRepositoryInterface $repository)
{
$posts = $repository->findActivePosts(['author']);
}
}
上面的代碼通過參數(shù)['author']的傳入壹店,將eager loading的操作封裝在了findActivePosts
之內(nèi),但是這樣子做将塑,反而讓調(diào)用方必須知道實(shí)現(xiàn)細(xì)節(jié)点寥,即本來是功能上的優(yōu)化敢辩,通過eager loading來解決N+1問題的方案盗冷,變?yōu)榱藰I(yè)務(wù)需要知道的業(yè)務(wù)的邏輯了仪糖,明顯是不合理的乓诽。
更可怕的時(shí)候鸠天,你可能會(huì)希望通過傳入?yún)?shù)讓findActivePosts
實(shí)現(xiàn)更多的功能,于是變?yōu)榱讼旅娴暮瘮?shù)findActivePostsInDateRange($start, $end, $eagerLoading = null)
剥纷,我們看到隨著項(xiàng)目復(fù)雜度的提升晦鞋,我們不得不通過通過參數(shù)來滿足更多的需求,但是這也使得接口變得更復(fù)雜确买,功能更多湾趾,到最后我們不得不面對(duì)各種ugly的代碼,那面對(duì)Eager Loading我們到底應(yīng)該怎么辦呢艺普?下面給出一個(gè)建議:
在提供非eager loading的方法同時(shí)衷敌,提供一個(gè)eager loading的方法。這可能會(huì)被人說:這也不是讓用戶知道了實(shí)現(xiàn)細(xì)節(jié)了嘛面氓。是的舌界,這方法是一個(gè)性能和使用上的妥協(xié)呻拌。
最后介紹Relations藐握,看到代碼
interface MemberInterface
{
public function getID();
public function getLoginName();
public function getDisplayName();
public function getPostCount();
public function incrementPostCount();
public function decrementPostCount();
public function getPosts();
public function getFavoritePosts();
}
class Member extends Model implements MemberInterface
{
...
public function posts()
{
return $this->hasMany(Post::class);
}
public function getPosts()
{
return $this->posts;
}
public function getFavoritePosts()
{
// I think this will work!
return $this->posts()
->newQuery()
->whereHas('favorites', function($q) {
$q->where(Favorite::ATTR_MEMBER_ID, $this->getID());
})->get();
}
...
}
我們沒有辦法將relation Method設(shè)置為protect或者private(這樣設(shè)置的目的是讓外面不使用本谜,限制使用范圍)乌助,但是這樣子會(huì)導(dǎo)致想whereHas這種方法執(zhí)行不成功炕泳。
此處還注意到一個(gè)問題培遵,我們此時(shí)使用的posts
是表示relation,但是之前是member的一個(gè)字段皇耗,明顯沖突了郎楼,我們需要修改字段名敌买,從posts到post_count,因?yàn)槲覀冎笆褂昧顺A縼矶x屬性膘融,因此只需要下面一行代碼就解決問題了:
const ATTR_POST_COUNT = ‘post_count’;
總結(jié)
介紹了這么多春畔,我們解決了一個(gè)核心問題:因?yàn)镋loquent的功能耦合,我們應(yīng)該正確的使用它线召,Eloquent的ActiveRecord模式可以讓我們非常容易的實(shí)現(xiàn)DataMapper缓淹,根據(jù)Clean architecture的定義讯壶,我們將domain services分為了Repositories,F(xiàn)actories躏吊,Services比伏,實(shí)現(xiàn)了關(guān)注點(diǎn)分離。
但是到目前悠菜,還有一個(gè)問題沒有解決,那就是通過Repository篙顺,我們很難實(shí)先Eloquent/Builder那樣豐富的查詢功能德玫,我們不得不每次新增一個(gè)查詢條件,就去新增接口或者參數(shù)椎麦,不慎其煩宰僧,就像之前的findActivePostsInDateRange
方法一樣丑陋,那到底有什么辦法解決呢观挎?盡情期待下一篇內(nèi)容琴儿,Repository的實(shí)作。
參考
Separation of Concerns with Laravel’s Eloquent Part 1: An Introduction