Laravel에서 관계 관리, 저장소 패턴 준수


120

Laravel의 좋은 디자인 패턴에 관한 T. Otwell의 책을 읽은 후 Laravel 4에서 앱을 만드는 동안 저는 애플리케이션의 모든 테이블에 대한 저장소를 만드는 것을 발견했습니다.

나는 다음 테이블 구조로 끝났습니다.

  • 학생 : 아이디, 이름
  • 과정 : id, name, teacher_id
  • 교사 : 아이디, 이름
  • 과제 : id, name, course_id
  • 점수 (학생과 과제 간의 피벗 역할) : student_id, assignment_id, scores

이 모든 테이블에 대해 찾기, 만들기, 업데이트 및 삭제 메서드가있는 저장소 클래스가 있습니다. 각 저장소에는 데이터베이스와 상호 작용하는 Eloquent 모델이 있습니다. 관계는 Laravel 문서에 따라 모델에 정의되어 있습니다 : http://laravel.com/docs/eloquent#relationships .

새 과정을 만들 때 내가하는 일은 Course Repository에서 create 메서드를 호출하는 것뿐입니다. 이 과정에는 과제가 있으므로 하나를 만들 때 해당 과정의 각 학생에 대한 점수 표에 항목을 만들고 싶습니다. 이 작업은 할당 저장소를 통해 수행합니다. 이는 할당 저장소가 할당 및 학생 모델을 사용하여 두 개의 Eloquent 모델과 통신 함을 의미합니다.

제 질문은이 앱이 아마도 크기가 커지고 더 많은 관계가 도입 될 것이므로 저장소의 다른 Eloquent 모델과 통신하는 것이 좋은 방법입니까, 아니면 다른 저장소를 사용하여 수행해야하는지 (할당 저장소에서 다른 저장소를 호출하는 것을 의미합니다) ) 아니면 Eloquent 모델에서 모두 함께해야합니까?

또한 점수 표를 과제와 학생 사이의 중심 축으로 사용하는 것이 좋은 방법입니까, 아니면 다른 곳에서 수행해야합니까?

답변:


71

당신이 의견을 요구한다는 것을 명심하십시오 : D

여기 내 것 :

TL; DR : 예, 괜찮습니다.

당신은 잘하고 있습니다!

나는 당신이 자주하는 일을 정확히하고 그것이 잘 작동한다는 것을 알게됩니다.

그러나 나는 종종 테이블 당 리포지토리를 갖는 대신 비즈니스 로직을 중심으로 리포지토리를 구성합니다. 이것은 응용 프로그램이 "비즈니스 문제"를 해결해야하는 방법을 중심으로하는 관점이므로 유용합니다.

코스는 속성 (제목, ID 등)과 다른 항목 (자체 속성과 가능한 항목이있는 과제)을 포함하는 "항목"입니다.

"코스"저장소는 코스 및 코스의 속성 / 과제 (과제 포함)를 반환 할 수 있어야합니다.

운 좋게도 Eloquent로이를 달성 할 수 있습니다.

