3 フォロワー

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

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

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

Yii 2.0 では、DAO は以下のデータベースをすぐにサポートしています

注: PHP 7 用の新しいバージョンの pdo_oci は、現在、ソースコードとしてのみ存在します。コンパイルするには コミュニティから提供された指示 に従うか、PDO エミュレーションレイヤーを使用してください。

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接続にアクセスできます。

ヒント: アプリケーションが複数のデータベースにアクセスする必要がある場合は、複数のDBアプリケーションコンポーネントを構成できます。

DB接続を構成する場合、常に 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=//:1521/mydatabase

ODBC経由でデータベースに接続する場合は、Yiiが実際のデータベースの種類を認識できるように、yii\db\Connection::$driverName プロパティを構成する必要があることに注意してください。たとえば、

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

dsn プロパティに加えて、username および password を構成する必要があることがよくあります。構成可能なプロパティの完全なリストについては、yii\db\Connection を参照してください。

情報: DB接続インスタンスを作成しても、最初のSQLを実行するか、明示的にopen()メソッドを呼び出すまで、データベースへの実際の接続は確立されません。

ヒント: データベース接続が確立された直後に、いくつかの環境変数を初期化するためにクエリを実行したい場合があります(例:タイムゾーンや文字セットの設定)。その場合は、データベース接続のafterOpenイベントのイベントハンドラを登録することで可能です。アプリケーションの設定でハンドラを直接登録できます。以下はその例です。

'db' => [
    // ...
    'on afterOpen' => function($event) {
        // $event->sender refers to the DB connection
        $event->sender->createCommand("SET time_zone = 'UTC'")->execute();
    }
],

MS SQL Serverでは、適切なバイナリデータ処理のために追加の接続オプションが必要です。

'db' => [
 'class' => 'yii\db\Connection',
    'dsn' => 'sqlsrv:Server=localhost;Database=mydatabase',
    'attributes' => [
        \PDO::SQLSRV_ATTR_ENCODING => \PDO::SQLSRV_ENCODING_SYSTEM
    ]
],

SQLクエリの実行

データベース接続インスタンスが作成されたら、次の手順でSQLクエリを実行できます。

  1. プレーンなSQLクエリでyii\db\Commandを作成します。
  2. パラメータをバインドします(オプション)。
  3. yii\db\CommandのSQL実行メソッドのいずれかを呼び出します。

次の例は、データベースからデータを取得するさまざまな方法を示しています。

// return a set of rows. each row is an associative array of column names and values.
// an empty array is returned if the query returned no results
$posts = Yii::$app->db->createCommand('SELECT * FROM post')
            ->queryAll();

// return a single row (the first row)
// false is returned if the query has no result
$post = Yii::$app->db->createCommand('SELECT * FROM post WHERE id=1')
           ->queryOne();

// return a single column (the first column)
// an empty array is returned if the query returned no results
$titles = Yii::$app->db->createCommand('SELECT title FROM post')
             ->queryColumn();

// return a scalar value
// false is returned if the query has no result
$count = Yii::$app->db->createCommand('SELECT COUNT(*) FROM post')
             ->queryScalar();

注意: 精度を保つため、データベースから取得したデータは、対応するデータベースの列タイプが数値であっても、すべて文字列として表現されます。

パラメータのバインド

パラメータを持つ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ステートメントでは、1つまたは複数のパラメータプレースホルダー(上記の例では:idなど)を埋め込むことができます。パラメータプレースホルダーは、コロンで始まる文字列である必要があります。その後、以下のパラメータバインドメソッドのいずれかを呼び出して、パラメータ値をバインドできます。

  • bindValue():単一のパラメータ値をバインドします。
  • bindValues():複数のパラメータ値を1回の呼び出しでバインドします。
  • bindParam()bindValue()と似ていますが、パラメータ参照のバインドもサポートします。

次の例は、パラメータをバインドする別の方法を示しています。

$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();
// ...

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変数にバインドし、その後の各実行前にその変数の値を変更していることに注意してください(これは多くの場合ループで行われます)。この方法でクエリを実行すると、異なるパラメータ値ごとに新しいクエリを実行するよりもはるかに効率的になります。

