Hatena::ブログ(Diary)

風柳メモ このページをアンテナに追加 RSSフィード Twitter

2014-10-01

連結リストになっているテーブルの関連するレコードのみをSELECTするためのSQL文を知りたい

たとえば、MySQL上で次のようなテーブルを定義して、

CREATE TABLE `t_history` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `memo` text NOT NULL,
  `next_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

次のようにレコードがセットされているときに、

mysql> SELECT * FROM t_history ORDER BY id;
+----+-----------------+---------+
| id | memo            | next_id |
+----+-----------------+---------+
|  1 | A               |       4 |
|  2 | B               |       7 |
|  3 | C               |       6 |
|  4 | A 変更その1     |       5 |
|  5 | A 変更その2     |       8 |
|  6 | C 変更その1     |    NULL |
|  7 | B 変更その1     |       9 |
|  8 | A 変更その3     |    NULL |
|  9 | B 変更その2     |    NULL |
+----+-----------------+---------+

id=8(A の変更の最新: next_id = NULL)を指定したら、

+----+-----------------+---------+
| id | memo            | next_id |
+----+-----------------+---------+
|  1 | A               |       4 |
|  4 | A 変更その1     |       5 |
|  5 | A 変更その2     |       8 |
|  8 | A 変更その3     |    NULL |
+----+-----------------+---------+

のような結果(A の履歴のみを抽出)を取得したいのだけれど、これを一発で取り出せるような SQL 文はないだろうか…?


うーん…やっぱり連結リストにするより、たとえば

CREATE TABLE `t_history` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `memo` text NOT NULL,
  `root_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

みたいな構造にして、

mysql> SELECT * FROM t_history ORDER BY id;
+----+-----------------+---------+
| id | memo            | root_id |
+----+-----------------+---------+
|  1 | A               |    NULL |
|  2 | B               |    NULL |
|  3 | C               |    NULL |
|  4 | A 変更その1     |       1 |
|  5 | A 変更その2     |       1 |
|  6 | C 変更その1     |       3 |
|  7 | B 変更その1     |       2 |
|  8 | A 変更その3     |       1 |
|  9 | B 変更その2     |       2 |
+----+-----------------+---------+

な感じにレコード挿入していって、

mysql> SELECT * FROM t_history WHERE id = 1 OR root_id = 1 ORDER BY id;
+----+-----------------+---------+
| id | memo            | root_id |
+----+-----------------+---------+
|  1 | A               |    NULL |
|  4 | A 変更その1     |       1 |
|  5 | A 変更その2     |       1 |
|  8 | A 変更その3     |       1 |
+----+-----------------+---------+

で履歴を取得、みたいな方が素直かなぁ…。

これなら途中のレコードを消しても、連結リストみたいに繋げなおす必要もないし。

2014-09-24

PHPのリファレンス(参照)について、自分なりにかみくだいてみる

経緯

最近、PDO で PDOStatement::bindParam を使う処理ではまったため。


bindParam()は、