(종종 테이블 당 리포지토리로 끝나지만 일부 리포지토리는 다른 리포지토리보다 훨씬 더 많이 사용되므로 더 많은 방법이 있습니다. 예를 들어 "강좌"리포지토리는 할당 리포지토리보다 훨씬 더 완전한 기능을 제공 할 수 있습니다. 응용 프로그램은 코스의 과제 모음보다는 코스를 중심으로합니다.

까다로운 부분

데이터베이스 작업을 수행하기 위해 종종 내 저장소 내부의 저장소를 사용합니다.

데이터를 처리하기 위해 Eloquent를 구현하는 모든 저장소는 Eloquent 모델을 반환 할 것입니다. 그런 점에서 코스 모델이 과제 (또는 다른 사용 사례)를 검색하거나 저장하기 위해 기본 제공 관계를 사용하는 것은 괜찮습니다. 우리의 "구현"은 Eloquent를 중심으로 구축되었습니다.

실용적인 관점에서 이것은 의미가 있습니다. 데이터 소스를 Eloquent가 처리 할 수없는 (비 SQL 데이터 소스로) 변경하지 않을 것입니다.

ORMS

이 설정에서 가장 까다로운 부분은 Eloquent가 실제로 우리를 돕거나 해치는지를 결정하는 것입니다. ORM은 실제적인 관점에서는 우리에게 큰 도움이되지만 "비즈니스 로직 엔티티"코드와 데이터 검색을 수행하는 코드를 결합하기 때문에 까다로운 주제입니다.

이러한 종류의 경우 저장소의 책임이 실제로 데이터를 처리하는지 아니면 엔터티 (비즈니스 도메인 엔터티)의 검색 / 업데이트를 처리하는지 여부를 혼란스럽게합니다.

더욱이, 그것들은 당신이 당신의 뷰에 전달하는 바로 그 객체의 역할을합니다. 나중에 저장소에서 Eloquent 모델을 사용하지 않으려면 뷰에 전달 된 변수가 동일한 방식으로 작동하는지 또는 동일한 메서드를 사용할 수 있는지 확인해야합니다. 그렇지 않으면 데이터 소스를 변경하여 뷰, 그리고 당신은 (부분적으로) 당신의 논리를 리포지토리로 추상화하는 목적을 잃어 버렸다. 프로젝트의 유지 보수 가능성이 떨어졌다.

어쨌든 이것들은 다소 불완전한 생각입니다. 언급했듯이 그들은 단지 내 의견 일 뿐이다. 이는 도메인 중심 디자인 을 읽고 작년에 Ruby Midwest에서 "삼촌 밥의"기조 연설 과 같은 비디오를 본 결과입니다 .


1
리포지토리가 웅변적인 객체 대신 데이터 전송 객체를 반환한다면 좋은 대안이 될까요? 물론 이것은 eloquent에서 dto 로의 추가 변환을 의미하지만, 이런 식으로 적어도 현재 orm 구현에서 컨트롤러 / 뷰를 분리합니다.
federivo 2014 년

1
나는 그것을 조금 실험 해 보았고 비실용적 인 측면에서 조금 발견했습니다. 즉, 나는 초록에서 그 아이디어를 좋아합니다. 그러나 Illuminate의 데이터베이스 Collection 객체는 배열과 똑같이 작동하고 Model 객체는 StdClass 객체처럼 충분히 작동하므로 실제로 말하면 Eloquent를 고수하고 나중에 필요할 때 배열 / 객체를 계속 사용할 수 있습니다.
fideloper

4
@fideloper 리포지토리를 사용하면 Eloquent가 제공하는 ORM의 전체적인 아름다움을 잃어 버릴 것 같습니다. 내 저장소 방법을 통해 계정 개체를 검색 할 때 $a = $this->account->getById(1)나는 단순히 같은 방법을 체인 수 없습니다 $a->getActiveUsers(). 좋아, 나는 사용할 수 $a->users->...있지만 Eloquent 컬렉션을 반환하고 stdClass 객체를 반환하지 않으며 다시 Eloquent에 연결됩니다. 이것에 대한 해결책은 무엇입니까? 사용자 저장소에서 다음과 같은 다른 방법을 선언합니다 $user->getActiveUsersByAccount($a->id);. 이 문제를 어떻게 해결하는지 듣고 싶습니다.
santacruz

1
ORM은 이와 같은 문제를 일으키기 때문에 Enterprise (ish) 수준 아키텍처에 끔찍합니다. 결국, 여러분의 애플리케이션에 가장 적합한 것이 무엇인지 결정해야합니다. 개인적으로 Eloquent와 함께 리포지토리를 사용할 때 (90 %의 시간!) 저는 Eloquent를 사용하고 stdClasses & Arrays와 같은 모델과 컬렉션을 다루기 위해 최선을 다합니다. (왜냐하면 당신이 할 수 있기 때문입니다!) 필요하다면 다른 것으로 전환하는 것이 가능합니다.
fideloper

5
계속해서 지연로드 모델을 사용하십시오. Eloquent를 사용하지 않으면 실제 도메인 모델이 그렇게 작동하도록 만들 수 있습니다. 그러나 심각하게, 당신은 거야 어느 웅변 밖으로 스위치? 1 페니에, 1 파운드에! ( "규칙"을 고수하려고 배 밖으로 나가지 마십시오! 나는 항상 내 모든 것을 어 깁니다).
fideloper

224

저는 Laravel 4를 사용하여 대규모 프로젝트를 마무리하고 있으며 지금 당장 질문하는 모든 질문에 답해야했습니다. Leanpub에서 사용 가능한 모든 Laravel 책과 수많은 인터넷 검색을 읽은 후 다음 구조를 생각해 냈습니다.

  1. 데이터 가능한 테이블 당 하나의 Eloquent 모델 클래스
  2. Eloquent 모델 당 하나의 리포지토리 클래스
  3. 여러 저장소 클래스간에 통신 할 수있는 서비스 클래스입니다.

그래서 제가 영화 데이터베이스를 만들고 있다고 가정 해 봅시다. 최소한 다음과 같은 Eloquent Model 클래스가 있습니다.

  • 영화
  • 사진관
  • 감독
  • 배우
  • 리뷰

저장소 클래스는 각 Eloquent Model 클래스를 캡슐화하고 데이터베이스에서 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);
}

서비스 클래스는 여러 저장소를 함께 연결하는 데 사용되며 응용 프로그램의 실제 "비즈니스 논리"를 포함합니다. 컨트롤러 는 만들기, 업데이트 및 삭제 작업을 위해 서비스 클래스와 통신합니다.

따라서 데이터베이스에 새 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는 데이터베이스에서 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
이 의견은 훨씬 더 깨끗하고 확장 가능하며 유지 관리가 가능한 접근 방식입니다.
Andreas

4
+1! 그것은 저에게 많은 도움이 될 것입니다. 우리와 공유해 주셔서 감사합니다! 가능한 경우 서비스 내부에서 항목의 유효성을 검사하는 방법이 궁금합니다. 수행 한 작업을 간략하게 설명해 주시겠습니까? 어쨌든 감사합니다! :)
Paulo Freitas 2014 년

