PHP에서 적절한 리포지토리 패턴 디자인?


291

서문 : 관계형 데이터베이스와 함께 MVC 아키텍처에서 리포지토리 패턴을 사용하려고합니다.

최근 PHP에서 TDD를 배우기 시작했으며 데이터베이스가 나머지 응용 프로그램과 너무 밀접하게 연결되어 있음을 깨닫고 있습니다. 리포지토리에 대해 읽고 IoC 컨테이너 를 사용하여 컨트롤러에 "주입"했습니다. 아주 멋진 것들. 그러나 이제 저장소 디자인에 대한 실질적인 질문이 있습니다. 다음 예를 고려하십시오.

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

이슈 # 1 : 너무 많은 필드

이러한 모든 find 메소드는 모든 필드 선택 ( SELECT *) 방법을 사용합니다. 그러나 내 응용 프로그램에서는 항상 오버 헤드를 추가하고 작업 속도를 늦추기 때문에 내가 얻는 필드 수를 제한하려고합니다. 이 패턴을 사용하는 사람들은 이것을 어떻게 처리합니까?

문제 # 2 : 너무 많은 방법

이 수업은 지금은 멋져 보이지만 실제 응용 프로그램에는 더 많은 방법이 필요하다는 것을 알고 있습니다. 예를 들면 다음과 같습니다.

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • 모두 찾기 나이 및 성별
  • 모두 모두와 성별 주문
  • 기타.

보시다시피, 가능한 방법 목록이 매우 길 수 있습니다. 그런 다음 위의 필드 선택 문제를 추가하면 문제가 악화됩니다. 과거에는 일반적으로 컨트롤러 에이 논리를 모두 넣었습니다.

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')
            ->byCountry('Canada')->orderBy('name')->rows();

        return View::make('users', array('users' => $users));
    }
}

내 저장소 접근 방식으로 다음과 같이 끝내고 싶지 않습니다.

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

문제 # 3 : 인터페이스와 일치하지 않습니다

리포지토리에 인터페이스를 사용하면 이점이 있으므로 테스트 목적으로 또는 구현을 위해 구현을 바꿀 수 있습니다. 인터페이스에 대한 이해는 구현이 따라야하는 계약을 정의한다는 것입니다. 이것은 저장소에 추가 메소드를 추가하기 시작할 때까지 좋습니다 findAllInCountry(). 이제 인터페이스를 업데이트 하여이 메소드를 가져와야합니다. 그렇지 않으면 다른 구현에는 없을 수 있으며 응용 프로그램이 손상 될 수 있습니다. 이것에 의해 미친 느낌이 듭니다 ... 꼬리가 개를 흔들리는 경우입니다.

사양 패턴?

이 리드 나 저장소는 (같은 방법의 고정 번호를 가지고해야한다고 생각합니다 save(), remove(), find(), findAll(), 등). 그러나 특정 조회를 어떻게 실행합니까? 사양 패턴에 대해 들어 보았지만 IsSatisfiedBy()데이터베이스를 가져 오는 경우 전체 레코드 세트 만 줄어 듭니다 .

도움?

분명히, 리포지토리를 사용할 때 조금 생각해야합니다. 누구든지 이것이 가장 잘 처리되는 방법을 밝힐 수 있습니까?

답변:


208

나는 내 자신의 질문에 답하는 데 열중 할 것이라고 생각했다. 다음은 원래 질문에서 1-3 문제를 해결하는 한 가지 방법입니다.

면책 조항 : 패턴이나 기술을 설명 할 때 항상 올바른 용어를 사용하지는 않습니다. 그 죄송합니다.

목표 :

  • 보고 편집 할 기본 컨트롤러의 완전한 예를 작성하십시오 Users.
  • 모든 코드는 완벽하게 테스트하고 조롱 할 수 있어야합니다.
  • 컨트롤러는 데이터가 저장된 위치를 알 수 없어야합니다 (즉, 변경할 수 있음).
  • SQL 구현을 보여주는 예제 (가장 일반적)
  • 성능을 극대화하려면 컨트롤러는 추가 필드없이 필요한 데이터 만 수신해야합니다.
  • 구현시 쉽게 개발할 수 있도록 일부 유형의 데이터 매퍼를 사용해야합니다.
  • 구현시 복잡한 데이터 조회를 수행 할 수 있어야합니다.

해결책

영구 저장소 (데이터베이스) 상호 작용을 R (읽기) 및 CUD (만들기, 업데이트, 삭제)의 두 가지 범주로 나누고 있습니다. 내 경험에 따르면 읽기는 실제로 응용 프로그램 속도를 저하시킵니다. 그리고 데이터 조작 (CUD)은 실제로 속도는 느리지 만 훨씬 덜 자주 발생하므로 걱정할 필요가 없습니다.

