Curry
PHP Framework

データアクセス

データベース接続オブジェクト

Curryのデータベースアクセスの最も核となるのがDbクラスです。基本的な役目としてはデータベースとの接続を確立し、接続オブジェクトを返すだけです。

$dbconf = array(
    'type'     => 'mysql'
    'host'     => 'localhost',
    'dbname'   => 'ec',
    'user'     => 'ec_user',
    'password' => 'xxxxxxxx' 
);
Db::setConfig($dbconf);
$db = Db::factory();

var_dump($db);
object(DbAdapterMysql)#1 (0) { } 

接続情報の設定

Dbクラスは実はPDOを内包するラッパー的なクラスです。得られる接続オブジェクトはPDOを継承したクラスのインスタンスになります。上のとおり、Dbクラスを利用する上で必ず必要なのは、setConfigメソッドです。これにより、接続するデータベースの情報をDbクラスに伝えます。

setConfigメソッドの引数はハッシュです。受け付けるキーとその内容は以下の通りです。

キー内容備考
type データベース種別 必須指定。"mysql"、"pgsql"、"sqlite"のいずれか
host データベースサーバーのIPまたはホスト名 mysql、pgsqlの場合は必須指定。"localhost"など。
dbname データベース名 mysql、pgsqlの場合は必須指定
user データベースのログインユーザー名 mysql、pgsqlの場合は必須指定
password データベースのログインパスワード mysql、pgsqlの場合は必須指定
port データベースサーバーの接続ポート 指定は任意。mysqlの場合デフォルト3306。pgsqlの場合デフォルト5432。
charset データベース文字コード 指定は任意。未指定の場合のデフォルトはutf8。EUC-JPの場合は"ujis"、SHIFT-JISの場合は"sjis"
source_directory SQLiteデータディレクトリ sqliteの場合のみ指定可能で、指定は任意。未指定の場合のデフォルトは"site/data/sqlite"
source_file SQLiteデータファイル名 sqliteの場合のみ指定可能で、指定は任意。未指定の場合のデフォルトは"data.sqlite"

MySQL、PostgreSQL

接続情報の最も基本的な設定方法はdatabase.iniでの設定です。
以下はMySQLの場合の設定例です。PostgreSQLの場合はほぼ同様です。

mysqlの場合のdatabase.ini設定例
[connection]
type     = mysql
host     = localhost
dbname   = ec
user     = ec_user
password = xxxxxxxx

SQLite

SQLiteは単一ファイルのみでデータベースが利用できる、アプリケーション型のRDBMSで、MySQLなどと比べてはるかに手軽に利用できるのが特徴です。

SQLiteはdatabase.iniでtype=sqliteを指定するだけで利用可能です。
デフォルトでは(サイトルート)/data/sqlite/内にデータファイルを作成します。
dataディレクトリが存在する前提となっていますので、dataディレクトリが存在しない場合は作成しておく必要があります。
デフォルトの挙動ではdataディレクトリ内にsqliteディレクトリを作成し、その中にdata.sqliteという名前でデータファイルの作成を試みます。そのためdataディレクトリにはシステムからの書き込みが可能なようにパーミッション設定が必要になります。

またdatabase.iniでデータ格納ディレクトリやデータファイル名を任意に指定することも可能です。

SQLiteの場合の設定例
[connection]
type = sqlite

;通常は指定不要
;source_directory = "/var/www/public_html/site/data/sqlite"
;source_file = "data.sqlite"
SQLite利用時の標準の関連ファイル構成
(site)
    ∟config
        ∟database.ini
    ∟data
        ∟sqlite
             ∟data.sqlite

シングルトンコネクション

モデルの章でも触れていますが、factoryメソッドを実行すると、そのたびに新たなデータベースコネクションが作られ、得られるインスタンスも個別のものになります。
DbクラスにはsetIsSingletonというメソッドが用意されており、この引数にtrueを指定することで、factoryメソッドによって得られるインスタンスは常に同一のものとなります。つまり、データベース接続が行われるのは最初の一回だけになります。

$db1 = Db::factory();
$db2 = Db::factory();
var_dump($db1 === $db2);

Db::setIsSingleton(true);
$db1 = Db::factory();
$db2 = Db::factory();
var_dump($db1 === $db2);
bool(false)
bool(true)