public bool PDOStatement::bindParam ( mixed $parameter , mixed &$variable [, int $data_type = PDO::PARAM_STR [, int $length [, mixed $driver_options ] )

PHP: PDOStatement::bindParam - Manual

第二引数($variable)が、SQL ステートメントパラメータにバインドする変数名になるので、当然ながらリファレンス(参照)渡しになっている。


それで、はまったときのコードは、

<?php
// (略) ※この部分にデータベースハンドル($dbh)取得処理等

$stmt = $dbh->prepare("SELECT * FROM table_sample WHERE id = :id");
$stmt->bindParam(':id', $id, PDO::PARAM_INT);

$id_list = array(1,2,3); // 行が存在する id のリスト
foreach ($id_list as &$id) { // ←【問題個所】 &$id を $id に書き換えることで、正常動作するようになる
    $stmt->execute();
    $row = $stmt->fetch(PDO::FETCH_ASSOC); // bool(false) となり、取得できない
    // 以下略
}

のようなもの。


変数 $id にバインドし、foreach でこれを次々と変更しながら実行する、という意図だったのだが、foreach で参照渡しにしていると、動作しない。


参照渡しを止めれば動作するようにはなるのだが、「どうしてなのか?」をきちんと説明できなかったので、そもそもPHPのリファレンスはどういう仕様なのか、というところから調べてみた次第。


さて、PHP におけるリファレンスとは?

まずは、マニュアルを参照。

PHP において、リファレンスとは同じ変数の内容を異なった名前で コールすることを意味します。これは C のポインタとは異なります。 リファレンスを使ってポインタの演算をすることはできませんし、 リファレンスは実メモリのアドレスでもありません。詳細は リファレンスが行わないこと を参照ください。 そうではなく、リファレンスはシンボルテーブルのエイリアスです。 PHP では、変数名と変数の内容は異なっており、 このため、同じ内容は異なった複数の名前を有する事が可能であることに 注意してください。最も良く似ているのは、Unix のファイル名とファイルの 関係です。この場合、変数名はディレクトリエントリ、変数の内容は ファイル自体に対応します。リファレンスは、Unix ファイルシステムの ハードリンクのようなものであると考えられます。

PHP: リファレンスとは? - Manual

むぅ、わかるようなわからないような…やっぱり、いまひとつピンとこない。

なまじ、C言語のポインタを知っているから、混乱しているのだろうか。


具体的に違和感を覚えていたポイント

以下のような PHP プログラムについて考えてみた。

<?php
$no = 1;
function    prn(&$a, &$b, &$c) {
    global $no;
    echo("({$no}) \$a={$a} \$b={$b} \$c={$c}\n");
    $no++;
}
          prn($a, $b, $c);  //  (1) $a= $b= $c=
$b = &$a; prn($a, $b, $c);  //  (2) $a= $b= $c=
$a = 'A'; prn($a, $b, $c);  //  (3) $a=A $b=A $c=
$b = 'B'; prn($a, $b, $c);  //  (4) $a=B $b=B $c=
$c = 'C'; prn($a, $b, $c);  //  (5) $a=B $b=B $c=C
$a = &$c; prn($a, $b, $c);  //  (6) $a=C $b=B $c=C
$a = "X"; prn($a, $b, $c);  //  (7) $a=X $b=B $c=X
$b = "Y"; prn($a, $b, $c);  //  (8) $a=X $b=Y $c=X
$c = "Z"; prn($a, $b, $c);  //  (9) $a=Z $b=Y $c=Z
$a = $b ; prn($a, $b, $c);  //  (10) $a=Y $b=Y $c=Y
$b = "O"; prn($a, $b, $c);  //  (11) $a=Y $b=O $c=Y
$c = $b ; prn($a, $b, $c);  //  (12) $a=O $b=O $c=O

違和感を覚えたのは、

  • (2) で、「$b = &$a」としてリファレンス代入を行い、その後は $a に値を代入すると $b にも反映されるようになっている(3)。
  • ところが、(6) で「$a = &$c」として、$a に対してリファレンス代入を行うと、$b に対しては直接何もしていないにも関わらず、以降は $a を変更しても、$b には反映されなくなってしまう(7)。

というところ。

直感的に「$a にリファレンス代入等の操作を加えたとしても、$b は影響されずに $a を指し示したままであり、$a の内容を書き換えると、そのまま $b にも反映される」ものとばかり思っていた。


自分なりのリファレンス代入の解釈

マニュアルを読み返すなどして、ようやく理解できた(かも)。

上述のプログラムの、動作概要を図に示すと、

http://f.st-hatena.com/images/fotolife/f/furyu-tei/20140924/20140924114654_original.png

のようになると思われる。

ただし、上記プログラム内では表示(echo)をしている関係上、まだ割り当てていない変数も参照され、このときにNULLが割り当てられてしまうので、図とは厳密には異なってくる。


ポイントとしては、

  • (2) で、$a が初めて参照されているが、変数が初めて参照される際には、新規に値"NULL"(もしくは、通常の代入の場合は右辺の値)が入った内容が確保され、変数(図では$a)は当該内容を示すシンボル(同s1)を保持する。
  • (2) の「$b = &$a」というリファレンス代入では、変数 $a の内容を示すシンボル(同s1)を、変数 $b にコピーしている。
    結果として、$a と $b は、同じ内容を指し示すシンボル(s1)*1を持った、完全に等価な変数となる。
  • (3) の「$a = "A"」では、$a が持つシンボル(s1)が示す内容の値を書き換える(NULL→"A")。すると、同じs1を保持する $b も同じ内容を持つことになる。
    また、(4) の「$b = "B"」では、逆に $b の値を書き換えることで、同じs1を保持する $a も同じ内容になる。
    すなわち、この時点での $a と $b とは、実際にまったくの等価であることを示している。
  • (6) の「$a = &$c」というリファレンス代入では、変数 $c の内容を示すシンボル(同s2)を、変数 $a にコピーしている。
    結果として、$c と $a は、同じ内容を指し示すシンボル(s1)を持った、完全に等価な変数となる。
    代わりに、$a(シンボル:s2) と $b(シンボル:s1) とでは、指し示す内容が異なることになり、違う値を示すようになる。

C言語畑の人向けに解説

C言語に慣れている人用に、PHP のリファレンス代入を、疑似的にC言語で表現してみた。

/*
 * PHPの変数代入とリファレンス代入の動作をC言語に置き換えるサンプル
 */

/*
<?php
function prn(&$a, &$b, &$c) {echo("\$a={$a} \$b={$b} \$c={$c}\n");}

$b = &$a;
$a = "A";
$b = "B";
$c = "C"; prn($a, $b, $c);
$a = &$c; prn($a, $b, $c);
$a = "X"; prn($a, $b, $c);
$b = "Y"; prn($a, $b, $c);
$c = "Z"; prn($a, $b, $c);
$a = $b ; prn($a, $b, $c);
$b = "O"; prn($a, $b, $c);
$c = $b ; prn($a, $b, $c);
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFER_SIZE 1024

void    prn(char * a, char * b, char * c) {printf("$a=%s $b=%s $c=%s\n", a, b, c);}

int     main(void) {
    char * a, * b, * c;
    char * s1, * s2;

    /* $b = &$a; */ s1 = calloc(BUFFER_SIZE, 1); a = s1; b = a;
    /* $a = "A"; */ strcpy(a, "A"); // $a = "A";
    /* $b = "B"; */ strcpy(b, "B"); // $b = "B";
    /* $c = "C"; */ s2 = calloc(BUFFER_SIZE, 1); c = s2; strcpy(c, "C"); prn(a, b, c);
    /* $a = &$c; */ a = c;                                               prn(a, b, c);
    /* $a = "X"; */ strcpy(a, "X");                                      prn(a, b, c);
    /* $b = "Y"; */ strcpy(b, "Y");                                      prn(a, b, c);
    /* $c = "Z"; */ strcpy(c, "Z");                                      prn(a, b, c);
    /* $a = $b ; */ strcpy(a, b);                                        prn(a, b, c);
    /* $b = "O"; */ strcpy(b, "O");                                      prn(a, b, c);
    /* $c = $b;  */ strcpy(c, b);                                        prn(a, b, c);

    return 0;
}

これを実行すると、

$a=B $b=B $c=C
$a=C $b=B $c=C
$a=X $b=B $c=X
$a=X $b=Y $c=X
$a=Z $b=Y $c=Z
$a=Y $b=Y $c=Y
$a=Y $b=O $c=Y
$a=O $b=O $c=O

こんな感じで、PHP と同様の結果になる。


ポイントとしては、PHP における変数 $a・$b に該当するものを、C言語におけるポインタ変数(char *) a・b とみなしたとき、

  • PHP の値代入($a = "A")は、C言語では strcpy(a, "A") に相当。
    ※このとき、ポインタ変数 a の値は変わらない。
  • PHP のリファレンス代入($b = &$a)は、C言語では b = a に相当。
    ※このとき、ポインタ変数 b の値が、ポインタ a のものに置き換わる(結果として、指し示す先が直前とは異なってくる)。

ということ。


C言語では、

b = &a;

のように記述する場合、b はポインタ(例えば int *型)であり、変数 a は実体(例えば int型)で、'&a' は a のアドレスを指す。つまり、変数 a と b とでは、そもそもの型が異なっている。


一方で、PHP では、

$b = &$a;

のように記述すると、これはリファレンス代入であり、(変数が内部的にもっている)内容を指し示すシンボルのコピーという意味あいであり、変数 $a と $b とは、本質的には等価である。


なので、('&' という記号だけをみて)上記を混同するとはまってしまう。

どちらかといえば、PHP におけるリファレンス代入は、C言語における「(変数等の)アドレスの、ポインタ変数への代入」よりも、「ポインタ変数からポインタ変数への代入」にイメージとしては近いと考えられる。


最初の問題については?

最初に書いた、bindParam() がらみの不具合だが、foreach ループの 1 回目で、

$id = &$id_list[0];

と等価になるが、これでは $id のシンボルが $id_list[0] のシンボルと同じものへと置換されてしまうため、その前の bindParam() 実行時点で指定した $id の保持していたシンボル(=PDOStatementオブジェクト($stmt)内部で保持している変数のシンボル)と異なってしまい、結果として、SQL ステートメントパラメータ(':id') には、bindParam()実行時点での $id の値(=NULL) が使われてしまうので、意図した動作にならなかった、と考えられる。


リファレンスの罠(追記:2014/09/24)

「配列内部のリファレンスは危険もある」、と。

しかし、配列の内部のリファレンスは危険もあるということに気をつけましょう。 通常の (リファレンスではない) 代入の右辺にリファレンスを使っても 左辺はリファレンスには変わりませんが、配列の内部のリファレンスは通常の代入のままとなります。 これは、関数をコールする際に配列をリファレンスで渡すときも同じです。

PHP: リファレンスが行うことは何ですか? - Manual

こ、これはわかりにくい上に、はまりそうだ…。

リファレンスを使用しない場合

単純に、配列(array)$1 を $2 に代入する場合。

<?php
$a1 = array(1);
$a2 = $a1;
$a2[0] ++; // $a1 == array(1), $a2 == array(2)
$a2[0] ++; // $a1 == array(1), $a2 == array(3)

http://f.st-hatena.com/images/fotolife/f/furyu-tei/20140924/20140924182500_original.png

まぁ、これは特におかしいことはない。

問題は次のようなケース。


配列内の要素をリファレンスする場合
<?php
$a1 = array(1);
$b = &$a1[0]; // $a1[0] を $b にリファレンス代入: $a1[0] と $b は同一リファレンスセットになる(共通のシンボルを持つ)
$a2 = $a1;
$a2[0] ++; // $a1 == array(2), $a2 == array(2) ← $a1[0] の方も変化してしまっている!
unset($b); // ← 後から $b を削除しても…
$a2[0] ++; // $a1 == array(3), $a2 == array(3) ← やっぱり $a1[0] も変化してしまう!

http://f.st-hatena.com/images/fotolife/f/furyu-tei/20140924/20140924182945_original.png

よくわからないが、

  • 配列内要素は、配列のコンテナとは独立して、各々がリファレンスセット(同一のシンボルを持つ、他の変数)を持ちうる。
  • 配列内要素が、他の変数との間でリファレンスセットを構成した後で、当該配列を別の変数に代入すると(リファレンス代入ではない)、当該要素のみが元の要素・変数と同一のリファレンスセットとされてしまう。

言い換えると、配列内のリファレンスの挙動はその要素ごとに決まるということです。 個々の要素のリファレンスに関する動きは、 配列コンテナがリファレンスであるかどうかとは独立しています。

PHP: リファレンスが行うことは何ですか? - Manual

うん、よくわかりません…。

foreach でのリファレンスの罠

配列の各要素を順に処理して変更を加えたい時に、ついつい foreach でリファレンス渡しを使ってしまうのだけれど…。

<?php
$list = array(0, 0, 0);
foreach ($list as &$item) {
    $item ++;
}
// この時点では、$list == array(1, 1, 1) で想定通り
// :
// :しばらく処理が続いて…
// :
$item = 'string'; // 忘れたころに、何気なく $item に値を代入すると…
// $list == array(1, 1, 'string') ← 一番最後の要素が置換されてしまう!

このように、一時的にリファレンス代入用に使用していた変数をうっかり解放し忘れると、思わぬわかりにくいバグの元となってしまう。

foreach でリファレンス渡しを行った後は、

unset($item);

解放を忘れずに。


というわけで、結局のところ…

というわけで、参照渡しをカジュアルにやるのが間違いなのです。関数の戻り値の型の整合性がとれず、やむなく出力引数で表さなければならない場合などを除いて、基本的には使わない。使う意味がない。参照の仕様から来る複雑さに関しては、PHPが悪いというより、基礎を押さえずに用途を勘違いして使うほうが悪いと思います。PHPの変数の基礎を知っていれば、ほとんどの場合使わなくていいということが、おのずとわかると思います。

PHPが糞言語なのはどう考えても参照をポインタだと思っているお前らが悪い - なんたらノート第三期ベータ

PHPのリファレンスの仕様はややこしい(特にarrayに使用する場合)ので、極力使わなくて済むように他の方法を検討するべき、ということ、かな。

少なくとも、パフォーマンス的な理由でリファレンスを使用するメリットというのはなさそうではあるし。

うん、勉強になった!…としておこう(哀)。


この辺りの記事も参考に…

PHPは代入と参照の違い - bravewood の日記

PHPでは配列ではなくオブジェクトに状態を持たせよ - なんたらノート第三期ベータ

*1:なお、"シンボル"という呼び名は、この記事中での便宜上のものである。適切な呼称はなんだろう?

2014-09-22

レンタルサーバでlog4phpを使用するための覚書

log4php とは…

Apache Logging ServicesプロジェクトのPHP用・高機能なログフレームワーク(ログ出力ツール)。

Apache log4php™ is a versatile logging framework for PHP.

Apache log4php - Welcome - Apache log4php

出力先には画面やファイル等を指定でき、ログレベル等も使え、一定サイズでログを切り替えたりといった機能も持つ。


レンタルサーバへのインストール(PEAR経由)

PEARがインストールされていて、正常に使用できていることが前提。

log4php のインストール
  1. Channel Management で "pear.apache.org/log4php" を [Discover Channel]
  2. "pear.apache.org/log4php" チャンネルから、"Apache_log4php" をインストール

使用サンプル

log4php用設定ファイル(log4php.properties)
# log4php.properties: log4php用設定ファイル
# 参考: http://logging.apache.org/log4php/docs/configuration.html


# === アペンダ(appenders)定義
# log4php.appender.{appender_name}
# 参考: http://logging.apache.org/log4php/docs/appenders.html
#       http://logging.apache.org/log4php/docs/layouts/pattern.html

# --- 出力無し設定
log4php.appender.dev_null = LoggerAppenderNull

# --- 画面出力設定
log4php.appender.stdout = LoggerAppenderEcho
log4php.appender.stdout.layout = LoggerLayoutPattern
log4php.appender.stdout.layout.conversionPattern = "%date{Y-m-d H:i:s.u} %-14logger %-8level [%-15X{ADDR} %X{HOST}] %message%newline"

# --- ファイル出力設定(一定サイズでログを切替)
#   http://logging.apache.org/log4php/docs/appenders/rolling-file.html
log4php.appender.file = LoggerAppenderRollingFile
# ■ log4php.appender.{name}.file は絶対パスで指定する必要あり
#  参考: http://stackoverflow.com/questions/15666893/log4php-file-size-error
log4php.appender.file.file = /path_to/app.log
log4php.appender.file.append = true
log4php.appender.file.maxFileSize = 5MB
log4php.appender.file.maxBackupIndex = 5
log4php.appender.file.compress = false
log4php.appender.file.layout = LoggerLayoutPattern
log4php.appender.file.layout.conversionPattern = "%date{Y-m-d H:i:s.u} %-14logger %-8level [%-15X{ADDR} %X{HOST}] %message%newline"


# === ロガー(logger)定義
# 参考: http://logging.apache.org/log4php/docs/loggers.html

# --- root ロガー
# log4php.rootLogger = {log_level}, {appender_name}[, {appender_name} ...]
# ※ rootLogger は、全ての Logger::getLogger({logger_name}) の継承元となる
#   {log_level} → http://logging.apache.org/log4php/docs/introduction.html
#   {appender_name) → アペンダ定義で指定した名称(log4php.appender.{appender_name})
#   <?php $root_logger = Logger::getRootLogger(); // RootLogger 取得 ?>
log4php.rootLogger = FATAL, dev_null


# --- 名前付きロガー
# log4php.logger.{logger_name} = {log_level}, {appender_name}[, {appender_name} ...]
# ※ 設定名({logger_name}) をLogger::getLogger({logger_name})で指定すると、log4php.rootLogger の継承+指定した設定のログとなる
# ※ 本設定ファイル内で定義されていない{logger_name}を指定すると、root ロガーと等価になる
#   {logger_name} → Logger::getLogger({logger_name})で指定すると、log4php.rootLogger の継承+指定したアペンダのロガーが取得される
#   <?php $sample_logger = Logger::getLogger('sample'); // 名前を指定して Logger 取得 ?>
log4php.logger.DefaultLogger = WARN, file
log4php.logger.DebugLogger = DEBUG, stdout, file


注意点として、


サンプルPHPファイル(test_log.php)
<?php
require_once('log4php/Logger.php');

// === 設定ファイル読込
Logger::configure('log4php.properties');

// === MDC (_mapped diagnostic contexts_) 設定
// ※ log4php.appender.{appender_name}.layout.conversionPattern で、'%X{ADDR}' のようにして参照可能
// http://logging.apache.org/log4php/apidocs/class-LoggerMDC.html
LoggerMDC::put('ADDR', isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '-');
LoggerMDC::put('HOST', isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : '-');

// === ロガー取得
$default_logger = Logger::getLogger('DefaultLogger');
$debug_logger = Logger::getLogger('DebugLogger');
$separator_logger = Logger::getLogger('separator');
$separator_logger->addAppender($debug_logger->getAppender('stdout'));
$separator_logger->addAppender($debug_logger->getAppender('file'));
$separator_logger->setLevel(LoggerLevel::getLevelDebug());

// === テスト
$separator_logger->debug('1) DefaultLogger<br />');
$default_logger->trace('trace1<br />');
$default_logger->debug('debug1<br />');
$default_logger->info('info1<br />');
$default_logger->warn('warn1<br />');
$default_logger->error('error1<br />');
$default_logger->fatal('fatal1<br />');
$separator_logger->debug('==========<br />');

$separator_logger->debug('2) DebugLogger<br />');
$debug_logger->trace('trace2<br />');
$debug_logger->debug('debug2<br />');
$debug_logger->info('info2<br />');
$debug_logger->warn('warn2<br />');
$debug_logger->error('error2<br />');
$debug_logger->fatal('fatal2<br />');
$separator_logger->debug('==========<br />');

$separator_logger->debug('3) DefaultLogger (TRACE)<br />');
$default_logger->setLevel(LoggerLevel::getLevelTrace());
$default_logger->trace('trace3<br />');
$default_logger->debug('debug3<br />');
$default_logger->info('info3<br />');
$default_logger->warn('warn3<br />');
$default_logger->error('error3<br />');
$default_logger->fatal('fatal3<br />');
$separator_logger->debug('==========<br />');

$separator_logger->debug('4) DebugLogger (TRACE)<br />');
$debug_logger->setLevel(LoggerLevel::getLevelTrace());
$debug_logger->trace('trace4<br />');
$debug_logger->debug('debug4<br />');
$debug_logger->info('info4<br />');
$debug_logger->warn('warn4<br />');
$debug_logger->error('error4<br />');
$debug_logger->fatal('fatal4<br />');
$separator_logger->debug('==========<br />');

$separator_logger->debug('5) DefaultLogger (FATAL)<br />');
$default_logger->setLevel(LoggerLevel::getLevelFatal());
$default_logger->trace('trace5<br />');
$default_logger->debug('debug5<br />');
$default_logger->info('info5<br />');
$default_logger->warn('warn5<br />');
$default_logger->error('error5<br />');
$default_logger->fatal('fatal5<br />');
$separator_logger->debug('==========<br />');

$separator_logger->debug('6) DebugLogger (FATAL)<br />');
$debug_logger->setLevel(LoggerLevel::getLevelFatal());
$debug_logger->trace('trace6<br />');
$debug_logger->debug('debug6<br />');
$debug_logger->info('info6<br />');
$debug_logger->warn('warn6<br />');
$debug_logger->error('error6<br />');
$debug_logger->fatal('fatal6<br />');
$separator_logger->debug('==========<br />');

結果(画面出力)
2014-09-21 23:38:50.836 separator DEBUG [xxx.xxx.xxx.xxx -] 1) DefaultLogger
2014-09-21 23:38:50.838 separator DEBUG [xxx.xxx.xxx.xxx -] ==========
2014-09-21 23:38:50.838 separator DEBUG [xxx.xxx.xxx.xxx -] 2) DebugLogger
2014-09-21 23:38:50.838 DebugLogger DEBUG [xxx.xxx.xxx.xxx -] debug2
2014-09-21 23:38:50.839 DebugLogger INFO [xxx.xxx.xxx.xxx -] info2
2014-09-21 23:38:50.839 DebugLogger WARN [xxx.xxx.xxx.xxx -] warn2
2014-09-21 23:38:50.839 DebugLogger ERROR [xxx.xxx.xxx.xxx -] error2
2014-09-21 23:38:50.840 DebugLogger FATAL [xxx.xxx.xxx.xxx -] fatal2
2014-09-21 23:38:50.840 separator DEBUG [xxx.xxx.xxx.xxx -] ==========
2014-09-21 23:38:50.840 separator DEBUG [xxx.xxx.xxx.xxx -] 3) DefaultLogger (TRACE)
2014-09-21 23:38:50.842 separator DEBUG [xxx.xxx.xxx.xxx -] ==========
2014-09-21 23:38:50.842 separator DEBUG [xxx.xxx.xxx.xxx -] 4) DebugLogger (TRACE)
2014-09-21 23:38:50.842 DebugLogger TRACE [xxx.xxx.xxx.xxx -] trace4
2014-09-21 23:38:50.843 DebugLogger DEBUG [xxx.xxx.xxx.xxx -] debug4
2014-09-21 23:38:50.843 DebugLogger INFO [xxx.xxx.xxx.xxx -] info4
2014-09-21 23:38:50.843 DebugLogger WARN [xxx.xxx.xxx.xxx -] warn4
2014-09-21 23:38:50.844 DebugLogger ERROR [xxx.xxx.xxx.xxx -] error4
2014-09-21 23:38:50.844 DebugLogger FATAL [xxx.xxx.xxx.xxx -] fatal4
2014-09-21 23:38:50.844 separator DEBUG [xxx.xxx.xxx.xxx -] ==========
2014-09-21 23:38:50.845 separator DEBUG [xxx.xxx.xxx.xxx -] 5) DefaultLogger (FATAL)
2014-09-21 23:38:50.845 separator DEBUG [xxx.xxx.xxx.xxx -] ==========
2014-09-21 23:38:50.846 separator DEBUG [xxx.xxx.xxx.xxx -] 6) DebugLogger (FATAL)
2014-09-21 23:38:50.846 DebugLogger FATAL [xxx.xxx.xxx.xxx -] fatal6
2014-09-21 23:38:50.846 separator DEBUG [xxx.xxx.xxx.xxx -] ==========

