CodeIgniterのソースを読んでみる
久しぶりのCodeIgniter関連エントリです。
以前から気になっていたのですが、データベースのトランザクションを管理するtrans_start()メソッドがうまく動いていないようです。
transt_start()メソッドについては、ユーザーガイドにて以下のように解説されています。
トランザクションの有効化
$this->db->trans_start() を使った瞬間にトランザクションが自動的に有効になります。トランザクションを無効にしたい場合は $this->db->trans_off() を実行することで無効化できます
(中略)
トランザクション機能が無効になっている場合、トランザクションなしにクエリが実行されたときのように、クエリは自動的にコミットされます。
テストモード
オプションで、クエリが正しい結果になった場合でもロールバックされる “テストモード” のトランザクションを実行することができます。テストモードを使うには、$this->db->trans_start() メソッドの第1引数にTRUEを設定するだけです
うまく動いていないのは上記の「テストモード」というやつで、メソッドの引数にTRUEを指定しても見事にコミットされてしまうようです><
まずは、簡単なテーブルとコードで実験してみました。MySQLで試しましたので、エンジンはInnoDBを指定しました。
mysql> CREATE TABLE item(
-> Id INTEGER NOT NULL PRIMARY KEY,
-> Name VARCHAR(64) NOT NULL,
-> Price INTEGER UNSIGNED NOT NULL
-> ) ENGINE = INNODB DEFAULT CHARSET utf8;
ユーザーガイドのサンプルコードそのままに、テストモードでtrans_start()メソッドを呼び出し、テーブルにレコードを追加するSQLを実行した後trans_complete()メソッドでトランザクションを終了してみます。
<php if (! defined('BASEPATH')) exit('No direct script access allowed');
class Test extends Controller
{
public function __construct()
{
parent::Controller();
}
public function index()
{
$this->load->database();
$this->db->trans_start(TRUE);
$sql = "INSERT INTO item VALUES (1, 'keyboard', 1500)";
$this->db->query($sql);
$this->db->trans_complete();
}
}
/**
* End of file test.php
*/
/**
* Location: ./system/application/controllers/test.php
*/
先日リリースされた最新版(1.7.2)の「日本語言語パック」をテスト環境の「ci」ディレクトリに配置し、初期設定・データベースへの接続設定等を行いましたので、http://localhost/ci/testにアクセスしてみます。
トランザクションをテストモードにしているので、ロールバックされてテーブルにはレコードが追加されていないはずなのですが、のぞいてみるとしっかり登録されちゃっています。
mysql> SELECT * FROM item; +----+----------+-------+ | Id | Name | Price | +----+----------+-------+ | 1 | keyboard | 1500 | +----+----------+-------+ 1 row in set (0.00 sec)
単純にバグなのかなぁ??と思いましたので、本家のフォーラムに情報が無いか検索してみました。すると、以下のような情報が見つかりました。
http://codeigniter.com/bug_tracker/bug/4339/
英語でのやり取りを拙いなりにも読み解いてみると、報告された方が添付されたコードは取り込まれたとの事なのですが、どうもそのコードが見当たりません。単純に上記のURLにあるコードをtrans_complete()メソッドに追記すれば動くようになりましたが、折角なので学習を兼ねてCodeIgniterのコアのソースを読んでみようと思います。
まずは、trans_start()での引数がどのように扱われているのか見てみます。メソッドの最後でtrans_begin()というメソッドを呼び、そのまま引数を渡しています。
[./system/database/DB_driver.php]
function trans_start($test_mode = FALSE)
{
if ( ! $this->trans_enabled)
{
return FALSE;
}
// When transactions are nested we only begin/commit/rollback the outermost ones
if ($this->_trans_depth > 0)
{
$this->_trans_depth += 1;
return;
}
$this->trans_begin($test_mode);
}
では、このtrans_begin()メソッドはどこで定義されているのでしょう??grepで検索したところ、このメソッドは各DBドライバの処理を記述している「(DB名)_driver.php」にありました。
$ grep -nr "function trans_begin" ./ ./system/database/drivers/mssql/mssql_driver.php:170: function trans_begin($test_mode = FALSE) ./system/database/drivers/mysql/mysql_driver.php:200: function trans_begin($test_mode = FALSE) ./system/database/drivers/mysqli/mysqli_driver.php:201: function trans_begin($test_mode = FALSE) ./system/database/drivers/oci8/oci8_driver.php:310: function trans_begin($test_mode = FALSE) ./system/database/drivers/odbc/odbc_driver.php:180: function trans_begin($test_mode = FALSE) ./system/database/drivers/sqlite/sqlite_driver.php:197: function trans_begin($test_mode = FALSE) ./system/database/drivers/postgre/postgre_driver.php:203: function trans_begin($test_mode = FALSE)
最近PostgreSQLを勉強中ですが、ここではいつも利用しているMySQLiドライバに限定して覗いていってみます。
「./system/database/drivers/mysqli/mysqli_driver.php」にあるtrans_begin()メソッドの定義を見てみると、渡ってきた引数を_trans_failureプロパティにセットしています。
function trans_begin($test_mode = FALSE)
{
if ( ! $this->trans_enabled)
{
return TRUE;
}
// When transactions are nested we only begin/commit/rollback the outermost ones
if ($this->_trans_depth > 0)
{
return TRUE;
}
// Reset the transaction failure flag.
// If the $test_mode flag is set to TRUE transactions will be rolled back
// even if the queries produce a successful result.
$this->_trans_failure = ($test_mode === TRUE) ? TRUE : FALSE;
$this->simple_query('SET AUTOCOMMIT=0');
$this->simple_query('START TRANSACTION'); // can also be BEGIN or BEGIN WORK
return TRUE;
}
この辺りで、トランザクションの完了時にこのプロパティの値を見て処理を切り分けているのかな??と想像出来ますね。では、_trans_failureはどこに定義されているのかというと…
これがどこにも定義されていません><
先ほどと同様にgrepで検索してみても、各ドライバ関連のソース内でセットしているだけで、この値を参照して何か処理を行っている感じもありませんので、それが理由でトランザクションがコミットされてしまう、という事でしょうか??
$ grep -nr _trans_failure ./ ./system/database/drivers/mssql/mssql_driver.php:186: $this->_trans_failure = ($test_mode === TRUE) ? TRUE : FALSE; ./system/database/drivers/mysql/mysql_driver.php:216: $this->_trans_failure = ($test_mode === TRUE) ? TRUE : FALSE; ./system/database/drivers/mysqli/mysqli_driver.php:217: $this->_trans_failure = ($test_mode === TRUE) ? TRUE : FALSE; ./system/database/drivers/oci8/oci8_driver.php:326: $this->_trans_failure = ($test_mode === TRUE) ? TRUE : FALSE; ./system/database/drivers/odbc/odbc_driver.php:196: $this->_trans_failure = ($test_mode === TRUE) ? TRUE : FALSE; ./system/database/drivers/sqlite/sqlite_driver.php:213: $this->_trans_failure = ($test_mode === TRUE) ? TRUE : FALSE; ./system/database/drivers/postgre/postgre_driver.php:219: $this->_trans_failure = ($test_mode === TRUE) ? TRUE : FALSE;
ここまで来ると、フォーラムで投稿されているコードの意味も分かってきますね。推測どおりトランザクションの完了時に_trans_failureの値を参照し、テストモードであればロールバックする、という処理になっています。
A hack is required. Insert into /system/database/DB_driver.php line 508 the next code:
// hack
if ($this->_trans_failure === TRUE) {
$this->trans_rollback();
log_message('debug', 'Database - Rollback Test Mode execution');
$this->_trans_failure = FALSE;
return TRUE;
}
ですので、この処理をtrans_complete()メソッド内(527行目あたり)に追記すれば、無事テストモードが動くようになる、という事ですね!!
確認の為、先ほどのテーブルからレコードを削除し、再度URLにアクセスしてみます。その後テーブルを見てみると…
mysql> SELECT * FROM item; Empty set (0.00 sec)
テストモードがちゃんと動作している事が確認出来ました^^
最後に、定義されていないプロパティにアクセスするのはどうにも落ち着かないので、プロパティを追加してパッチファイルの作成にも挑戦してみました。
--- DB_driver.php.org 2009-10-12 21:16:20.846146038 +0900
+++ DB_driver.php.new 2009-10-12 21:22:07.018146829 +0900
@@ -70,6 +70,7 @@
var $curs_id;
var $limit_used;
+ var $_trans_failure = FALSE;
/**
@@ -524,7 +525,19 @@
$this->_trans_depth -= 1;
return TRUE;
}
-
+
+ // hack
+ if ($this->_trans_failure === TRUE)
+ {
+ $this->trans_rollback();
+
+ log_message('debug', 'Database - Rollback Test Mode execution');
+
+ $this->_trans_failure = FALSE;
+
+ return TRUE;
+ }
+
// The query() function will set this flag to FALSE in the event that a query failed
if ($this->_trans_status === FALSE)
{
@@ -1363,4 +1376,4 @@
/* End of file DB_driver.php */
-/* Location: ./system/database/DB_driver.php */
\ ファイル末尾に改行がありません
+/* Location: ./system/database/DB_driver.php */
相も変わらず、これで良いのかどうかサッパリ分からないのが残念ではありますが、「ソースを読む」という勉強が出来たので良しとしてしまいます。間違っていれば、詳しい方からのツッコミが入るでしょう(そんな方々がこのブログを読んでいるのかどうかは別の問題)
CodeIgniterはシンプルでソースも分かりやすいので、こういった学習にもってこいですね!!
今日はここまで。