6
@PauloFreitas가 말했듯이, 유효성 검사 부분을 처리하는 방법을 보는 것은 흥미로울 것이며 예외 부분에도 관심이있을 것입니다. 서비스에서 부울 리턴을 통해 컨트롤러?). 감사!
Nicolas

11
컨트롤러가 리포지토리에서 직접 아무것도하지 말아야하므로 movieRepository를 MovieController에 삽입하는 이유를 잘 모르겠습니다. 또한 movieRepository를 사용하는 postCreate 메서드도 실수로 남겨 두었다고 가정합니다. ?
davidnknight

15
이에 대한 질문 :이 예제에서 리포지토리를 사용하는 이유는 무엇입니까? 이것은 정직한 질문입니다. 저에게는 리포지토리를 사용하는 것처럼 보이지만 적어도이 예제에서는 리포지토리가 실제로 아무것도하지 않고 Eloquent와 동일한 인터페이스를 제공하며 결국 Eloquent에 묶여 있습니다. 서비스 클래스가 eloquent를 직접 사용하고 있습니다 ( $studio->movies()->associate($movie);).
Kevin Mitchell

5

나는 그것을 "옳고 그름"보다는 내 코드가하는 일과 그에 대한 책임이라는 관점에서 생각하고 싶다. 이것이 제가 책임을 분리하는 방법입니다.

  • 컨트롤러는 HTTP 계층이며 기본 API를 통해 요청을 라우팅합니다 (즉, 흐름을 제어 함).
  • 모델은 데이터베이스 스키마를 나타내며, 데이터가 어떤 모양인지, 어떤 관계가 있는지, 그리고 필요할 수있는 모든 전역 속성 (예 : 연결된 성과 이름을 반환하기위한 이름 메서드)을 애플리케이션에 알려줍니다.
  • 리포지토리는 모델과의 더 복잡한 쿼리 및 상호 작용을 나타냅니다 (모델 메서드에 대한 쿼리는 수행하지 않음).
  • 검색 엔진-복잡한 검색 쿼리를 작성하는 데 도움이되는 클래스입니다.

이 점을 염두에두고 저장소를 사용할 때마다 의미가 있습니다 (interface.etc를 만드는지 여부는 완전히 다른 주제입니다). 이 접근 방식이 마음에 듭니다. 특정 작업을해야 할 때 어디로 가야하는지 정확히 알 수 있기 때문입니다.

또한 기본 저장소, 일반적으로 기본 기본값 (기본적으로 CRUD 작업)을 정의하는 추상 클래스를 빌드하는 경향이 있습니다. 그런 다음 각 자식은 필요에 따라 메서드를 확장하고 추가하거나 기본값을 오버로드 할 수 있습니다. 모델을 삽입하면이 패턴이 매우 강력 해집니다.


BaseRepository 구현을 보여줄 수 있습니까? 저도 실제로이 일을하는데 당신이 뭘했는지 궁금합니다.
Odyssee

getById, getByName, getByTitle, 저장 유형 메소드 등을 생각하십시오. -일반적으로 다양한 도메인 내의 모든 저장소에 적용되는 방법.
Oddman

5

리포지토리를 ORM뿐만 아니라 데이터의 일관된 파일 캐비넷으로 생각하십시오. 아이디어는 일관되고 사용하기 쉬운 API로 데이터를 수집하려는 것입니다.

Model :: all (), Model :: find (), Model :: create () 만 수행하는 경우 리포지토리를 추상화해도 많은 이점을 얻지 못할 것입니다. 반면에 쿼리 나 작업에 좀 더 많은 비즈니스 로직을 수행하려면 데이터를 처리하기 위해 API를 더 쉽게 사용할 수 있도록 저장소를 만들 수 있습니다.

관련 모델을 연결하는 데 필요한 더 자세한 구문을 처리하는 가장 좋은 방법이 리포지토리인지 묻는 것 같습니다. 상황에 따라 몇 가지 조치를 취할 수 있습니다.

  1. 상위 모델의 오프 새 자식 모델을 매달려 (일대일 또는 한 많은), 내가 좋아하는 아이 저장소 뭔가에 메서드를 추가 createWithParent($attributes, $parentModelInstance)하고이 단지를 추가 $parentModelInstance->idparent_id속성과 전화 생성의 필드.

  2. 다 대다 관계를 연결하여 실제로 $ instance-> attachChild ($ childInstance)를 실행할 수 있도록 모델에 함수를 만듭니다. 이를 위해서는 양쪽에 기존 요소가 필요합니다.

  3. 한 번의 실행으로 관련 모델을 생성하고 게이트웨이라고하는 무언가를 생성합니다 (Fowler의 정의에서 약간 벗어난 것일 수 있음). 변경 될 수 있거나 컨트롤러 또는 명령에있는 논리를 복잡하게 만드는 논리 무리 대신 $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes)를 호출 할 수있는 방법입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.