CUD (만들기, 업데이트, 삭제)는 쉽습니다. 여기에는 실제 모델 작업이 포함 Repositories되며 지속성을 위해 전달됩니다 . 내 리포지토리는 여전히 Read 메서드를 제공하지만 단순히 개체를 만들지 만 표시하지는 않습니다. 나중에 더 자세히.

R (읽기)은 쉽지 않습니다. 여기에 모델이 없으며 값이 object 입니다. 원하는 경우 배열을 사용하십시오 . 이러한 객체는 단일 모델이거나 많은 모델이 혼합 된 형태 일 수 있습니다. 이것 자체는 그다지 흥미롭지 않지만 어떻게 생성 되는가입니다. 내가 부르는 것을 사용하고 Query Objects있습니다.

코드:

사용자 모델

기본 사용자 모델부터 간단하게 시작하겠습니다. ORM 확장 또는 데이터베이스 항목이 전혀 없습니다. 순수한 모델의 영광. 게터, 세터, 유효성 검사 등을 추가하십시오.

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

리포지토리 인터페이스

사용자 저장소를 작성하기 전에 저장소 인터페이스를 작성하려고합니다. 이것은 내 컨트롤러가 사용하기 위해 리포지토리가 따라야하는 "계약"을 정의합니다. 내 컨트롤러는 데이터가 실제로 저장된 위치를 알지 못합니다.

내 리포지토리에는이 세 가지 방법 만 포함됩니다. 이 save()메소드는 사용자 오브젝트에 ID 세트가 있는지 여부에 따라 사용자 작성 및 업데이트를 담당합니다.

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

SQL 리포지토리 구현

이제 인터페이스 구현을 만듭니다. 언급했듯이 내 예제는 SQL 데이터베이스를 사용하는 것입니다. 반복적 인 SQL 쿼리를 작성하지 않으려면 데이터 매퍼 를 사용하십시오 .

class SQLUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function find($id)
    {
        // Find a record with the id = $id
        // from the 'users' table
        // and return it as a User object
        return $this->db->find($id, 'users', 'User');
    }

    public function save(User $user)
    {
        // Insert or update the $user
        // in the 'users' table
        $this->db->save($user, 'users');
    }

    public function remove(User $user)
    {
        // Remove the $user
        // from the 'users' table
        $this->db->remove($user, 'users');
    }
}

쿼리 개체 인터페이스

이제 저장소에서 CUD (Create, Update, Delete)를 처리하여 R (읽기) 에 집중할 수 있습니다 . 쿼리 개체는 단순히 일부 유형의 데이터 조회 논리를 캡슐화 한 것입니다. 이들은 쿼리 빌더 가 아닙니다 . 저장소와 같이 추상화함으로써 구현을 변경하고 쉽게 테스트 할 수 있습니다. 쿼리 개체의 예는 AllUsersQuery또는 또는 또는 AllActiveUsersQuery일 수 있습니다 MostCommonUserFirstNames.

"내 쿼리에서 해당 쿼리에 대한 메소드를 작성할 수는 없습니까?" 예, 그러나 내가 이것을하지 않는 이유는 다음과 같습니다.

  • 내 저장소는 모델 객체로 작업하기위한 것입니다. 실제 앱에서 password모든 사용자를 나열 하려면 왜 필드를 가져와야합니까?
  • 리포지토리는 종종 모델마다 다르지만 쿼리에는 종종 두 개 이상의 모델이 포함됩니다. 그래서 어떤 저장소에 메소드를 넣었습니까?
  • 이렇게하면 부풀린 클래스가 아닌 내 리포지토리를 매우 간단하게 유지할 수 있습니다.
  • 모든 쿼리는 이제 자체 클래스로 구성됩니다.
  • 실제로이 시점에서 저장소는 단순히 내 데이터베이스 계층을 추상화하기 위해 존재합니다.

이 예제에서는 "AllUsers"를 조회하는 쿼리 객체를 작성합니다. 인터페이스는 다음과 같습니다.

interface AllUsersQueryInterface
{
    public function fetch($fields);
}

쿼리 객체 구현

여기에서 데이터 매퍼를 다시 사용하여 개발 속도를 높일 수 있습니다. 반환 된 데이터 세트 인 필드를 한 번 조정할 수 있습니다. 이것은 수행 된 쿼리 조작과 관련이 있습니다. 내 쿼리 개체는 쿼리 작성기가 아닙니다. 그들은 단순히 특정 쿼리를 수행합니다. 그러나 여러 가지 상황에서 아마도이 것을 많이 사용할 것이라는 것을 알고 있기 때문에 필자는 필드를 지정할 수있는 능력을 부여하고 있습니다. 필요없는 필드를 반환하고 싶지 않습니다!

class AllUsersQuery implements AllUsersQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch($fields)
    {
        return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
    }
}