結果(ログファイル:app.log)
2014-09-21 23:38:50.836 separator      DEBUG    [xxx.xxx.xxx.xxx -] 1) DefaultLogger<br />
2014-09-21 23:38:50.837 DefaultLogger  WARN     [xxx.xxx.xxx.xxx -] warn1<br />
2014-09-21 23:38:50.837 DefaultLogger  ERROR    [xxx.xxx.xxx.xxx -] error1<br />
2014-09-21 23:38:50.837 DefaultLogger  FATAL    [xxx.xxx.xxx.xxx -] fatal1<br />
2014-09-21 23:38:50.838 separator      DEBUG    [xxx.xxx.xxx.xxx -] ==========<br />
2014-09-21 23:38:50.838 separator      DEBUG    [xxx.xxx.xxx.xxx -] 2) DebugLogger<br />
2014-09-21 23:38:50.838 DebugLogger    DEBUG    [xxx.xxx.xxx.xxx -] debug2<br />
2014-09-21 23:38:50.839 DebugLogger    INFO     [xxx.xxx.xxx.xxx -] info2<br />
2014-09-21 23:38:50.839 DebugLogger    WARN     [xxx.xxx.xxx.xxx -] warn2<br />
2014-09-21 23:38:50.839 DebugLogger    ERROR    [xxx.xxx.xxx.xxx -] error2<br />
2014-09-21 23:38:50.840 DebugLogger    FATAL    [xxx.xxx.xxx.xxx -] fatal2<br />
2014-09-21 23:38:50.840 separator      DEBUG    [xxx.xxx.xxx.xxx -] ==========<br />
2014-09-21 23:38:50.840 separator      DEBUG    [xxx.xxx.xxx.xxx -] 3) DefaultLogger (TRACE)<br />
2014-09-21 23:38:50.841 DefaultLogger  TRACE    [xxx.xxx.xxx.xxx -] trace3<br />
2014-09-21 23:38:50.841 DefaultLogger  DEBUG    [xxx.xxx.xxx.xxx -] debug3<br />
2014-09-21 23:38:50.841 DefaultLogger  INFO     [xxx.xxx.xxx.xxx -] info3<br />
2014-09-21 23:38:50.841 DefaultLogger  WARN     [xxx.xxx.xxx.xxx -] warn3<br />
2014-09-21 23:38:50.841 DefaultLogger  ERROR    [xxx.xxx.xxx.xxx -] error3<br />
2014-09-21 23:38:50.842 DefaultLogger  FATAL    [xxx.xxx.xxx.xxx -] fatal3<br />
2014-09-21 23:38:50.842 separator      DEBUG    [xxx.xxx.xxx.xxx -] ==========<br />
2014-09-21 23:38:50.842 separator      DEBUG    [xxx.xxx.xxx.xxx -] 4) DebugLogger (TRACE)<br />
2014-09-21 23:38:50.842 DebugLogger    TRACE    [xxx.xxx.xxx.xxx -] trace4<br />
2014-09-21 23:38:50.843 DebugLogger    DEBUG    [xxx.xxx.xxx.xxx -] debug4<br />
2014-09-21 23:38:50.843 DebugLogger    INFO     [xxx.xxx.xxx.xxx -] info4<br />
2014-09-21 23:38:50.843 DebugLogger    WARN     [xxx.xxx.xxx.xxx -] warn4<br />
2014-09-21 23:38:50.844 DebugLogger    ERROR    [xxx.xxx.xxx.xxx -] error4<br />
2014-09-21 23:38:50.844 DebugLogger    FATAL    [xxx.xxx.xxx.xxx -] fatal4<br />
2014-09-21 23:38:50.844 separator      DEBUG    [xxx.xxx.xxx.xxx -] ==========<br />
2014-09-21 23:38:50.845 separator      DEBUG    [xxx.xxx.xxx.xxx -] 5) DefaultLogger (FATAL)<br />
2014-09-21 23:38:50.845 DefaultLogger  FATAL    [xxx.xxx.xxx.xxx -] fatal5<br />
2014-09-21 23:38:50.845 separator      DEBUG    [xxx.xxx.xxx.xxx -] ==========<br />
2014-09-21 23:38:50.846 separator      DEBUG    [xxx.xxx.xxx.xxx -] 6) DebugLogger (FATAL)<br />
2014-09-21 23:38:50.846 DebugLogger    FATAL    [xxx.xxx.xxx.xxx -] fatal6<br />
2014-09-21 23:38:50.846 separator      DEBUG    [xxx.xxx.xxx.xxx -] ==========<br />

