Quản lý các mối quan hệ trong Laravel, tuân theo mô hình kho lưu trữ


120

Trong khi tạo một ứng dụng trong Laravel 4 sau khi đọc cuốn sách của T. Otwell về các mẫu thiết kế tốt trong Laravel, tôi thấy mình đã tạo kho lưu trữ cho mọi bảng trên ứng dụng.

Tôi đã kết thúc với cấu trúc bảng sau:

  • Sinh viên: id, tên
  • Các khóa học: id, name, teacher_id
  • Giáo viên: id, name
  • Bài tập: id, name, course_id
  • Điểm (đóng vai trò như một trục xoay giữa sinh viên và bài tập): student_id, task_id, điểm số

Tôi có các lớp kho lưu trữ với các phương thức tìm, tạo, cập nhật và xóa cho tất cả các bảng này. Mỗi kho lưu trữ có một mô hình Eloquent tương tác với cơ sở dữ liệu. Các mối quan hệ được định nghĩa trong mô hình theo tài liệu của Laravel: http://laravel.com/docs/eloquent#relationships .

Khi tạo một khóa học mới, tất cả những gì tôi làm là gọi phương thức create trên Kho lưu trữ khóa học. Khóa học đó có các bài tập, vì vậy khi tạo một bài tập, tôi cũng muốn tạo một mục trong bảng điểm cho từng học sinh trong khóa học. Tôi thực hiện việc này thông qua Kho lưu trữ Bài tập. Điều này ngụ ý rằng kho lưu trữ bài tập giao tiếp với hai mô hình Eloquent, với mô hình Bài tập và Sinh viên.

Câu hỏi của tôi là: vì ứng dụng này có thể sẽ phát triển về quy mô và nhiều mối quan hệ hơn sẽ được giới thiệu, liệu có nên giao tiếp với các mô hình Eloquent khác nhau trong kho lưu trữ hay không hay nên thực hiện việc này bằng cách sử dụng các kho lưu trữ khác (ý ​​tôi là gọi các kho lưu trữ khác từ kho Bài tập ) hay nên thực hiện tất cả các mô hình Eloquent?

Ngoài ra, việc sử dụng bảng điểm làm bảng tổng hợp giữa bài tập và học sinh có tốt không hay nên thực hiện ở một nơi khác?

Câu trả lời:


71

Hãy nhớ rằng bạn đang hỏi ý kiến: D

Đây là của tôi:

TL; DR: Vâng, tốt thôi.

Bạn đang làm tốt!

Tôi làm chính xác những gì bạn đang làm thường xuyên và thấy nó hoạt động tuyệt vời.

Tuy nhiên, tôi thường tổ chức các kho lưu trữ xung quanh logic nghiệp vụ thay vì có một kho trên mỗi bảng. Điều này rất hữu ích vì đó là một quan điểm tập trung vào cách ứng dụng của bạn nên giải quyết "vấn đề kinh doanh" của bạn.

Khóa học là một "thực thể", có các thuộc tính (tiêu đề, id, v.v.) và thậm chí các thực thể khác (Bài tập, có thuộc tính riêng và có thể là thực thể).

Kho lưu trữ "Khóa học" của bạn sẽ có thể trả về Khóa học và các thuộc tính / Bài tập của Khóa học (bao gồm Bài tập).

Bạn có thể hoàn thành điều đó với Eloquent, thật may mắn.

(Tôi thường kết thúc với một kho lưu trữ trên mỗi bảng, nhưng một số kho lưu trữ được sử dụng nhiều hơn các kho lưu trữ khác và do đó có nhiều phương pháp hơn. Kho lưu trữ "các khóa học" của bạn có thể đầy đủ tính năng hơn nhiều so với kho Bài tập của bạn, nếu ứng dụng tập trung nhiều hơn vào các Khóa học và ít hơn về bộ sưu tập Bài tập của một Khóa học).

Phần khó

Tôi thường sử dụng kho lưu trữ bên trong kho lưu trữ của mình để thực hiện một số hành động đối với cơ sở dữ liệu.

Bất kỳ kho lưu trữ nào triển khai Eloquent để xử lý dữ liệu sẽ có khả năng trả về các mô hình Eloquent. Xét về khía cạnh đó, sẽ tốt nếu mô hình Khóa học của bạn sử dụng các mối quan hệ có sẵn để truy xuất hoặc lưu Bài tập (hoặc bất kỳ trường hợp sử dụng nào khác). "Triển khai" của chúng tôi được xây dựng xung quanh Eloquent.

Từ quan điểm thực tế, điều này có ý nghĩa. Chúng tôi không thể thay đổi nguồn dữ liệu thành thứ mà Eloquent không thể xử lý (thành nguồn dữ liệu không phải sql).