컨트롤러로 넘어 가기 전에 이것이 얼마나 강력한지를 보여주는 또 다른 예를 보여 드리고자합니다. 보고 엔진이 있고에 대한 보고서를 만들어야 할 수도 있습니다 AllOverdueAccounts. 내 데이터 매퍼로 까다로울 수 있으며이 SQL상황에서 실제를 작성하고 싶을 수도 있습니다. 문제 없습니다.이 쿼리 객체는 다음과 같습니다.

class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch()
    {
        return $this->db->query($this->sql())->rows();
    }

    public function sql()
    {
        return "SELECT...";
    }
}

이렇게하면이 보고서에 대한 모든 논리가 한 클래스로 유지되며 테스트하기 쉽습니다. 나는 그것을 내 마음 내용에 조롱하거나 심지어 다른 구현을 전적으로 사용할 수 있습니다.

컨트롤러

이제 재미있는 부분은 모든 조각을한데 모으는 것입니다. 의존성 주입을 사용하고 있습니다. 일반적으로 종속성은 생성자에 주입되지만 실제로는 컨트롤러 메소드 (라우트)에 직접 주입하는 것을 선호합니다. 이것은 컨트롤러의 객체 그래프를 최소화하며 실제로 더 읽기 쉽습니다. 이 접근법이 마음에 들지 않으면 전통적인 생성자 메소드를 사용하십시오.

class UsersController
{
    public function index(AllUsersQueryInterface $query)
    {
        // Fetch user data
        $users = $query->fetch(['first_name', 'last_name', 'email']);

        // Return view
        return Response::view('all_users.php', ['users' => $users]);
    }

    public function add()
    {
        return Response::view('add_user.php');
    }

    public function insert(UserRepositoryInterface $repository)
    {
        // Create new user model
        $user = new User;
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the new user
        $repository->save($user);

        // Return the id
        return Response::json(['id' => $user->id]);
    }

    public function view(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('view_user.php', ['user' => $user]);
    }

    public function edit(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('edit_user.php', ['user' => $user]);
    }

    public function update(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Update the user
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the user
        $repository->save($user);

        // Return success
        return true;
    }

    public function delete(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Delete the user
        $repository->delete($user);

        // Return success
        return true;
    }
}

마지막 생각들:

여기서주의해야 할 중요한 사항은 엔터티를 수정 (작성, 업데이트 또는 삭제) 할 때 실제 모델 객체로 작업하고 리포지토리를 통해 지속성을 수행한다는 것입니다.

그러나 표시 할 때 (데이터를 선택하고보기로 전송) 모델 객체가 아닌 일반 오래된 값 객체로 작업하고 있습니다. 필요한 필드 만 선택하고 데이터 조회 성능을 극대화 할 수 있도록 설계되었습니다.

내 리포지토리는 매우 깨끗하게 유지되며 대신이 "메시"가 내 모델 쿼리로 구성됩니다.

일반적인 작업을 위해 반복적 인 SQL을 작성하는 것은 우스운 일이므로 데이터 매퍼를 사용하여 개발에 도움을줍니다. 그러나 필요한 경우 SQL을 작성할 수 있습니다 (복잡한 쿼리,보고 등). 그리고 당신이 할 때, 그것은 적절하게 명명 된 클래스로 멋지게 자리 잡았습니다.

내 접근 방식에 대한 귀하의 의견을 듣고 싶습니다!


2015 년 7 월 업데이트 :

나는이 모든 것으로 끝나는 의견에 질문을 받았다. 글쎄, 실제로 그렇게 멀지는 않습니다. 사실, 나는 여전히 저장소를 좋아하지 않습니다. 나는 기본 조회 (특히 이미 ORM을 사용하고있는 경우)에 대해 과잉이며 더 복잡한 쿼리로 작업 할 때 지저분하다는 것을 알았습니다.

일반적으로 ActiveRecord 스타일 ORM을 사용하므로 대부분의 경우 응용 프로그램 전체에서 직접 해당 모델을 참조합니다. 그러나 더 복잡한 쿼리가있는 상황에서는 쿼리 개체를 사용하여 더 재사용 할 수 있도록합니다. 또한 항상 내 모델을 내 메소드에 주입하여 테스트에서 쉽게 조롱 할 수 있습니다.


4
@PeeHaa 다시, 예제를 단순하게 유지하는 것이 었습니다. 특정 주제와 관련이없는 경우 코드 조각을 예제에서 제외하는 것이 매우 일반적입니다. 실제로, 나는 의존성을 전달할 것입니다.
Jonathan

4
읽기에서 만들기, 업데이트 및 삭제를 분할 한 것이 흥미 롭습니다. 공식적으로 그렇게하는 CQRS (Command Query Responsibility Segregation)를 언급 할 가치가 있다고 생각했습니다. martinfowler.com/bliki/CQRS.html
Adam