情報: パラメータのバインドは、プレーンSQLを含む文字列に値を挿入する必要がある場所でのみ使用されます。クエリビルダーアクティブレコードのようなより高い抽象化レイヤーの多くの場所では、SQLに変換される値の配列を指定することがよくあります。これらの場所では、パラメータのバインドはYiiによって内部的に行われるため、手動でパラメータを指定する必要はありません。

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を記述する代わりに、それぞれinsert()update()delete()を呼び出して、対応するSQLを作成できます。これらのメソッドは、テーブル名と列名を適切に引用符で囲み、パラメータ値をバインドします。例:

// INSERT (table name, column values)
Yii::$app->db->createCommand()->insert('user', [
    'name' => 'Sam',
    'age' => 30,
])->execute();

// UPDATE (table name, column values, condition)
Yii::$app->db->createCommand()->update('user', ['status' => 1], 'age > 30')->execute();

// DELETE (table name, condition)
Yii::$app->db->createCommand()->delete('user', 'status = 0')->execute();

batchInsert()を呼び出して、一度に複数の行を挿入することもできます。これは、一度に1行ずつ挿入するよりもはるかに効率的です。

// table name, column names, column values
Yii::$app->db->createCommand()->batchInsert('user', ['name', 'age'], [
    ['Tom', 30],
    ['Jane', 20],
    ['Linda', 25],
])->execute();

もう1つの便利なメソッドは、upsert()です。Upsertは、データベーステーブルにまだ存在しない行(一意制約に一致)を挿入するか、存在する場合は更新するアトミック操作です。

Yii::$app->db->createCommand()->upsert('pages', [
    'name' => 'Front page',
    'url' => 'https://example.com/', // url is unique
    'visits' => 0,
], [
    'visits' => new \yii\db\Expression('visits + 1'),
], $params)->execute();

上記のコードは、新しいページレコードを挿入するか、その訪問回数をアトミックにインクリメントします。

前述のメソッドはクエリを作成するだけであり、実際に実行するには常にexecute()を呼び出す必要があることに注意してください。

テーブル名と列名の引用符

データベースに依存しないコードを作成する場合、データベースによって名前の引用符ルールが異なるため、テーブル名と列名を適切に引用符で囲むことは頭痛の種となることがよくあります。この問題を解決するために、Yiiによって導入された次の引用符構文を使用できます。

  • [[column name]]:引用符で囲む列名を二重角括弧で囲みます。
  • {{table name}}:引用符で囲むテーブル名を二重中括弧で囲みます。

Yii DAOは、このような構造を、DBMS固有の構文を使用して、対応する引用符で囲まれた列名またはテーブル名に自動的に変換します。例:

// executes this SQL for MySQL: SELECT COUNT(`id`) FROM `employee`
$count = Yii::$app->db->createCommand("SELECT COUNT([[id]]) FROM {{employee}}")
            ->queryScalar();

テーブルプレフィックスの使用

DBテーブル名のほとんどが共通のプレフィックスを共有している場合は、Yii DAOによって提供されるテーブルプレフィックス機能を使用できます。

まず、アプリケーション設定でyii\db\Connection::$tablePrefixプロパティを介してテーブルプレフィックスを指定します。

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

次に、コード内で、そのようなプレフィックスを含む名前のテーブルを参照する必要がある場合は、構文{{%table_name}}を使用します。パーセント文字は、DB接続を構成するときに指定したテーブルプレフィックスに自動的に置き換えられます。例:

// executes this SQL for MySQL: SELECT COUNT(`id`) FROM `tbl_employee`
$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();
    // ... executing other SQL statements ...
});

上記のコードは次のコードと同等であり、エラー処理コードをより詳細に制御できます。

