Laravelでの関係の管理、リポジトリパターンの順守


120

Laravelの優れたデザインパターンに関するT. Otwellの本を読んだ後、Laravel 4でアプリを作成しているときに、アプリケーションのすべてのテーブルのリポジトリを作成していることに気付きました。

私は次のテーブル構造になりました:

  • 学生:ID、名前
  • コース:id、name、teacher_id
  • 教師:ID、名前
  • 割り当て:id、name、course_id
  • スコア(学生と課題の間のピボットとして機能):student_id、assignment_id、scores

これらすべてのテーブルの検索、作成、更新、削除メソッドを備えたリポジトリクラスがあります。各リポジトリには、データベースと対話するEloquentモデルがあります。関係は、Laravelのドキュメント(http://laravel.com/docs/eloquent#relationships)のモデルで定義されています。

新しいコースを作成するときは、コースリポジトリのcreateメソッドを呼び出すだけです。そのコースには課題があるので、課題を作成するときに、コースの各生徒のスコアのテーブルにエントリも作成します。これはアサインメントリポジトリを通じて行います。これは、課題リポジトリが2つのEloquentモデル、つまり課題モデルと生徒モデルと通信することを意味します。

私の質問は次のとおりです:このアプリはおそらくサイズが大きくなり、より多くの関係が導入されるため、リポジトリ内の異なるEloquentモデルと通信することは良い習慣ですか、またはこれを代わりに他のリポジトリを使用して行うべきですか(割り当てリポジトリから他のリポジトリを呼び出すことを意味します) )それともEloquentモデルで一緒に行う必要がありますか?

また、スコアテーブルを課題と生徒の間のピボットとして使用することは良い習慣ですか、それとも別の場所で行う必要がありますか?

回答:


71

あなたが意見を求めていることに留意してください:D

これが私のものです:

TL; DR:はい、それで結構です。

あなたはよくやっている!

私はあなたが頻繁に行っていることを正確に行い、それがうまく機能することを発見しました。

ただし、リポジトリごとのテーブルではなく、ビジネスロジックを中心にリポジトリを整理することがよくあります。これは、アプリケーションが「ビジネス上の問題」をどのように解決するかを中心とした視点であるので便利です。

コースは「エンティティ」であり、属性(タイトル、IDなど)と他のエンティティ(割り当て、独自の属性および場合によってはエンティティを持つ)さえ含まれます。

「コース」リポジトリは、コースとコースの属性/割り当て(割り当てを含む)を返すことができる必要があります。

Eloquentで幸運にもそれを達成できます。

(テーブルごとにリポジトリが作成されることがよくありますが、一部のリポジトリは他のリポジトリよりもはるかに多く使用されているため、より多くの方法があります。たとえば、アプリケーションの中心はコースであり、コースの割り当てのコレクションではありません)。

トリッキーな部分

データベースアクションを実行するために、リポジトリ内でリポジトリを使用することがよくあります。

データを処理するためにEloquentを実装するリポジトリは、おそらくEloquentモデルを返します。その観点から、コースモデルが割り当て(またはその他のユースケース)を取得または保存するために組み込みの関係を使用することは問題ありません。私たちの「実装」はEloquentを中心に構築されています。

実用的な観点からは、これは理にかなっています。Eloquentが処理できないもの(SQL以外のデータソース)にデータソースを変更することはほとんどありません。

ORMS

少なくとも私にとっては、このセットアップの最も難しい部分は、Eloquentが実際に私たちを助けているか害しているかを判断することです。ORMは、実用的な観点からは非常に役立ちますが、「ビジネスロジックエンティティ」コードとデータ検索を実行するコードを結合するため、扱いにくいテーマです。

この種のことは、リポジトリの責任が実際にデータの処理なのか、エンティティ(ビジネスドメインエンティティ)の取得/更新の処理なのかを混乱させます。

さらに、ビューに渡すまさにオブジェクトとして機能します。リポジトリでEloquentモデルを使用しないようにする必要がある場合は、ビューに渡される変数が同じように動作するか、同じメソッドを使用できることを確認する必要があります。そうしないと、データソースを変更すると、ビュー、そしてそもそもあなたはロジックをリポジトリに抽象化する目的を(部分的に)失いました-プロジェクトの保守性は低下します。