2
@Jonathan 자신의 질문에 대답 한 지 1 년 반이되었습니다. 나는 당신이 여전히 당신의 대답에 만족하고 이것이 대부분의 프로젝트에 대한 주요 해결책인지 궁금합니다. 지난 몇 주 동안 리포지토리에 대한 할당량을 읽었으며 많은 사람들이 리포지토리를 구현하는 방법에 대한 자체 해석을하는 것을 보았습니다. 그것을 호출하면 객체를 쿼리하지만 이것이 기존 패턴입니까? 다른 언어로 사용되는 것을 보았습니다.
Boedy

1
@Jonathan : 사용자가 "ID"가 아니라 "username"또는 둘 이상의 조건을 가진 더 복잡한 쿼리에 의한 쿼리를 처리하는 방법은 무엇입니까?
Gizzmo

1
@Gizzmo 쿼리 개체를 사용하면 더 복잡한 쿼리에 도움이되는 추가 매개 변수를 전달할 수 있습니다. 예를 들어 생성자에서 다음을 수행 할 수 있습니다 new Query\ComplexUserLookup($username, $anotherCondition). 또는 setter 메소드를 통해이를 수행하십시오 $query->setUsername($username);. 실제로 이것을 디자인 할 수는 있지만 특정 응용 프로그램에 적합하며 쿼리 객체에는 많은 유연성이 있습니다.
Jonathan

48

내 경험을 바탕으로 귀하의 질문에 대한 답변은 다음과 같습니다.

Q : 필요하지 않은 필드를 다시 가져 오려면 어떻게해야합니까?

A : 경험상, 이것은 완전한 엔티티 대 임시 쿼리를 다루는 것으로 끝납니다.

완전한 실체는 User 객체 . 속성과 메서드 등이 있습니다. 코드베이스에서 일류 시민입니다.

임시 쿼리는 일부 데이터를 반환하지만 그 이상의 것을 알 수는 없습니다. 데이터가 응용 프로그램 주위로 전달되면 컨텍스트없이 수행됩니다. 그것은 User입니까? User일부 Order정보는 첨부? 우리는 정말로 모른다.

나는 완전한 실체를 다루는 것을 선호합니다.

자주 사용하지 않는 데이터를 다시 가져올 수는 있지만, 다양한 방법으로이를 해결할 수 있습니다.

  1. 엔터티를 적극적으로 캐시하므로 데이터베이스에서 한 번만 읽기 가격을 지불하십시오.
  2. 엔터티를 모델링하는 데 더 많은 시간을 할애하여 엔터티를 잘 구분하십시오. (큰 엔터티를 두 개의 작은 엔터티 등으로 나누는 것을 고려하십시오.)
  3. 여러 버전의 엔티티를 고려하십시오. User백엔드 용 및 UserSmallAJAX 통화 용을 가질 수 있습니다 . 하나에는 10 개의 속성이 있고 하나에는 3 개의 속성이 있습니다.

임시 쿼리 작업의 단점 :

  1. 많은 쿼리에서 본질적으로 동일한 데이터로 끝납니다. 예를 들어,를 사용하면 User본질적으로 똑같이 작성하게됩니다.select * 많은 통화에 대해 됩니다. 하나의 통화는 10 개의 필드 중 8 개를, 하나는 5 개의 10을 받고, 하나는 7의 10을받습니다. 이것이 나쁜 이유는 리팩터링 / 테스트 / 모의로 살인하기 때문입니다.
  2. 시간이 지남에 따라 코드에 대해 높은 수준으로 추론하기가 매우 어려워집니다. "왜User 그렇게 느린가?"일회성 쿼리를 추적하면 버그 수정이 작고 현지화되는 경향이 있습니다.
  3. 기본 기술을 대체하기가 정말 어렵습니다. MySQL에 모든 것을 저장하고 MongoDB로 옮기려면 소수의 엔티티보다 100 개의 임시 호출을 교체하는 것이 훨씬 어렵습니다.

Q : 저장소에 너무 많은 메소드가 있습니다.

A : 통화 통합 이외의 다른 방법은 실제로 보지 못했습니다. 저장소의 메소드 호출은 실제로 애플리케이션의 기능에 맵핑됩니다. 더 많은 기능, 더 많은 데이터 특정 통화. 기능을 되돌리고 유사한 통화를 하나로 통합 할 수 있습니다.

하루가 끝날 때의 복잡성은 어딘가에 존재해야합니다. 리포지토리 패턴을 사용하여 저장 프로 시저를 만들지 않고 리포지토리 인터페이스로 푸시했습니다.

때때로 나는 "어딘가에 주어야했다!은 총알이 없다"고 스스로에게 말해야한다.


매우 철저한 답변에 감사드립니다. 당신은 지금 생각하고있어. 여기서 가장 큰 관심사는 내가 읽은 모든 내용이하지 말고 SELECT *필요한 필드 만 선택한다는 것입니다. 예를 들어이 질문을 참조하십시오 . 당신이 말하는 모든 애드혹 쿼리에 관해서는, 나는 당신이 어디에서 왔는지 확실히 알고 있습니다. 현재 많은 앱이있는 매우 큰 앱이 있습니다. 그건 내 "어딘가에 주어야했다!" 순간, 나는 최대 성능을 선택했습니다. 그러나 이제는 다른 쿼리의 LOTS를 처리하고 있습니다.
Jonathan