$db = Yii::$app->db;
$transaction = $db->beginTransaction();
try {
    $db->createCommand($sql1)->execute();
    $db->createCommand($sql2)->execute();
    // ... executing other SQL statements ...
    
    $transaction->commit();
} catch(\Exception $e) {
    $transaction->rollBack();
    throw $e;
} catch(\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

beginTransaction()メソッドを呼び出すことにより、新しいトランザクションが開始されます。トランザクションは、$transaction変数に格納されたyii\db\Transactionオブジェクトとして表されます。次に、実行されるクエリはtry...catch...ブロックで囲まれます。すべてのクエリが正常に実行された場合、commit()メソッドが呼び出されてトランザクションがコミットされます。それ以外の場合、例外がトリガーされてキャッチされた場合、rollBack()メソッドが呼び出されて、トランザクションで失敗したクエリより前に行われたクエリによる変更がロールバックされます。throw $eは、例外をキャッチしなかったかのように再スローするため、通常のエラー処理プロセスが処理します。

注: 上記のコードでは、PHP 5.xとPHP 7.xとの互換性のために、2つのcatchブロックがあります。\ExceptionはPHP 7.0以降で\Throwableインターフェースを実装しているため、アプリがPHP 7.0以上のみを使用している場合は、\Exceptionの部分を省略できます。

分離レベルの指定

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

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

Yii::$app->db->transaction(function ($db) {
    ....
}, $isolationLevel);
 
// or alternatively

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

Yiiは、最も一般的な分離レベルに4つの定数を提供します。

分離レベルを指定するために上記の定数を使用するだけでなく、使用しているDBMSでサポートされている有効な構文を持つ文字列を使用することもできます。たとえば、PostgreSQLでは"SERIALIZABLE READ ONLY DEFERRABLE"を使用できます。

一部のDBMSでは、接続全体に対してのみ分離レベルを設定できることに注意してください。後続のトランザクションは、何も指定しない場合でも同じ分離レベルになります。この機能を使用する場合は、設定の競合を避けるために、すべてのトランザクションの分離レベルを明示的に設定する必要がある場合があります。この記事の執筆時点では、MSSQLとSQLiteのみがこの制限の影響を受けます。

注: SQLiteは2つの分離レベルのみをサポートしているため、READ UNCOMMITTEDSERIALIZABLEのみを使用できます。他のレベルを使用すると、例外がスローされます。

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

ネストされたトランザクション

DBMSがセーブポイントをサポートしている場合は、次のように複数のトランザクションをネストできます。

Yii::$app->db->transaction(function ($db) {
    // outer transaction
    
    $db->transaction(function ($db) {
        // inner transaction
    });
});

または、別の方法として、

$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;
    } catch (\Throwable $e) {
        $innerTransaction->rollBack();
        throw $e;
    }

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

レプリケーションと読み取り/書き込みの分割

多くのDBMSは、データベースの可用性を向上させ、サーバーの応答時間を短縮するために、データベースレプリケーションをサポートしています。データベースレプリケーションでは、データはいわゆるマスターサーバーからスレーブサーバーに複製されます。すべての書き込みと更新はマスターサーバーで実行する必要があり、読み取りはスレーブサーバーでも実行できます。

データベースレプリケーションを利用して読み取り/書き込みの分割を実現するには、次のようにyii\db\Connectionコンポーネントを構成できます。

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

    // configuration for the master
    'dsn' => 'dsn for master server',
    'username' => 'master',
    'password' => '',

    // common configuration for slaves
    'slaveConfig' => [
        'username' => 'slave',
        'password' => '',
        'attributes' => [
            // use a smaller connection timeout
            PDO::ATTR_TIMEOUT => 10,
        ],
    ],

    // list of slave configurations
    'slaves' => [
        ['dsn' => 'dsn for slave server 1'],
        ['dsn' => 'dsn for slave server 2'],
        ['dsn' => 'dsn for slave server 3'],
        ['dsn' => 'dsn for slave server 4'],
    ],
]

上記の構成は、単一のマスターと複数のスレーブによる設定を指定します。スレーブのいずれかが接続され、読み取りクエリの実行に使用されますが、マスターは書き込みクエリの実行に使用されます。このような読み取り/書き込み分割は、この構成で自動的に実行されます。例:

// create a Connection instance using the above configuration
Yii::$app->db = Yii::createObject($config);

// query against one of the slaves
$rows = Yii::$app->db->createCommand('SELECT * FROM user LIMIT 10')->queryAll();

