Piece_ORM短期集中コース
Copyright (c) 2007 Piece Project, All rights reserved.
このドキュメントは、クリエイティブ・コモンズ・ライセンスの下でライセンスされています。
目次
- はじめに
- 準備
- レコードの作成(INSERT)
- メソッドとSQLクエリ
- find系メソッドによる単一レコードの検索(SELECT)
- findAll系メソッドによる複数レコードの検索(SELECT)
- レコードの更新(UPDATE)
- レコードの削除(DELETE)
- リアルドメインオブジェクトへの変換
- Piece_ORMをどこで使うべきか?
- おわりに
はじめに
この記事は、PHPのためのオブジェクトリレーショナルマッピングフレームワークPiece_ORMを、すぐに使い始められるように書かれています。是非、実際に手を動かしながら記事を読み進めてみてください。
準備
最初に、短期集中コースを進めるにあたって必要なものを準備します。
- CLI版PHP
- RDBMS(PostgreSQLあるいはMySQL)
- Piece_ORM >= 0.3.0, 上記RDBMSに合わせたMDB2ドライバ1
CLI版PHPとRDBMSの準備ができたら、このコースで使うディレクトリ構造を作成します。
[TOP (C:\path\to\crashcourse)] | +- [cache] | +- [config] | | | +- [mappers] | +- [scripts]
- 1. ユーザーズマニュアルのインストールの章を参照のこと。
Piece_ORMの設定
では、早速Piece_ORMを使ってデータベースのレコードを取得してみましょう。下記は、personテーブルのレコードを全件取得し、ダンプするスクリプトです。
scripts\find.php:
<?php require_once 'Piece/ORM.php'; require_once 'Piece/ORM/Error.php'; Piece_ORM_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';')); $mapper = &Piece_ORM::getMapper('Person'); var_dump($mapper->findAll()); ?>
このスクリプトを実行してみましょう。
> php -f find.php
array(7) {
["code"]=>
int(-6)
["params"]=>
array(0) {
}
["package"]=>
string(9) "Piece_ORM"
["level"]=>
string(9) "exception"
["time"]=>
float(1178862684.8802)
["context"]=>
array(4) {
["file"]=>
string(34) "C:\path\to\pear\Piece\ORM.php"
["line"]=>
int(144)
["function"]=>
string(9) "getMapper"
["class"]=>
string(9) "Piece_ORM"
}
["message"]=>
string(58) "getMapper method must be called after calling configure()."
}
おっと、いきなりエラーが発生してしまいました。エラーメッセージによると、getMapper()のコールの前にconfigure()をコールしないといけないことがわかります。では、先ほどのスクリプトにPiece_ORM::configure()のコールを追加しましょう。
scripts\find.php:
<?php require_once 'Piece/ORM.php'; require_once 'Piece/ORM/Error.php'; Piece_ORM_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';')); Piece_ORM::configure('../config', '../cache', '../config/mappers'); $mapper = &Piece_ORM::getMapper('Person'); var_dump($mapper->findAll()); ?>
Piece_ORM::configure()
このスクリプトを実行する前に、Piece_ORM::configure()の引数を説明します。
- 第1引数には、Piece_ORMの設定ファイル、piece-orm-config.yamlの配置先ディレクトリを指定します。piece-orm-config.yamlには、DSNを中心とした接続先情報を記述します。
- 第2引数には、Piece_ORMが出力する各種キャッシュファイルの保存先ディレクトリを指定します。指定されたディレクトリに、piece-orm-config.yaml, テーブル定義、マッパー定義ファイルのキャッシュが保存されます。
- 第3引数には、マッパー定義ファイルの配置先ディレクトリを指定します。マッパー定義ファイルには、マッパークラスのメソッド、メソッドに対応するSQLクエリ、リレーションシップ定義?を記述します。
では、スクリプトを実行してみましょう。
> php -f find.php
array(7) {
["code"]=>
int(-2)
["params"]=>
array(0) {
}
["package"]=>
string(9) "Piece_ORM"
["level"]=>
string(9) "exception"
["time"]=>
float(1178895093.056)
["context"]=>
array(4) {
["file"]=>
string(49) "C:\path\to\pear\Piece\ORM\Mapper\Factory.php"
["line"]=>
int(364)
["function"]=>
string(5) "_load"
["class"]=>
string(24) "Piece_ORM_Mapper_Factory"
}
["message"]=>
string(67) "The configuration file [ ../config/mappers/Person.yaml ] not found."
}
またエラーが発生してしまいました。今度は、personテーブル用のマッパー定義ファイルPerson.yamlファイルが見つからないようです。
マッパー定義ファイル
前述のように、マッパー定義ファイルには、マッパークラスのメソッド、メソッドに対応するSQLクエリ、リレーションシップ定義?を記述しますが、それ以前に、マッパーオブジェクトを取得するためには、必ずマッパー定義ファイルが必要となる、という決まりごとを覚えておいてください。では、空のファイルconfig\mappers\Person.yamlを作成し、再度スクリプトを実行してみましょう。
> php -f find.php
array(8) {
["code"]=>
int(-1)
["params"]=>
array(0) {
}
["package"]=>
string(9) "Piece_ORM"
["level"]=>
string(9) "exception"
["time"]=>
float(1178896567.5199)
["context"]=>
array(4) {
["file"]=>
string(42) "C:\path\to\pear\Piece\ORM\Context.php"
["line"]=>
int(213)
["function"]=>
string(13) "getConnection"
["class"]=>
string(17) "Piece_ORM_Context"
}
["message"]=>
string(51) "Failed to invoke MDB2::singleton() for any reasons."
["repackage"]=>
array(6) {
["code"]=>
int(-4)
["message"]=>
string(21) "MDB2 Error: not found"
["params"]=>
array(2) {
["userinfo"]=>
string(25) "no RDBMS driver specified"
["debuginfo"]=>
string(25) "no RDBMS driver specified"
}
["package"]=>
string(4) "PEAR"
["level"]=>
string(9) "exception"
["time"]=>
float(1178896567.5197)
}
}
エラーがしつこく発生しています。今度は、データベースドライバが指定されていない、というエラーです。Piece_ORMでデータベースドライバを指定する方法は二通りあります。ひとつは、piece-orm-config.yamlで指定する方法、もうひとつは、メソッドから設定する方法です。今回は基本となる設定方法である、piece-orm-config.yamlを使います。
piece-orm-config.yaml - Piece_ORM設定ファイル
では、下記のようにpiece-orm-config.yamlを作成してください。
config\piece-orm-config.yaml(PostgreSQL):
- name: crashcourse dsn: pgsql://piece:piece@localhost/crashcourse
config\piece-orm-config.yaml(MySQL):
- name: crashcourse dsn: mysql://piece:piece@localhost/crashcourse
さぁ、今度はエラーなく実行できるでしょうか?やってみましょう。
> php -f find.php
array(8) {
["code"]=>
int(-1)
["params"]=>
array(0) {
}
["package"]=>
string(9) "Piece_ORM"
["level"]=>
string(9) "exception"
["time"]=>
float(1178898149.1207)
["context"]=>
array(4) {
["file"]=>
string(51) "C:\path\to\pear\Piece\ORM\Metadata\Factory.php"
["line"]=>
int(249)
["function"]=>
string(27) "_createMetadataFromDatabase"
["class"]=>
string(26) "Piece_ORM_Metadata_Factory"
}
["message"]=>
string(55) "Failed to invoke $reverse->tableInfo() for any reasons."
["repackage"]=>
array(6) {
["code"]=>
int(-26)
["message"]=>
string(28) "MDB2 Error: no such database"
["params"]=>
array(2) {
["userinfo"]=>
string(138) "connect: [Error message: Could not select the database: crashcourse]
[Native code: 1049]
[Native message: Unknown database 'crashcourse']
"
["debuginfo"]=>
string(138) "connect: [Error message: Could not select the database: crashcourse]
[Native code: 1049]
[Native message: Unknown database 'crashcourse']
"
}
["package"]=>
string(4) "PEAR"
["level"]=>
string(9) "exception"
["time"]=>
float(1178898149.1206)
}
}
残念ながら、またエラーが発生しました。エラーメッセージによると、データベースcrashcourseが存在しないことが原因のようです。
データベースの作成
ここで、短期集中コースで使用するデータベースcrashcourseを作成します。さらに、crashcourseへの接続に使用するユーザアカウントpiece(パスワードpiece)を準備してください。データベース作成、ユーザアカウント追加については、各RDBMSのマニュアルを参照してください。
準備ができたら、再度スクリプトを実行してみましょう。
> php -f find.php
array(8) {
["code"]=>
int(-1)
["params"]=>
array(0) {
}
["package"]=>
string(9) "Piece_ORM"
["level"]=>
string(9) "exception"
["time"]=>
float(1178899051.2884)
["context"]=>
array(4) {
["file"]=>
string(51) "C:\path\to\pear\Piece\ORM\Metadata\Factory.php"
["line"]=>
int(249)
["function"]=>
string(27) "_createMetadataFromDatabase"
["class"]=>
string(26) "Piece_ORM_Metadata_Factory"
}
["message"]=>
string(55) "Failed to invoke $reverse->tableInfo() for any reasons."
["repackage"]=>
array(6) {
["code"]=>
int(-18)
["message"]=>
string(25) "MDB2 Error: no such table"
["params"]=>
array(2) {
["userinfo"]=>
string(182) "_doQuery: [Error message: Could not execute statement]
[Last executed query: SHOW COLUMNS FROM person]
[Native code: 1146]
[Native message: Table 'crashcourse.person' doesn't exist]
"
["debuginfo"]=>
string(182) "_doQuery: [Error message: Could not execute statement]
[Last executed query: SHOW COLUMNS FROM person]
[Native code: 1146]
[Native message: Table 'crashcourse.person' doesn't exist]
"
}
["package"]=>
string(4) "PEAR"
["level"]=>
string(9) "exception"
["time"]=>
float(1178899051.2882)
}
}
予想どおりのエラーが発生しました。今度は、データベースcrashcourseにpersonテーブルが存在しない、というものです。
テーブルの作成
では、personテーブルを作成します。
personテーブル(PostgreSQL):
CREATE TABLE person ( id serial, first_name varchar(255) NOT NULL, last_name varchar(255) NOT NULL, login_name varchar(255) NOT NULL, login_password varchar(255) NOT NULL, rdate timestamp with time zone NOT NULL DEFAULT current_timestamp, mdate timestamp with time zone NOT NULL DEFAULT current_timestamp, PRIMARY KEY(id), UNIQUE(login_name) );
personテーブル(MySQL):
CREATE TABLE person ( id int(11) NOT NULL AUTO_INCREMENT, first_name varchar(255) NOT NULL, last_name varchar(255) NOT NULL, login_name varchar(255) NOT NULL, login_password varchar(255) NOT NULL, rdate datetime NOT NULL, mdate timestamp, PRIMARY KEY(id), UNIQUE(login_name) );
もう何度目になるでしょうか?しかし、しつこくスクリプトを実行してみましょう。
> php -f find.php
array(0) {
}
ようやく成功したようです。しかし、personテーブルにはまだレコードが1件も存在しないため、空の配列が返ってきています。
レコードの作成(INSERT)
では、新規にレコードを作成します。Piece_ORMでレコードを作成するには、対象テーブルに対応したマッパーのオブジェクトを使います。
scripts\insert.php:
<?php require_once 'Piece/ORM.php'; require_once 'Piece/ORM/Error.php'; Piece_ORM_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';')); Piece_ORM::configure('../config', '../cache', '../config/mappers'); $mapper = &Piece_ORM::getMapper('Person'); $person = &$mapper->createObject(); $person->firstName = 'Atsuhiro'; $person->lastName = 'Kubo'; $person->loginName = 'kuboa'; $person->loginPassword = 'akubo'; var_dump($mapper->insert($person)); var_dump($mapper->findAll()); ?>
ここでのポイントは、$mapper->createObject()によるオブジェクトの作成と、$mapper->insert()によるレコードの作成です。
$mapper->createObject()
$mapper->createObject()は、テーブル定義に基づいたstdClassオブジェクトを作成します。テーブルのフィールド名は、先頭が小文字のキャメルケースに変換されます。
また、$mapper->createObject()に対する簡易アクセスを提供する、Piece_ORM::createObject()も用意されています。
<?php $mapper = &Piece_ORM::getMapper('Person'); $person = &$mapper->createObject(); ?>
上記のコードは、
<?php $person = &Piece_ORM::createObject('Person'); ?>
と等価です。
$mapper->insert()
$mapper->insert()は、対象テーブルへの新規のレコードとしてオブジェクトをデータベースに保存します。INSERTクエリは、マッパー定義ファイルのロード時に自動生成されます。自動生成されるINSERTクエリは、対象テーブルのフィールドのうち、デフォルト値を持たないフィールドで構成されます。今回の例では、下記のようなINSERTクエリが生成されることになります。
PostgreSQL:
INSERT INTO person (first_name, last_name, login_name, login_password) VALUES ($firstName, $lastName, $loginName, $loginPassword)
MySQL:
INSERT INTO person (first_name, last_name, login_name, login_password, rdate) VALUES ($firstName, $lastName, $loginName, $loginPassword, $rdate)
オブジェクトプロパティの展開
ここで、先ほどのオブジェクト作成部分をもう一度みてみましょう。
<?php $person = &$mapper->createObject(); $person->firstName = 'Atsuhiro'; $person->lastName = 'Kubo'; $person->loginName = 'kuboa'; $person->loginPassword = 'akubo'; ?>
$mapper->insert()に渡されたオブジェクトは、マッパー内部でINSERTクエリ内の変数に対して展開されます。つまり、$person->firstNameは、INSERTクエリ内の変数$firstNameに対して展開されます。
では、スクリプトを実行してみましょう。
PostgreSQL:
$ php -f insert.php
int(1)
array(1) {
[0]=>
&object(stdClass)#14 (7) {
["id"]=>
string(1) "1"
["firstName"]=>
string(8) "Atsuhiro"
["lastName"]=>
string(4) "Kubo"
["loginName"]=>
string(5) "kuboa"
["loginPassword"]=>
string(5) "akubo"
["rdate"]=>
string(25) "2007-05-12 01:54:52.50+09"
["mdate"]=>
string(25) "2007-05-12 01:54:52.50+09"
}
}
成功です。$mapper->insert()の戻り値は、実行されたINSERTクエリによって挿入されたレコードのプライマリキーの値になります。また、$mapper->findAll()の戻り値に、先ほど追加されたオブジェクトが格納されていることがわかります。このように、オブジェクトプロパティがSQLクエリ内の変数に対して展開されることがわかりました。ただし、現在のところ展開可能なプロパティの型は、スカラ変数及びNULLのみに制限されています。
MySQL:
$ php -f insert.php
array(8) {
["code"]=>
int(-1)
["params"]=>
array(0) {
}
["package"]=>
string(9) "Piece_ORM"
["level"]=>
string(9) "exception"
["time"]=>
float(1178902672.9117)
["context"]=>
array(4) {
["file"]=>
string(48) "C:\path\to\pear\Piece\ORM\Mapper\Common.php"
["line"]=>
int(432)
["function"]=>
string(12) "executeQuery"
["class"]=>
string(23) "Piece_ORM_Mapper_Common"
}
["message"]=>
string(60) "Failed to invoke MDB2_Driver_mysql::query() for any reasons."
["repackage"]=>
array(6) {
["code"]=>
int(-29)
["message"]=>
string(51) "MDB2 Error: null value violates not-null constraint"
["params"]=>
array(2) {
["userinfo"]=>
string(276) "_doQuery: [Error message: Could not execute statement]
[Last executed query: INSERT INTO person (first_name, last_name, login_name, login_password, rdate) VALUES ('Atsuhiro', 'Kubo', 'kuboa', 'akubo', NULL)]
[Native code: 1048]
[Native message: Column 'rdate' cannot be null]
"
["debuginfo"]=>
string(276) "_doQuery: [Error message: Could not execute statement]
[Last executed query: INSERT INTO person (first_name, last_name, login_name, login_password, rdate) VALUES ('Atsuhiro', 'Kubo', 'kuboa', 'akubo', NULL)]
[Native code: 1048]
[Native message: Column 'rdate' cannot be null]
"
}
["package"]=>
string(4) "PEAR"
["level"]=>
string(9) "exception"
["time"]=>
float(1178902672.9116)
}
}
こちらは失敗です。NOT NULL制約が付与されているrdateフィールドにNULLが与えられたことが原因です。では、どうすればいいのでしょうか?対応方法はふたつあります。ひとつは、単純にrdateプロパティに値を設定することです。
<?php $person->rdate = date('Y-m-d H:i:s');
もうひとつは、マッパー定義ファイルでINSERTクエリを定義することです。
SQLクエリの上書き
何度か述べたように、マッパー定義ファイルにメソッドに対応するSQLクエリを記述できます。ユーザ独自のメソッドに対してだけでなく、insert()のようなビルトインメソッドに対しても、SQLクエリを定義することができます。
config\mappers\Person.yaml:
- name: insert query: INSERT INTO person (first_name, last_name, login_name, login_password, rdate) VALUES ($firstName, $lastName, $loginName, $loginPassword, CURRENT_TIMESTAMP)
上記のようにPerson.yamlを変更し、スクリプトを実行してみましょう。
> php -f insert.php
int(1)
array(1) {
[0]=>
&object(stdClass)#12 (7) {
["id"]=>
string(1) "1"
["firstName"]=>
string(8) "Atsuhiro"
["lastName"]=>
string(4) "Kubo"
["loginName"]=>
string(5) "kuboa"
["loginPassword"]=>
string(5) "akubo"
["rdate"]=>
string(19) "2007-05-12 14:35:36"
["mdate"]=>
string(19) "2007-05-12 14:35:36"
}
}
うまくいきましたか?マッパー定義ファイルのSQLクエリには、何ら制限はありません。JOIN, IN, サブクエリ、その他RDBMSがサポートするものならどんなクエリ構文でも使うことができます。JOINについては、ユーザーズマニュアルのリレーションシップ?の項も参照してください。
メソッドとSQLクエリ
Piece_ORMはテーブル定義からいくつかのメソッドとメソッドに対応するSQLクエリを自動生成します。
| メソッド | SQLクエリ |
| findByFieldName | SELECT * FROM table_name WHERE field_name = $fieldName |
| findAll | SELECT * FROM table_name |
| findAllByFieldName | SELECT * FROM table_name WHERE field_name = $fieldName |
| insert | INSERT INTO table_name (field_name [, ...]) VALUES ($fieldName [, ...]) |
| update | UPDATE table_name SET field_name = $fidleName [, ...] WHERE primary_key = $primaryKey [AND ...] |
| delete | DELETE FROM table_name WHERE primary_key = $primaryKey [AND ...] |
それぞれのメソッドをみていきましょう。
findByFieldName
単一のフィールド値による1件のレコードを取得するためのメソッドです。テーブルのフィールドのうち、整数及びテキストに分類されるものが自動生成の対象となります。personテーブルを例にとると、下記のメソッドが生成されます。
- findById
- findByFirstName
- findByLastName
- findByLoginName
- findByLoginPassword
findAll
すべてのレコードを取得するためのメソッドです。
findAllByFieldName
単一のフィールド値による全件のレコードを取得するためのメソッドです。findByFieldNameと同じく、テーブルのフィールドのうち、整数及びテキストに分類されるものが自動生成の対象となります。personテーブルを例にとると、下記のメソッドが生成されます。
- findAllById
- findAllByFirstName
- findAllByLastName
- findAllByLoginName
- findAllByLoginPassword
insert
オブジェクトを新規のレコードとして保存するためのメソッドです。前述のように、自動生成されるINSERTクエリは、対象テーブルのフィールドのうち、デフォルト値を持たないフィールドで構成されます。
update
既存のレコードにマッピングされたオブジェクトを保存するためのメソッドです。テーブルのフィールドのうち、プライマリキーを除く全フィールドを更新対象とし、プライマリキーの値で対象のレコードを検索するSQLクエリが自動生成されます。
delete
既存のレコードにマッピングされたオブジェクトを基に、対象レコードを削除するためのメソッドです。プライマリキーの値で対象のレコードを検索するSQLクエリが自動生成されます。
find系メソッドによる単一レコードの検索(SELECT)
find系による単一レコードの検索は、主にプライマリキーやユニークキーによって単一のレコードに到達するために使用されます。自動生成されるメソッドで目的の検索が行えない場合は、自動生成されるメソッドのSQLクエリを上書きするか、自動生成されるメソッドとまったく同様に機能する、ユーザ独自のファインダメソッドを定義することができます。
ユーザ独自のファインダメソッド
ユーザ独自のファインダメソッドは単一レコードの検索が目的の場合にfindで始まる名称、複数レコードの検索が目的の場合にfindAllで始まる名称で定義する必要があります。下記は、personテーブルからlogin_name及びlogin_passwordの検索を行うためのファインダメソッド定義です。
config\mappers\Person.yaml:
- name: findForAuthentication query: SELECT * FROM person WHERE login_name = $loginName AND login_password = $loginPassword
scripts\find-for-authentication.php:
<?php require_once 'Piece/ORM.php'; require_once 'Piece/ORM/Error.php'; Piece_ORM_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';')); Piece_ORM::configure('../config', '../cache', '../config/mappers'); $criteria = &Piece_ORM::createObject('Person'); $criteria->loginName = 'kuboa'; $criteria->loginPassword = 'akubo'; $mapper = &Piece_ORM::getMapper('Person'); var_dump($mapper->findForAuthentication($criteria)); $criteria = &Piece_ORM::createObject('Person'); $criteria->loginName = 'kuboa'; $criteria->loginPassword = 'invalid password'; var_dump($mapper->findForAuthentication($criteria)); ?>
ファインダメソッドは単一のスカラー値あるいはオブジェクトを受け取ります。コールされるファインダメソッド名がByFieldName形式の場合、単一のスカラー値を与えることも、オブジェクトを与えることもできます。ファインダメソッド名がByFieldName形式でない場合、オブジェクトのみ与えることができます。
上記のスクリプトの実行結果は下記のようになります。
$ php -f find-for-authentication.php
object(stdClass)#11 (7) {
["id"]=>
string(1) "1"
["firstName"]=>
string(8) "Atsuhiro"
["lastName"]=>
string(4) "Kubo"
["loginName"]=>
string(5) "kuboa"
["loginPassword"]=>
string(5) "akubo"
["rdate"]=>
string(19) "2007-05-12 14:35:36"
["mdate"]=>
string(19) "2007-05-12 14:35:36"
}
NULL
検索結果が0件の場合は、findはnullを返します。
findAll系メソッドによる複数レコードの検索(SELECT)
次は複数レコードの検索です。一旦personテーブルのデータを削除し、複数レコード検索のためのレコードを作成します。
データ作成SQL(MySQL):
TRUNCATE person; INSERT INTO person (first_name, last_name, login_name, login_password, rdate) VALUES ('Roland', 'Deschain', 'roland', 'roland', CURRENT_TIMESTAMP); INSERT INTO person (first_name, last_name, login_name, login_password, rdate) VALUES ('Jake', 'Chambers', 'jake', 'jake', CURRENT_TIMESTAMP); INSERT INTO person (first_name, last_name, login_name, login_password, rdate) VALUES ('Eddie', 'Dean', 'eddie', 'eddie', CURRENT_TIMESTAMP); INSERT INTO person (first_name, last_name, login_name, login_password, rdate) VALUES ('Susannah', 'Dean', 'susannah', 'susannah', CURRENT_TIMESTAMP); INSERT INTO person (first_name, last_name, login_name, login_password, rdate) VALUES ('Oy', '', 'oy', 'oy', CURRENT_TIMESTAMP);
これで、personテーブルのレコードは下記のようになります。
| id | first_name | last_name | login_name | login_password |
| 1 | Roland | Deschain | roland | roland |
| 2 | Jake | Chambers | jake | jake |
| 3 | Eddie | Dean | eddie | eddie |
| 4 | Susannah | Dean | susannah | susannah |
| 5 | Oy | oy | oy |
前述のscripts\find.phpを実行して上記のレコードが正しく表示されるか確認してみましょう。
> php -f find.php
array(5) {
[0]=>
&object(stdClass)#10 (7) {
["id"]=>
string(1) "1"
["firstName"]=>
string(6) "Roland"
["lastName"]=>
string(8) "Deschain"
["loginName"]=>
string(6) "roland"
["loginPassword"]=>
string(6) "roland"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-15 23:38:19"
}
[1]=>
&object(stdClass)#11 (7) {
["id"]=>
string(1) "2"
["firstName"]=>
string(4) "Jake"
["lastName"]=>
string(8) "Chambers"
["loginName"]=>
string(4) "jake"
["loginPassword"]=>
string(4) "jake"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-15 23:38:19"
}
...
これで、自動生成されたfindAll()を使った複数レコードが、問題なく検索できることがわかりました。次に、単一レコードの場合と同様に、ユーザ独自のファインダメソッドを定義してみましょう。
ユーザ独自のファインダメソッド
前述のように、複数レコードの検索のためのファインダメソッドは、findAllで始まる名称で定義する必要があります。下記は、personテーブルからfirst_name, last_name, login_nameのパターンマッチによる検索を行うためのファインダメソッド定義です。
config\mappers\Person.yaml:
- name: findAllByCriteria
query: |
SELECT
*
FROM
person
WHERE
(($firstName IS NULL OR $firstName = '') OR first_name LIKE $firstName)
AND (($lastName IS NULL OR $lastName = '') OR last_name LIKE $lastName)
AND (($loginName IS NULL OR $loginName = '') OR login_name LIKE $loginName)
では、上記のSQLクエリを使って、last_nameの先頭がDeで始まるレコードを検索してみましょう。
scripts\find-all-by-criteria.php:
<?php require_once 'Piece/ORM.php'; require_once 'Piece/ORM/Error.php'; Piece_ORM_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';')); Piece_ORM::configure('../config', '../cache', '../config/mappers'); $criteria = &Piece_ORM::createObject('Person'); $criteria->lastName = 'De%'; $mapper = &Piece_ORM::getMapper('Person'); var_dump($mapper->findAllByCriteria($criteria)); var_dump($mapper->getLastQuery()); ?>
> php -f find-all-by-criteria.php:
array(3) {
[0]=>
&object(stdClass)#11 (7) {
["id"]=>
string(1) "1"
["firstName"]=>
string(6) "Roland"
["lastName"]=>
string(8) "Deschain"
["loginName"]=>
string(6) "roland"
["loginPassword"]=>
string(6) "roland"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-15 23:38:19"
}
[1]=>
&object(stdClass)#12 (7) {
["id"]=>
string(1) "3"
["firstName"]=>
string(5) "Eddie"
["lastName"]=>
string(4) "Dean"
["loginName"]=>
string(5) "eddie"
["loginPassword"]=>
string(5) "eddie"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-15 23:38:19"
}
[2]=>
&object(stdClass)#13 (7) {
["id"]=>
string(1) "4"
["firstName"]=>
string(8) "Susannah"
["lastName"]=>
string(4) "Dean"
["loginName"]=>
string(8) "susannah"
["loginPassword"]=>
string(8) "susannah"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-15 23:38:19"
}
}
string(198) "SELECT * FROM person WHERE ((NULL IS NULL OR NULL = '') OR first_name LIKE NULL) AND (('De%' IS NULL OR 'De%' = '') OR last_name LIKE 'De%') AND ((NULL IS NULL OR NULL = '') OR login_name LIKE NULL)"
検索結果のソート
$mapper->addOrder()を使うと、あらゆるファインダメソッドによる検索の際に、検索結果をソートすることができます。先ほどのスクリプトを変更し、検索結果がlast_nameの昇順、first_nameの降順になるようにしてみましょう。
scripts\find-all-by-criteria.php:
<?php require_once 'Piece/ORM.php'; require_once 'Piece/ORM/Error.php'; Piece_ORM_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';')); Piece_ORM::configure('../config', '../cache', '../config/mappers'); $criteria = &Piece_ORM::createObject('Person'); $criteria->lastName = 'De%'; $mapper = &Piece_ORM::getMapper('Person'); $mapper->addOrder('last_name'); $mapper->addOrder('first_name', true); var_dump($mapper->findAllByCriteria($criteria)); var_dump($mapper->getLastQuery()); ?>
> php -f find-all-by-criteria.php:
array(3) {
[0]=>
&object(stdClass)#11 (7) {
["id"]=>
string(1) "4"
["firstName"]=>
string(8) "Susannah"
["lastName"]=>
string(4) "Dean"
["loginName"]=>
string(8) "susannah"
["loginPassword"]=>
string(8) "susannah"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-15 23:38:19"
}
[1]=>
&object(stdClass)#12 (7) {
["id"]=>
string(1) "3"
["firstName"]=>
string(5) "Eddie"
["lastName"]=>
string(4) "Dean"
["loginName"]=>
string(5) "eddie"
["loginPassword"]=>
string(5) "eddie"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-15 23:38:19"
}
[2]=>
&object(stdClass)#13 (7) {
["id"]=>
string(1) "1"
["firstName"]=>
string(6) "Roland"
["lastName"]=>
string(8) "Deschain"
["loginName"]=>
string(6) "roland"
["loginPassword"]=>
string(6) "roland"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-15 23:38:19"
}
}
string(238) "SELECT * FROM person WHERE ((NULL IS NULL OR NULL = '') OR first_name LIKE NULL) AND (('De%' IS NULL OR 'De%' = '') OR last_name LIKE 'De%') AND ((NULL IS NULL OR NULL = '') OR login_name LIKE NULL) ORDER BY last_name ASC, first_name DESC"
どうでしょうか?期待通りの結果になりましたか?なお、$mapper->addOrder()によるソート順の設定は、ファインダメソッドの実行後に毎回クリアされますので、次回以降の検索に影響を与えることはありません。また、$mapper->addOrder()はfind系メソッドにも問題なく使うことができます。
最大レコード数の制限とオフセットの指定
$mapper->setLimit()を使うと、あらゆるファインダメソッドによる検索の際に、検索結果の最大レコード数を制限することができます。また、$mapper->setLimit()の第2引数では、戻り値として返す最初のレコードまでのオフセットを指定することができます。では、先ほどのスクリプトの検索結果のうち、3件目(オフセット2)から1件のみ(id: 1, Roland Deschain)を取得できるか試してみましょう。
scripts\find-all-by-criteria.php:
<?php require_once 'Piece/ORM.php'; require_once 'Piece/ORM/Error.php'; Piece_ORM_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';')); Piece_ORM::configure('../config', '../cache', '../config/mappers'); $criteria = &Piece_ORM::createObject('Person'); $criteria->lastName = 'De%'; $mapper = &Piece_ORM::getMapper('Person'); $mapper->addOrder('last_name'); $mapper->addOrder('first_name', true); $mapper->setLimit(1, 2); var_dump($mapper->findAllByCriteria($criteria)); var_dump($mapper->getLastQuery()); ?>
> php -f find-all-by-criteria.php:
array(1) {
[0]=>
&object(stdClass)#11 (7) {
["id"]=>
string(1) "1"
["firstName"]=>
string(6) "Roland"
["lastName"]=>
string(8) "Deschain"
["loginName"]=>
string(6) "roland"
["loginPassword"]=>
string(6) "roland"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-15 23:38:19"
}
}
string(249) "SELECT * FROM person WHERE ((NULL IS NULL OR NULL = '') OR first_name LIKE NULL) AND (('De%' IS NULL OR 'De%' = '') OR last_name LIKE 'De%') AND ((NULL IS NULL OR NULL = '') OR login_name LIKE NULL) ORDER BY last_name ASC, first_name DESC LIMIT 2, 1"
$mapper->setLimit()による最大レコード数の制限とオフセットの指定は、$mapper->addOrder()と同じくファインダメソッドの実行後に毎回クリアされますので、次回以降の検索に影響を与えることはありません。また、$mapper->setLimit()はfind系メソッドにも問題なく使うことができます。
レコードの更新(UPDATE)
次に、レコードを更新します。Piece_ORMでレコードを更新するには、レコードの作成と同様に、レコードに対応したオブジェクトを使います。id: 1, Roland Deschainのlogin_nameフィールドを、rolandからgunslingerに変更してみましょう。
scripts\update.php:
<?php require_once 'Piece/ORM.php'; require_once 'Piece/ORM/Error.php'; Piece_ORM_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';')); Piece_ORM::configure('../config', '../cache', '../config/mappers'); $mapper = &Piece_ORM::getMapper('Person'); var_dump($mapper->findByLoginName('gunslinger')); $person = &$mapper->findById(1); $person->loginName = 'gunslinger'; var_dump($mapper->update($person)); var_dump($mapper->findByLoginName('gunslinger')); ?>
では、早速実行してみましょう。
> php -f update.php
NULL
int(1)
object(stdClass)#14 (7) {
["id"]=>
string(1) "1"
["firstName"]=>
string(6) "Roland"
["lastName"]=>
string(8) "Deschain"
["loginName"]=>
string(10) "gunslinger"
["loginPassword"]=>
string(6) "roland"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-15 23:38:19"
}
成功です。$mapper->update()の戻り値は、実行されたUPDATEクエリによって影響を受けた行の数になります。$mapper->update()のコール前には、login_nameフィールド値がgunslingerのレコードは存在しませんでしたが、$mapper->update()のコール後には、存在することが確認できました。しかし、よくみるとmdateフィールド(変更日)の値が変更されていないのがわかります。
$mapper->update()
$mapper->update()は、対象テーブルに対してプライマリキーを使って更新対象のレコードを検索し、オブジェクトプロパティの値で対象レコードを更新します。UPDATEクエリは、マッパー定義ファイルのロード時に自動生成されます。自動生成されるUPDATEクエリは、対象テーブルのすべてのフィールドのうちプライマリキーを除いたものを更新対象とします。今回の例では、下記のようなUPDATEクエリが生成されることになります。
PostgreSQL, MySQL:
UPDATE person SET first_name = $firstName, last_name = $lastName, login_name = $loginName, login_password = $loginPassword, rdate = $rdate, mdate = $mdate WHERE id = $id
SQLクエリの上書き
今回のpersonテーブルの場合は、rdateフィールド(登録日)の更新は不要のため、フィールドリストから除外した方がいいでしょう。また、mdateフィールド(変更日)には、UPDATEを実行した日時を設定することが望まれます。PostgreSQLの場合、mdateにCURRENT_TIMESTAMPを設定するようにし、MySQLの場合、mdate(変更日)は自動的に更新されるフィールドになっているため、フィールドを除外するようにします。では、$mapper->update()に対応するSQLクエリを定義してみましょう。
PostgreSQL:
- name: update query: UPDATE person SET first_name = $firstName, last_name = $lastName, login_name = $loginName, login_password = $loginPassword, mdate = CURRENT_TIMESTAMP WHERE id = $id
MySQL:
- name: update query: UPDATE person SET first_name = $firstName, last_name = $lastName, login_name = $loginName, login_password = $loginPassword WHERE id = $id
そして、id: 1, Roland Deschainのlogin_nameフィールドを、gunslingerからrolandに戻してみます。
scripts\update.php:
<?php require_once 'Piece/ORM.php'; require_once 'Piece/ORM/Error.php'; Piece_ORM_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';')); Piece_ORM::configure('../config', '../cache', '../config/mappers'); $mapper = &Piece_ORM::getMapper('Person'); var_dump($mapper->findByLoginName('roland')); $person = &$mapper->findById(1); $person->loginName = 'roland'; var_dump($mapper->update($person)); var_dump($mapper->findByLoginName('roland')); ?>
> php -f update.php
NULL
int(1)
object(stdClass)#15 (7) {
["id"]=>
string(1) "1"
["firstName"]=>
string(6) "Roland"
["lastName"]=>
string(8) "Deschain"
["loginName"]=>
string(6) "roland"
["loginPassword"]=>
string(6) "roland"
["rdate"]=>
string(19) "2007-05-15 23:38:19"
["mdate"]=>
string(19) "2007-05-16 13:31:39"
}
今度はmdateフィールドもきちんと更新されたでしょうか?
レコードの削除(DELETE)
次に、レコードを削除してみましょう。Piece_ORMでレコードを削除するには、レコードの作成やレコードの更新と同様に、レコードに対応したオブジェクトを使います。id: 1のRoland Deschainを削除してみましょう。
scripts\delete.php:
<?php require_once 'Piece/ORM.php'; require_once 'Piece/ORM/Error.php'; Piece_ORM_Error::pushCallback(create_function('$error', 'var_dump($error); return ' . PEAR_ERRORSTACK_DIE . ';')); Piece_ORM::configure('../config', '../cache', '../config/mappers'); $mapper = &Piece_ORM::getMapper('Person'); var_dump($mapper->delete($mapper->findById(1))); var_dump($mapper->findById(1)); ?>
では、実行してみましょう。
> php -f delete.php int(1) NULL
$mapper->delete()の戻り値は、$mapper->update()の戻り値と同様に、実行されたDELETEクエリによって影響を受けた行の数になります。
$mapper->delete()
$mapper->delete()は、対象テーブルに対してプライマリキーを使って削除対象のレコードを検索し、削除します。DELETEクエリは、マッパー定義ファイルのロード時に自動生成されます。自動生成されるDELETEクエリは、RDBMSによらずWHERE句にプライマリキーが設定されます。
PostgreSQL, MySQL:
DELETE FROM person WHERE id = $id
今回はSQLクエリの上書きは不要ですが、追加の削除条件が必要な場合、これまでのメソッドと同様にSQLクエリを上書きすることができます。
リアルドメインオブジェクトへの変換
多くのORMフレームワークでは、ビジネスロジックの配置先となるクラス(多くのフレームワークでモデルと呼ばれるクラス)をフレームワークから生成することができます。しかし、Piece_ORMにはその機能は備わっていません。ファイダメソッドから返されるオブジェクトは常にstdClassオブジェクトです。Piece_ORMではこのstdClassオブジェクトを仮想ドメインオブジェクトと呼んでいます。では、何らかのビジネスロジックを実行したい時はどのようにすればいいのでしょうか?
Piece_ORM::dressObject()
Piece_ORM::dressObject()は、単に一方のオブジェクトのすべてのプロパティをもう一方のオブジェクトにコピーします。何らかのビジネスロジックを実行したい時は、仮想ドメインオブジェクトからビジネスロジックを備えたオブジェクト(リアルドメインオブジェクト)への変換を行った後、ビジネスロジックを実行するようにします。例えば、personテーブルのlogin_passwordフィールド値を格納する前に、パスワードをSHA1のハッシュ値に変換するメソッドconvertPasswordを実行する場合を考えます。
<?php $mapper = &Piece_ORM::getMapper('Person'); $person = &$mapper->createObject(); $person->firstName = 'Atsuhiro'; $person->lastName = 'Kubo'; $person->loginName = 'kuboa'; $person->loginPassword = 'akubo'; $realPerson = &Piece_ORM::dressObject($person, new MyPerson()); $realPerson->convertPassword(); $mapper->insert($realPerson); ?>
このように、仮想ドメインオブジェクトからリアルドメインオブジェクトへの変換を行うことで、ビジネスロジックを実行することができます。また、stdClass以外のオブジェクトも問題なく$mapper->insert(), $mapper->update(), $mapper->delete()に渡すことができます。
Piece_ORMをどこで使うべきか?
ここまでの内容で、Piece_ORMを使ったデータアクセスについて一通り理解頂けたことと思います。あとは実践あるのみ、といったところですが、その前に、Piece_ORMを使ったデータアクセスコードを、Webアプリケーション全体のどの位置で使うべきかについて少し述べておきます。
アクションクラスへの配置
ここまでの内容にあったような、単純な処理を行う場合は、データアクセスコードをアクションクラスへ配置しましょう。
サービスレイヤへの配置
いくつかのデータアクセスをまとめて行うトランザクションを実行する場合1や、ドメインオブジェクトに対する条件分岐コードがある場合、その他単純でないコードが含まれる場合は、それらをサービスレイヤ2のメソッドに配置しましょう。
- 1. 自分でコードを書く前に、リレーションシップ?の使用を検討してください。
- 2. http://capsctrl.que.jp/kdmsnr/wiki/PofEAA/?ServiceLayer
Piece_ORMをどこで使わないべきか?
Piece_ORMはDataMapper?パターン1を採用したORMフレームワークです。ドメインオブジェクトの独立性を維持するために、リアルドメインオブジェクトのメソッド内でマッパーオブジェクトを使うべきではありません。
おわりに
お疲れさまでした。以上で短期集中コースは終了です。