ORMS

Đối với tôi, phần khó nhất của thiết lập này là xác định xem Eloquent thực sự đang giúp đỡ hay làm hại chúng ta. ORM là một chủ đề phức tạp, bởi vì mặc dù chúng giúp chúng tôi rất nhiều từ quan điểm thực tế, chúng cũng ghép mã "các thực thể logic nghiệp vụ" của bạn với mã thực hiện truy xuất dữ liệu.

Loại này làm xáo trộn liệu trách nhiệm của kho lưu trữ của bạn có thực sự là xử lý dữ liệu hay xử lý truy xuất / cập nhật các thực thể (thực thể miền doanh nghiệp) hay không.

Hơn nữa, chúng hoạt động như chính những đối tượng mà bạn truyền cho các quan điểm của mình. Nếu sau này bạn phải ngừng sử dụng các mô hình Eloquent trong kho lưu trữ, bạn sẽ cần đảm bảo các biến được chuyển đến các chế độ xem của bạn hoạt động theo cùng một cách hoặc có các phương thức tương tự, nếu không, việc thay đổi nguồn dữ liệu sẽ dẫn đến việc thay đổi và bạn (một phần) đã mất mục đích trừu tượng hóa logic của mình ra khỏi kho lưu trữ ngay từ đầu - khả năng bảo trì của dự án của bạn giảm xuống.

Dù sao thì đó cũng là những suy nghĩ có phần chưa hoàn thiện. Như đã nói, chúng chỉ đơn thuần là ý kiến ​​của tôi, là kết quả của việc đọc Thiết kế theo hướng miền và xem các video như bài phát biểu của "chú bob" tại Ruby Midwest trong năm ngoái.


1
Theo bạn, sẽ là một lựa chọn tốt nếu các kho lưu trữ trả về các đối tượng truyền dữ liệu thay vì các đối tượng hùng hồn? Tất nhiên điều này sẽ ngụ ý một chuyển đổi bổ sung từ hùng hồn sang dto nhưng bằng cách này, ít nhất, bạn tách biệt bộ điều khiển / chế độ xem của mình khỏi triển khai orm hiện tại.
Federationrivo

1
Tôi đã tự mình thử nghiệm điều đó một chút và thấy nó hơi phi thực tế. Điều đó đang được nói, tôi thích ý tưởng đó trong phần tóm tắt. Tuy nhiên, các đối tượng Collection trong cơ sở dữ liệu của Illuminate hoạt động giống như các mảng và các đối tượng Model hoạt động giống như các đối tượng StdClass đủ để chúng ta có thể, thực tế mà nói, gắn bó với Eloquent và vẫn sử dụng mảng / đối tượng trong tương lai nếu chúng ta cần.
fideloper

4
@fideloper Tôi cảm thấy rằng nếu tôi sử dụng các kho lưu trữ, tôi sẽ mất toàn bộ vẻ đẹp của ORM mà Eloquent cung cấp. Khi truy xuất một đối tượng tài khoản thông qua phương thức kho lưu trữ của $a = $this->account->getById(1)tôi, tôi không thể chỉ đơn giản là các phương thức chuỗi như $a->getActiveUsers(). Được rồi, tôi có thể sử dụng $a->users->..., nhưng sau đó tôi đang trả về một bộ sưu tập Eloquent và không có đối tượng stdClass và được liên kết lại với Eloquent. Giải pháp cho điều này là gì? Khai báo phương thức khác trong kho người dùng như thế $user->getActiveUsersByAccount($a->id);nào? Rất muốn nghe cách bạn giải quyết vấn đề này ...
santacruz

1
ORM rất tệ đối với kiến ​​trúc cấp Enterprise (ish) vì chúng gây ra các vấn đề như thế này. Cuối cùng, bạn phải quyết định điều gì có ý nghĩa nhất cho ứng dụng của mình. Cá nhân khi sử dụng kho lưu trữ với Eloquent (90% thời gian!) Tôi sử dụng Eloquent và cố gắng hết sức để xử lý các mô hình & bộ sưu tập như stdClasses & Arrays (vì bạn có thể!) Vì vậy, nếu cần, tôi có thể chuyển sang thứ khác.
fideloper

5
Hãy tiếp tục và sử dụng các mô hình lười tải. Bạn có thể làm cho các mô hình miền thực hoạt động như vậy nếu bạn bỏ qua việc sử dụng Eloquent. Nhưng nghiêm túc, bạn sẽ switch ra hùng biện bao giờ? Đổi một xu, đổi một pound! (Đừng đi quá đà khi cố gắng tuân theo "các quy tắc"! Tôi phá vỡ tất cả các quy tắc của tôi mọi lúc).
fideloper

