遵循存储库模式管理Laravel中的关系


120

在阅读了T. Otwell关于Laravel良好设计模式的书后,在Laravel 4中创建了一个应用程序时,我发现自己为应用程序中的每个表创建了存储库。

我最终得到以下表结构:

  • 学生:身份证,姓名
  • 课程:id,名称,teacher_id
  • 老师:身份证,姓名
  • 作业:id,名称,course_id
  • 分数(充当学生和作业之间的枢纽):student_id,assignment_id,分数

我具有所有这些表的查找,创建,更新和删除方法的存储库类。每个存储库都有一个与数据库交互的Eloquent模型。关系根据Laravel的文档在模型中定义:http ://laravel.com/docs/eloquent#relationships 。

创建新课程时,我要做的就是在课程库上调用create方法。该课程有作业,因此在创建作业时,我还想在分数表中为该课程中的每个学生创建一个条目。我是通过分配库来完成的。这意味着作业库与两个Eloquent模型(作业和学生模型)进行通信。

我的问题是:由于此应用程序的大小可能会增加,并且会引入更多的关系,因此是与存储库中不同的Eloquent模型进行通信的好习惯还是应该使用其他存储库来代替(我的意思是从Assignment存储库调用其他存储库)还是应该在Eloquent模型中一起完成?

另外,将分数表用作作业和学生之间的枢纽是一种好习惯,还是应该在其他地方完成?

Answers:


71

请记住,您正在征求意见:D

这是我的:

TL; DR:是的,很好。

你做的不错!

我完全按照您的常用方法工作,发现效果很好。

但是,我经常围绕业务逻辑来组织存储库,而不是使用每张表。这很有用,因为它是围绕应用程序应如何解决“业务问题”的观点。

课程是“实体”,具有属性(标题,ID等),甚至具有其他实体(作业,它们具有自己的属性,也可能具有实体)。

您的“课程”存储库应该能够返回课程和课程的属性/作业(包括作业)。

幸运的是,您可以通过Eloquent完成。

(我通常以每个表为一个存储库,但是某些存储库的使用量比其他存储库要多,因此有更多的方法。例如,如果您的“课程”存储库功能比“工作分配”存储库功能更全,应用程序以课程为中心,而以课程的作业集合为中心)。

棘手的部分

我经常在存储库内部使用存储库以执行一些数据库操作。

任何实现Eloquent以便处理数据的存储库都可能返回Eloquent模型。因此,如果您的课程模型使用内置关系来检索或保存作业(或任何其他用例),那就很好了。我们的“实现”是围绕Eloquent建立的。

从实际的角度来看,这是有道理的。我们不太可能将数据源更改为Eloquent无法处理的内容(更改为非sql数据源)。

ORMS

至少对我而言,这种设置中最棘手的部分是确定Eloquent是否确实在帮助或伤害了我们。ORM是一个棘手的主题,因为尽管从实际的角度来看它们对我们有很大帮助,但它们也将您的“业务逻辑实体”代码与进行数据检索的代码结合在一起。

这种混淆使您的存储库的责任实际上是处理数据还是处理实体(业务域实体)的检索/更新。

此外,它们是您传递给视图的对象。如果以后不得不放弃在存储库中使用Eloquent模型,则需要确保传递给视图的变量的行为相同或具有可用的方法,否则更改数据源将导致更改您的视图。视图,并且(部分地)失去了将逻辑抽象到存储库的目的-项目的可维护性下降了。

无论如何,这些都是不完整的想法。如前所述,它们只是我的看法,而这恰恰是去年在Ruby Midwest 上阅读Domain Driven Design并观看诸如“ Uncle Bob's”主题演讲之类的视频的结果。


1
您认为,如果存储库返回数据传输对象而不是雄辩的对象,这将是一个很好的选择吗?当然,这将意味着从雄辩到dto的额外转换,但是至少这样,您可以将控制器/视图与当前orm实现隔离。
federivo 2013年

1
自己做了一些试验,发现不切实际的一面。话虽如此,我确实喜欢抽象的想法。但是,Illuminate的数据库Collection对象的行为就像数组,而Model对象的行为就像StdClass对象,因此实际上,我们可以坚持Eloquent并在将来需要时仍然使用数组/对象。
fideloper 2013年

4
@fideloper我感觉如果使用存储库,我会失去Eloquent提供的ORM的全部美感。通过我的存储库方法检索帐户对象时,我$a = $this->account->getById(1)不能简单地链接诸如之类的方法$a->getActiveUsers()。好的,我可以使用$a->users->...,但是随后我返回了一个Eloquent集合,没有stdClass对象,并再次与Eloquent绑定在一起。有什么解决方案?在用户存储库中声明另一个方法,如 $user->getActiveUsersByAccount($a->id);?很想听听您如何解决这个问题……
santacruz 2014年

