在 Laravel 中管理关系,遵循存储库模式
Posted
技术标签:
【中文标题】在 Laravel 中管理关系,遵循存储库模式【英文标题】:Managing relationships in Laravel, adhering to the repository pattern 【发布时间】:2013-09-19 23:44:04 【问题描述】:在阅读了 T. Otwell 关于 Laravel 中良好设计模式的书后,在 Laravel 4 中创建应用程序时,我发现自己为应用程序中的每个表都创建了存储库。
我最终得到了以下表格结构:
学生:身份证、姓名 课程:id、姓名、teacher_id 教师:身份证、姓名 作业:id、name、course_id 分数(作为学生和作业之间的枢纽):student_id、assignment_id、分数我有所有这些表的查找、创建、更新和删除方法的存储库类。每个存储库都有一个与数据库交互的 Eloquent 模型。根据 Laravel 的文档在模型中定义了关系:http://laravel.com/docs/eloquent#relationships。
创建新课程时,我所做的只是调用课程存储库上的 create 方法。该课程有作业,因此在创建作业时,我还想在分数表中为课程中的每个学生创建一个条目。我通过作业存储库执行此操作。这意味着作业存储库与两个 Eloquent 模型进行通信,即 Assignment 和 Student 模型。
我的问题是:由于这个应用程序的大小可能会增长,并且会引入更多的关系,所以在存储库中与不同的 Eloquent 模型进行通信是一种好习惯,还是应该使用其他存储库来完成(我的意思是从分配存储库)还是应该在 Eloquent 模型中一起完成?
另外,使用分数表作为作业和学生之间的枢纽是一种好习惯,还是应该在其他地方完成?
【问题讨论】:
【参考方案1】:请记住,您是在征求意见:D
这是我的:
TL;DR:是的,没关系。
你做得很好!
我做的正是你经常做的事情,发现效果很好。
然而,我经常围绕业务逻辑组织存储库,而不是为每个表创建一个 repo。这很有用,因为它是围绕您的应用程序应该如何解决“业务问题”的观点。
课程是一个“实体”,具有属性(标题、id 等)甚至其他实体(作业,具有自己的属性和可能的实体)。
您的“课程”存储库应该能够返回课程和课程的属性/作业(包括作业)。
幸运的是,您可以使用 Eloquent 实现这一目标。
(我通常会为每个表创建一个存储库,但有些存储库的使用率远高于其他存储库,因此有更多方法。例如,您的“课程”存储库可能比您的作业存储库功能更全面,如果您的申请更多地围绕课程而不是课程的作业集合)。
棘手的部分
我经常在我的存储库中使用存储库来执行一些数据库操作。
任何实现 Eloquent 以处理数据的存储库都可能返回 Eloquent 模型。有鉴于此,如果您的 Course 模型使用内置关系来检索或保存作业(或任何其他用例),那就没问题了。我们的“实现”是围绕 Eloquent 构建的。
从实际的角度来看,这是有道理的。我们不太可能将数据源更改为 Eloquent 无法处理的(非 sql 数据源)。
ORMS
至少对我而言,此设置中最棘手的部分是确定 Eloquent 是否真的在帮助或伤害我们。 ORM 是一个棘手的主题,因为虽然从实践的角度来看它们对我们有很大帮助,但它们还将您的“业务逻辑实体”代码与执行数据检索的代码耦合在一起。
这种混淆了您的存储库的职责实际上是处理数据还是处理实体(业务领域实体)的检索/更新。
此外,它们充当您传递给视图的对象。如果您以后不得不在存储库中使用 Eloquent 模型,则需要确保传递给视图的变量以相同的方式运行或具有相同的可用方法,否则更改数据源将导致更改您的数据源视图,并且您(部分)首先失去了将逻辑抽象到存储库的目的 - 您的项目的可维护性下降了。
无论如何,这些都是不完整的想法。如前所述,它们只是我的观点,这恰好是去年在 Ruby Midwest 阅读 Domain Driven Design 和观看 "uncle bob's" keynote 之类的视频的结果。
【讨论】:
在您看来,如果存储库返回数据传输对象而不是 eloquent 对象,这会是一个不错的选择吗?当然,这意味着从 eloquent 到 dto 的额外转换,但这样至少可以将控制器/视图与当前的 orm 实现隔离开来。 我已经experimented with that myself 有点,发现它有点不切实际。话虽如此,我确实喜欢抽象的想法。但是,Illuminate 的数据库 Collection 对象的行为就像数组一样,而 Model 对象的行为就像 StdClass 对象一样,因此我们实际上可以坚持使用 Eloquent,并且在将来需要时仍然使用数组/对象。 @fideloper 我感觉如果我使用存储库,我就会失去 Eloquent 提供的 ORM 的全部美感。当通过我的存储库方法$a = $this->account->getById(1)
检索帐户对象时,我不能简单地链接方法,如$a->getActiveUsers()
。好的,我可以使用$a->users->...
,但随后我返回了一个 Eloquent 集合并且没有 stdClass 对象,并且再次与 Eloquent 绑定。有什么办法解决这个问题?在用户存储库中声明另一个方法,如$user->getActiveUsersByAccount($a->id);
?很想听听您如何解决这个问题...
ORM 对于企业(ish)级架构来说很糟糕,因为它们会导致这样的问题。 最后,您必须决定什么对您的应用程序最有意义。 就个人而言,在将存储库与 Eloquent 一起使用时(90% 的时间!)我使用 Eloquent 并尽最大努力处理模型和集合像 stdClasses & Arrays (因为你可以!)所以如果我需要,切换到其他东西是可能的。
继续使用延迟加载模型。如果你曾经跳过使用 Eloquent,你可以让真正的领域模型像这样工作。但说真的,你会打算退出 Eloquent 吗?一分钱,一磅! (不要太过分地试图遵守“规则”!我总是打破我的一切)。【参考方案2】:
我喜欢根据我的代码在做什么以及它负责什么来思考它,而不是“对或错”。这就是我分解职责的方式:
控制器是 HTTP 层,将请求路由到底层 api(也就是控制流程) 模型代表数据库模式,并告诉应用程序数据是什么样子,它可能有什么关系,以及可能需要的任何全局属性(例如用于返回串联的名字和姓氏的名称方法) 存储库代表更复杂的查询以及与模型的交互(我不对模型方法进行任何查询)。 搜索引擎 - 帮助我构建复杂搜索查询的类。考虑到这一点,每次使用存储库都是有意义的(您是否创建interfaces.etc。完全是另一个话题)。我喜欢这种方法,因为这意味着当我需要做某些工作时,我确切地知道该去哪里。
我也倾向于构建一个基础存储库,通常是一个定义主要默认值的抽象类 - 基本上是 CRUD 操作,然后每个孩子都可以根据需要扩展和添加方法,或者重载默认值。注入你的模型也有助于这种模式变得非常健壮。
【讨论】:
你能展示你的 BaseRepository 的实现吗?实际上我也这样做,我很好奇你做了什么。 想想getById、getByName、getByTitle、保存类型methods.etc。 - 通常适用于各个域中的所有存储库的方法。【参考方案3】:将存储库视为您数据的一致文件柜(不仅仅是您的 ORM)。这个想法是您希望以一致且易于使用的 API 获取数据。
如果您发现自己只是在执行 Model::all()、Model::find()、Model::create(),您可能不会从存储库中抽象出太多好处。另一方面,如果您想对查询或操作执行更多业务逻辑,您可能需要创建一个存储库,以便更轻松地使用 API 来处理数据。
我想您是在问,存储库是否是处理连接相关模型所需的一些更冗长的语法的最佳方式。根据情况,我可能会做一些事情:
从父模型(一对一或一对多)中挂起一个新的子模型,我会在子存储库中添加一个类似createWithParent($attributes, $parentModelInstance)
的方法,这只会将$parentModelInstance->id
添加到属性的parent_id
字段并调用create。
附加多对多关系,我实际上在模型上创建函数,以便我可以运行 $instance->attachChild($childInstance)。请注意,这需要双方都存在现有元素。
在一次运行中创建相关模型,我创建了一个我称之为网关的东西(它可能与 Fowler 的定义有点不同)。我可以调用 $gateway->createParentAndChild($parentAttributes, $childAttributes) 而不是一堆可能会改变或会使控制器或命令中的逻辑复杂化的逻辑。
【讨论】:
【参考方案4】:我正在使用 Laravel 4 完成一个大型项目,并且必须回答您现在提出的所有问题。在阅读了 Leanpub 上所有可用的 Laravel 书籍以及大量谷歌搜索之后,我想出了以下结构。
-
每个数据表一个 Eloquent Model 类
每个 Eloquent 模型一个存储库类
可以在多个存储库类之间进行通信的服务类。
假设我正在构建一个电影数据库。我至少会有以下 Eloquent Model 类:
电影 工作室 导演 演员 审核存储库类将封装每个 Eloquent Model 类并负责数据库上的 CRUD 操作。存储库类可能如下所示:
电影资料库 StudioRepository DirectorRepository ActorRepository 评论存储库每个存储库类都将扩展一个实现以下接口的 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 类用于将多个存储库粘合在一起,并包含应用程序的真正“业务逻辑”。控制器仅与创建、更新和删除操作的服务类通信。
所以当我想在数据库中创建一条新的电影记录时,我的 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.
您可以决定如何将数据 POST 到控制器,但假设 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 模型。控制器不关心存储库,他们只是交出从用户那里收集的数据并将其传递给适当的服务。该服务不关心它接收到的数据如何保存到数据库中,它只是将控制器提供的相关数据交给适当的存储库。
【讨论】:
这条评论是迄今为止更简洁、更具可扩展性和可维护性的方法。 +1!这对我有很大帮助,感谢您与我们分享!想知道你是如何设法验证服务内部的东西的,如果可能的话,你能简要解释一下你做了什么吗?不管怎样,谢谢你! :) 就像@PauloFreitas 所说,看看你如何处理验证部分会很有趣,我也会对异常部分感兴趣(你使用异常、事件还是只处理正如您似乎通过服务中的布尔返回在您的控制器中建议的那样?)。谢谢! 写得好,虽然我不确定你为什么将movieRepository注入MovieController,因为控制器不应该直接对存储库做任何事情,你的postCreate方法也不应该使用movieRepository,所以我猜你是误把它丢了? 关于这个的问题:你为什么在这个例子中使用存储库?这是一个诚实的问题 - 对我来说,看起来你正在使用存储库,但至少在这个示例中,存储库并没有真正做任何事情,只是提供与 Eloquent 相同的接口,最后你仍然与 Eloquent 绑定,因为您的服务类直接在其中使用 eloquent ($studio->movies()->associate($movie);
)。以上是关于在 Laravel 中管理关系,遵循存储库模式的主要内容,如果未能解决你的问题,请参考以下文章