若将数据库逻辑都写在model,会造成model的肥大而难以维护,基于SOLID原则,我们应该使用Repository模式辅助model,将相关的数据库逻辑封装在不同的repository,方便中大型项目的维护。

Version:Laravel 5.1.22


数据库逻辑

在CRUD中,CUD比较稳定,但R的部分则千变万化,大部分的数据库逻辑都在描述R的部分,若将数据库逻辑写在controller或model都不适当,会造成controller与model肥大,造成日后难以维护。

Model

使用repository之后,model仅当成Eloquent class即可,不要包含数据库逻辑,仅保留以下部分:

  • Property:如$table,$fillable…等。
  • Mutator:包括mutator与accessor。
  • Method:relation类的method,如使用hasMany()与belongsTo()。
  • 注释:因为Eloquent会根据数据库字段动态产生property与method,等。若使用Laravel IDE Helper,会直接在model加上@property@method描述model的动态property与method。

User.php

  1. app/User.php
  2. namespace MyBlog;
  3. use Illuminate\Auth\Authenticatable;
  4. use Illuminate\Database\Eloquent\Model;
  5. use Illuminate\Auth\Passwords\CanResetPassword;
  6. use Illuminate\Foundation\Auth\Access\Authorizable;
  7. use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
  8. use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
  9. use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
  10. /**
  11. * MyBlog\User
  12. *
  13. * @property integer $id
  14. * @property string $name
  15. * @property string $email
  16. * @property string $password
  17. * @property string $remember_token
  18. * @property \Carbon\Carbon $created_at
  19. * @property \Carbon\Carbon $updated_at
  20. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereId($value)
  21. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereName($value)
  22. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereEmail($value)
  23. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User wherePassword($value)
  24. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereRememberToken($value)
  25. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereCreatedAt($value)
  26. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereUpdatedAt($value)
  27. */
  28. class User extends Model implements AuthenticatableContract,
  29. AuthorizableContract,
  30. CanResetPasswordContract
  31. {
  32. use Authenticatable, Authorizable, CanResetPassword;
  33. /**
  34. * The database table used by the model.
  35. *
  36. * @var string
  37. */
  38. protected $table = 'users';
  39. /**
  40. * The attributes that are mass assignable.
  41. *
  42. * @var array
  43. */
  44. protected $fillable = ['name', 'email', 'password'];
  45. /**
  46. * The attributes excluded from the model's JSON form.
  47. *
  48. * @var array
  49. */
  50. protected $hidden = ['password', 'remember_token'];
  51. }

 

12行

  1. /**
  2. * MyBlog\User
  3. *
  4. * @property integer $id
  5. * @property string $name
  6. * @property string $email
  7. * @property string $password
  8. * @property string $remember_token
  9. * @property \Carbon\Carbon $created_at
  10. * @property \Carbon\Carbon $updated_at
  11. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereId($value)
  12. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereName($value)
  13. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereEmail($value)
  14. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User wherePassword($value)
  15. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereRememberToken($value)
  16. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereCreatedAt($value)
  17. * @method static \Illuminate\Database\Query\Builder|\MyBlog\User whereUpdatedAt($value)
  18. */

 

IDE-Helper帮我们替model加上注释,让我们可以在PhpStorm的语法提示使用model的property与method

Repository

初学者常会在controller直接调用model写数据库逻辑:

  1. public function index()
  2. {
  3. $users = User::where('age', '>', 20)
  4. ->orderBy('age')
  5. ->get();
  6. return view('users.index', compact('users'));
  7. }

 

