코드이그나이터4에서 모델은 데이터베이스 테이블과 매핑되는 PHP 클래스입니다. 모델을 통해서 데이터베이스에 생성, 읽기, 수정, 삭제(Create, Read, Update, Delete) 를 할 수 있습니다.
혹시 ORM을 다루어본 적이 있으시다면 주의해야 할 점이 있습니다. 코드이그나이터의 모델은 "컬럼"과 1:1로 매핑되지 않습니다. 오히려 리파지터리(repository)에 가까운 객체입니다.
이번 챕터의 코드는 https://github.com/koeunyeon/ci4/tree/model-basic 에 있습니다.
이제 모델을 생성해 보겠습니다. /app/Models
디렉토리 아래에 SampleModel.php
파일을 생성하고 아래의 내용으로 대체합니다.
/app/Models/SampleModel.php
<?php
namespace App\Models; // (1)
use CodeIgniter\Model; // (2)
class SampleModel extends Model // (3)
{
protected $table = 'sample'; // (4)
protected $allowedFields = ['name','age']; // (5)
protected $primaryKey = "sample_id"; // (6)
}
// (7)
한 줄씩 살펴보겠습니다.
(1) 네임스페이스를 파일 경로와 동일하게 App\Models
로 설정합니다.
(2) (3) 코드이그나이터4에서 모든 모델은 CodeIgniter\Model
클래스를 상속합니다. 따라서 CodeIgniter\Model
클래스를 사용하기 위해 use
문으로 클래스를 가지고 옵니다.
(3) Model
클래스를 상속한 MemberModel
을 선언합니다. 모델 클래스 이름은 PHP 표준 권고안인 PSR-1
에 따라 PascalCase를 따릅니다.
(4) 데이터베이스 테이블 이름을 $table
변수에 설정합니다. $table
변수는 모델의 필수값입니다.
(5) allowedFields
은 외부에서 설정 가능한 열을 나열합니다.
sample
테이블을 예시로 들어보면, ['name', 'age']
는 allowedFields
에 설정되어 있으므로 데이터베이스에 열의 값 생성, 수정이 가능합니다. 하지만 sample_id
열은 allowedFields
항목에 지정되어 있지 않으므로 혹여 모델에 값을 설정했다고 하더라도 실제 데이터베이스에 저장되지 않습니다.
$allowedFields
변수는 모델의 필수값입니다.
(6) 데이터베이스의 키 이름을 $primarKey
변수에 지정합니다. 반드시 필요한 것은 아니지만, update
, delete
메소드를 사용할 때는 필요합니다. 생략되면 id
라고 가정합니다.
(7) PSR-12(구버전-PSR2)에 따라 PHP code만 존재하는 파일에서는 닫는 ?>
태그를 생략합니다. 이는 닫는 태그 ?>
이후 공백이 있으면 출력에 영향이 있을 수도 있기 때문입니다.
모델 기능을 테스트하기 위해 간단한 컨트롤러를 만들겠습니다. /app/Controllers
디렉토리 아래에 Model.php
파일을 생성하고 아래의 내용으로 대체합니다.
/app/Controllers/Model.php
<?php
namespace App\Controllers;
use App\Models\SampleModel; // (1)
class Model extends BaseController
{
public function create()
{
$sampleModel = new SampleModel(); // (2)
$data = [ // (3)
'name' => 'ci4',
'age' => 1
];
try { // (4)
$result = $sampleModel->insert($data); // (5)
return "$result"; // (6)
} catch (\ReflectionException $e) { // (7)
return $e->getMessage();
} catch(\DataException $e){ // (8)
return $e->getMessage();
}
}
}
한 줄씩 살펴보겠습니다.
(1) 모델을 사용하기 위해 use
문으로 모델을 불러옵니다.
(2) 모델 객체를 $sampleModel
이라는 이름으로 생성합니다. 일반 객체와 사용법은 동일합니다.
(3) 입력할 데이터를 설정합니다. 대부분의 경우 이 데이터는 어디선가에서 전달받은 값이겠지만, 지금은 기능 확인을 위해 임의의 값을 넣었습니다.
(4) 코드이그나이터4에서는 insert
가 실패할 경우 오류를 발생시킵니다. 오류 메세지가 사용자에게 그대로 보여지는 것은 좋지 않으므로 try catch
로 감쌉니다.
전통적으로 PHP는 예외(exception
)를 느슨하게 사용하고 오류(error
)를 적극적으로 사용하며 왠만해서는 경고(warning
) 으로 그치는데다가 심각한 경우에만 치명적인 오류(fatal error
)를 발생시키는 언어였습니다. 이는 언어의 디자인과 철학이 "어떻게든 돌아가게는 만든다"였기 때문에, "논리적인 오류가 있어도 무슨 수를 써서든 동작은 된다"는 논리와 동일했기 때문입니다.
처음 PHP가 나왔던 시절에는 괜찮은 생각이었을 지도 모르겠으나, 지금은 그다지 현명한 방법으로 여겨지는 것은 아닙니다. try catch
로 감싸도 오류가 날 수 있다는 것은 시스템의 안정성에 문제가 있다는 뜻이거든요.
다행이도 PHP7에서는 오류도 try catch
로 잡아낼 수 있게 변경되었습니다. 이제 치명적인 오류만 아니라면 PHP 는 정상적으로 실행됩니다.
(5) $sampleModel
인스턴스를 이용해 데이터를 입력합니다. 파라미터는 입력할 데이터입니다. 이미 SampleModel
클래스는 어떤 테이블에 데이터를 넣어야 할 지와 어떤 필드를 사용해야 할 지 알고 있기 때문에 데이터를 입력할 수 있습니다.
/app/Models/SampleModel.php
protected $table = 'sample';
protected $allowedFields = ['name','age'];
(6) 정상적으로 값이 입력되었다면 $result
변수에는 자동으로 sample
테이블의 PK인 sample_id
의 값이 담깁니다.
(7) 코드이그나이터4의 insert
메소드 정의에는 ReflectionException
을 발생시킨다고 정의하고 있습니다.
vender/codeigniter4/framework/system/Model.php
* @throws \ReflectionException
따라서 ReflectionException
을 잡을 수 있도록 catch
를 설정합니다.
catch (\ReflectionException $e)
(8) 하지만 실제 코드이그나이터4 소스코드를 보면 throw
구문이 DataException
을 던집니다.
vender/codeigniter4/framework/system/Model.php
throw DataException::forEmptyDataset('insert');
DataException
클래스와 ReflectionException
클래스는 아무런 연관관계가 없으므로 주석 버그가 아닌지 싶기는 합니다만, 정확한 내부 동작을 몰라 일단 ReflectionException
과 DataException
둘 다 catch
에서 잡아내도록 정의합니다.
catch(\DataException $e)
브라우저에서 http://localhost:8080/model/create에 접속해 봅니다.
실제로 데이터베이스에 데이터가 들어갔는지 확인해 보겠습니다.
PHPStorm - Database - Schemas - ci4db - sample 항목을 더블클릭해서 데이터를 확인할 수 있습니다.
모델을 이용해서 데이터베이스의 데이터를 읽는 방법을 알아보겠습니다. Model
컨트롤러에 아래의 메소드 세개를 추가합니다. 각각의 메소드는 여러개의 목록을 반환하는 방법, 첫번째 행을 반환하는 방법, 그리고 ID로 편리하게 첫번째 행을 찾는 방법을 보여줍니다.
/app/Controllers/Model.php
public function readall(): \CodeIgniter\HTTP\Response // (1)
{
$sampleModel = new SampleModel();
$data = $sampleModel
->where('name', 'ci4') // (2)
->findAll(); // (3)
$last_query = $sampleModel->db->getLastQuery(); // (4)
error_log(print_r($last_query, true)); // (5)
return $this->response->setJSON($data);
}
public function readfirst(): \CodeIgniter\HTTP\Response //(6)
{
$sampleModel = new SampleModel();
$data = $sampleModel
->where(['name'=>'ci4']) // (7)
->orderBy('sample_id', 'desc') // (8)
->first(); // (9)
error_log($sampleModel->db->showLastQuery()); // (10)
return $this->response->setJSON($data);
}
public function find(){ // (11)
$sampleModel = new SampleModel();
$data = $sampleModel->find(1); // (12)
return $this->response->setJSON($data);
}
(1) 첫번째 메소드 readall
은 모든 데이터를 읽습니다.
(2) where
는 "검색 조건"을 나타냅니다. 데이터베이스의 where
와 동일한 의미입니다. where(키,값)
형태로 쓰이면 키 = 값
, 즉 "키
가 값
과 같다면" 이라는 뜻이 됩니다.
where
처럼 모델에 메소드를 통해 데이터베이스에 질의를 하는 것을 "쿼리 빌더(Query Builder)" 라고 부릅니다.
(3) findAll
메소드는 모든 데이터를 순차배열 형태로 가지고 올 때 사용합니다.
(4) 코드이그나이터4에서 마지막으로 실행된 쿼리를 가져오기 위해서는 모델->db->getLastQuery()
로 읽을 수 있습니다.
다만 객체를 DUMP하는 것은 가능하지만, 객체 내부는 protected
로 선언되어 있으므로 직접 사용은 불가능합니다. 만약 getLastQuery()
의 내부 데이터를 꼭 가져와야 한다면 CodeIgniter\Database\Query
를 상속한 클래스 A를 만들고, 결과를 A로 캐스팅한 다음 getter
등을 이용해 가져와야 합니다.
(5) getLastQuery
메소드의 결과를 출력합니다.
error_log
는 출력을 브라우저에 보내는 응답이 아니라 시스템 로그에 씁니다. print_r
함수의 두번째 파라미터가 true
이면 출력을 바로 응답(response) 스트림에 보내는 것이 아니라 출력을 반환하므로, 반환한 값을 error_log
함수를 이용해 시스템 로그에 쓰게 되는 것입니다.
getLastQuery()
는 원본 쿼리, 최종 쿼리, 파라미터, 커넥션 정보 등 여러가지 정보를 한번에 보여줍니다. 보통은 쿼리와 파라미터가 실행되는 것을 볼 일이 많으므로 두가지 정보만 예제로 보면 아래와 같습니다.
[originalQueryString:protected] => SELECT *
FROM `sample`
WHERE `name` = :name:
[finalQueryString:protected] => SELECT *
FROM `sample`
WHERE `name` = 'ci4'
[binds:protected] => Array
(
[name] => Array
(
[0] => ci4
[1] => 1
)
)
originalQueryString
은 원본 쿼리 문자열을, binds
는 쿼리 파라미터를 나타냅니다.finalQueryString
은 binds
쿼리 파라미터가 originalQueryString
에 바인딩 된 후의 최종 쿼리 문자열입니다.
(6) 두번째 엔드포인트 readfirst
메소드는 검색 결과 중 첫번째 열만 가지고 옵니다.
(7) 쿼리 빌더의 where
메소드는 (2) 처럼 where(키,값)
파라미터 형태로 사용할 수도 있지만 where(연관배열)
형태로 사용할 수도 있습니다. 주로 조건이 하나인 경우에는 키,값
형태를 사용하고 조건이 여러개인 경우에는 연관배열 형태를 사용하게 됩니다.아래의 두개의 where
는 똑같은 쿼리를 생성합니다.
where('name', 'ci4')
where(['name' => 'ci4'])
(8) 쿼리 빌더에서 정렬은 데이터베이스처럼 orderBy
메소드를 이용합니다. 첫번째 인수는 컬럼 이름, 두번째 인수는 순차/역순 정렬 여부로 각각 'asc' 혹은 'desc' 입니다. 두번째 인수가 생략되면 순차 정렬을 뜻하는 'asc'로 가정합니다.
(9)first
함수는 "첫번째" 행만 가지고 옵니다.
(10) showLastQuery()
메소드는 마지막 실행한 쿼리를 "문자열"로 가지고 옵니다. 예시의 출력은 아래와 같습니다.
SELECT *
FROM `sample`
WHERE `name` = 'ci4'
ORDER BY `sample_id` DESC
LIMIT 1
코드이그나이터4는 데이터베이스 종류에 따라 서로 다른 쿼리를 만들어내는데, MySQL 계열의 데이터베이스에서는 갯수를 제한하기 위해 limit 1
을 사용하는 것을 알 수 있습니다.
(11) 세번째 엔드포인트 메소드 find
는 데이터베이스의 주 키(Primary Key)를 가지고 간단하게 데이터를 가져올 수 있는 방법을 보여줍니다.
(12) 모델의 find
메소드는 파라미터로 primary key
의 값을 입력받습니다. find
메소드를 사용하려면 반드시 모델 클래스의 $primaryKey
멤버변수를 설정하거나, 혹은 데이터베이스의 주 키 이름이 id
여야 합니다.
브라우저로 http://localhost:8080/model/readall 에 접속해서 데이터가 모두 잘 나오는지 확인해 봅시다.
브라우저로 http://localhost:8080/model/readfirst 에 접속해서 데이터가 잘 나오는지 확인해 봅시다.
브라우저로 http://localhost:8080/model/find 에 접속해서 데이터가 잘 나오는지 확인해 봅시다.
모델을 이용해서 데이터베이스의 데이터를 수정하는 방법을 알아보겠습니다.
Model
컨트롤러에 아래 세개의 메소드를 추가합니다.
/app/Controllers/Model.php
public function update() // (1)
{
$sampleModel = new SampleModel();
$sampleModel->update(1, ['name' => 'update']); // (2)
return $this->response->setJSON($sampleModel->find(1)); // (3)
}
public function save() // (4)
{
$sampleModel = new SampleModel();
$sample_data = $sampleModel->find(1); // (5)
$sample_data['name'] = "save"; // (6)
$sampleModel->save($sample_data); // (7)
return $this->response->setJSON($sampleModel->find(1));
}
public function qbupdate() // (8)
{
$sampleModel = new SampleModel();
$sampleModel
->where("sample_id", 1) // (9)
->set(['name' => 'qbupdate']) // (10)
->update(); // (11)
return $this->response->setJSON($sampleModel->where('sample_id', 1)->first());
}
코드를 확인해 보겠습니다.
(1) 첫번째 엔드포인트 update
는 모델 클래스의 update
메소드를 이용해 데이터를 수정하는 방법을 보여줍니다.
(2) 모델 클래스의 update
메소드는 파라미터를 2개 입력받습니다. 첫번째는 주 키의 값, 두번째는 갱신할 데이터입니다.
find
메소드와 마찬가지로 update
메소드도 주 키 이름이 모델 클래스->$primaryKey
에 설정되어 있거나 데이터베이스 테이블 주 키 이름이 id
여야 합니다.
두번째 파라미터는 연관 배열입니다. 각 키는 열 이름, 값은 변경할 데이터입니다.
(3) 정상적으로 수정되었는지 확인하기 위해 데이터베이스에 결과를 조회한 후 JSON 형식으로 응답합니다. 이렇게 해 두면 직접 데이터베이스의 데이터를 조회해 보지 않아도 결과를 확인할 수 있습니다.
(4) 두번째 엔드포인트 save
는 모델 클래스의 save
메소드를 이용해 데이터를 수정하는 방법을 보여줍니다.
(5) 모델의 find
메소드로 데이터를 검색합니다.
(6) 저장할 데이터의 name
속성을 save
로 변경합니다.
(7) 모델의 save
메소드를 이용해서 데이터를 저장합니다. 파라미터로는 연관배열 혹은 클래스를 취합니다.
save
메소드가 재미있는 점은 upsert
로 작동한다는 것입니다. 즉, 만약 기존의 데이터가 존재한다면 update
를, 기존의 데이터가 존재하지 않는다면 insert
를 합니다.
다만 주의해야 할 것이 코드이그나이터4에서 기존의 데이터가 존재하는지 확인하는 것은 실제로 데이터베이스에 조회를 해 보는 것이 아니라 주 키에 해당하는 데이터가 있는지 여부입니다.
본 예시에서 sample_data
는 find(1)
메소드를 통해 가져왔습니다. 이 시점에서 이미 sample_data['sample_id']
의 값은 1
임이 정해져 있는 상태입니다. 따라서 코드이그나이터는 save
메소드가 호출될 때 SampleModel->$primaryKey
는 sample_id
이고, sample_data
에는 sample_id
라는 키가 있으므로 update
라고 판단합니다.
(8) 세번째 엔드포인트 qbupdate
는 쿼리 빌더를 통한 업데이트 방법을 보여줍니다.
많은 경우 업데이트는 주 키를 통해서 하지만, 늘 그런 것은 아닙니다. 조건은 얼마든지 다양화할 수 있지요. 특정 조건에 따른 데이터 수정을 하고 싶을 때는 쿼리 빌더를 통해 업데이트를 할 수 있습니다.
또한 쿼리 빌더를 통한 업데이트는 모델->$primaryKey
가 설정되어 있지 않아도 동작한다는 장점이 있습니다.
(9) 수정할 조건을 where
로 지정합니다.
(10) 수정할 데이터를 set
메소드를 통해 설정합니다.
(11) update()
메소드에는 파라미터가 없는 것에 주의하세요. 모든 파라미터는 이미 where
와 set
을 통해 설정되어 있습니다.
브라우저로 http://localhost:8080/model/update 에 접속해서 데이터가 수정되었는지 확인해 봅시다.
브라우저로 http://localhost:8080/model/save 에 접속해서 데이터가 수정되었는지 확인해 봅시다.
브라우저로 http://localhost:8080/model/qbupdate 에 접속해서 데이터가 수정되었는지 확인해 봅시다.
모델을 이용해서 데이터베이스의 데이터를 삭제하는 방법을 알아보겠습니다.
Model
컨트롤러에 아래 메소드를 추가합니다.
public function delete()
{
$sampleModel = new SampleModel();
$sampleModel->delete(1); // (1)
return $this->response->setJSON($sampleModel->find(1));
}
(1) 모델의 delete
메소드를 이용해 데이터를 삭제합니다. 첫번째 파라미터는 주 키 입니다. 삭제의 경우는 조회, 수정과는 다르게 대부분의 경우 주 키를 이용해 삭제하게 됩니다.
브라우저로 http://localhost:8080/model/delete 에 접속해서 데이터가 수정되었는지 확인해 봅시다.
데이터베이스 뷰에서도 확인하면 확실하겠죠?