とにかく、これらはやや不完全な考えです。述べられたように、それらは単に私の意見であり、これはたまたま、昨年、ドメインドリブンデザインを読み、Ruby Midwestで「アンクルボブの」基調講演のようなビデオを視聴した結果です。


1
あなたの意見では、リポジトリが雄弁なオブジェクトの代わりにデータ転送オブジェクトを返す場合、それは良い選択肢でしょうか?もちろん、これは雄弁からdtoへの追加の変換を意味しますが、この方法では、少なくとも、コントローラー/ビューを現在のorm実装から分離します。
federivo

1
自身も少し実験してみましたが、実際的ではないことが少しわかりました。そうは言っても、私は抽象的にその考えが好きです。ただし、Illuminateのデータベースコレクションオブジェクトは配列と同じように機能し、モデルオブジェクトはStdClassオブジェクトと同じように十分に機能するので、実質的にはEloquentを使い続け、将来も必要に応じて配列/オブジェクトを使用できます。
fideloper 2013

4
@fideloperリポジトリを使用すると、Eloquentが提供するORMのすべての美しさを失うと感じています。リポジトリメソッドを介してアカウントオブジェクトを取得するとき、の$a = $this->account->getById(1)ようなメソッドを単純にチェーンすることはできません$a->getActiveUsers()。さて、私はを使用できました$a->users->...が、その後、Eloquentコレクションを返し、stdClassオブジェクトは返さず、再びEloquentに関連付けられました。これに対する解決策は何ですか?ユーザーリポジトリで別のメソッドを宣言します $user->getActiveUsersByAccount($a->id);か?あなたがこれをどのように解決するかを聞いてみたいです...
サンタクルス2014年

1
ORMは、このような問題を引き起こすため、エンタープライズレベルのアーキテクチャではひどいものです。最後に、アプリケーションにとって最も意味のあるものを決定する必要があります。個人的にEloquentでリポジトリを使用するとき(時間の90%です!)私はEloquentを使用して、stdClassesや配列などのモデルとコレクションを処理するために一生懸命に取り組んでいます(可能な場合!)。
fideloper 2014年

5
先に進んで、遅延読み込みモデルを使用してください。Eloquentの使用をスキップした場合は、実際のドメインモデルをそのように機能させることができます。しかし真剣に、あなたはEloquentをこれまでに切り替えるつもりですか?ペニーのために、ポンドのために!(「規則」に固執しようと船外に行かないでください!私はいつも私のすべてを壊します)。
fideloper、2014年

224

私はLaravel 4を使用して大規模なプロジェクトを仕上げており、あなたが今求めているすべての質問に答える必要がありました。Leanpubで入手可能なすべてのLaravelの本、および大量のグーグルを読んだ後、次の構造を思いつきました。

  1. データブルテーブルごとに1つのEloquent Modelクラス
  2. Eloquentモデルごとに1つのリポジトリクラス
  3. 複数のリポジトリクラス間で通信できるサービスクラス。

それでは、映画データベースを構築しているとしましょう。私は少なくとも以下のEloquent Modelクラスを持っています:

  • 映画
  • スタジオ
  • ディレクター
  • 俳優
  • レビュー

リポジトリクラスは、各Eloquentモデルクラスをカプセル化し、データベースでのCRUD操作を担当します。リポジトリクラスは次のようになります。

  • MovieRepository
  • StudioRepository
  • DirectorRepository
  • ActorRepository
  • ReviewRepository

各リポジトリクラスは、次のインターフェイスを実装する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クラスは、複数のリポジトリを結合するために使用され、アプリケーションの実際の「ビジネスロジック」を含みます。コントローラー、Create、Update、およびDeleteアクションの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.
}

コントローラーにデータを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は、データベースにアクター、ディレクター、またはスタジオのレコードを作成する方法を認識していないため、次のような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
このコメントは、はるかにクリーンで、スケーラブルで保守可能なアプローチです。
アンドレアス