数据库逻辑是要抓20岁以上的数据。
在中大型项目,会有几个问题:

  1. 将数据库逻辑写在controller,造成controller的肥大难以维护。
  2. 违反SOLID的单一职责原则:数据库逻辑不应该写在controller。
  3. controller直接相依于model,使得我们无法对controller做单元测试。
    比较好的方式是使用repository:
  4. 将model依赖注入到repository。
  5. 将数据库逻辑写在repository。
  6. 将repository依赖注入到service。
    UserRepository.php

    1. app/Repositories/UserRepository.php
    2. namespace MyBlog\Repositories;
    3. use Doctrine\Common\Collections\Collection;
    4. use MyBlog\User;
    5. class UserRepository
    6. {
    7. /** @var User 注入的User model */
    8. protected $user;
    9. /**
    10. * UserRepository constructor.
    11. * @param User $user
    12. */
    13. public function __construct(User $user)
    14. {
    15. $this->user = $user;
    16. }
    17. /**
    18. * 回传大于?年纪的数据
    19. * @param integer $age
    20. * @return Collection
    21. */
    22. public function getAgeLargerThan($age)
    23. {
    24. return $this->user
    25. ->where('age', '>', $age)
    26. ->orderBy('age')
    27. ->get();
    28. }
    29. }

     

    第 8 行

    1. /** @var User 注入的User model */
    2. protected $user;
    3. /**
    4. * UserRepository constructor.
    5. * @param User $user
    6. */
    7. public function __construct(User $user)
    8. {
    9. $this->user = $user;
    10. }

     

    将相依的User model依赖注入到UserRepository。
    21 行

    1. /**
    2. * 回传大于?年纪的数据
    3. * @param integer $age
    4. * @return Collection
    5. */
    6. public function getAgeLargerThan($age)
    7. {
    8. return $this->user
    9. ->where('age', '>', $age)
    10. ->orderBy('age')
    11. ->get();
    12. }

     

    将抓20岁以上的数据的数据库逻辑写在getAgeLargerThan()。
    不是使用User facade,而是使用注入的$this->user
    UserController.php
    app/Http/Controllers/UserController.php

    1. namespace App\Http\Controllers;
    2. use App\Http\Requests;
    3. use MyBlog\Repositories\UserRepository;
    4. class UserController extends Controller
    5. {
    6. /** @var UserRepository 注入的UserRepository */
    7. protected $userRepository;
    8. /**
    9. * UserController constructor.
    10. *
    11. * @param UserRepository $userRepository
    12. */
    13. public function __construct(UserRepository $userRepository)
    14. {
    15. $this->userRepository = $userRepository;
    16. }
    17. /**
    18. * Display a listing of the resource.
    19. *
    20. * @return \Illuminate\Http\Response
    21. */
    22. public function index()
    23. {
    24. $users = $this->userRepository
    25. ->getAgeLargerThan(20);
    26. return view('users.index', compact('users'));
    27. }
    28. }

     

    第8行

    1. /** @var UserRepository 注入的UserRepository */
    2. protected $userRepository;
    3. /**
    4. * UserController constructor.
    5. *
    6. * @param UserRepository $userRepository
    7. */
    8. public function __construct(UserRepository $userRepository)
    9. {
    10. $this->userRepository = $userRepository;
    11. }

     

    将相依的UserRepository依赖注入到UserController。
    26行

    1. /**
    2. * Display a listing of the resource.
    3. *
    4. * @return \Illuminate\Http\Response
    5. */
    6. public function index()
    7. {
    8. $users = $this->userRepository
    9. ->getAgeLargerThan(20);
    10. return view('users.index', compact('users'));
    11. }

     

    从原本直接相依的User model,改成依赖注入的UserRepository。
    改用这种写法,有几个优点:

  • 将数据库逻辑写在repository,解决controller肥大问题。
  • 符合SOLID的单一职责原则:数据库逻辑写在repository,没写在controller。
  • 符合SOLID的依赖反转原则:controller并非直接相依于repository,而是将repository依赖注入进controller。

实务上建议repository仅依赖注入于service,而不要直接注入在controller,本示例因为还没介绍到servie模式,为了简化起见,所以直接注入于controller。