1
ORM对于企业级的体系结构来说很糟糕,因为它们会引起诸如此类的问题。最后,您必须决定什么对您的应用程序最有意义。就个人而言,当使用Eloquent的存储库(90%的时间!)时,我会使用Eloquent并尽力处理模型和集合,例如stdClasses和Arrays(因为您可以!),因此,如果需要的话,可以切换到其他方法。
fideloper 2014年

5
继续并使用延迟加载的模型。如果您曾经不使用Eloquent,则可以使真实域模型像这样工作。但是说真的,你永远放弃口才吗?一分钱,一磅!(不要过于努力地坚持“规则”!我一直都在破坏我的一切)。
fideloper 2014年

224

我正在使用Laravel 4完成一个大型项目,不得不回答您现在要问的所有问题。在精读Leanpub上所有可用的Laravel书和大量的Googling之后,我想到了以下结构。

  1. 每个数据表一个雄辩的模型类
  2. 每个雄辩模型一个存储库类
  3. 一个服务类,可以在多个存储库类之间进行通信。

假设我正在建立一个电影数据库。我至少将具有以下以下Eloquent Model类:

  • 电影
  • 工作室
  • 导向器
  • 演员
  • 评论

存储库类将封装每个Eloquent Model类,并负责数据库上的CRUD操作。存储库类可能如下所示:

  • 电影资料库
  • Studio存储库
  • 总监资料库
  • 演员库
  • 评论库

每个存储库类都会扩展BaseRepository类,该类实现以下接口:

interface BaseRepositoryInterface
{
    public function errors();

    public function all(array $related = null);

    public function get($id, array $related = null);

    public function getWhere($column, $value, array $related = null);

    public function getRecent($limit, array $related = null);

    public function create(array $data);

    public function update(array $data);

    public function delete($id);

    public function deleteWhere($column, $value);
}

Service类用于将多个存储库粘合在一起,并包含应用程序的真实“业务逻辑”。控制器与用于创建,更新和删除操作的服务类通信。

因此,当我想在数据库中创建新的Movie记录时,我的MovieController类可能具有以下方法:

public function __construct(MovieRepositoryInterface $movieRepository, MovieServiceInterface $movieService)
{
    $this->movieRepository = $movieRepository;
    $this->movieService = $movieService;
}

public function postCreate()
{
    if( ! $this->movieService->create(Input::all()))
    {
        return Redirect::back()->withErrors($this->movieService->errors())->withInput();
    }

    // New movie was saved successfully. Do whatever you need to do here.
}

由您决定如何将数据发布到控制器,但是让我们说由postCreate()方法中Input :: all()返回的数据看起来像这样:

$data = array(
    'movie' => array(
        'title'    => 'Iron Eagle',
        'year'     => '1986',
        'synopsis' => 'When Doug\'s father, an Air Force Pilot, is shot down by MiGs belonging to a radical Middle Eastern state, no one seems able to get him out. Doug finds Chappy, an Air Force Colonel who is intrigued by the idea of sending in two fighters piloted by himself and Doug to rescue Doug\'s father after bombing the MiG base.'
    ),
    'actors' => array(
        0 => 'Louis Gossett Jr.',
        1 => 'Jason Gedrick',
        2 => 'Larry B. Scott'
    ),
    'director' => 'Sidney J. Furie',
    'studio' => 'TriStar Pictures'
)

由于MovieRepository不应该知道如何在数据库中创建Actor,Director或Studio记录,因此我们将使用MovieService类,该类可能看起来像这样:

public function __construct(MovieRepositoryInterface $movieRepository, ActorRepositoryInterface $actorRepository, DirectorRepositoryInterface $directorRepository, StudioRepositoryInterface $studioRepository)
{
    $this->movieRepository = $movieRepository;
    $this->actorRepository = $actorRepository;
    $this->directorRepository = $directorRepository;
    $this->studioRepository = $studioRepository;
}

