[CakePHP] ORM을 이용한 테이블 Fetch 설정


Study/PHP  2019. 10. 10. 09:59

안녕하세요. 명월입니다.


이 글은 CakePHP에서 ORM을 이용한 테이블 Fetch 설정에 대한 글입니다.


이전에는 CakePHP의 ORM Query식으로 데이터를 검색하는 것을 했습니다.

링크 - [CakePHP] CakePHP ORM의 Query식과 Entity, Table 클래스, Resultset


ORM Fetch란 데이터 베이스 테이블에서 Reference된 테이블틔 데이터를 연결하는 작업을 뜻합니다.

예를 들면, User 테이블에서 파생 테이블인 Info가 있고, Info 테이블의 UserID는 User테이블의 ID로 foreign key로 묶였다고 생각합니다.

이때, 프로그램에서 User 테이블을 검색을 하면 파생 테이블인 Info 테이블도 List 형식으로 같이 검색되는 것을 말합니다.

1:1 관계라면 하나의 클래스로 변수로 인스턴스 되고, 1:n 관계이면 List타입에 인스턴스가 생성됩니다.

자바에서도 비슷한 기능이 있습니다.

링크 - [Java강좌 - 49] JPA의 entity 클래스 설정 - 2(cascade, fetch)


테스트를 위해서 테이블 구조를 새로 생성하겠습니다.

-- 유저 테이블 (키는 id)
create table user(
  id varchar(255) not null,
  name nvarchar(255) not null,
  
  primary key(id)
);
 
-- User의 파생 테이블인 info 테이블. 1:n관계
create table info(
  idx int not null auto_increment,
  id varchar(255) not null,
  age int not null,
  
  primary key(idx),
  -- User 테이블의 id를 Reference 했다.
  foreign key(id) references user(id)
);
-- info의 파생 테이블인 info2테이블. 1:n관계
create table info2(
  idx int not null auto_increment,
  info_idx int not null,
  birth date,
  
  primary key(idx),
  -- info테이블 키를info_idx에 reference했다.
  foreign key(info_idx) references info(idx)
);
-- user 파생 테이블의 permission 테이블, m:n 관계
create table permission(
  code char(4) not null,
  name varchar(255) not null,

  primary key(code)
);
-- user테이블과 permission테이블을 m:n관계로 묶는 맵
create table permission_map(
  id varchar(255) not null,
  code char(4) not null,
  
  foreign key(id) references user(id),
  foreign key(code) references permission(code)
);
-- User 테이블에 id가 「nowonbun」을 insert한다.
insert into user values('nowonbun', 'Tester');
-- info 테이블에 유저 테이블 키가 「nowonbun」인 두 개의 데이터를 insert한다.
insert into info (id,age)values('nowonbun', 20);
insert into info (id,age)values('nowonbun', 25);
-- permission 테이블에 2개의 데이터를 insert한다.
insert into permission values('a','a type');
insert into permission values('b','b type');

-- 유저 테이블 키 「nowonbun」의 데이터를 permission 테이블의 「a」에 연결한다.
insert into permission_map values('nowonbun','a');
-- 유저 테이블 키 「nowonbun」의 데이터를 permission 테이블의 「a」에 연결한다.
insert into permission_map values('nowonbun','b');

출처 - [Java강좌 - 48] Eclipse에서 JPA의 persistance.xml 설정과 entity 클래스 설정 - 1(@GeneratedValue 설정)


데이터 베이스에 테이블이 생성되면 Table클래스와 Entity 클래스도 생성합니다.

CakePHP ORM에서는 Fetch 기능에 대한 설명은 없습니다. Fetch 기능이 아닌 Join을 이용해서 데이터를 가져와서 사용하는 방법을 소개합니다.

링크 - https://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html#changing-fetching-strategies