2014-07-11

「Jコミ」改め「絶版マンガ図書館」の初期不具合等

本日、Jコミが「絶版マンガ図書館」としてリニューアルされましたが、大幅な改定を行ったためか、ざっと見たところでも不具合等が目につきましたので、覚書を兼ねて。

まぁ、徐々に改善されていくのだと思いますが(初回の分は、Jコミ情報室!さん及び赤松氏に報告済み。その後は随時お問い合わせ等でも報告)。

→対応されたもの・新たに見つけたもの等を随時更新。

Web 版

不具合
No.不具合内容対応備考
1マイページの本棚で「本棚から削除」が出来ない。削除できるようになった(2014/07/12)。 一旦消えたように見えても、ページをリロードすると削除できていない。
サーバとの通信が行われていないように見える。
2シリーズ作品において、巻数順にソートされていないものがある。
作品一覧や、作品詳細ページのシリーズ作品の案内、RSS等
ソートされるようになった模様(2014/07/13)。
→作品一覧については、公開日順に表示されるようになった(2014/07/28)。
例:ハイスクール!奇面組 スクリーンショット
全10巻以上の長編
3作品詳細ページの「著作者の作品案内」に何も表示されない。「著作者の作品案内」欄がなくなった(2014/07/12)。
4http://r18.zeppan.com/ の右下のランキングが表示されない。表示されるようになった(2014/07/12)。500エラーが発生している模様。
5http://zeppan.com/ の右下のランキングが総合ランキングしかない。一般・成人向け共に「総合ランキング」のみに統一された(2014/07/12)。R18の方は7日間ランキング・30日間ランキングもある→無くなった(2014/07/12)。
→一般/R18共に、7日間ランキング・30日間ランキングが表示されるようになった(2014/08)。
6「作品一覧」などで、画面を下にスクロールした際に継ぎ足された作品については、「本棚に追加」「本棚から削除」ができない。継ぎ足された作品に関しても、追加・削除共にできるようになった(2014/07/14)。
7マイページの本棚にXSS脆弱性有り。報告分に関しては対策された(2014/07/13)。2014/07/13に「お問い合わせ」から報告。
8「作品一覧」などで、最初に表示されたり、継ぎ足されたりする作品数が50個だったりたまに10個だったりで一定しない。改善されたように思える(2014/07/14)。 2014/07/14に「お問い合わせ」から報告。
9マンガ検索フォームにXSS脆弱性有り。報告分に関しては対策された(2014/07/15)。2014/07/14に「お問い合わせ」から報告。
前日までは存在しない脆弱性だったはず、改修過程で入り込んだ模様。
10タイトル50音順一覧に出てこない作品がある。報告分に関しては対策された模様(2014/07/15)。2014/07/15に「お問い合わせ」から報告。
ら行を最後までスクロールしても、「LoVe/EGOISTIC」(著作者 ぐりすぐり)が表示されない。
11リニューアル後に掲載された作品で、著作者名が表示されないものがある。対応された模様(2014/07/15)。蓬萊学園の冒険!!(未報告、JComi_Updateのツイートで気づいてはいたが、そういうものだと思っていた)→2014/07/15対応済み。
おしえて♡お姉さん (2014/07/15、ツイートで報告)→2014/07/15対応済み。
その後も発生している(データベースへの登録ミスか?)。
12旧Jコミの「Jコミで印刷できるってよHD」のPDFが(再)ダウンロードできない。対応された模様(対応日不明、2014/07/25以前)。 503エラーでアクセス不可(2014/07/18)。
2014/07/18に「お問い合わせ」から報告。
2014/07/19時点の回答:サイトリニューアルに伴い、一時的に閉鎖中、近日中に再オープンとのこと(「現在、このページはメンテナンスを行っております。後日改めてアクセス下さい。ご迷惑をおかけてしておりますが、よろしくお願い申し上げます。」表示になった)。
13新着公開作品等の作品一覧が、公開日順に表示されていない。公開日順に表示されるようになった(2014/07/28)。
ただし、この影響で同一シリーズ内の順番は巻数順ではなくなったなった模様()。
作品番号の降順(ただし、シリーズ作品内では巻数順)で表示されているようだが、アップロード作品も公開作品と同じ番号体系になっているため、作品が許諾されたタイミングによっては表示順が後になってしまう模様。