public function create(array $input)
{
    $movieData    = $input['movie'];
    $actorsData   = $input['actors'];
    $directorData = $input['director'];
    $studioData   = $input['studio'];

    // In a more complete example you would probably want to implement database transactions and perform input validation using the Laravel Validator class here.

    // Create the new movie record
    $movie = $this->movieRepository->create($movieData);

    // Create the new actor records and associate them with the movie record
    foreach($actors as $actor)
    {
        $actorModel = $this->actorRepository->create($actor);
        $movie->actors()->save($actorModel);
    }

    // Create the director record and associate it with the movie record
    $director = $this->directorRepository->create($directorData);
    $director->movies()->associate($movie);

    // Create the studio record and associate it with the movie record
    $studio = $this->studioRepository->create($studioData);
    $studio->movies()->associate($movie);

    // Assume everything worked. In the real world you'll need to implement checks.
    return true;
}

因此,我们剩下的是很好的,明智的关注点分离。存储库只知道它们插入和从数据库检索的Eloquent模型。控制器不在乎存储库,它们只是传递从用户那里收集的数据并将其传递给适当的服务。该服务不在乎接收到的数据如何保存到数据库中,它只是将控制器提供的相关数据移交给相应的存储库。


8
到目前为止,此评论是一种更清洁,更具伸缩性和可维护性的方法。
2014年

4
+1!这对我有很大帮助,谢谢您与我们分享!想知道如何设法验证服务中的内容(如果可能),能否简要说明一下您所做的事情?还是很感谢你!:)
Paulo Freitas 2014年

6
就像@PauloFreitas所说的,很高兴看到您如何处理验证部分,我也对异常部分感兴趣(您使用异常,事件还是按照您似乎在建议中的方式处理它?控制器通过服务中的布尔值返回?)。谢谢!
Nicolas

11
写得很好,尽管我不确定为什么您要将movieRepository注入MovieController中,因为控制器不应直接对存储库执行任何操作,也不应该使用movieRepository使用postCreate方法,所以我假设您误将其留在了?
davidnknight 2014年

15
有关此问题:为什么在此示例中使用存储库?这是一个诚实的问题-对我来说,看来您正在使用存储库,但至少在此示例中,存储库实际上没有做任何事情,但提供了与Eloquent相同的接口,最后您仍然与Eloquent绑定在一起,因为您的服务类别直接在其中使用雄辩($studio->movies()->associate($movie);)。
凯文·米切尔

5

我喜欢从我的代码在做什么和它负责的角度来思考它,而不是“对与错”。这是我分开职责的方式:

  • 控制器是HTTP层,将请求路由到基础api(也就是它控制流程)
  • 模型代表数据库模式,并告诉应用程序数据看起来像什么,它可能具有什么关系,以及可能需要的任何全局属性(例如用于返回串联的姓氏和名字的名称方法)
  • 存储库表示更复杂的查询以及与模型的交互(我对模型方法不做任何查询)。
  • 搜索引擎-帮助我构建复杂搜索查询的类。

考虑到这一点,每次使用存储库都是有意义的(无论您是否创建interfaces.etc。都是另一个主题)。我喜欢这种方法,因为它意味着我确切地知道当我需要执行某些工作时应该去哪里。

我也倾向于建立一个基础存储库,通常是一个抽象类,该类定义了主要的默认值-基本是CRUD操作,然后每个子级都可以根据需要扩展和添加方法,或者重载默认值。注入模型也有助于使这种模式变得非常健壮。


您可以显示BaseRepository的实现吗?我实际上也这样做,我很好奇您的所作所为。
奥德赛(Odyssee)

考虑一下getById,getByName,getByTitle,保存类型method.etc。-通常适用于各个域内所有存储库的方法。
Oddman

5

将存储库视为数据的一致文件柜(不仅仅是ORM)。这个想法是,您想使用一致的,易于使用的API来获取数据。

如果您发现自己只是在执行Model :: all(),Model :: find(),Model :: create(),那么抽象存储库可能不会给您带来太多好处。另一方面,如果您想对查询或操作执行更多的业务逻辑,则可能需要创建一个存储库,以使API易于使用来处理数据。

我想您是在问存储库是否是处理连接相关模型所需的一些更冗长语法的最佳方法。根据情况,我可以做一些事情:

  1. 将新的子模型挂在父模型(一对一或一对多)上,我将向子存储库添加类似的方法createWithParent($attributes, $parentModelInstance),这只会将属性添加$parentModelInstance->idparent_id属性字段并调用create。

  2. 附加许多关系,实际上我在模型上创建了函数,以便可以运行$ instance-> attachChild($ childInstance)。请注意,这需要在两侧都存在现有元素。

  3. 在一次运行中创建相关模型,我创建了一个我称为网关的东西(它可能与Fowler的定义有点不同)。我可以调用$ gateway-> createParentAndChild($ parentAttributes,$ childAttributes)的方式,而不是一堆可能更改或使我在控制器或命令中具有的逻辑复杂化的逻辑。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.