그런데 개인적으로 이런 CakePHP ORM에서 join을 이용한 데이터를 가져오는 것이 꽤 복잡합니다. 왜냐하면 Entity 클래스에서 데이터를 처리되는 것이 아니고 Controller에서 $table->find() 함수를 통해 데이터 Join을 하고 있습니다.

이런 식으로 작성된다고 하면 Entity 클래스는 몇 스탭 없지만 Table의 Join 기능까지 Controller에 몰리니 Controller 클래스가 엄청 복잡해 질 것입니다.

그래서 저의 경우는 CakePHP에서 사용하는 방법이 아닌 개인적으로 Table클래스에 Fetch기능을 넣어보면 어떨까해서 작성했습니다.

Table클래스는 이전과 크게 다를 건 없습니다.

링크 - [CakePHP] CakePHP ORM의 Query식과 Entity, Table 클래스, Resultset


Entity 클래스에서 property를 추가하여 Fetch 기능을 만들겠습니다.

<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
use Cake\ORM\TableRegistry;

// User 테이블에 Fetch를 설정
class User extends Entity {
  // info 테이블의 references 변수
  private $infos;
  // $엔티티->infos(프로퍼티)로 요청할 때 불리는 함수
  protected function _getInfos() {
    // flyweight 패턴
    if ($this->infos === null) {
      // 데이터를 설정한다.
      $this->setFetchInfos();
    }
    return $this->infos;
  }
  // infos의 Fetch설정
  public function setFetchInfos($condition = null) {
    // info 테이블의 레지스트리를 취득한다.
    $table = TableRegistry::get('info');
    // 검색 조건을 추가한다. user의 id로 user 테이블을 검색한다.
    $query = $table->find()->where(["id" => $this->id]);
    if ($condition != null) {
      $query = $query->where($condition);
    }
    // 취득해서 변수에 설정
    $this->infos = $query->toList();
  }
  // permission 테이블의 references 변수
  private $permissions;
  // $엔티티->permissions(프로퍼티)로 요청할 때 불리는 함수
  protected function _getPermissions() {
    // flyweight 패턴
    if ($this->permissions === null) {
      // 데이터를 설정한다.
      $this->setFetchPermissions();
    }
    return $this->permissions;
  }
  // Permission의 Fetch설정
  public function setFetchPermissions($condition = null) {
    // permissionMap 테이블의 레지스트리를 취득한다.
    $table = TableRegistry::get('permissionMap');
    // 검색 조건을 추가한다. user의 id로 permission 테이블을 검색한다.
    $query = $table->find()->where(["id" => $this->id]);
    if ($condition != null) {
      $query = $query->where($condition);
    }
    // 결과를 취득
    $maplist = $query->toArray();
    // permission 테이블의 references 변수를 초기화
    $this->permissions = [];
    // permissionMap 인스턴스의 permission 프로퍼티 값을 취득하여 넣는다.
    foreach ($maplist as $item) {
      array_push($this->permissions, $item->permission);
    }
  }
}

일단 아래의 Permission은 아래에 다시 설명하고 일단 Info만 확인하겠습니다.

Controller 클래스에서 User 테이블을 검색하겠습니다.

<?php
namespace App\Controller;
use Cake\ORM\TableRegistry;

class HomeController extends AppController {
  public function index() {
    // User 테이블의 레지스트리를 취득
    $table = TableRegistry::get('User');
    // Query식을 취득
    $query = $table->find();
    // User 테이블의 id가 nowonbun인 데이터를 취득
    $query = $query->where(['id' => 'nowonbun']);
    // 검색 결과
    $user = $query->first();
    // 화면에 표시한다.
    debug($user);
    // User Entity 클래스의 infos 프로퍼티를 확인한다.
    debug($user->infos);
    //debug($user->permissions);
    //debug($user->infos[0]->user);
  }
}

infos의 프로퍼티 데이터를 확인하면 User 테이블의 id에 의해 연결된 info 테이블의 데이터가 표시됩니다.