-総作品数が一覧で表示される作品数と一致しない。 2014/07/14に「お問い合わせ」から報告。
■一般向け作品
総作品数:1,475
新着公開作品:1416件
最新アップロード作品:54件
※5件少ない。
■成人向け作品
総作品数:272
新着公開作品:261件
最新アップロード作品:11件
※こちらは一致。

要望等
No.現状要望対応備考
1Jコミではシリーズ作品をまとめたページがあったが、これがなくなり、一覧表示などでも1巻ずつ表示される。
このため、特に全10巻以上の長編等では非常に見通しが悪いように感じられる。
一覧・検索表示では、シリーズ作品はまとめてひとつとして欲しい(好みの問題かもしれないが…)。赤松氏:「ニコ動のように、同じ作品でも画質の違うバージョンが複数上がってくる可能性があるため、巻ごとの管理になりました。 」 現状でも、作品詳細ページには、シリーズ作品がまとめて表示はされる。
2「本棚に追加」が単巻ずつしかできない。シリーズものについてはまとめて追加したい。
3既存のJコミ作品ページ等へのリンクが全て絶版マンガ図書館のトップページにリダイレクトされてしまう。既存のJコミの各作品へのリンクは、当該作品のページへリダイレクトして欲しい。 各作品の旧リンクがリダイレクトされるようになった(2014/07/12)。 作者の方等にも負担になっている。(参考
4Jコミで有志の入力による作品データが存在したが、これがなくなっている。
旧作品ページのキャッシュで、「>> 作品データを見る」をクリックしたときに出てきたもの。
既存の作品データについては予めWikiに反映しておくなどの配慮が欲しい。 既存の有志の方のモチベーションを下げてしまう。(参考
赤松氏:「絶版マンガ図書館では、wikipediaに準じた様式で書いて欲しい希望はあります。編集ツールもwikipedia準拠になっています。IPアドレスなども残ります。これまでとは結構違いますのでお気を付け下さい。しばらく様子見することをお薦め致します。 」
5サムネイルをクリックしても作品詳細ページに移行しない(マウスオーバ→作品詳細ページへをクリックする手順が必要)。サムネイルクリックでも作品詳細ページへ移行して欲しい。対応された(2014/07/15?)。要望として認識はされており、いずれ直されるとのこと(2014/07/14・参考

Androidアプリ

使用機種:WX04K(Android 4.1.1)、旧Jコミ用JComi Viewer+はアンインストール済み。

不具合
No.不具合内容対応備考
1DLのためにログインしようとすると、「ページの読み込み中…」が断続的に表示される等して、ログイン入力ができず、ダウンロードできない。ログイン&ダウンロードが正常に出来るようになった(2014/07/12)。 2014/07/15現在、まだたまに同様の現象が発生してログインできないこともある。サーバ側の負荷などの問題なのか?
2R18の方で、DLを選択しているのに、(「ページの読み込み中…」が断続的に表示される等した後で、リダイレクトされて(?))PC版の画面が表示されて正常動作しない。ログイン&ダウンロードが正常に出来るようになった(2014/07/12)。同上。

2014-06-15

32ビット環境だと2GBを超えるファイルサイズが正常に取れないのか…

PHP_INT_MAX = 2147483647 = 0x7FFFFFFF となっている環境だと、

注意: PHP の数値型は符号付整数であり、 多くのプラットフォームでは 32 ビットの整数を取るため、 ファイルシステム関数の中には 2GB より大きなファイルについては期待とは違う値を返すものがあります。

PHP: filesize - Manual

のように、filesize() の返り値が保証されないということを今更ながらに気付く。

64ビット環境でテストしていたから、32ビット環境で動かしてみてしばらく悩んでしまった。

やむを得ず、代用関数を考えて見る

<?php
function    get_filesize($filepath) {
    $filesize = '';
    for (;;) {
        if (!file_exists($filepath)) break;
        exec("/usr/bin/wc -c < {$filepath}", $output, $return_var);
        foreach ($output as $line) if (($filesize = trim($line)) !== '') break;
        break;
    }
    return $filesize;
}   //  end of get_filesize()

"wc -c"の結果を取得して、文字列で返しているだけ。ファイルが存在しない場合等は ""(空文字列) が返る。


BCMathが有効な環境であれば、使えるかな?

RHEL6.3で、HTTP GET時に5分以上受信データがないとだんまりになる

現象

とあるレンタルサーバ(telnetやsshは未サポート)上のデータをローカル(RHEL6.3 サーバ)上に定期的にバックアップを取る必要があり、ファイル数が多くFTPだと時間がかかって仕方がないので、

  1. レンタルサーバ上のPHPスクリプトを呼び出し、tar コマンドにより全ファイルをアーカイブ。
  2. アーカイブした tar ファイルを FTP でダウンロード。

という方法を取っていたのだが、ある時点から、正常にバックアップが取れなくなってしまった。


調べてみたところ、

  • 1. で、HTTP GET Request 後に、一定時間(5分)以上受信データが無い状態が続くと、HTTP クライアントがその後のデータを受信しないままフリーズしてしまう。

状態であることがわかった。


ちなみに、HTTP クライアントには wget を使用していたが、

  • 同一バージョンの wget を使用しても、個人持ちの CentOS 6.5 上ではフリーズせずに問題なく完了。
  • 当該 RHEL6.3 サーバ上では、wget 以外の方法であっても、同様の現象が発生してしまう。

RHEL6.3 サーバ上で使用している socket 関連の共有ライブラリ(あるいはその設定)に問題があるのであろうところまでしかわかっていない。


どなたか、このような場合の対策をご存じの方がおられたら、教えてほしい。

もっとも、5分以上もデータが無い状態が続くと、そもそも他のレンタルサーバとかだと Apache とかのタイムアウトの方でひっかかってしまう気がしなくもないので、どちらにしてもレンタルサーバ側の処理も見直す必要があるのだろうけれども。


再現方法

次のようなPHPスクリプトをレンタルサーバ上に設置し、

/test/wait.php
<?php
$WAIT_MIN = 5;  //  <=4:OK, >5:NG

if (isset($_GET['wait']) && is_numeric($_GET['wait'])) $WAIT_MIN = intval($_GET['wait']);

$WAIT_SEC = $WAIT_MIN * 60;
$WAIT_UNIT_SEC = 10;
$WAIT_COUNT = (int) ($WAIT_SEC / $WAIT_UNIT_SEC);

set_time_limit($WAIT_SEC * 2);

function    echo_flush($str) {
    echo($str);
    ob_flush();
    flush();
}   //  end of flush_output()

ob_start();
header("Content-Type: text/plain; charset=utf-8");
ob_end_flush(); // バッファフラッシュ&バッファリングをOFFに

echo_flush("wait {$WAIT_MIN} minutes ({$WAIT_SEC} seconds) ...\n");

for ($ci=0; $ci < $WAIT_COUNT; $ci++) {
    sleep($WAIT_UNIT_SEC);
    //↓の行を有効にして、10秒毎にデータを送信するようにすればクライアント側でフリーズしない
    //echo_flush(sprintf("%5d sec.\n", $WAIT_UNIT_SEC*(1+$ci)));
}
echo_flush("done.\n");

exit(0);

// ■ end of file


RHEL6.3 サーバ上で wget を実行すると、

$ wget "http://example.com/test/wait.php?wait=4" -q -O -
wait 4 minutes (240 seconds) ...
done.

のように、4分までは問題なく完了するのに、

$ wget "http://example.com/test/wait.php?wait=5" -q -O -
wait 5 minutes (300 seconds) ...

5分からは(wget側でタイムアウトするまで)だんまりになる。


また、telnet で試しても、

$ telnet example.com 80
Trying xxx.xxx.xxx.xxx...
Connected to example.com.
Escape character is '^]'.
GET /test/wait.php?wait=4 HTTP/1.0
User-Agent: telnet
Host: example.com

HTTP/1.1 200 OK
Date: Sun, 15 Jun 2014 00:00:00 GMT
Server: Apache
Connection: close
Content-Type: text/plain; charset=utf-8

wait 4 minutes (240 seconds) ...
done.
Connection closed by foreign host.

のように、4分までは問題なく完了するのに、

$ telnet example.com 80
# : (中略)
GET /test/wait.php?wait=5 HTTP/1.0
# : (中略)
wait 5 minutes (300 seconds) ...

5分からは、この状態でだんまりになる。


暫定対策

レンタルサーバ側のPHPスクリプトで、次のような関数を使って tar コマンドをバックグランドで呼び出した後、処理が終わるまで一定時間毎にポーリングし、何らかのデータを出力するようにしている。

<?php
function    exec_nowait($cmdline, &$outfile, &$errfile) {
    $outfile = tempnam('./', 'OUT_');
    $errfile = tempnam('./', 'ERR_');
    $cmdline = "{$cmdline} >{$outfile} 2>{$errfile} & echo \$!";
    $pid = null;
    exec($cmdline, $output, $rcode);
    foreach ($output as $line) if (($pid = trim($line)) !== '') break;
    return $pid;
}   //  end of exec_nowait()

$pid = exec_nowait("tar cvf backup.tar /path/to/target", $outfile, $errfile);
while ($pid) {
    sleep(10);
    if (/*バックグラウンド処理のチェックを行い、終了していたら*/) break;
    echo(date("Y-m-d H:i:s") . "\n"); // データ出力
    ob_flush();
    flush();
}
// この辺で後処理を入れる
unlink($outfile);
unlink($errfile);

バックグラウンド処理の終了チェックは、psコマンドが使える場合は、

<?php
function    get_proc_dict() {
    $proc_dict = array();
    $cmd = "/bin/ps -A -o pid= -o comm=";
    $fp = popen($cmd, "r");
    while (($line=fgets($fp))!==false) {
        $line = trim($line);
        $parts = explode(' ', $line, 2);
        $proc_dict[$parts[0]] = $parts[1];
    }
    pclose($fp);
    return $proc_dict;
}   //  end of get_proc_dict()

function    get_proc_name($pid) {
    $proc_dict = get_proc_dict();
    return isset($proc_dict[$pid]) ? $proc_dict[$pid] : null;
}   //  end of get_proc_name()

// while ループ内のチェック部分で、if (!get_proc_name($pid)) break;

のような関数を用意して使うのがよさそう。


ps コマンドが存在しない場合は…tar の標準出力($outfile)をチェックして、例えばサイズが変わらなくなったら終了、とか。


関連?

RHEL5.1でFTPのダウンロードが正常に完了しない場合がある? - 風柳メモ