## 如何使用 Repository 模式
若將數據庫邏輯都寫在 Model 里,會造成 model 代碼的臃腫難以維護,基于 SOLID 原則,我們應該使用 **Repository** 模式輔助 Model,將相關的數據庫邏輯封裝在不同的 Repository,方便后期項目的維護。
### Laravel 框架版本
Laravel 5.4.17
### 數據庫邏輯
在 CURD 中,CUR 比較穩定,但 Read 的部分則變化萬千,大部分的數據庫邏輯都在描述 Read 部分,若將數據庫邏輯寫在 Controller 或 Model 都不合適,會造成 Controller 或 Model 代碼臃腫,如后難以維護。
### Model
使用 Repository 模式之后,Model 僅僅當成 Eloquent Class 即可,不需要包含數據庫邏輯,僅保留如下部分:
* Property: 如 `$table` `$fillable` ..
* Mutator: 包括 mutator 與 accessor
* Method: relation 類的方法,比如使用 `hasMany()` 與 `belongsTo()`
單一對應關系:
* hasOne
* belongsTo
* morphTo
* morphOne
多個對應關系指的是使用以下關鍵詞定義的關聯模型:
* hasMany
* belongsToMany
* morphMany
* morphToMany
* morphedByMany
> 因為 Eloquent 會根據數據庫字段動態的產生 property 與 method等,若使用 [Laravel IDE Helper](https://github.com/barryvdh/laravel-ide-helper) ,會直接在Model加上 `@property` 與 `@method` 描述model的動態 proerty 與 method。 如下`app\User.php`中安裝完`Laravel IDE Helper`后執行`php artisan ide-helper:models`后自動生成的內容:
```
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
/**
* App\User
*
* @property int $id
* @property string $name
* @property string $email
* @property string $password
* @property string $remember_token
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
* @method static \Illuminate\Database\Query\Builder|\App\User whereCreatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereEmail($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereId($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereName($value)
* @method static \Illuminate\Database\Query\Builder|\App\User wherePassword($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereRememberToken($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereUpdatedAt($value)
* @mixin \Eloquent
*/
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
}
```
### Repository
在開發時常常會在 Controller 直接調用 Model 寫數據庫邏輯,如下:獲取數據庫中用戶 `age>20`的數據。
```
public function index()
{
return User::where('age','>',20)->orderBy('age')->get();
}
```
這樣寫邏輯會有幾個問題:
* 將數據庫邏輯寫在 Controller,造成 Controller 代碼臃腫難以維護。
* 違反了 SOLID 的單一職責原則,數據庫邏輯不應該寫在 Controller 中。
* Controller 直接操作 Model,使得對 Controller 做單元測試困難。
比較好的方式是使用 Repository:
* 將 Model 依賴注入到 Repository。
* 將數據庫邏輯寫在 Repository。
* 將 Repository 依賴注入到 Service。
`app/Repositories/UserRepostitory.php` 中的內容:
```
<?php
namespace App\Repositories;
use App\User;
/**
* Class UserRepository
* @package App\Repositories
*/
class UserRepository
{
/**
* @var User
*/
private $user;
/**
* UserRepository constructor.
* @param $user
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* @param $age
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAgeLargerThan($age)
{
return $this->user
->where('age', '>', $age)
->orderBy('age')
->get();
}
}
```
在控制器`app\Controllers\UserController.php`中使用依賴注入:
```
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
use Illuminate\Http\Request;
/**
* Class UserController
*
* @package App\Http\Controllers
*/
class UserController extends Controller
{
/**
* @var \App\Repositories\UserRepository
*/
protected $userRepository;
/**
* UserController constructor.
* @param $userRepository
*/
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function index()
{
return $this->userRepository->getAgeLargerThan(20);
}
}
```
將相依的 `UserRepository` 依賴注入到 `UserController`,并從原本直接依賴 `User` Model改成依賴注入的 `UserRepository`
#### 優點
* 將數據庫邏輯寫在 Repository 里,解決了 Controller 代碼臃腫的問題。
* 符合 SOLID 的單一職責原則:數據庫邏輯寫在 Repository 里,沒寫在 Controller 里。
* 符合 SOLID 的依賴反轉原則:Controller 并非直接相依與 Repositroy,而是將 Repository 依賴注入進 Controller。
> 實際上建議 Repository 僅依賴注入進 Service,而不是直接注入在 Controller。
#### 是否該建立 Repository Interface?
理論上使用依賴注入時,應該使用 Interface ,不過 Interface 目的在于更換數據庫,讓代碼達到開放封閉的要求,但是實際上要更改 Reposiroty 的機會也不多,除非是從 MySQL 更換到 MongoDB,此時就應該建立 Repository Interface。
不過由于我們使用了依賴注入,將來要從 Class 改成 Interface 也很方便,只要在 Constructor 的 type hint 改成 Interface 即可,維護成本很低,所以在此大可使用 Repository Class 即可,不一定得用Interface而造成 Over Design,等真正需要修改時,再重構 Interface 即可。
#### 是否該使用 Query Scope?
Laravel 4.2 就有 QueryScope,到 Laravel5.1 都還保留著,它讓我們可以將邏輯代碼寫在 Model ,解決了維護與重復使用的問題。
如 `app/User.php` 里的代碼:
```
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
/**
* App\User
*
* @property int $id
* @property string $name
* @property string $email
* @property string $password
* @property string $remember_token
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
* @method static \Illuminate\Database\Query\Builder|\App\User whereCreatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereEmail($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereId($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereName($value)
* @method static \Illuminate\Database\Query\Builder|\App\User wherePassword($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereRememberToken($value)
* @method static \Illuminate\Database\Query\Builder|\App\User whereUpdatedAt($value)
* @mixin \Eloquent
*/
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
*
* @param Builder $query
* @param integer $age
*
* @return Builder
*/
public function scopeGetAgerLargerThan($query, $age)
{
return $query->where('age', '>', $age)
->orderBy('age');
}
}
```
QueryScope 必須以 `scope`開頭,第一個參數為 queryBuilder,一定要加上;第二個參數以后為自己要傳入的參數。
由于回傳必須是一個 queryBuilder ,因此不需要加上 `get()`
`app/Controllers/UserController.php` 中使用代碼:
```
<?php
namespace App\Http\Controllers;
use App\Repositories\UserRepository;
use App\User;
use Illuminate\Http\Request;
/**
* Class UserController
*
* @package App\Http\Controllers
*/
class UserController extends Controller
{
/**
* @var \App\Repositories\UserRepository
*/
protected $userRepository;
/**
* UserController constructor.
* @param $userRepository
*/
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function index()
{
return User::getAgerLargerThan(20)->get();
}
}
```
在 Controller 中使用 QueryScope 時,不需要加上 Prefix,由于其本質是 queryBuilder,所以還要加上 `get()` 才能獲得 Conllection 數據。
由于 QueryScope 是寫在 Model,不是寫在 Controller,所以基本上解決了 Controller 臃腫違反 SOLID 的單一職責原則的問題, Controller 也可以重復使用 QueryScope ,已經比直接將資料庫邏輯寫在 Controlelr 中好很多。
不過若在中大型項目中,仍然有以下問題:
* Model 已經有原來的責任,若再加上 queryScope,造成 Model 過于臃腫難以維護。
* 若數據庫邏輯很多,可能拆成多個 Repository,可是確很難拆成多個 Model。
* 單元測試困難,必須面臨 mock Eloquent 的問題。
### 最后
實際開發時,可以一開始 1 個 Repository 對應 1 個 Model,但是也不必太過執著于 1 個 Repository,一定要對應 1 個 Model,可將 Repository 視為邏輯上的數據庫邏輯類別即可,可以橫跨多個Model處理,也可以 1 個 Model 拆成多個 Repository,視情況而定。
Repository 使得數據庫邏輯從 Controller 或 Model 中解放,不僅更容易維護、更容易拓展、更容易重復使用,也更容易測試。
- 介紹
- Laravel5發送郵件使用Service隔離業務
- 如何使用Repository模式
- 如何使用Service模式
- 如何使用Presenter模式
- Laravel 5.* 執行遷移文件報錯:Specified key was too long error
- EloquentORM關聯關系
- EloquentORM關聯關系之一對一
- EloquentORM關聯關系之一對多
- EloquentORM關聯關系之遠層一對多
- EloquentORM關聯關系之多對多
- EloquentORM關聯關系之多態關聯
- EloquentORM關聯關系之多對多多態關聯
- Laravel測試
- Laravel中涉及認證跳轉地址的修改的地方
- Laravel中Collection的基本使用
- all
- avg
- chuck
- collapse
- combine
- contains
- containsStrict
- count
- diff
- diffAssoc
- diffKeys
- each
- every
- except
- filter
- first
- flatMap
- flatten
- flip
- forget
- forPage
- get
- groupBy
- has
- implode
- intersect
- intersectKey
- isEmpty
- isNotEmpty
- keyBy
- keys
- last
- map
- mapWithKeys
- max
- median
- merge
- min
- mode
- nth
- only
- partition
- pipe
- pluck
- pop
- prepend
- pull
- push
- put
- random
- reduce
- reject
- reverse
- search
- shift
- shuffle
- slice
- sort
- sortBy
- sortByDesc
- splice
- split
- sum
- take
- tap
- times
- toArray
- toJson
- transform
- union
- unique
- uniqueStrict
- values
- when
- where
- whereStrict
- whereIn
- whereInStrict
- whereNotIn
- whereNotInStrict
- zip
- Laravel中Collection的實際使用
- collection中sum求和
- collection格式化計算數據
- collection格式化計算數據計算github事件得分總和
- collection格式化markdown數據列表
- collection格式化計算兩個數組的數據
- collection中reduce創建lookup數組
- TODO