User 테이블을 검색하면 info테이블을 따로 검색하지 않아도, User 테이블에 연결되어 가져옵니다.


다시 돌아와서 user entity 클래스의 permission 프로퍼티를 보겠습니다. permissionMap entity에서 permission데이터를 취득합니다.

<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;
use Cake\ORM\TableRegistry;
// PermissionMap 테이블의 Fetch 설정
class PermissionMap extends Entity {
  // user 테이블의 references 변수
  private $user;
  // $엔티티->user(프로퍼티)를 호출할 때 불리는 함수
  protected function _getUser() {
    // flyweight패턴
    if ($this->user === null) {
      // 변수에 데이터를 설정
      $this->setFetchUser();
    }
    return $this->user;
  }
  // user 변수에 데이터를 설정
  public function setFetchUser($condition = null) {
    // User 테이블의 레지스트리를 취득
    $table = TableRegistry::get('user');
    // User 테이블의 id키로 검색한다.
    $query = $table->find()->where(["id" => $this->id]);
    if ($condition != null) {
      $query = $query->where($condition);
    }
    // 검색
    $this->user = $query->first();
  }
  // permission 테이블의 references 변수
  private $permission;
  // $엔티티->permission(프로퍼티)를 호출할 때 불리는 함수
  protected function _getPermission() {
    if ($this->permission === null) {
      // 변수에 데이터를 설정한다.
      $this->setFetchPermission();
    }
    return $this->permission;
  }
  // permission 변수에 데이터를 설정한다.
  public function setFetchPermission($condition = null){
    // permission 테이블의 레지스트리를 취득
    $table = TableRegistry::get('permission');
    // Permission 테이블의 code 키로 검색
    $query = $table->find()->where(["code" => $this->code]);
    if ($condition != null) {
      $query = $query->where($condition);
    }
    // 검색
    $this->permission = $query->first();
  }
}

permissionMap테이블에도 같은 패턴으로 id와 user테이블을, code와 Permission테이블을 연결합니다.

그래서 User테이블에서 permissionMap을 취득해서 permission프로퍼티를 호출하면 permission데이터가 검색되어 나오는 것입니다.

<?php
namespace App\Controller;
use Cake\ORM\TableRegistry;

class HomeController extends AppController {
  public function index() {
    // User테이블의 레지스트리
    $table = TableRegistry::get('User');
    // query식을 취득
    $query = $table->find();
    // 조건에 id가 nowonbun인 데이터를 검색
    $query = $query->where(['id' => 'nowonbun']);
    // 데이터를 취득
    $user = $query->first();
    // 데이터를 확인
    debug($user);
    // user테이블의 infos 프로퍼티 값을 확인한다.
    debug($user->infos);
    echo "<br />";
    // 데이터의 permission데이터를 확인한다.
    debug($user->permissions);
    // info데이터의 user데이터를 취득한다.
    // Fetch의 반복 검색입니다. user데이터의 info데이터를 취득하고 다시 info데이터의 user데이터를 가져오면 user와 info의 user는 같은 데이터가 됩니다.
    debug($user->infos[0]->user);
  }
}

지금까지 설명한 방식은 모두 Join하여 조회한 것이 아니고, 키에 의해 다시 검색하는 타입이므로 성능이 조금 떨어질 가능성이 있습니다.

하지만 소스가 직관적으로 작성되었기 때문에 프로그램을 작성하고 관리하는 데는 더 편할 수도 있다고 생각합니다.

CakePHP는 Table 클래스에서 위처럼 Fetch 기능을 추가하는 것 이외에 자유롭게 쿼리를 통합하고 작성할 수 있기 때문에 필요하다면 별도의 쿼리를 작성하는 것이 좋은 방법이라고 생각합니다.


좀 더 좋은 방식이 있으면 공유해 주세요.


여기까지 CakePHP에서 ORM을 이용한 테이블 Fetch 설정에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.