これにより、トランザクション処理の管理がしやすくなる上、データベースサーバーへの負担の軽減にも貢献します。

SQLを直接実行

Dbクラスのfactoryメソッドで得られるのはPDOインスタンスのため、PDOで出来ることは全てそのまま同じ事ができます。

$db = Db::factory();
$stmt = $db->query("SELECT * FROM cart");
$rows = $stmt->fetchAll();

queryメソッドなどにより、生のSQL文の実行が可能です。
PDOの機能についてはPDOのマニュアルサイトの方を参照してください。

SQLインジェクションに適切に対策する

入力フォームなどからユーザーの入力値がそのままテーブルのフィールドに保存されるような場合、一般的にデータのサニタイジングが行われます。これは、悪意を持ったユーザーがSQL構文を悪用し、データの改ざんなどを防ぐために適切にエスケープ処理などを施すものです。

しかしPDOにはプレースホルダの仕組みが備わっており、これを利用して条件指定などをパラメータ化することで、サニタイジングなど意識せずに安全にSQLが実行できます。

// 危険
$stmt = $db->query("SELECT * FROM products WHERE product_name = '" . $word . "'");
$rows = $stmt->fetchAll();

// プレースホルダ機構を利用して条件指定をパラメータ化
$stmt = $db->prepare("SELECT * FROM products WHERE product_name = :product_name");
$stmt->bindValue(':product_name', $word);
$stmt->execute();
$rows = $stmt->fetchAll();

Modelクラスの利用

CurryのModelクラスはテーブルアクセスをサポートしてくれます。
最も有効な利用方法は、Modelクラスを継承したサブクラスを定義することです。Modelクラスを継承すると、参照するテーブルの名前をクラス名から判断してくれます。

class CartProduct extends Model
{
    // テーブル"cart_product"と関連付けられる
}

クラス名に、テーブル名との関連の名前規則を適用したくない場合もあるでしょう。その場合はフィールドでテーブル名を指定することで可能です。

class CartProduct extends Model
{
    protected $name = 'tbl_cart_product';
}

ModelクラスはコンストラクタでDb::factoryメソッドを実行し、接続インスタンスをフィールドに保持します。Modelクラスを利用する際は接続インスタンスの事は意識せずに処理を行うことができます。
しかしこのため、複数のModelのサブクラスを利用する場合、それぞれに別々の接続インスタンスを保持することになります。そうすると複数のテーブルを絡めたトランザクション処理が出来なくなるため、基本的にはModel利用時は事前にModel::setDefaultConnectionメソッドによってデフォルトの接続を設定しておくか、Db::setIsSingleton(true);を実行しておくことをお勧めします。

Modelクラスの直接利用

Modelクラスのもう一つの利用方法があります。
それは、継承をせずにModelクラスを直接利用することです。
Modelクラスのコンストラクタの第1引数にはテーブル名を指定することができます。これにより、継承したのと同じ事ができます。

class CartProduct extends Model
{
}

// この二つはまったく同じ意味
$cartProduct1 = new CartProduct();
$cartProduct2 = new Model('cart_product');

ちなみに第2引数には接続インスタンスが指定できます。この場合、コンストラクタで新たに接続インスタンスの生成は行わずに、引数で受け取った接続で処理を行わせることが出来ます。

SELECT文の構築

Modelクラスの持つselectメソッドはDbSelectクラスのインスタンスを返します。
DbSelectクラスは、SELECT文の生成~実行を行うクラスです。まずは単純なSELECTの例を見てみましょう。

class Product extends Model
{
    public function searchProduct($word)
    {
        // SELECT * FROM product WHERE (product_name = :product_name)
        $sel = $this->select();
        $sel->where('product_name', $word);
        $rows = $sel->fetchAll();
    }
}

単純な条件による抽出のクエリです。
DbSelectのwhereメソッドによる条件指定は全て内部でプレースホルダ化されますので、SQLインジェクション対策は意識をする必要がありません。

WHERE条件の指定方法

上の例では第1引数にフィールド名、第2引数に値を指定しています。続けて複数のwhereメソッドを指定すると、条件は追加されていきます。

// SELECT * FROM product WHERE (product_name = :product_name) AND (price = :price)
$sel = $this->select();
$sel->where('product_name', $word);
$sel->where('price', 1000);

whereメソッドの指定方法は他にもあります。