224

Tôi đang hoàn thành một dự án lớn bằng Laravel 4 và phải trả lời tất cả các câu hỏi bạn đang hỏi ngay bây giờ. Sau khi đọc tất cả các sách Laravel hiện có trên Leanpub và hàng tấn Google Googling, tôi đã nghĩ ra cấu trúc sau.

  1. Một lớp Mô hình Eloquent cho mỗi bảng có thể dữ liệu
  2. Một lớp Kho lưu trữ cho mỗi Mô hình Eloquent
  3. Một lớp Dịch vụ có thể giao tiếp giữa nhiều lớp Kho lưu trữ.

Vì vậy, giả sử tôi đang xây dựng một cơ sở dữ liệu phim. Tôi sẽ có ít nhất các lớp Mô hình Hùng biện sau:

  • Bộ phim
  • Studio
  • Giám đốc
  • Diễn viên
  • Ôn tập

Một lớp kho lưu trữ sẽ đóng gói từng lớp Eloquent Model và chịu trách nhiệm về các hoạt động CRUD trên cơ sở dữ liệu. Các lớp kho lưu trữ có thể trông giống như sau:

  • MovieRepository
  • StudioRepository
  • DirectorRepository
  • ActorRepository
  • ReviewRepository

Mỗi lớp kho lưu trữ sẽ mở rộng một lớp BaseRepository triển khai giao diện sau:

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);
}

Một lớp Dịch vụ được sử dụng để gắn nhiều kho lưu trữ lại với nhau và chứa "logic nghiệp vụ" thực của ứng dụng. Bộ điều khiển chỉ giao tiếp với các lớp Dịch vụ cho các hành động Tạo, Cập nhật và Xóa.

Vì vậy, khi tôi muốn tạo một bản ghi Phim mới trong cơ sở dữ liệu, lớp MovieController của tôi có thể có các phương thức sau:

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.
}

Tùy thuộc vào bạn để xác định cách bạn ĐĂNG dữ liệu lên bộ điều khiển của mình, nhưng giả sử dữ liệu được trả về bởi Input :: all () trong phương thức postCreate () trông giống như sau:

$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'
)

Vì MovieRepository không biết cách tạo các bản ghi Actor, Director hoặc Studio trong cơ sở dữ liệu, chúng tôi sẽ sử dụng lớp MovieService của mình, lớp này có thể trông giống như sau:

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;
}

Vì vậy, những gì chúng ta còn lại là một sự tách biệt tốt đẹp, hợp lý của các mối quan tâm. Các kho lưu trữ chỉ biết đến mô hình Eloquent mà chúng chèn và truy xuất từ ​​cơ sở dữ liệu. Bộ điều khiển không quan tâm đến kho lưu trữ, họ chỉ chuyển dữ liệu họ thu thập từ người dùng và chuyển nó đến dịch vụ thích hợp. Dịch vụ không quan tâm dữ liệu nó nhận được lưu vào cơ sở dữ liệu như thế nào , nó chỉ chuyển dữ liệu có liên quan mà bộ điều khiển cung cấp cho các kho lưu trữ thích hợp.


8
Nhận xét này cho đến nay là cách tiếp cận sạch hơn, dễ mở rộng hơn và có thể bảo trì.
Andreas

4
+1! Điều đó sẽ giúp ích cho tôi rất nhiều, cảm ơn bạn đã chia sẻ với chúng tôi! Tự hỏi làm thế nào bạn quản lý để xác thực những thứ bên trong các dịch vụ, nếu có thể, bạn có thể giải thích ngắn gọn những gì bạn đã làm không? Cảm ơn bạn anyway! :)
Paulo Freitas

6
Giống như @PauloFreitas đã nói, sẽ rất thú vị khi xem cách bạn xử lý phần xác thực và tôi cũng quan tâm đến phần ngoại lệ (bạn có sử dụng ngoại lệ, sự kiện hay bạn chỉ xử lý điều này như bạn đề xuất trong điều khiển thông qua lợi nhuận boolean trong các dịch vụ của bạn?). Cảm ơn!
Nicolas

11
Viết tốt lên, mặc dù tôi không chắc tại sao bạn lại tiêm movieRepository vào MovieController vì bộ điều khiển không nên làm bất cứ điều gì trực tiếp với kho lưu trữ, cũng như phương pháp postCreate của bạn bằng movieRepository, vì vậy tôi cho rằng bạn đã nhầm lẫn ?
davidnknight