1
하나의 후속 생각. R-CUD 접근법을 사용하는 것이 좋습니다. reads성능 문제가 발생하는 경우가 많으 므로 실제 비즈니스 개체로 변환되지 않는보다 사용자 지정 쿼리 방식을 사용할 수 있습니다. 그런 다음에 대한 create, update그리고 delete전체 개체를 작동하는 ORM을 사용합니다. 그 접근법에 대한 생각이 있습니까?
Jonathan

1
"select *"를 사용하기위한 참고 사항입니다. 나는 과거에 그것을했고 varchar (max) 필드를 칠 때까지 정상적으로 작동했습니다. 그들은 우리의 질문을 죽였습니다. 따라서 int, 작은 텍스트 필드 등이있는 테이블이 있다면 그렇게 나쁘지 않습니다. 부 자연스럽게 느껴지지만 소프트웨어는 그렇게됩니다. 나쁜 것은 갑자기 좋으며 그 반대도 마찬가지입니다.
ryan1234

1
R-CUD 접근법은 실제로 CQRS입니다.
MikeSW

2
@ ryan1234 "하루의 복잡성은 어딘가에 존재해야합니다." 감사합니다. 기분이 좋아집니다.
johnny

20

다음 인터페이스를 사용합니다.

  • Repository -엔터티로드, 삽입, 업데이트 및 삭제
  • Selector -저장소에서 필터를 기반으로 엔티티를 찾습니다.
  • Filter -필터링 로직을 캡슐화

나는 Repository데이터베이스에 구애받지 않는다. 실제로 지속성을 지정하지 않습니다. 그것이 무엇이든 될 수있다 : 능력을 검색하는 등 SQL 데이터베이스, XML 파일, 원격 서비스, 우주에서 외계인의 Repository구축합니다 Selector, 필터링 할 수 있습니다 LIMIT-ed는, 분류 및 계산. 결국, 선택기 Entities는 지속성에서 하나 이상을 가져옵니다 .

샘플 코드는 다음과 같습니다.

<?php
interface Repository
{
    public function addEntity(Entity $entity);

    public function updateEntity(Entity $entity);

    public function removeEntity(Entity $entity);

    /**
     * @return Entity
     */
    public function loadEntity($entityId);

    public function factoryEntitySelector():Selector
}


interface Selector extends \Countable
{
    public function count();

    /**
     * @return Entity[]
     */
    public function fetchEntities();

    /**
     * @return Entity
     */
    public function fetchEntity();
    public function limit(...$limit);
    public function filter(Filter $filter);
    public function orderBy($column, $ascending = true);
    public function removeFilter($filterName);
}

interface Filter
{
    public function getFilterName();
}

그런 다음 하나의 구현 :

class SqlEntityRepository
{
    ...
    public function factoryEntitySelector()
    {
        return new SqlSelector($this);
    }
    ...
}

class SqlSelector implements Selector
{
    ...
    private function adaptFilter(Filter $filter):SqlQueryFilter
    {
         return (new SqlSelectorFilterAdapter())->adaptFilter($filter);
    }
    ...
}
class SqlSelectorFilterAdapter
{
    public function adaptFilter(Filter $filter):SqlQueryFilter
    {
        $concreteClass = (new StringRebaser(
            'Filter\\', 'SqlQueryFilter\\'))
            ->rebase(get_class($filter));

        return new $concreteClass($filter);
    }
}

이념은 일반적인 Selector용도 Filter이지만 구현 SqlSelectorSqlFilter; 는 SqlSelectorFilterAdapter일반적인 적응 Filter콘크리트에 SqlFilter.

클라이언트 코드는 Filter객체 (일반 필터)를 생성하지만 선택기의 구체적인 구현에서는 이러한 필터가 SQL 필터로 변환됩니다.

와 같은 다른 선택기 구현은 그들의 특정 을 사용하여로 InMemorySelector변환 한다. 따라서 모든 선택기 구현에는 자체 필터 어댑터가 제공됩니다.FilterInMemoryFilterInMemorySelectorFilterAdapter

이 전략을 사용하면 클라이언트 코드 (bussines 레이어)가 특정 리포지토리 또는 선택기 구현에 신경 쓰지 않습니다.

/** @var Repository $repository*/
$selector = $repository->factoryEntitySelector();
$selector->filter(new AttributeEquals('activated', 1))->limit(2)->orderBy('username');
$activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit()
$activatedUsers = $selector->fetchEntities();

추신 : 이것은 내 실제 코드의 단순화입니다