$sel = $this->select();
$sel->where(array(
    'product_name' => $word,
    'price' => 1000
));

ハッシュにより、複数条件を一度に指定する方法です。

WHERE条件文を直接指定することもできます。上のような例では対応できないような複雑なWHERE条件の場合はこの指定方法になるでしょう。
この場合でプレースホルダを利用するには、後からparamsメソッドでパラメータを指定します。
直接、フォームよりの入力値をそのまま条件文に文字列結合により埋め込むのはSQLインジェクションに対して脆弱になるため、避けるべきです。

$sel = $this->select();
$sel->where('product_name = :product_name AND price = :price');
$sel->params(array(
    'product_name' => $word,
    'price' => 1000
));

whereメソッドによって複数条件を指定した場合、各条件はANDで接続されます。それに対してORで条件をつなぎたい場合はorWhereメソッドによって条件指定します。条件指定が1つだけの場合、whereもorWhereも同じ働きをしますが、2つ目以降の条件にorWhereを使用すると、その条件はORで接続されます。

$sel = $this->select();
$sel->where('product_name', $word1);
$sel->orWhere('product_name', $word2);

// WHERE (product_name = :product_name1) OR (product_name = :product_name2)

それではANDとORの複合条件はどのようにするかですが、whereとorWhereは無造作に混用してもANDとORが入り混じるだけで条件指定がうまくいきませんので以下のようにします。

$sel = $this->select();
$sel->where('product_name = :product_name1 OR product_name = :product_name2');
$sel->where('price < 1000');

// WHERE (product_name = :product_name1 OR product_name = :product_name2) AND (price < 100)

抽出対象フィールドの指定

抽出対象フィールドはfieldsメソッドで指定します。
指定方法は3種類あります。

// 以下の3パターンはいずれも同じ結果です。
// SELECT product_name,price FROM product

// SELECT句をそのまま指定
$sel = $this->select();
$sel->fields('product_name, price');

// 1フィールドずつ指定
$sel = $this->select();
$sel->fields('product_name');
$sel->fields('price');

// 配列で指定
$sel = $this->select();
$sel->fields(array('product_name', 'price'));

フィールドの指定をしなかった場合には"*"になります。

// SELECT * FROM product

$sel  = $this->select();
$rows = $sel->fethAll();

ORDER BY

ORDER BYはorderメソッドによって指定します。
指定方法はfieldsメソッドと同様に3種類あります。

// 以下の3パターンはいずれも同じ結果です。
// SELECT * FROM product ORDER BY product_name, price DESC

// ORDER BY条件文をそのまま指定
$sel = $this->select();
$sel->order('product_name, price DESC');

// 1フィールドずつ指定
$sel = $this->select();
$sel->order('product_name');
$sel->order('price DESC');

// 配列で指定
$sel = $this->select();
$sel->order(array('product_name', 'price DESC'));

GROUP BY

GROUP BYの指定はgroupメソッドで行います。
GROUP BYの指定方法もORDER BYとほぼ同様です。

// 以下の3パターンはいずれも同じ結果です。
// SELECT product_type, publisher, MAX(price) FROM product GROUP BY product_type, pulisher

$fields = array('product_type', 'pulisher', 'MAX(price)');

// GROUP BY条件文をそのまま記述
$sel = $this->select();
$sel->fields($fields);
$sel->group('product_type, pulisher');

// 1フィールドずつ指定
$sel = $this->select();
$sel->fields($fields);
$sel->group('product_type');
$sel->group('pulisher');

// 配列で指定
$sel = $this->select();
$sel->fields($fields);
$sel->group(array('product_type', 'pulisher'));

LIMIT、OFFSET

LIMITやOFFSETの指定はそれぞれlimitメソッド、offsetメソッドによって行います。
ただしOFFSETの指定はLIMITも指定されている必要があります。OFFSETのみ指定の場合には無視されます。

// SELECT * FROM product ORDER BY price LIMIT 10 OFFSET 20
$sel = $this->select();
$sel->order('price');
$sel->limit(10);
$sel->offset(20)

メソッドチェーンによる連続的な条件指定

SqlSelectクラスの条件指定メソッドは全てインスタンス自身を返します。つまり、メソッドチェーンが利用できます。単純な条件であれば1行で記述してしまうことも可能です。