15
Câu hỏi về điều này: tại sao bạn sử dụng kho lưu trữ trong ví dụ này? Đây là một câu hỏi trung thực - đối với tôi, có vẻ như bạn đang sử dụng kho lưu trữ nhưng ít nhất trong ví dụ này, kho lưu trữ không thực sự làm gì ngoài việc cung cấp giao diện giống như Eloquent, và cuối cùng thì bạn vẫn bị ràng buộc với Eloquent vì lớp dịch vụ của bạn đang sử dụng elolo trực tiếp trong nó ( $studio->movies()->associate($movie);).
Kevin Mitchell

5

Tôi thích nghĩ về nó theo nghĩa mã của tôi đang làm và những gì nó chịu trách nhiệm, hơn là "đúng hay sai". Đây là cách tôi phá vỡ trách nhiệm của mình:

  • Bộ điều khiển là lớp HTTP và định tuyến các yêu cầu thông qua apis bên dưới (hay còn gọi là nó kiểm soát luồng)
  • Mô hình đại diện cho lược đồ cơ sở dữ liệu và cho ứng dụng biết dữ liệu trông như thế nào, các mối quan hệ mà nó có thể có cũng như bất kỳ thuộc tính toàn cục nào có thể cần thiết (chẳng hạn như phương thức tên để trả về họ và tên được ghép nối)
  • Các kho lưu trữ đại diện cho các truy vấn và tương tác phức tạp hơn với các mô hình (tôi không thực hiện bất kỳ truy vấn nào trên các phương thức mô hình).
  • Công cụ tìm kiếm - các lớp giúp tôi xây dựng các truy vấn tìm kiếm phức tạp.

Với suy nghĩ này, việc sử dụng một kho lưu trữ sẽ rất hợp lý (cho dù bạn tạo ra các interface.etc. Là một chủ đề hoàn toàn khác). Tôi thích cách tiếp cận này, vì nó có nghĩa là tôi biết chính xác nơi cần đến khi tôi cần làm một số công việc nhất định.

Tôi cũng có xu hướng xây dựng một kho lưu trữ cơ sở, thường là một lớp trừu tượng xác định các mặc định chính - về cơ bản là các hoạt động CRUD và sau đó mỗi đứa trẻ có thể chỉ cần mở rộng và thêm các phương thức khi cần thiết hoặc nạp chồng các giá trị mặc định. Việc tiêm thuốc cho mô hình của bạn cũng giúp mô hình này khá mạnh mẽ.


Bạn có thể chỉ ra việc triển khai BaseRepository của bạn không? Tôi thực sự cũng làm điều này và tôi tò mò bạn đã làm gì.
Odyssee

Hãy nghĩ getById, getByName, getByTitle, save type method.etc. - các phương pháp thường áp dụng cho tất cả các kho lưu trữ trong các miền khác nhau.
Oddman

5

Hãy coi Kho lưu trữ như một tủ lưu trữ dữ liệu nhất quán của bạn (không chỉ ORM của bạn). Ý tưởng là bạn muốn lấy dữ liệu trong một API đơn giản nhất quán để sử dụng.

Nếu bạn thấy mình chỉ làm Model :: all (), Model :: find (), Model :: create (), bạn có thể sẽ không được lợi nhiều từ việc trừu tượng hóa kho lưu trữ. Mặt khác, nếu bạn muốn thực hiện thêm một chút logic nghiệp vụ cho các truy vấn hoặc hành động của mình, bạn có thể muốn tạo một kho lưu trữ để tạo ra một API dễ sử dụng hơn để xử lý dữ liệu.

Tôi nghĩ rằng bạn đang hỏi liệu một kho lưu trữ có phải là cách tốt nhất để giải quyết một số cú pháp dài dòng hơn cần thiết để kết nối các mô hình liên quan hay không. Tùy thuộc vào tình hình, có một số điều tôi có thể làm:

  1. Treo một mô hình con mới khỏi mô hình mẹ (một-một hoặc một-nhiều), tôi sẽ thêm một phương thức vào kho lưu trữ con một cái gì đó giống như vậy createWithParent($attributes, $parentModelInstance)và điều này sẽ chỉ thêm trường $parentModelInstance->idvào parent_idtrường thuộc tính và gọi tạo.

  2. Gắn kết nhiều mối quan hệ, tôi thực sự tạo các hàm trên các mô hình để tôi có thể chạy $ instance-> attachmentChild ($ childInstance). Lưu ý rằng điều này yêu cầu các yếu tố hiện có ở cả hai bên.

  3. Tạo các mô hình liên quan trong một lần chạy, tôi tạo ra một thứ mà tôi gọi là Cổng (nó có thể hơi khác so với định nghĩa của Fowler). Cách tôi có thể gọi $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes) thay vì một loạt logic có thể thay đổi hoặc điều đó sẽ làm phức tạp logic mà tôi có trong bộ điều khiển hoặc lệnh.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.