"리포지토리-엔터티로드, 삽입, 업데이트 및 삭제" "서비스 계층", "DAO", "BLL"이 수행 할 수있는 작업
Yousha Aleayoub

5

나는 현재이 모든 것을 스스로 파악하려고 노력하고 있으므로 이것에 조금 추가 할 것입니다.

# 1과 2

이것은 ORM이 무거운 물건을 들기에 완벽한 장소입니다. 어떤 종류의 ORM을 구현하는 모델을 사용하는 경우 이러한 방법을 사용하는 방법을 사용할 수 있습니다. 필요한 경우 Eloquent 메소드를 구현하는 함수로 자신의 orderBy를 작성하십시오. 예를 들어 Eloquent 사용 :

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

당신이 찾고있는 것은 ORM입니다. 리포지토리를 기반으로 할 수없는 이유는 없습니다. 이것은 사용자가 웅변 확장해야하지만 개인적으로는 문제가되지 않습니다.

그러나 ORM을 피하려면 원하는 것을 얻기 위해 "자신의 롤"을해야합니다.

#삼

인터페이스는 어렵고 빠른 요구 사항이 아닙니다. 뭔가 인터페이스를 구현하고 추가 할 수 있습니다. 할 수없는 것은 해당 인터페이스의 필수 기능을 구현하지 못하는 것입니다. 클래스와 같은 인터페이스를 확장하여 DRY를 유지할 수 있습니다.

즉, 나는 막 이해하기 시작했지만 이러한 실현이 나를 도왔습니다.


1
이 방법에 대해 내가 싫어하는 것은 MongoUserRepository가 있으면 DbUserRepository가 다른 객체를 반환한다는 것입니다. Db는 Eloquent \ Model을 반환하고 Mongo 자체의 것을 반환합니다. 더 나은 구현은 두 저장소 모두 별도의 Entity \ User 클래스의 인스턴스 / 컬렉션을 반환하도록하는 것입니다. 당신이 MongoRepository 사용으로 전환 할 때 mistakingly없는이 방법은 설득력 \ 모델의 DB 방법에 의존
danharper

1
나는 그것에 대해 당신에게 분명히 동의합니다. 내가 피할 수있는 일은 Eloquent 요구 클래스 외부에서 해당 메소드를 사용하지 않는 것입니다. 따라서 get 함수는 개인용이어야하며 클래스 내에서만 사용해야합니다. 지적했듯이 다른 리포지토리로는 할 수없는 것을 반환 할 것입니다.

3

우리가 (내 회사에서)이 문제를 처리하는 방식에 대해서만 언급 할 수 있습니다. 우선 모든 성능은 우리에게 큰 문제가 아니지만 깨끗하고 적절한 코드를 갖는 것입니다.

먼저 UserModelORM을 사용하여 객체를 만드는 모델과 같은 모델을 정의 UserEntity합니다. UserEntity모델에서 a를로드 하면 모든 필드가로드됩니다. 외부 엔티티를 참조하는 필드의 경우 적절한 외부 모델을 사용하여 해당 엔티티를 작성합니다. 해당 엔티티의 경우 데이터가 요청시로드됩니다. 이제 당신의 초기 반응은 ... ??? ... !!! 예를 들어 보겠습니다.

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

우리의 경우 $db엔터티를로드 할 수있는 ORM입니다. 모델은 ORM에 특정 유형의 엔티티 세트를로드하도록 지시합니다. ORM은 맵핑을 포함하며이를 사용하여 해당 엔티티의 모든 필드를 엔티티에 주입합니다. 그러나 외부 필드의 경우 해당 오브젝트의 ID 만로드됩니다. 이 경우 , 참조 된 주문의 ID만으로를 OrderModel작성 OrderEntity합니다. 엔티티에 PersistentEntity::getField의해 호출 되면 OrderEntity모델에 모든 필드를 지연로드하도록 지시합니다 OrderEntity. OrderEntity하나의 UserEntity와 연관된 모든 항목이 하나의 결과 집합으로 처리되어 한 번에로드됩니다.

여기서 마술은 모델과 ORM이 모든 데이터를 엔티티에 주입하고 엔티티가에서 제공하는 일반 getField메소드에 대한 래퍼 함수 만 제공한다는 것 PersistentEntity입니다. 요약하면 항상 모든 필드를로드하지만 외부 엔티티를 참조하는 필드는 필요할 때로드됩니다. 여러 필드를로드하는 것만으로는 성능 문제가 아닙니다. 가능한 모든 외부 엔티티를로드하지만 성능이 크게 저하 될 수 있습니다.

이제 where 절을 기반으로 특정 사용자 집합을로드합니다. 함께 붙일 수있는 간단한 표현을 지정할 수있는 객체 지향 클래스 패키지를 제공합니다. 예제 코드에서 나는 그것을 명명했다 GetOptions. 선택 쿼리에 대한 모든 가능한 옵션을위한 래퍼입니다. 여기에는 where 절, group by 절 및 기타 모든 항목이 포함됩니다. where 절은 매우 복잡하지만 분명히 더 간단한 버전을 쉽게 만들 수 있습니다.

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