// query against the master
Yii::$app->db->createCommand("UPDATE user SET username='demo' WHERE id=1")->execute();

情報: yii\db\Command::execute()を呼び出して実行されたクエリは書き込みクエリと見なされますが、yii\db\Commandの「query」メソッドのいずれかを通して実行された他のすべてのクエリは読み取りクエリです。現在アクティブなスレーブ接続は、Yii::$app->db->slaveを使用して取得できます。

Connectionコンポーネントは、スレーブ間の負荷分散とフェイルオーバーをサポートしています。読み取りクエリを初めて実行すると、Connectionコンポーネントはランダムにスレーブを選択して接続を試みます。スレーブが「ダウン」していることが判明した場合、別のスレーブを試します。いずれのスレーブも利用できない場合は、マスターに接続します。サーバー状態キャッシュを構成すると、「ダウン」したサーバーを記憶できるため、一定期間は再度試行されません。

情報: 上記の構成では、すべてのスレーブに対して10秒の接続タイムアウトが指定されています。つまり、スレーブに10秒以内に到達できない場合は、「ダウン」していると見なされます。このパラメータは、実際の環境に基づいて調整できます。

複数のマスターを複数のスレーブで構成することもできます。例:

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

    // common configuration for masters
    'masterConfig' => [
        'username' => 'master',
        'password' => '',
        'attributes' => [
            // use a smaller connection timeout
            PDO::ATTR_TIMEOUT => 10,
        ],
    ],

    // list of master configurations
    'masters' => [
        ['dsn' => 'dsn for master server 1'],
        ['dsn' => 'dsn for master server 2'],
    ],

    // common configuration for slaves
    'slaveConfig' => [
        'username' => 'slave',
        'password' => '',
        'attributes' => [
            // use a smaller connection timeout
            PDO::ATTR_TIMEOUT => 10,
        ],
    ],

    // list of slave configurations
    'slaves' => [
        ['dsn' => 'dsn for slave server 1'],
        ['dsn' => 'dsn for slave server 2'],
        ['dsn' => 'dsn for slave server 3'],
        ['dsn' => 'dsn for slave server 4'],
    ],
]

上記の設定では、2つのマスターと4つのスレーブを指定しています。Connectionコンポーネントは、スレーブ間と同様に、マスター間でもロードバランシングとフェイルオーバーをサポートしています。ただし、マスターが利用できない場合は例外がスローされる点が異なります。

注意: masters プロパティを使用して1つまたは複数のマスターを設定する場合、Connectionオブジェクト自体でデータベース接続を指定する他のすべてのプロパティ(例:dsnusernamepassword)は無視されます。

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

$db = Yii::$app->db;
// the transaction is started on the master connection
$transaction = $db->beginTransaction();

try {
    // both queries are performed against the master
    $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;
} catch(\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

スレーブ接続でトランザクションを開始したい場合は、次のように明示的に行う必要があります。

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

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

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

Yii::$app->db->enableSlavesfalseに設定して、すべてのクエリをマスター接続に送ることもできます。

データベーススキーマの操作

Yii DAOは、新しいテーブルの作成、テーブルからの列の削除など、データベーススキーマを操作するための一連のメソッドを提供します。これらのメソッドは次のとおりです。

これらのメソッドは次のように使用できます。

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

上記配列は、作成する列の名前と型を記述しています。列の型については、Yiiは抽象データ型のセットを提供しており、これによりデータベースに依存しないスキーマを定義できます。これらは、テーブルが作成されるデータベースに応じて、DBMS固有の型定義に変換されます。詳細については、createTable()メソッドのAPIドキュメントを参照してください。

データベーススキーマを変更するだけでなく、DB接続のgetTableSchema()メソッドを使用して、テーブルの定義情報を取得することもできます。例えば、

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

このメソッドは、テーブルの列、プライマリキー、外部キーなどの情報を含むyii\db\TableSchemaオブジェクトを返します。この情報は主に、データベースに依存しないコードの記述を支援するために、クエリビルダーアクティブレコードによって利用されます。

誤字を発見しましたか?またはこのページを改善する必要があると思いますか?
githubで編集 !