나는 내 자신의 질문에 답하는 데 열중 할 것이라고 생각했다. 다음은 원래 질문에서 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을 사용하므로 대부분의 경우 응용 프로그램 전체에서 직접 해당 모델을 참조합니다. 그러나 더 복잡한 쿼리가있는 상황에서는 쿼리 개체를 사용하여 더 재사용 할 수 있도록합니다. 또한 항상 내 모델을 내 메소드에 주입하여 테스트에서 쉽게 조롱 할 수 있습니다.