// SELECT * FROM product WHERE product_name = :product_name ORDER BY price 
$sel = $this->select()->where('product_name', $word)->order('price');

コーディング的には複数行に分けてもいいでしょう。

// SELECT product_name FROM product WHERE product_name = :product_name
// ORDER BY price, product_name LIMIT 10 OFFSET 20
$sel = $this->select()
    ->fields('product_name')
    ->where('product_name', $word)
    ->order('price')
    ->order('product_name')
    ->limit(10)
    ->offset(20);

SELECT文の実行

条件の指定後、実際にクエリを実行して結果を取得します。SqlSelectクラスには実行メソッドがいくつか用意されています。

複数行の結果取得

条件に合致する複数行を得る場合はfetchAllメソッドを使用します。得られる結果は行×列の2次元配列になります。

$sel = $this->select();
$sel->order('price');
$sel->limit(10);
// 複数行の2次元配列を取得
$rows = $sel->fetchAll();

単一行の結果取得

例えばテーブルのプライマリーキーのフィールドをWHERE条件で指定すれば、結果は必ず1行になるはずです。そのような場合、fetchAllで実行するよりも、最初から結果が1行のみ、つまり1次元配列である方が便利です。そのような場合はfetchRowを使用します。

$sel = $this->select();
$sel->where('product_id', $productId);
// 単一行の1次元配列を取得
$row = $sel->fetchRow();

単一行の単一項目の値を取得

結果が配列ではなく、単純なスカラー値で取得したい場合があります。例えばキーフィールドで絞りこんだ上でその行の単一項目のみが欲しい場合などです。そのような場合にはfetchScalarを使用します。fetchScalarは取得したい値のフィールド名を引数に指定します。

$sel = $this->select();
$sel->where('product_id', $productId);
// 単一行から指定項目の値のみを取得
$productName = $sel->fetchScalar('product_name');

行数をカウント

条件に合致する行の行数を取得するにはfetchCountメソッドを利用します。

// 条件に合致する行数を取得
// 以下の2パターンの処理は同じ結果です。

// fetchCountメソッドを使用
$sel = $this->select();
$sel->where('price < 1000');
$cnt = $sel->fetchCount();

// fetchScalarメソッドを使用
$sel = $this->select();
$sel->fields('COUNT(*) AS cnt');
$sel->where('price < 1000');
$cnt = $sel->fetchScalar('cnt');

指定項目の最大値を取得

条件に合致する行の中の特定のフィールドの最大値を得るにはfetchMaxメソッドを利用します。

// 以下の2パターンの処理は同じ結果です。

// fetchMaxメソッドを使用
$sel = $this->select();
$sel->where('price < 1000');
$price = $sel->fetchMax('price');

// fetchScalarメソッドを使用
$sel = $this->select();
$sel->fields('MAX(price) AS price');
$sel->where('price < 1000');
$price = $sel->fetchScalar('price');

指定項目の最小値を取得

条件に合致する行の中の特定のフィールドの最小値を得るにはfetchMinメソッドを利用します。

// 以下の2パターンの処理は同じ結果です。

// fetchMinメソッドを使用
$sel = $this->select();
$sel->where('price > 1000');
$price= $sel->fetchMin('price');

// fetchScalarメソッドを使用
$sel = $this->select();
$sel->fields('MIN(price) AS price');
$sel->where('price > 1000');
$price = $sel->fetchScalar('price');

指定項目の合計値を取得

条件に合致する全ての行の特定フィールドの合計値を取得するにはfetchSumメソッドを利用します。

// 以下の2パターンの処理は同じ結果です。

// fetchMinメソッドを使用
$sel = $this->select();
$sel->where('price < 1000');
$price = $sel->fetchSum('price');

// fetchScalarメソッドを使用
$sel = $this->select();
$sel->fields('SUM(price) AS price');
$sel->where('price < 1000');
$price = $sel->fetchScalar('price');

テーブル結合

複数テーブルを結合するSELECT文を構築するにはjoinInnerメソッドやjoinLeftメソッドを利用します。
複数のテーブルを利用する処理の場合はサービスクラスを利用することをおすすめします。