4
+1!それは私を大いに助けます、私たちと共有してくれてありがとう!可能であれば、サービス内の物事をどのように検証できたのでしょうか。とにかくありがとうございました!:)
パウロフレイタス

6
@PauloFreitasが言ったように、検証部分の処理方法を確認するのは興味深いでしょう。また、例外部分にも興味があります(例外、イベントを使用するのか、それとも、あなたのサービスのブールリターンを介してコントローラ?)ありがとう!
ニコラ

11
コントローラはリポジトリに対して直接何もしないで、movieRepositoryを使用してpostCreateメソッドを実行してはならないため、movieRepositoryをMovieControllerに注入する理由がわかりません。 ?
davidnknight 2014

15
これに関する質問:なぜこの例でリポジトリを使用しているのですか?これは正直な質問です。私には、リポジトリを使用しているように見えますが、少なくともこの例では、リポジトリは実際には何も実行せず、Eloquentと同じインターフェイスを提供しています。サービスクラスが直接eloquentを使用しています($studio->movies()->associate($movie);)。
Kevin Mitchell

5

「正しいか間違っているか」ではなく、自分のコードが何をしていて責任があるかという観点から考えたいと思います。これが私の責任を分解する方法です:

  • コントローラーはHTTPレイヤーであり、要求を基になるAPIにルーティングします(別名、フローを制御します)
  • モデルはデータベーススキーマを表し、データがどのように見えるか、データにどのような関係があるか、必要なグローバル属性(名前と姓を連結して返すための名前メソッドなど)をアプリケーションに伝えます。
  • リポジトリは、より複雑なクエリとモデルとの相互作用を表します(私はモデルメソッドに対してクエリを実行しません)。
  • 検索エンジン-複雑な検索クエリの作成に役立つクラス。

これを念頭に置いて、リポジトリを使用することはいつでも理にかなっています(インターフェースなどを作成するかどうかは別のトピックです)。私がこのアプローチを気に入っているのは、特定の作業を行う必要があるときにどこに行くべきかが正確にわかるということです。

また、ベースリポジトリ、通常はメインのデフォルトを定義する抽象クラス(基本的にCRUD操作)を構築する傾向があります。その後、各子は必要に応じてメソッドを拡張および追加するか、デフォルトをオーバーロードできます。モデルを注入すると、このパターンが非常に堅牢になるのにも役立ちます。


BaseRepositoryの実装を示すことができますか?私も実際にこれをやっていて、あなたが何をしたか知りたいです。
Odyssee

getById、getByName、getByTitle、保存タイプのメソッドなどを考えてください。-一般に、さまざまなドメイン内のすべてのリポジトリに適用される方法。
オッドマン、

5

リポジトリは、(ORMだけでなく)データの一貫したファイリングキャビネットと考えてください。一貫したシンプルなAPIでデータを取得したいという考えです。

Model :: all()、Model :: find()、Model :: create()を実行しているだけの場合は、リポジトリを抽象化してもあまりメリットはありません。一方、クエリやアクションに対してもう少しビジネスロジックを実行したい場合は、リポジトリを作成して、データを処理するためのAPIをより使いやすくすることができます。

関連するモデルを接続するために必要なより冗長な構文のいくつかを処理するには、リポジトリが最善の方法かどうかとあなたは尋ねていたと思います。状況に応じて、私ができることはいくつかあります。

  1. 親モデル(1対1または1対多)から新しい子モデルをぶら下げて、子リポジトリにメソッドをcreateWithParent($attributes, $parentModelInstance)追加します。これにより、属性$parentModelInstance->idparent_idフィールドにが追加され、createが呼び出されます。

  2. 多対多の関係をアタッチして、実際にモデルに関数を作成して、$ instance-> attachChild($ childInstance)を実行できるようにします。これには、両側に既存の要素が必要であることに注意してください。

  3. 1回の実行で関連モデルを作成し、ゲートウェイと呼ぶものを作成します(ファウラーの定義から少しずれている可能性があります)。変更する可能性のある一連のロジックではなく、$ gateway-> createParentAndChild($ parentAttributes、$ childAttributes)を呼び出して、コントローラーやコマンドのロジックを複雑にする方法です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.