이 시스템의 가장 간단한 버전은 쿼리의 WHERE 부분을 문자열로 모델에 직접 전달하는 것입니다.

이 복잡한 답변에 대해 죄송합니다. 프레임 워크를 최대한 빠르고 명확하게 요약하려고했습니다. 추가 질문이 있으면 언제든지 문의하여 답변을 업데이트하겠습니다.

편집 : 또한 실제로 일부 필드를로드하지 않으려는 경우 ORM 매핑에서 지연로드 옵션을 지정할 수 있습니다. 모든 필드는 결국 getField메소드를 통해 로드되므로 해당 메소드가 호출 될 때 몇 분 동안 일부 필드를로드 할 수 있습니다. 이것은 PHP에서 큰 문제는 아니지만 다른 시스템에는 권장하지 않습니다.


3

이것들은 내가 본 다른 솔루션입니다. 그들 각각에는 장단점이 있지만, 당신이 결정해야합니다.

이슈 # 1 : 너무 많은 필드

이는 인덱스 전용 스캔 을 고려할 때 특히 중요한 측면 입니다. 이 문제를 해결하기위한 두 가지 해결책이 있습니다. 반환 할 열 목록이 포함 된 선택적 배열 매개 변수를 사용하도록 함수를 업데이트 할 수 있습니다. 이 매개 변수가 비어 있으면 쿼리의 모든 열이 반환됩니다. 이것은 조금 이상 할 수 있습니다. 매개 변수를 기반으로 객체 또는 배열을 검색 할 수 있습니다. 동일한 쿼리를 실행하는 두 개의 고유 한 함수를 갖도록 모든 함수를 복제 할 수 있지만 하나는 열 배열을 반환하고 다른 하나는 개체를 반환합니다.

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

문제 # 2 : 너무 많은 방법

나는 1 년 전에 Propel ORM 과 함께 잠시 일 했고 이것은 그 경험에서 내가 기억할 수있는 것에 근거합니다. Propel에는 기존 데이터베이스 스키마를 기반으로 클래스 구조를 생성 할 수있는 옵션이 있습니다. 각 테이블에 대해 두 개의 오브젝트를 작성합니다. 첫 번째 객체는 현재 나열한 것과 비슷한 긴 액세스 기능 목록입니다. findByAttribute($attribute_value). 다음 객체는이 첫 번째 객체에서 상속됩니다. 이 하위 오브젝트를 업데이트하여 더 복잡한 getter 함수를 빌드 할 수 있습니다.

다른 솔루션은 __call()정의되지 않은 함수를 실행 가능한 것으로 매핑 하는 데 사용 됩니다. 여러분의 __call것입니다 방법은 다른 쿼리에 findById 메소드와 경우 FindByName을 구문 분석 할 수있을 것입니다.

public function __call($function, $arguments) {
    if (strpos($function, 'findBy') === 0) {
        $parameter = substr($function, 6, strlen($function));
        // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
    }
}

나는 이것이 적어도 어떤 도움이되기를 바랍니다.



0

@ ryan1234에 동의합니다. 코드 내에서 완전한 객체를 전달해야하며 일반적인 쿼리 메소드를 사용하여 해당 객체를 가져와야합니다.

Model::where(['attr1' => 'val1'])->get();

외부 / 엔드 포인트 사용의 경우 GraphQL 방법이 정말 좋습니다.

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}

0

문제 # 3 : 인터페이스와 일치하지 않습니다

리포지토리에 인터페이스를 사용하면 이점을 볼 수 있으므로 테스트 목적 또는 기타 용도로 구현을 교체 할 수 있습니다. 인터페이스에 대한 이해는 구현이 따라야하는 계약을 정의한다는 것입니다. findAllInCountry ()와 같은 추가 메소드를 리포지토리에 추가하기 시작할 때까지 유용합니다. 이제 인터페이스를 업데이트 하여이 메소드를 가져와야합니다. 그렇지 않으면 다른 구현에는 없을 수 있으며 응용 프로그램이 손상 될 수 있습니다. 이것에 의해 미친 느낌이 듭니다 ... 꼬리 개가 흔들리는 경우.

내 직감에 따르면 일반적인 방법과 함께 쿼리 최적화 방법을 구현하는 인터페이스가 필요할 수 있습니다. 성능에 민감한 쿼리는 대상 메서드를 가져야하지만 드물거나 가벼운 쿼리는 일반 처리기에서 처리합니다.

일반적인 방법을 사용하면 모든 쿼리를 구현할 수 있으므로 전환 기간 동안 변경 내용이 중단되는 것을 방지 할 수 있습니다. 대상 지정 방법을 사용하면 이해가되었을 때 통화를 최적화 할 수 있으며 여러 서비스 제공 업체에 적용 할 수 있습니다.