class CartService extends Service
{
    public function getProducts()
    {
        $cartPrd= $this->model('CartProduct', 'CP');
        $prd = $this->model('Product', 'P');

        // SELECT PR.product_name, PR.price FROM cart_product AS CP
        // INNER JOIN product AS PR ON PR.product_id = CP.product_id
        $sel = $cartPrd->select();
        $sel->fields(array(
            'P.product_name',
            'P.price'
        ));
        $sel->joinInner($prd, array(
            'P.product_id = CP.product_id'
        ));
        $rows = $sel->fetchAll();

    }
}

join系のメソッドの第一引数に結合したいテーブルのモデルクラスのインスタンスを指定し、第二引数にON条件を指定します。
ON条件はそのままON句に書くべき条件文を書いてもいいですが、配列で複数条件を指定すると、自動的にANDで結合して条件文を組み立ててくれます。

    // 以下の二つは同じ結果です。

        // 配列で指定
        $on = array(
            'P.product_id = CP.product_id',
            'P.sales_flg = 1'
        ));
       $sel->joinInner($prd, $on);

        // 条件文そのままを文字列で指定
        $on = 'P.product_id = CP.product_id AND P.sales_flg = 1';
       $sel->joinInner($prd, $on);

テーブル結合を行う場合、SQL文が冗長にならないように、テーブル名にエイリアス(別名)をつけることが出来ますが、コントローラーやサービスのmodelメソッドの第2引数で、テーブルエイリアスを設定することが出来ます。
第2引数は未指定でModelクラスのインスタンスを得た後、ModelクラスのインスタンスのsetAliasメソッドで設定しても同様です。

    // 以下の2つのパターンは同じ結果です。

        // modelメソッドの第2引数で指定
        $cartPrd = $this->model('CartProduct', 'CP');
        $prd = $this->model('Product', 'P');

        // ModelのインスタンスのsetAliasメソッドで指定
        $cartPrd = $this->model('CartProduct');
        $cartPrd->setAlias('CP');
        $prd = $this->model('Product');
        $prd ->setAlias('P');

ちなみにjoin系メソッドの第一引数はテーブル名を文字列で指定することも可能です。

    // 以下の2つのパターンは同じ結果です。

        // 結合対象テーブルをModelインスタンスで指定
        $cartPrd = $this->model('CartProduct', 'CP');
        @@B$prd = $this->model('Product', 'P');@
        $sel = $cartPrd ->select();
        $sel->joinInner($prd, array(
            'P.product_id = CP.product_id'
        ));

        // テーブル名を文字列で直接指定
        $cartPrd = $this->model('CartProduct', 'CP');
        $sel = $cartPrd ->select();
        $sel->joinInner('product AS P', array(
            'P.product_id = CP.product_id'
        ));

INSERTの実行

INSERT文の実行は、SqlInsertクラスを利用します。ModelクラスのinsertメソッドはSqlInsertクラスのインスタンスを返します。これによってえられたインスタンスは、モデルクラスと関連づけられたテーブルに対するINSERT文の構築~実行を行います。

class User extends Model
{
    public function addUser($name, $address)
    {
        $values = array(
            'name' => $name,
            'address' => $address,
            'regist_date' => date('YmdHis')
        );
        $ins = $this->insert();
        $ins->values($values);
        $res = $ins->execute();

        // INSERT INTO user (name, address, regist_date)
        // VALUES (:name, :address, :regist_date)
    }
}

SqlSelectと同じくチェーンによってもっと簡略記述することも可能です。

$values = array(
    'name' => $name,
    'address' => $address,
    'regist_date' => date('YmdHis')
);
$res = $this->insert()->values($values)->execute();

valuesメソッドの引数は上の例のように配列で指定が可能ですが、フィールド名と値の組み合わせで個別に指定することも可能です。

$ins = $this->insert();
$ins->values('name', $name);
$ins->values('address', $address);
$ins->values('regist_date', date('YmdHis'));
$res = $ins->execute();

UPDATEの実行

UPDATE文の実行は、SqlUpdateクラスを利用します。ModelクラスのupdateメソッドはSqlUpdateクラスのインスタンスを返します。これによってえられたインスタンスは、モデルクラスと関連づけられたテーブルに対するUPDATE文の構築~実行を行います。

class User extends Model
{
    public function updateInfo($userId, $name, $address)
    {
        $values = array(
            'name'    => $name,
            'address' => $address
        );
        $upd = $this->update();
        $upd->values($values);
        $upd->where('user_id', $userId);
        $res = $upd->execute();

        // UPDATE user SET cart_id = :cart_id, product_id = :product_id
        // WHERE user_id = :user_id
    }
}

whereの指定方法はSELECTの場合と同じです。valuesメソッドによる更新データの設定はINSERTと同様です。
また同じくメソッドチェーンにより、以下のように簡略的な記述も可能です。

$values = array(
    'name'    => $name,
    'address' => $address
);
$where = array(
    'user_id' => $userId
);
$res = $this->update()
    ->values($values)
    ->where($where)
    ->execute();

DELETEの実行

DELETE文の実行は、SqlDeleteクラスを利用します。ModelクラスのdeleteメソッドはSqlDeleteクラスのインスタンスを返します。これによってえられたインスタンスは、モデルクラスと関連づけられたテーブルに対するDELETE文の構築~実行を行います。

class User extends Model
{
    public function deleteUser($userId)
    {
        $del= $this->delete();
        $del->where('user_id', $userId);
        $res = $del->execute();

        // DELETE FROM user WHERE user_id = :user_id
    }
}

whereの指定方法はSELECTの場合と同じです。
また同じくメソッドチェーンにより、以下のように簡略的な記述も可能です。

$where = array('user_id' => $userId);
$res = $this->delete()->where($where)->execute();

サブクエリの利用

Curryではサブクエリを含むSQL構築に対応しています。

SELECTのテーブル結合にサブクエリを利用する

SqlSelectのjoin系メソッドの第一引数に更にSqlSelectのインスタンスを指定することで、サブクエリの結合が可能です。
サブクエリには必ずエイリアスを指定する必要があるため、元となるテーブルへエイリアス指定は必須となります。

class Product extends Model
{
    public function getSalesProducts()
    {
        $this->setAlias('PR');
        $subQuery = $this->select();
        $subQuery->where('sales_flg = 1');
                
        $this->setAlias('CP');
        $sel = $this->select();
        $sel->fields(array('PR.product_name', 'PR.price'));
        $sel->joinInner($subQuery, array(
            'PR.product_id = CP.product_id'
        ));
        $rows = $sel->fetchAll();
        
        /*
        SELECT * FROM cart_product AS CP
        INNER JOIN (SELECT * FROM product WHERE sales_flg = 1) AS PR
        ON PR.product_id = CP.product_id
        */
    }
}

INSERT文でサブクエリを利用する

INSERT文にサブクエリを利用したい場面は時折発生します。例えば数値のID項目などでデータベースのオートインクリメントに頼らない場合などです。これを実現するには更新値にSqlSelectのインスタンスを設定します。

class User extends Model
{
    public function add($name, $address)
    {
        $this->setAlias('SUB');      // エイリアスの指定は必須です
        $subQuery = $this->select();
        $subQuery->fields('IFNULL(MAX(user_id), 0) + 1 AS user_id');

        $values = array(
            'user_id' => $subQuery,
            'name' => $name,
            'address' => $address,
            'regist_date' => date('YmdHis')
        );
        $res = $this->insert()->values($values)->execute();

        // INSERT INTO user (user_id, name, address, regist_date)
        // VALUES (
        //     (SELECT IFNULL(MAX(user_id), 0) + 1 AS user_id FROM user AS SUB1),
        //     :name,
        //     :address,
        //     :regist_date
        // )
    }
}

UPDATE文でサブクエリを利用する

UPDATEの場合もINSERTと同じく更新値にSqlSelectのインスタンスを設定することでサブクエリが利用可能です。
例ではフラグ項目の値の切り替えをサブクエリで実現しています。

class User extends Model
{
    public function switchBlackFlag($userId)
    {
        $this->setAlias('SUB');      // エイリアスの指定は必須です
        $subQuery = $this->select();
        $subQuery->fields('CASE WHEN black_flg = 1 THEN 0 ELSE 1 END');
        $subQuery->where('SUB.user_id = UPD.user_id');

        $this->setAlias('UPD');      // エイリアスの指定は必須です
        $res = $this->update()
            ->values('black_flg', $subQuery)
            ->where('user_id', $userId)
            ->execute();

        // UPDATE user AS UPD SET black_flg = (
        //     SELECT CASE WHEN black_flg = 1 THEN 0 ELSE 1 END
        //     FROM (SELECT * FROM user) AS SUB
        //     WHERE SUB.user_id = UPD.user_id
        // )
        // WHERE user_id = :user_id
    }
}

キー項目指定でクエリ結果を取得する

findメソッド

Modelクラスを継承したクラスでは、基本的にはselectメソッドにより、SELECT文オブジェクトを取得し、それにいろいろな条件を指定した上で実行、という形になりますが、findメソッドを使用することにより、このような手続きを踏まずにいきなり結果の行データを得ることが出来ます。

findメソッドはテーブルの主キー項目の数だけ引数で値を指定し、条件に合致する単一行を取得するメソッドです。なので、テーブルによって指定すべき引数の数は可変となります。単一フィールドで主キーをなすテーブルの場合は引数一つ、2フィールドで主キーをなす複合キーのテーブルの場合は引数二つです。

CREATE TABLE user
(
    user_id varchar(20) primary key,
    name varchar(50),
    address varchar(100)
          ・
          ・
          ・
)

このようなテーブルがあったとしましょう。
これに対し、モデルクラス"User"で、以下のような2パターンの処理は同じ結果となります。

class UserService extends Service
{
    public function getUserInfo($userId)
    {
        $user = $this->model('User');

    // 以下の2つのパターンは同じ結果です。

        // findメソッド利用
        $row = $user->find($userId);

        // selectメソッドを利用したの通常の方法
        $sel = $user->select();
        $sel->where('user_id', $userId);
        $row = $sel->fetchRow();
    }
}

findメソッドでは、そのテーブルの定義情報をデータベースより取り出し、主キーフィールドを検出してその項目に対して引数で指定された値をWHERE条件を指定します。そのため、テーブルに主キーが設定されていないとか、主キーフィールドの数と引数の数があっていないような場合にはfindメソッドはfalseを返します。また複合キーの場合、引数指定の順番は機械的にテーブル定義上のフィールドの順番で割り当てるため、順番も正しく指定する必要があります。
また、手軽に利用できる反面、定義情報の取得のためのSQLが一旦発行されるため、わずかながらパフォーマンス的に不利になります。

条件つきfindメソッド

findメソッドは主キーフィールドの値を指定する事により、単一行を返しますが、主キー以外のフィールドの値を指定して単一行を取得したい場合があります。この場合、メソッド名に条件を指定したいフィールド名を付加することで可能になります。指定したいフィールドが複数の場合はフィールド名をAndでつなげます。

class UserService extends Service
{
    public function getUserInfo($name, $address)
    {
        $user = $this->model('User');

    // 以下の2つのパターンは同じ結果です。

        // findBy~メソッドを利用
        $row = $user->findByNameAndAddress($name, $address);

        // selectメソッドを利用した通常の方法
        $sel = $user->select();
        $sel->where('name', $name);
        $sel->where('address', $address);
        $row = $sel->fetchRow();
    }
}

このfindByNameAndAddressというメソッドは実際には存在しない、仮想のメソッドです。メソッド名は必ず"findBy"で始まり、以降、フィールド名を先頭大文字で接尾し、複数フィールドの場合は更にAndを付加した上で、次のフィールド名を追加していきます。
findBy以降の名前をもとにWHERE文を組み立てるため、指定するフィールド名が実際には存在しない場合にはSQLエラーによる例外が発生します。

また仮に条件に合致する行が複数存在する場合でも、このメソッドはその第一行目のみを返します。

条件付きselectメソッド

条件付きfindメソッドでは必ず単一行を返しますが、複数行の結果が欲しい場合もあります。
この場合、条件付きfindメソッドのfindをselectに置き換えるだけで、実現可能です。
更に、メソッド名で指定したフィールド名より一つ引数を多くし、フィールド名を指定することで、その最後の引数はORDER BYフィールドとして指定することができます。

class UserService extends Service
{
    public function searchUsers($pref, $age)
    {
        $user = $this->model('User');

    // 以下の2つのパターンは同じ結果です。

        // selectBy~メソッドを利用
        $rows = $user->selectByPrefAndAge($pref, $age, 'name');

        // selectメソッドを利用した通常の方法
        $sel = $user->select();
        $sel->where('pref', $pref);
        $sel->where('age', $age);
        $sel->order('name');
        $rows = $sel->fetchAll();
    }
}