是否该建立Repository Interface?
理论上使用依赖注入时,应该使用interface,不过interface目的在于抽象化方便抽换,让代码达到开放封闭的要求,但是实务上要抽换repository的机会不高,除非你有抽换数据库的需求,如从MySQL抽换到MongoDB,此时就该建立repository interface。
不过由于我们使用了依赖注入,将来要从class改成interface也很方便,只要在constructor的type hint改成interface即可,维护成本很低,所以在此大可使用repository class即可,不一定得用interface而造成over design,等真正需求来时再重构成interface即可。
是否该使用Query Scope?
Laravel 4.2就有query scope,到5.1都还留着,它让我们可以将商业逻辑写在model,解决了维护与重复使用的问题。
User.php
app/User.php

  1. namespace MyBlog;
  2. use Illuminate\Auth\Authenticatable;
  3. use Illuminate\Database\Eloquent\Builder;
  4. use Illuminate\Database\Eloquent\Model;
  5. use Illuminate\Auth\Passwords\CanResetPassword;
  6. use Illuminate\Foundation\Auth\Access\Authorizable;
  7. use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
  8. use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
  9. use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
  10. /**
  11. * (註解:略)
  12. */
  13. class User extends Model implements AuthenticatableContract,
  14. AuthorizableContract,
  15. CanResetPasswordContract
  16. {
  17. use Authenticatable, Authorizable, CanResetPassword;
  18. /**
  19. * The database table used by the model.
  20. *
  21. * @var string
  22. */
  23. protected $table = 'users';
  24. /**
  25. * The attributes that are mass assignable.
  26. *
  27. * @var array
  28. */
  29. protected $fillable = ['name', 'email', 'password'];
  30. /**
  31. * The attributes excluded from the model's JSON form.
  32. *
  33. * @var array
  34. */
  35. protected $hidden = ['password', 'remember_token'];
  36. /**
  37. * 回传大于?年纪的数据
  38. * @param Builder $query
  39. * @param integer $age
  40. * @return Builder
  41. */
  42. public function scopeGetAgerLargerThan($query, $age)
  43. {
  44. return $query->where('age', '>', $age)
  45. ->orderBy('age');
  46. }
  47. }

 

42行

  1. /**
  2. * 回传大于?年纪的数据
  3. * @param Builder $query
  4. * @param integer $age
  5. * @return Builder
  6. */
  7. public function scopeGetAgerLargerThan($query, $age)
  8. {
  9. return $query->where('age', '>', $age)
  10. ->orderBy('age');
  11. }

 

Query scope必须以scope为prefix,第1个参数为query builder,一定要加,是Laravel要用的。
第2个参数以后为自己要传入的参数。
由于回传也必须是一个query builder,因此不加上get()。
UserController.php

  1. app/Http/Controllers/UserController.php
  2. namespace App\Http\Controllers;
  3. use App\Http\Requests;
  4. use MyBlog\User;
  5. class UserController extends Controller
  6. {
  7. /**
  8. * Display a listing of the resource.
  9. *
  10. * @return \Illuminate\Http\Response
  11. */
  12. public function index()
  13. {
  14. $users = User::getAgerLargerThan(20)->get();
  15. return view('users.index', compact('users'));
  16. }
  17. }

 

在controller呼叫query scope时,不要加上prefix,由于其本质是query builder,所以还要加上get()才能抓到Collection。
由于query scope是写在model,不是写在controller,所以基本上解决了controller肥大与违反SOLID的单一职责原则的问题,controller也可以重复使用query scope,已经比直接将数据库逻辑写在controller好很多了。

不过若在中大型项目,仍有以下问题:

  1. Model已经有原来的责任,若再加上query scope,造成model过于肥大难以维护。
  2. 若数据库逻辑很多,可以拆成多repository,可是却很难拆成多model。
  3. 单元测试困难,必须面临mock Eloquent的问题。

Conclusion

实务上可以一开始1个repository对应1个model,但不用太执着于1个repository一定要对应1个model,可将repository视为逻辑上的数据库逻辑类别即可,可以横跨多个model处理,也可以1个model拆成多个repository,端看需求而定。
Repository使得数据库逻辑从controller或model中解放,不仅更容易维护、更容易扩展、更容易重复使用,且更容易测试。
Sample Code
完整的示例可以在我的GitHub上找到。

转载自:http://www.sangeng.org/blog/detail_518.html

最后更新于 2022年6月26日

Laravel框架中使用 Repository 模式
标签: