Yii2 framework backup
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

31 KiB

データベースアクセスオブジェクト

PDO の上に構築された Yii DAO (データベースアクセスオブジェクト) は、リレーショナルデータベースにアクセスするためのオブジェクト指向 API を提供するものです。 これは、データベースにアクセスする他のもっと高度な方法、例えば クエリビルダアクティブレコード の基礎でもあります。

Yii DAO を使うときは、主として素の SQL と PHP 配列を扱う必要があります。 結果として、Yii DAO はデータベースにアクセスする方法としては最も効率的なものになります。 しかし、SQL の構文はデータベースによってさまざまに異なる場合がありますので、Yii DAO を使用するということは、特定のデータベースに依存しないアプリケーションを作るためには追加の労力が必要になる、ということをも同時に意味します。

Yii は下記の DBMS のサポートを内蔵しています。

DB 接続を作成する

データベースにアクセスするために、まずは、データベースに接続するために yii\db\Connection のインスタンスを作成する必要があります。

$db = new yii\db\Connection([
    'dsn' => 'mysql:host=localhost;dbname=example',
    'username' => 'root',
    'password' => '',
    'charset' => 'utf8',
]);

DB 接続は、たいていは、さまざまな場所でアクセスする必要がありますので、次のように、アプリケーションコンポーネント の形式で構成するのが通例です。

return [
    // ...
    'components' => [
        // ...
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=example',
            'username' => 'root',
            'password' => '',
            'charset' => 'utf8',
        ],
    ],
    // ...
];

こうすると Yii::$app->db という式で DB 接続にアクセスすることが出来るようになります。

Tip: あなたのアプリケーションが複数のデータベースにアクセスする必要がある場合は、複数の DB アプリケーションコンポーネントを構成することが出来ます。

DB 接続を構成するときは、つねに yii\db\Connection::dsn プロパティによってデータソース名 (DSN) を指定しなければなりません。 DSN の形式はデータベースによってさまざまに異なります。 詳細は PHP マニュアル を参照して下さい。 下記にいくつかの例を挙げます。

  • MySQL, MariaDB: mysql:host=localhost;dbname=mydatabase
  • SQLite: sqlite:/path/to/database/file
  • PostgreSQL: pgsql:host=localhost;port=5432;dbname=mydatabase
  • CUBRID: cubrid:dbname=demodb;host=localhost;port=33000
  • MS SQL Server (sqlsrv ドライバ経由): sqlsrv:Server=localhost;Database=mydatabase
  • MS SQL Server (dblib ドライバ経由): dblib:host=localhost;dbname=mydatabase
  • MS SQL Server (mssql ドライバ経由): mssql:host=localhost;dbname=mydatabase
  • Oracle: oci:dbname=//localhost:1521/mydatabase

ODBC 経由でデータベースに接続しようとする場合は、yii\db\Connection::driverName プロパティを構成して、Yii に実際のデータベースのタイプを知らせなければならないことに注意してください。 例えば、

'db' => [
    'class' => 'yii\db\Connection',
    'driverName' => 'mysql',
    'dsn' => 'odbc:Driver={MySQL};Server=localhost;Database=test',
    'username' => 'root',
    'password' => '',
],

yii\db\Connection::dsn プロパティに加えて、たいていは yii\db\Connection::usernameyii\db\Connection::password も構成しなければなりません。 構成可能なプロパティの全てのリストは yii\db\Connection を参照して下さい。

Info: DB 接続のインスタンスを作成するとき、実際のデータベース接続は、最初の SQL を実行するか、yii\db\Connection::open() メソッドを明示的に呼ぶかするまでは確立されません。

Tip: 時として、何らかの環境変数を初期化するために、データベース接続を確立した直後に何かクエリを実行したい場合があるでしょう (例えば、タイムゾーンや文字セットを設定するなどです)。 そうするために、データベース接続の yii\db\Connection::EVENT_AFTER_OPEN イベントに対するイベントハンドラを登録することが出来ます。 以下のように、アプリケーションの構成情報に直接にハンドラを登録してください。

'db' => [
    // ...
    'on afterOpen' => function($event) {
        // $event->sender は DB 接続を指す
        $event->sender->createCommand("SET time_zone = 'UTC'")->execute();
    }
]

SQL クエリを実行する

いったんデータベース接続のインスタンスを得てしまえば、次の手順に従って SQL クエリを実行することが出来ます。

  1. 素の SQL クエリで yii\db\Command を作成する。
  2. パラメータをバインドする (オプション)。
  3. yii\db\Command の SQL 実行メソッドの一つを呼ぶ。

次に、データベースからデータを読み出すさまざまな方法を例示します。

// 行のセットを返す。各行は、カラム名と値の連想配列。
// クエリが結果を返さなかった場合は空の配列が返される。
$posts = Yii::$app->db->createCommand('SELECT * FROM post')
            ->queryAll();

// 一つの行 (最初の行) を返す。
// クエリの結果が無かった場合は false が返される。
$post = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=1')
           ->queryOne();

// 一つのカラム (最初のカラム) を返す。
// クエリが結果を返さなかった場合は空の配列が返される。
$titles = Yii::$app->db->createCommand('SELECT title FROM post')
             ->queryColumn();

// スカラ値を返す。
// クエリの結果が無かった場合は false が返される。
$count = Yii::$app->db->createCommand('SELECT COUNT(*) FROM post')
             ->queryScalar();

Note: 精度を保つために、対応するデータベースカラムの型が数値である場合でも、データベースから取得されたデータは、全て文字列として表現されます。

パラメータをバインドする

パラメータを持つ SQL から DB コマンドを作成するときは、SQL インジェクション攻撃を防止するために、ほとんど全ての場合においてパラメータをバインドする手法を用いるべきです。 例えば、

$post = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=:id AND status=:status')
           ->bindValue(':id', $_GET['id'])
           ->bindValue(':status', 1)
           ->queryOne();

SQL 文において、一つまたは複数のパラメータプレースホルダ (例えば、上記のサンプルにおける :id) を埋め込むことが出来ます。 パラメータプレースホルダは、コロンから始まる文字列でなければなりません。 そして、次に掲げるパラメータをバインドするメソッドの一つを使って、パラメータの値をバインドします。

次の例はパラメータをバインドする方法の選択肢を示すものです。

$params = [':id' => $_GET['id'], ':status' => 1];

$post = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=:id AND status=:status')
           ->bindValues($params)
           ->queryOne();
           
$post = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=:id AND status=:status', $params)
           ->queryOne();

パラメータバインディングは プリペアドステートメント によって実装されています。 パラメータバインディングには、SQL インジェクション攻撃を防止する以外にも、SQL 文を一度だけ準備して異なるパラメータで複数回実行することにより、パフォーマンスを向上させる効果もあります。 例えば、

$command = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=:id');

$post1 = $command->bindValue(':id', 1)->queryOne();
$post2 = $command->bindValue(':id', 2)->queryOne();
// ...

yii\db\Command::bindParam() はパラメータを参照渡しでバインドすることをサポートしていますので、上記のコードは次のように書くことも出来ます。

$command = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=:id')
              ->bindParam(':id', $id);

$id = 1;
$post1 = $command->queryOne();

$id = 2;
$post2 = $command->queryOne();

クエリの実行の前にプレースホルダを変数 $id にバインドし、そして、後に続く各回の実行の前にその変数の値を変更していること (これは、たいてい、ループで行います) に着目してください。 このやり方でクエリを実行すると、パラメータの値が違うごとに新しいクエリを実行するのに比べて、はるかに効率を良くすることが出来ます。

SELECT しないクエリを実行する

今までのセクションで紹介した queryXyz() メソッドは、すべて、データベースからデータを取得する SELECT クエリを扱うものでした。 データを返さないクエリのためには、代りに yii\db\Command::execute() メソッドを呼ばなければなりません。 例えば、

Yii::$app->db->createCommand('UPDATE post SET status=1 WHERE id=1')
   ->execute();

yii\db\Command::execute() メソッドは SQL の実行によって影響を受けた行の数を返します。

INSERT、UPDATE および DELETE クエリのためには、素の SQL を書く代りに、それぞれ、yii\db\Command::insert()yii\db\Command::update()yii\db\Command::delete() を呼んで、対応する SQL を構築することが出来ます。 これらのメソッドは、テーブルとカラムの名前を適切に引用符で囲み、パラメータの値をバインドします。 例えば、

// INSERT (テーブル名, カラムの値)
Yii::$app->db->createCommand()->insert('user', [
    'name' => 'Sam',
    'age' => 30,
])->execute();

// UPDATE (テーブル名, カラムの値, 条件)
Yii::$app->db->createCommand()->update('user', ['status' => 1], 'age > 30')->execute();

// DELETE (テーブル名, 条件)
Yii::$app->db->createCommand()->delete('user', 'status = 0')->execute();

yii\db\Command::batchInsert() を呼んで複数の行を一気に挿入することも出来ます。 この方法は、一度に一行を挿入するよりはるかに効率的です。

// テーブル名, カラム名, カラムの値
Yii::$app->db->createCommand()->batchInsert('user', ['name', 'age'], [
    ['Tom', 30],
    ['Jane', 20],
    ['Linda', 25],
])->execute();

上述のメソッド群はクエリを生成するだけであり、実際にそれを実行するためには、常に yii\db\Command::execute() を呼び出す必要があることに注意してください。

テーブルとカラムの名前を引用符で囲む

特定のデータベースに依存しないコードを書くときには、テーブルとカラムの名前を適切に引用符で囲むことが、たいてい、頭痛の種になります。 データベースによって名前を引用符で囲む規則がさまざまに異なるからです。 この問題を克服するために、次のように、Yii によって導入された引用符の構文を使用することが出来ます。

  • [[カラム名]]: 引用符で囲まれるカラム名を二重角括弧で包む。
  • {{テーブル名}}: 引用符で囲まれるテーブル名を二重波括弧で包む。

Yii DAO は、このような構文を、DBMS 固有の文法に従って、適切な引用符で囲まれたカラム名とテーブル名に自動的に変換します。 例えば、

// MySQL では SELECT COUNT(`id`) FROM `employee` という SQL が実行される
$count = Yii::$app->db->createCommand("SELECT COUNT([[id]]) FROM {{employee}}")
            ->queryScalar();

テーブル接頭辞を使う

あなたの DB テーブル名のほとんどが何か共通の接頭辞を持っている場合は、Yii DAO によってサポートされているテーブル接頭辞の機能を使うことが出来ます。

最初に、アプリケーションの構成情報で、yii\db\Connection::tablePrefix プロパティによって、テーブル接頭辞を指定します。

return [
    // ...
    'components' => [
        // ...
        'db' => [
            // ...
            'tablePrefix' => 'tbl_',
        ],
    ],
];

そして、あなたのコードの中で、そのテーブル接頭辞を名前に持つテーブルを参照しなければならないときには、いつでも {{%テーブル名}} という構文を使います。 パーセント記号は DB 接続を構成したときに指定したテーブル接頭辞に自動的に置き換えられます。 例えば、

// MySQL では SELECT COUNT(`id`) FROM `tbl_employee` という SQL が実行される
$count = Yii::$app->db->createCommand("SELECT COUNT([[id]]) FROM {{%employee}}")
            ->queryScalar();

トランザクションを実行する

一続きになった複数の関連するクエリを実行するときは、データの整合性と一貫性を保証するために、一連のクエリをトランザクションで囲む必要がある場合があります。 一連のクエリのどの一つが失敗した場合でも、データベースは、何一つクエリが実行されなかったかのような状態へとロールバックされます。

次のコードはトランザクションの典型的な使用方法を示すものです。

Yii::$app->db->transaction(function($db) {
    $db->createCommand($sql1)->execute();
    $db->createCommand($sql2)->execute();
    // ... その他の SQL 文を実行 ...
});

上記のコードは、次のものと等価です。こちらの方が、エラー処理のコードをより細かく制御することが出来ます。

$db = Yii::$app->db;
$transaction = $db->beginTransaction();

try {
    $db->createCommand($sql1)->execute();
    $db->createCommand($sql2)->execute();
    // ... その他の SQL 文を実行 ...

    $transaction->commit();

} catch(\Exception $e) {

    $transaction->rollBack();

    throw $e;
}

yii\db\Connection::beginTransaction() メソッドを呼ぶことによって、新しいトランザクションが開始されます。 トランザクションは、変数 $transaction に保存された yii\db\Transaction オブジェクトとして表現されます。 そして、実行されるクエリが try...catch... ブロックで囲まれます。 全てのクエリの実行が成功した場合には、トランザクションをコミットするために yii\db\Transaction::commit() が呼ばれます。 そうでなく、例外がトリガされてキャッチされた場合は、yii\db\Transaction::rollBack() が呼ばれて、トランザクションの中で失敗したクエリに先行するクエリによって行なわれた変更が、ロールバックされます。 そして、throw $e が、まるでそれをキャッチしなかったかのように、例外を再スローしますので、通常のエラー処理プロセスがその例外の面倒を見ることになります。

分離レベルを指定する

Yii は、トランザクションの 分離レベル の設定もサポートしています。 デフォルトでは、新しいトランザクションを開始したときは、データベースシステムによって設定された分離レベルを使用します。 デフォルトの分離レベルは、次のようにしてオーバーライドすることが出来ます。

$isolationLevel = \yii\db\Transaction::REPEATABLE_READ;

Yii::$app->db->transaction(function ($db) {
    ....
}, $isolationLevel);
 
// あるいは

$transaction = Yii::$app->db->beginTransaction($isolationLevel);

Yii は、最もよく使われる分離レベルのために、四つの定数を提供しています。

  • \yii\db\Transaction::READ_UNCOMMITTED - 最も弱いレベル。ダーティーリード、非再現リード、ファントムが発生しうる。
  • \yii\db\Transaction::READ_COMMITTED - ダーティーリードを回避。
  • \yii\db\Transaction::REPEATABLE_READ - ダーティーリードと非再現リードを回避。
  • \yii\db\Transaction::SERIALIZABLE - 最も強いレベル。上記の問題を全て回避。

分離レベルを指定するためには、上記の定数を使う以外に、あなたが使っている DBMS によってサポートされている有効な構文の文字列を使うことも出来ます。 例えば、PostreSQL では、SERIALIZABLE READ ONLY DEFERRABLE を使うことが出来ます。

DBMS によっては、接続全体に対してのみ分離レベルの設定を許容しているものがあることに注意してください。 その場合、すべての後続のトランザクションは、指定しなくても、それと同じ分離レベルで実行されます。 従って、この機能を使用するときは、矛盾する設定を避けるために、全てのトランザクションについて分離レベルを明示的に指定しなければなりません。 このチュートリアルを書いている時点では、この制約の影響を受ける DBMS は MSSQL と SQLite だけです。

Note: SQLite は、二つの分離レベルしかサポートしていません。すなわち、READ UNCOMMITTEDSERIALIZABLE しか使えません。 他のレベルを使おうとすると、例外が投げられます。

Note: PostgreSQL は、トランザクションを開始する前に分離レベルを指定することを許容していません。 すなわち、トランザクションを開始するときに、分離レベルを直接に指定することは出来ません。 この場合、トランザクションを開始した後に yii\db\Transaction::setIsolationLevel() を呼び出す必要があります。

トランザクションを入れ子にする

あなたの DBMS が Savepoint をサポートしている場合は、次のように、複数のトランザクションを入れ子にすることが出来ます。

Yii::$app->db->transaction(function ($db) {
    // 外側のトランザクション
    
    $db->transaction(function ($db) {
        // 内側のトランザクション
    });
});

あるいは、

$db = Yii::$app->db;
$outerTransaction = $db->beginTransaction();
try {
    $db->createCommand($sql1)->execute();

    $innerTransaction = $db->beginTransaction();
    try {
        $db->createCommand($sql2)->execute();
        $innerTransaction->commit();
    } catch (\Exception $e) {
        $innerTransaction->rollBack();
        throw $e;
    }

    $outerTransaction->commit();
} catch (\Exception $e) {
    $outerTransaction->rollBack();
    throw $e;
}

レプリケーションと読み書きの分離

多くの DBMS は、データベースの可用性とサーバのレスポンスタイムを向上させるために、データベースレプリケーション をサポートしています。 データベースレプリケーションによって、データはいわゆる マスタサーバ から スレーブサーバ に複製されます。 データの書き込みと更新はすべてマスタサーバ上で実行されなければなりませんが、データの読み出しはスレーブサーバ上でも可能です。

データベースレプリケーションを活用して読み書きの分離を達成するために、yii\db\Connection コンポーネントを下記のように構成することが出来ます。

[
    'class' => 'yii\db\Connection',

    // マスタの構成
    'dsn' => 'マスタサーバの DSN',
    'username' => 'master',
    'password' => '',

    // スレーブの共通の構成
    'slaveConfig' => [
        'username' => 'slave',
        'password' => '',
        'attributes' => [
            // 短かめの接続タイムアウトを使う
            PDO::ATTR_TIMEOUT => 10,
        ],
    ],

    // スレーブの構成のリスト
    'slaves' => [
        ['dsn' => 'スレーブサーバ 1 の DSN'],
        ['dsn' => 'スレーブサーバ 2 の DSN'],
        ['dsn' => 'スレーブサーバ 3 の DSN'],
        ['dsn' => 'スレーブサーバ 4 の DSN'],
    ],
]

上記の構成は、一つのマスタと複数のスレーブを指定するものです。 読み出しのクエリを実行するためには、スレーブの一つが接続されて使用され、書き込みのクエリを実行するためには、マスタが使われます。 そのような読み書きの分離が、この構成によって、自動的に達成されます。例えば、

// 上記の構成を使って Connection のインスタンスを作成する
$db = Yii::createObject($config);

// スレーブの一つに対してクエリを実行する
$rows = Yii::$app->db->createCommand('SELECT * FROM user LIMIT 10')->queryAll();

// マスタに対してクエリを実行する
Yii::$app->db->createCommand("UPDATE user SET username='demo' WHERE id=1")->execute();

Info: yii\db\Command::execute() を呼ぶことで実行されるクエリは、書き込みのクエリと見なされ、yii\db\Command の "query" メソッドのうちの一つによって実行されるその他すべてのクエリは、読み出しクエリと見なされます。 現在アクティブなスレーブ接続は Yii::$app->db->slave によって取得することが出来ます。

Connection コンポーネントは、スレーブ間のロードバランス調整とフェイルオーバーをサポートしています。 読み出しクエリを最初に実行するときに、Connection コンポーネントはランダムにスレーブを選んで接続を試みます。 そのスレーブが「死んでいる」ことが分かったときは、他のスレーブを試します。 スレーブが一つも使用できないときは、マスタに接続します。 yii\db\Connection::serverStatusCache を構成することによって、「死んでいる」サーバを記憶し、yii\db\Connection::serverRetryInterval はそのサーバへの接続を再試行しないようにすることが出来ます。

Info: 上記の構成では、すべてのスレーブに対して 10 秒の接続タイムアウトが指定されています。 これは、10 秒以内に接続できなければ、そのスレーブは「死んでいる」と見なされることを意味します。 このパラメータは、実際の環境に基づいて調整することが出来ます。

複数のマスタと複数のスレーブという構成にすることも可能です。例えば、

[
    'class' => 'yii\db\Connection',

    // マスタの共通の構成
    'masterConfig' => [
        'username' => 'master',
        'password' => '',
        'attributes' => [
            // 短かめの接続タイムアウトを使う
            PDO::ATTR_TIMEOUT => 10,
        ],
    ],

    // マスタの構成のリスト
    'masters' => [
        ['dsn' => 'マスタサーバ 1 の DSN'],
        ['dsn' => 'マスタサーバ 2 の DSN'],
    ],

    // スレーブの共通の構成
    'slaveConfig' => [
        'username' => 'slave',
        'password' => '',
        'attributes' => [
            // 短かめの接続タイムアウトを使う
            PDO::ATTR_TIMEOUT => 10,
        ],
    ],

    // スレーブの構成のリスト
    'slaves' => [
        ['dsn' => 'スレーブサーバ 1 の DSN'],
        ['dsn' => 'スレーブサーバ 2 の DSN'],
        ['dsn' => 'スレーブサーバ 3 の DSN'],
        ['dsn' => 'スレーブサーバ 4 の DSN'],
    ],
]

上記の構成は、二つのマスタと四つのスレーブを指定しています。 Connection コンポーネントは、スレーブ間での場合と同じように、マスタ間でのロードバランス調整とフェイルオーバーをサポートしています。 一つ違うのは、マスタが一つも利用できないときは例外が投げられる、という点です。

Note: yii\db\Connection::masters プロパティを使って一つまたは複数のマスタを構成する場合は、データベース接続を定義する Connection オブジェクト自体のその他のプロパティ (例えば、dsnusernamepassword) は全て無視されます。

デフォルトでは、トランザクションはマスタ接続を使用します。そして、トランザクション内では、全ての DB 操作はマスタ接続を使用します。 例えば、

$db = Yii::$app->db;
// トランザクションはマスタ接続で開始される
$transaction = $db->beginTransaction();

try {
    // クエリは両方ともマスタに対して実行される
    $rows = $db->createCommand('SELECT * FROM user LIMIT 10')->queryAll();
    $db->createCommand("UPDATE user SET username='demo' WHERE id=1")->execute();

    $transaction->commit();
} catch(\Exception $e) {
    $transaction->rollBack();
    throw $e;
}

スレーブ接続を使ってトランザクションを開始したいときは、次のように、明示的にそうする必要があります。

$transaction = Yii::$app->db->slave->beginTransaction();

時として、読み出しクエリの実行にマスタ接続を使うことを強制したい場合があります。 これは、useMaster() メソッドを使うによって達成できます。

$rows = Yii::$app->db->useMaster(function ($db) {
    return Yii::$app->db->createCommand('SELECT * FROM user LIMIT 10')->queryAll();
});

直接に Yii::$app->db->enableSlaves を false に設定して、全てのクエリをマスタ接続に向けることも出来ます。

データベーススキーマを扱う

Yii DAO は、新しいテーブルを作ったり、テーブルからカラムを削除したりなど、データベーススキーマを操作することを可能にする一揃いのメソッドを提供しています。 以下がそのソッドのリストです。

これらのメソッドは次のようにして使うことが出来ます。

// CREATE TABLE
Yii::$app->db->createCommand()->createTable('post', [
    'id' => 'pk',
    'title' => 'string',
    'text' => 'text',
]);

上記の配列は、生成されるカラムの名前と型を記述しています。 Yii はカラムの型のために一連の抽象データ型を提供しているため、データベースの違いを意識せずにスキーマを定義することが可能です。 これらの抽象データ型は、テーブルが作成されるデータベースに依存する DBMS 固有の型定義に変換されます。 詳しい情報は yii\db\Command::createTable() メソッドの API ドキュメントを参照してください。

データベースのスキーマを変更するだけでなく、テーブルに関する定義情報を DB 接続の yii\db\Connection::getTableSchema() メソッドによって取得することも出来ます。 例えば、

$table = Yii::$app->db->getTableSchema('post');

このメソッドは、テーブルのカラム、プライマリキー、外部キーなどの情報を含む yii\db\TableSchema オブジェクトを返します。 これらの情報は、主として クエリビルダアクティブレコード によって利用されて、特定のデータベースに依存しないコードを書くことを助けてくれています。