オープンソースで構築する Web フォーラム (2)
オープンソースで構築する Web フォーラム (1) の続きです。
前回「Subject Prefix にシーケンス番号を追加したいが、どうしよう?」でした。
普通に考えると、シーケンス番号を取得するためのテーブルを用意して、SQL で取得すれば良いのでは?と考えます。
じゃあ、先ず何から調べれば良いのか、全てはソースコードの中に〜♪。
M2F が使用するテーブル一覧を調べてみると、"_seq" の suffix が付くテーブルが幾つかあります。例えば、m2f_message_hash_seq テーブル情報を表示させてみると、id カラムのみ、且つ auto_increment と言うオプションが設定されています。このテーブルはシーケンス番号を取得するために使っているテーブルと見て間違いなさそうです。M2F のソースコードをお手本に、このテーブルからシーケンス番号を取得している処理を参考にすれば、実現できそうな見通しが出てきます。
mysql> desc m2f_message_hash_seq;
+-------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | | PRI | NULL | auto_increment |
+-------+------------------+------+-----+---------+----------------+
1 row in set (0.01 sec)
# less -N m2f_mailinglist.php
...
216 $id = $m2f_db->nextId(_M2F_MESSAGE_HASH_TABLE_, TRUE);
...
m2f_mailinglist.php の中で、 nextId() 関数を用いて、シーケンス番号を取得しています。何となく、この nextId() 関数を使えば良いのだろうなと推測できますが、もう少し掘り下げて追いかけてみると、もっと色んなことが分かってきます。nextId() 関数は、M2F で定義されている関数ではなく、PEAR(PHP Extension and Application Repository) と呼ばれる php の拡張ライブラリで提供されています。ML21/30 の場合は php パッケージに、ML40/AXS3 の場合は php-pear パッケージに含まれています。
(ML40の場合)
# rpm -q php-pear
php-pear-5.0.5-8.15AX
PEAR を使用するために、システムのインクルードパスを調べます。(実際は、デフォルトで M2F にバンドルされている PEAR ライブラリを使用するのですが、PEAR/DB クラスライブラリの中身は同じなので、ここでは、/usr/share/pear 配下のファイルを見てみます)
# php -i | grep include_path
include_path => .:/usr/share/pear => .:/usr/share/pear
m2f_common.php を見ると、103行目の require_once で DB.php を読み込んでいます。118行目の DB::connect でデータベースへ接続します。引数の $type_db に "mysql" がセットされ、DB.php の535行目において、@include_once で DB/mysql.php が読み込まれます。/usr/share/pear/DB 配下を見れば分かりますが、各々の DB 用のライブラリが用意されています。
# less -N m2f_common.php
...
61 // phpBB uses mysql4 dbms when necessary, but PEAR makes no distinction in the db type
62 switch ($type_db) {
63 case 'mysql4':
64 case 'mysql':
65 $type_db = 'mysql';
66 require_once(M2F_ROOT_PATH."db/m2f_schema_mysql.php"); // include schema functions for this db
67 break ;
68
69 case 'postgres':
70 $type_db = 'pgsql';
71 require_once(M2F_ROOT_PATH."db/m2f_schema_pgsql.php"); // include schema functions for this db
72 break;
73 }
...
103 require_once('DB.php'); // PEAR DB layer
...
...
117 // Instatiate database reusable object
118 $m2f_db = DB::connect( "$type_db://$user_db:$pass_db@$host_db/$name_db" );
...
# less -N /usr/share/pear/DB.php
...
518 function &connect($dsn, $options = array())
519 {
520 $dsninfo = DB::parseDSN($dsn);
521 $type = $dsninfo['phptype'];
522
523 if (!is_array($options)) {
524 /*
525 * For backwards compatibility. $options used to be boolean,
526 * indicating whether the connection should be persistent.
527 */
528 $options = array('persistent' => $options);
529 }
530
531 if (isset($options['debug']) && $options['debug'] >= 2) {
532 // expose php errors with sufficient debug level
533 include_once "DB/${type}.php";
534 } else {
535 @include_once "DB/${type}.php";
536 }
...
# ls /usr/share/pear/DB
common.php fbsql.php ifx.php mssql.php mysqli.php odbc.php sqlite.php sybase.php
dbase.php ibase.php msql.php mysql.php oci8.php pgsql.php storage.php
DB.php で DB タイプを判別し、各々の DB 毎にライブラリが実装されているため、ユーザは使用する DB タイプを意識することなく(phpBB2/M2F では postgresql と mysql をサポート)、コーディングすることができます。ここで nextId() 関数は、DB/mysql.php を見れば良いことが分かりました。この辺まで来ると、当初の目的を放り出して、興味本位でコードを追いかけてみたくなってきますよね。
いよいよ nextId() 関数に辿り着きました。この関数を使用する上で2つ要点があります。
1点は、587〜588行目の UPDATE 文で id をインクリメントした後、mysql_insert_id() 関数で直近に生成された id 、つまり、直前に UPDATE 文でインクリメントした id を取得しています。従って、もし、手動でシーケンス番号をリセットした場合、リセットした値 +1 のシーケンス番号が取得されることが分かります。
もう1点は、第1引数で指定したテーブルが存在しない、且つ第2引数のブール値に "TRUE" をセットした場合、627行目の createSequence() 関数を呼び、新規にテーブルを作成していることが分かります。つまり、nextId() 関数を呼び出す際、シーケンス番号用のテーブルの存在有無を意識しなくて良いことも分かります。
他にもこんな風にエラー制御しているんだなと参考になります。
- mysql におけるシーケンス番号の管理は LAST_INSERT_ID を使えば良さそう
- テーブルにデータが存在しない場合、ロックを取得して REPLACE 文で挿入している
- getCode() 関数を使えば、PEAR/DB のデバッグに使えそう
# less -N /usr/share/pear/DB/mysql.php
...
568 /**
569 * Returns the next free id in a sequence
570 *
571 * @param string $seq_name name of the sequence
572 * @param boolean $ondemand when true, the seqence is automatically
573 * created if it does not exist
574 *
575 * @return int the next id number in the sequence.
576 * A DB_Error object on failure.
577 *
578 * @see DB_common::nextID(), DB_common::getSequenceName(),
579 * DB_mysql::createSequence(), DB_mysql::dropSequence()
580 */
581 function nextId($seq_name, $ondemand = true)
582 {
583 $seqname = $this->getSequenceName($seq_name);
584 do {
585 $repeat = 0;
586 $this->pushErrorHandling(PEAR_ERROR_RETURN);
587 $result = $this->query("UPDATE ${seqname} ".
588 'SET id=LAST_INSERT_ID(id+1)');
589 $this->popErrorHandling();
590 if ($result === DB_OK) {
591 // COMMON CASE
592 $id = @mysql_insert_id($this->connection);
593 if ($id != 0) {
594 return $id;
595 }
596 // EMPTY SEQ TABLE
597 // Sequence table must be empty for some reason, so fill
598 // it and return 1 and obtain a user-level lock
599 $result = $this->getOne("SELECT GET_LOCK('${seqname}_lock',10)");
600 if (DB::isError($result)) {
601 return $this->raiseError($result);
602 }
603 if ($result == 0) {
604 // Failed to get the lock
605 return $this->mysqlRaiseError(DB_ERROR_NOT_LOCKED);
606 }
607
608 // add the default value
609 $result = $this->query("REPLACE INTO ${seqname} (id) VALUES (0)");
610 if (DB::isError($result)) {
611 return $this->raiseError($result);
612 }
613
614 // Release the lock
615 $result = $this->getOne('SELECT RELEASE_LOCK('
616 . "'${seqname}_lock')");
617 if (DB::isError($result)) {
618 return $this->raiseError($result);
619 }
620 // We know what the result will be, so no need to try again
621 return 1;
622
623 } elseif ($ondemand && DB::isError($result) &&
624 $result->getCode() == DB_ERROR_NOSUCHTABLE)
625 {
626 // ONDEMAND TABLE CREATION
627 $result = $this->createSequence($seq_name);
628 if (DB::isError($result)) {
629 return $this->raiseError($result);
630 } else {
631 $repeat = 1;
632 }
633
634 } elseif (DB::isError($result) &&
635 $result->getCode() == DB_ERROR_ALREADY_EXISTS)
636 {
637 // BACKWARDS COMPAT
638 // see _BCsequence() comment
639 $result = $this->_BCsequence($seqname);
640 if (DB::isError($result)) {
641 return $this->raiseError($result);
642 }
643 $repeat = 1;
644 }
645 } while ($repeat);
646
647 return $this->raiseError($result);
648 }
...
PEAR とよく似た名前の PECL(PHP Extension Community Library) と呼ばれる php の拡張モジュールがあります。大雑把な理解ですが、PEAR は php で書かれた拡張ライブラリで、PECL は C 言語で書かれた拡張モジュール(バイナリ)です。PEAR/DB の機能を PECL で提供しようという試みに PDO(PHP Data Objects) と言うのがあります。メリットは、PEAR/DB と比較してパフォーマンスが優れている点です。
例えば、DB/mysql.php の592行目で mysql_insert_id によって、id を取得していますが、この関数は標準で提供されている拡張モジュールに相当します。
# readelf -a /usr/lib64/php/modules/mysql.so | grep mysql_insert_id
00000010bbe8 001d00000001 R_X86_64_64 0000000000008010 zif_mysql_insert_id + 0
00000010b6d0 002600000007 R_X86_64_JUMP_SLO 0000000000000000 mysql_insert_id + 0
29: 0000000000008010 193 FUNC GLOBAL DEFAULT 10 zif_mysql_insert_id
38: 0000000000000000 15 FUNC GLOBAL DEFAULT UND mysql_insert_id
最後に試験的に作成したパッチです。[xxx:00001] のように、Subject Prefix の両端を "[", "]" で括り、5桁のシーケンス番号を追加するようにしてみました。本当にきちんと対応しようとすると、設定画面からシーケンス番号の桁数や有無を指定したり、メーリングリストの削除時にそのテーブルを削除する処理も必要になります。nextId() 関数を使うことで DB とのやり取りやエラー制御の処理が省けて、ほんの数行のパッチで済みました。ライブラリを使用することによって、随分、簡単にできてしまうんだなと感心してしまった次第です。また PEAR/DB から php のコーディングやエラー制御の方法も学ぶことができました。コードを読んで、学んで、改変してと言うのがオープンソースゆえの面白さだなと改めて実感しました。
# diff -uNr m2f_mailinglist.php.orig m2f_mailinglist.php
--- m2f_mailinglist.php.orig 2007-12-05 21:02:26.000000000 +0900
+++ m2f_mailinglist.php 2008-01-18 19:03:20.000000000 +0900
@@ -361,7 +362,12 @@
if ('' !== $m2f_subject_prefix)
{
$mail_msg->headers['Subject'] = str_replace($m2f_subject_prefix . ' ' , '' , $mail_msg->headers['Subject']);
- $mail_msg->headers['Subject'] = $m2f_subject_prefix . ' ' . $mail_msg->headers['Subject'];
+ $m2f_subject_prefix_no_ends = preg_match("[\w]", substr($m2f_subject_prefix, 0, 1) . substr($m2f_subject_prefix, -1))
+ ? $m2f_subject_prefix : substr($m2f_subject_prefix, 1, -1);
+ $seq_table_name = _M2F_MAILINGLIST_TABLE_ . '_' . $m2f_subject_prefix_no_ends;
+ $seq_num = sprintf('%05d', $m2f_db->nextId($seq_table_name, TRUE));
+ $m2f_subject_prefix_with_seq = '[' . $m2f_subject_prefix_no_ends . ':' . $seq_num . ']';
+ $mail_msg->headers['Subject'] = $m2f_subject_prefix_with_seq . ' ' . $mail_msg->headers['Subject'];
}if ($edited === TRUE)
今回の環境、並びに phpBB + M2F のインストール方法を次回に書きます。
順序が逆かな(p_-)o




コメント