이러한 접근 방식은 특정 최적화 작업을 수행하는 하드웨어 구현과 유사하지만 소프트웨어 구현은 간단한 작업 또는 유연한 구현입니다.


0

그런 경우 graphQL 은 데이터 저장소의 복잡성을 증가시키지 않으면 서 대규모 쿼리 언어를 제공하는 좋은 후보 라고 생각 합니다.

그러나 지금 당장 graphQL을 원하지 않는다면 또 다른 해결책이 있습니다. 프로세스 간,이 경우 서비스 / 컨트롤러와 리포지토리 간 데이터 전송에 개체 를 사용하는 DTO 를 사용 합니다.

위의 우아한 답변 이 이미 제공되어 있지만 더 간단하고 새로운 프로젝트의 출발점으로 생각할 수있는 또 다른 예를 제시하려고 노력할 것입니다.

코드에서 볼 수 있듯이 CRUD 작업에는 4 가지 방법 만 필요합니다. 이 find메소드는 객체 인수를 전달하여 나열하고 읽는 데 사용됩니다. 백엔드 서비스는 URL 쿼리 문자열 또는 특정 매개 변수를 기반으로 정의 된 쿼리 개체를 작성할 수 있습니다.

쿼리 객체 ( SomeQueryDto)는 필요한 경우 특정 인터페이스를 구현할 수도 있습니다. 복잡성을 추가하지 않고 나중에 쉽게 확장 할 수 있습니다.

<?php

interface SomeRepositoryInterface
{
    public function create(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function update(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function delete(int $id): void;

    public function find(SomeEnitityQueryInterface $query): array;
}

class SomeRepository implements SomeRepositoryInterface
{
    public function find(SomeQueryDto $query): array
    {
        $qb = $this->getQueryBuilder();

        foreach ($query->getSearchParameters() as $attribute) {
            $qb->where($attribute['field'], $attribute['operator'], $attribute['value']);
        }

        return $qb->get();
    }
}

/**
 * Provide query data to search for tickets.
 *
 * @method SomeQueryDto userId(int $id, string $operator = null)
 * @method SomeQueryDto categoryId(int $id, string $operator = null)
 * @method SomeQueryDto completedAt(string $date, string $operator = null)
 */
class SomeQueryDto
{
    /** @var array  */
    const QUERYABLE_FIELDS = [
        'id',
        'subject',
        'user_id',
        'category_id',
        'created_at',
    ];

    /** @var array  */
    const STRING_DB_OPERATORS = [
        'eq' => '=', // Equal to
        'gt' => '>', // Greater than
        'lt' => '<', // Less than
        'gte' => '>=', // Greater than or equal to
        'lte' => '<=', // Less than or equal to
        'ne' => '<>', // Not equal to
        'like' => 'like', // Search similar text
        'in' => 'in', // one of range of values
    ];

    /**
     * @var array
     */
    private $searchParameters = [];

    const DEFAULT_OPERATOR = 'eq';

    /**
     * Build this query object out of query string.
     * ex: id=gt:10&id=lte:20&category_id=in:1,2,3
     */
    public static function buildFromString(string $queryString): SomeQueryDto
    {
        $query = new self();
        parse_str($queryString, $queryFields);

        foreach ($queryFields as $field => $operatorAndValue) {
            [$operator, $value] = explode(':', $operatorAndValue);
            $query->addParameter($field, $operator, $value);
        }

        return $query;
    }

    public function addParameter(string $field, string $operator, $value): SomeQueryDto
    {
        if (!in_array($field, self::QUERYABLE_FIELDS)) {
            throw new \Exception("$field is invalid query field.");
        }
        if (!array_key_exists($operator, self::STRING_DB_OPERATORS)) {
            throw new \Exception("$operator is invalid query operator.");
        }
        if (!is_scalar($value)) {
            throw new \Exception("$value is invalid query value.");
        }

        array_push(
            $this->searchParameters,
            [
                'field' => $field,
                'operator' => self::STRING_DB_OPERATORS[$operator],
                'value' => $value
            ]
        );

        return $this;
    }

    public function __call($name, $arguments)
    {
        // camelCase to snake_case
        $field = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));

        if (in_array($field, self::QUERYABLE_FIELDS)) {
            return $this->addParameter($field, $arguments[1] ?? self::DEFAULT_OPERATOR, $arguments[0]);
        }
    }

    public function getSearchParameters()
    {
        return $this->searchParameters;
    }
}

사용법 예 :

$query = new SomeEnitityQuery();
$query->userId(1)->categoryId(2, 'ne')->createdAt('2020-03-03', 'lte');
$entities = $someRepository->find($query);

// Or by passing the HTTP query string
$query = SomeEnitityQuery::buildFromString('created_at=gte:2020-01-01&category_id=in:1,2,3');
$entities = $someRepository->find($query);
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.