如何使用 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 ,会直接在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 中解放,不仅更容易维护、更容易拓展、更容易重复使用,也更容易测试。