MySQLの最近のブログ記事

たま~にforkして子プロセスを起動する処理を呼んだとき、親プロセスで上記の「Lost connection to MySQL server during query」エラーや、httpヘッダエラーが発生していた。

特に決済まわりで使っているわけでもないし、トランザクションが絶対必要なわけでもないし、子プロセス内の処理はとりあえず最後まで完了してるし、いままで「ま、いっか」って感じで放置していたが、最近頻発するようになってきたので、ようやく真面目に取り組んでみる。

DBIに接続したときのステートメントハンドルの扱い方が悪いんじゃないかっていいうのはうすうす気づいてたが、下記のような資料を発見。ありがたや。
http://d.hatena.ne.jp/hiratara/20110122/1295711939
http://nihen.hatenablog.com/entry/2011/11/17/102557


これの真似して、子プロセス生成直後に
$dbh->{InactiveDestroy} = 1;
$dbh = $dbh->clone({InactiveDestroy => 0});
を呼んだらなんとあっさり解決。

以前にこんなこと↓
http://www.igreks.jp/dev/2010/06/forkmysql.html
書いてたけど、やっぱ親プロセスと同じコネクションつかっちゃまずいわけね。
Apacheのせいにしてごめんなさいって感じですね。


例えば以下のようなテーブルがあったとき。

user_tbl(会員テーブル)
id name address
1 山田 東京都
3 鈴木 千葉県
6 高橋 北海道
12 佐藤 大阪府

shitumon_tbl(質問テーブル)
id title answer_type
1 あなたの身長 テキスト入力
2 あなたの体重 テキスト入力
3 住居形態 択一選択

sentakushi_tbl(質問の選択肢テーブル)
id shitsumon_id sentakushi_name
1 3 一戸建て
2 3 マンション
3 3 アパート
4 3 ダンボール

user_info_tbl(会員情報テーブル)
id user_id shitsumon_id answer
(テキスト入力の時はその値、
択一選択の時は選択肢ID)
9 1(山田) 1(身長) 175(cm)
10 1(山田) 2(体重) 68(kg)
11 1(山田) 3(住居形態) 3(アパート)
12 3(鈴木) 1(身長) 168(cm)
13 3(鈴木) 3(住居形態) 1(一戸建て)
14 6(高橋) 2(体重) 74(kg)
15 6(高橋) 3(住居形態) 2(マンション)
16 12(佐藤) 1(身長) 182(cm)
17 12(佐藤) 3(住居形態) 3(アパート)


上記のとき、
※「身長」が「170」cm以上で、 ※「住居形態」が「アパート」の会員(条件数=2)の「会員テーブル」だけを表示する。

SQLの例
-----------------------------------------------------------------------------------------
SELECT `user_tbl` . *
FROM `user_tbl`

LEFT JOIN INNER JOIN (
# 副問い合わせ

# まず質問IDが1(身長)で、答えが170(cm)以上のuser_idを抽出
(
SELECT `user_info_tbl`.user_id
FROM `user_info_tbl`
WHERE
`user_info_tbl`.question_id = 1
AND
`user_info_tbl`.answer >= 170
)

# 次に質問IDが3(住居形態)で、答えが3(アパート)のレコードをUNION ALL で結合
# ※「UNION ALL」にすることで、わざと重複させる。
UNION ALL (
SELECT `user_info_tbl`.user_id
FROM `user_info_tbl`
WHERE
`user_info_tbl`.question_id = 3
AND
`user_info_tbl`.answer = 3
)

) AS uni
# ↑副問い合わせでUNIONするときは固有名をつけないと、「ERROR 1248 (42000): Every derived table must have its own alias」になる。

# ユーザIDが同じものを外部内部結合
ON user_tbl.id = `uni`.user_id

# 重複数が2(=条件数)のレコードのみ表示
GROUP BY `user_tbl`.id
HAVING COUNT( `user_tbl`.id ) = 2
-----------------------------------------------------------------------------------------
結果
id name address
1 山田 東京都
12 佐藤 大阪府


これだと、会員・質問・選択肢を自由に追加・削除しても、情報の整合性がとれる。
会員の自由項目情報を扱ったりするときに便利。
(もちろん削除時、変更時にはそれに対応する各々のテーブルデータを書き換える必要はあるが・・・)


※追記
「LEFT JOIN」だと条件が1つのときにuser_tbl他のレコードも一緒にくっついてきてしまってまずいので「INNER JOIN」が正解。
前回に引き続きphpBB(phpBB2)ネタ。

当然ながら日本語でphpBBを運用する場合は、普通、MySQLのマルチバイト文字セットはEUCかUTF8になっている。

しかし、デフォルトのまだと、システムから送られてくるメールの、DBのマルチバイトを参照した部分の文字列が化ける。

以下対策

・language/language_japanese/email の中のファイル全てを、MySQLの文字コードに合わせてエンコードを修正する。
(SJISなどでアップされていたらEUCかUTF8で保存して上書き)

・各ファイルの「Charset: iso-2022-jp」と書かれているところを、全て
「Charset: EUC-JP」 か、
「Charset: UTF8」
に書き換える。

以上
久々の投稿。しかもネタが今更感のあるこれ。
でも今までphpBBを使ったことが無かったので、まあまあ勉強になった。

伝統的なスパム対策は下記らしい。
http://support.hiikun.net/bbs/topic-299.html

ちなみにmodの使い方などはこちら。
http://all.netgamers.jp/adcat5.html

でも、今時のphpBBスパムシステムは、画像認証やMDハッシュを突破したりしてかなり賢い。
上記の対策をしても相変わらず1日100件くらいは登録スパムがやってくる。

まあ、ユーザ登録されるだけならまだいいが、トピックを荒らされるのだけは困るので、
以下の対策を実施。
http://garnote.com/2010/01/spam.html

これはかなり有効だったようで、スパム投稿がピタリと止まった。
MilkyStepのコミュニティでコアサーバを使ったとき、メルマガのデフォルト登録完了画面が文字化けする(日本語部分が???になる)との報告を受け、一時期かなり調査を行ったが解決せず。
それから約2カ月後、他のユーザから同様の現象が報告された。

開発環境では全くそのような現象が起こらないので、仕方なく、コアサーバーを実際に借りてMilkyStepを設置し徹底的に調査することにした。

表示させるデフォルト画面のHTMLソースは、ユーザが自由に編集できるようになっているのだが、そのソースはMySQLに保存される。

そこからデータを引っ張って、少し置換処理してからプログラムで出力する。

確かにコアサーバで実際に試してみると、5回中4回くらいの割合で日本語が「?」になる。
(成功する場合もある)

MySQLの文字セットはutf8、照合順序もutf8_general_ci、さらにSQL発行時に毎回「SET NAMES utf8」(MySQL4.1以降で有効)しているので、データ通信間で化けるとは考えにくい。
もちろんスクリプトのファイルもUTF8。

とりあえず、ブラウザの文字コードの誤判定臭いので、日本語を多めに入れたり、EUCでいうところの「美乳テーブル」である、郵便くんマーク(〠←これ)を冒頭に入れたりしてみたが、変わらず。

次に、Perlでutf8扱う時によくある、utf8フラグあるなしの問題かとも思い、「use utf8」したりEncode::encode/decodeしたりいろいろやったが、どうやら関係ないっぽい。

というか、なんかMySQLから参照した時点ですでに化けてるっぽい。

試しに、SELECT時に、以下のような感じで、明示的に照合順序を指定してみる。

まず、
--------------------------------------------------------------------------------
SELECT column_name COLLATE utf8_general_ci FROM ・・・・・
--------------------------------------------------------------------------------

・・・変化なし。

次に、
--------------------------------------------------------------------------------
SELECT COERCIBILITY(column_name COLLATE utf8_general_ci) FROM ・・・
--------------------------------------------------------------------------------

お、1回目はうまくいった。
もう1回。

--------------------------------------------------------------------------------
Software error:

COLLATION 'utf8' is not valid for CHARACTER SET 'latin1_swedish_ci' at ......
--------------------------------------------------------------------------------

あれあれ????

「utf8_general_ciでの照合はCHARACTER SET 'latin1'では有効ではありません」

latin1??なんで勝手に文字セット変わってんだよ!
utf8で固定だろーが!

なぜ、コアサーバだけ、しかも特定のカラムの文字列だけ参照するときにこうなるのか・・・

MySQL4から5に強引に移行した時の忘れものか?


とにかくこの、参照時に勝手にテーブルの文字コードをLatin1と判別するわけのわからない現象と格闘すること数時間。

そもそもこの処理って、forkした後の親プロセスの中でやっていたんだが、このforkを切ったらなぜか正常になった。
ということで、この処理はforkの中から外に出すことに決定。

いまだに原因はよくわからないが、fork時は内部的に変数を子プロセスにコピーするが、その時点で(コアサーバでは)なんか問題があるらしいってことはぼんやりわかった。

サーバサイドのアプリを広く対応するのはやっぱ一苦労だな・・・

milystepはメルマガごとに配信履歴テーブル(his)とクリック測定用テーブル(cs)がある。

クリック測定テーブルには全てのURLのデータが入っていて、各レコードはどの配信履歴に対応しているか判別するためのナンバーが格納されている。

簡単に書くとこんな感じ

csテーブル
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 no   | url    | click  | his_no(対応するhisのナンバー)  
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 1    | http://.... | 3    |  2
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 2    | http://.... | 1    |  2
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 3    | http://.... | 0    |  3
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 4    | http://.... | 12   |  3

        ・
        ・
        ・
hisテーブル
ーーーーーーーーーーーーーーーーーーーーーーー
 no  | subject   | body 
ーーーーーーーーーーーーーーーーーーーーーーー
 1   | こんにちは | ご無沙汰してます...
ーーーーーーーーーーーーーーーーーーーーーーー
 2   | 忘年会の件 | 場所が決まりました...
ーーーーーーーーーーーーーーーーーーーーーーー
 3   | ハロー   | 元気ですか?...
ーーーーーーーーーーーーーーーーーーーーーーー
 4   | 了解です  | 例の件ですが...
        ・
        ・
        ・



このとき、hisのテーブルに、csの対応するhisナンバーのクリック数(click)の合計をtotal_clickとしてSELECTしたい。

イメージとしては、こんな感じ。

ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 no  | subject   | body        | total_click
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 1   | こんにちは | ご無沙汰してます... | 0
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 2   | 忘年会の件 | 場所が決まりました...| 4(cs.his_noが2のclickの合計)
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 3   | ハロー   | 元気ですか?...   | 12(cs.his_noが3のclickの合計)
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
 4   | 了解です  | 例の件ですが...   | 0
        ・
        ・
        ・


なので最初は、サブクエリを使って、
-------------------------------------------------------------
SELECT
 *,
 (
  SELECT
   SUM(cs.click) FROM cs
  WHERE cs.his_no = his.no
 ) AS total_click
FROM his
-------------------------------------------------------------

みたいにやってたのだが、あまりにも遅い。
300件程度の処理で明らかに体感速度が数秒違うので、これはヤバい。

というわけで、一旦テンポラリテーブルを作ってそれをLEFT JOINさせる。

-------------------------------------------------------------
CREATE TEMPORARY TABLE temp AS
 SELECT
  his_no,
  SUM(click) AS total_click
 FROM cs
 GROUP BY his_no;

SELECT
 *,
 temp.total_click
FROM his
LEFT JOIN temp ON
 his.no = temp.his_no;
-------------------------------------------------------------

これでだいぶ改善。

データが数千件になったときはどうなるかまだわからない。


参考ページ
http://homepage2.nifty.com/sak/w_sak3/doc/sysbrd/mysql_09.htm
http://oss.timedia.co.jp/show/MySQL%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%AE%E6%97%85/%E4%B8%80%E6%99%82%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%AE%E5%A0%B4%E5%90%88%E3%81%AF%E3%81%A9%E3%81%86%E3%81%AA%E3%82%8B%E3%81%8B
http://labs.mythos-jp.com/blog/archives/246


私はDocomoの携帯しか持ってないので、事前にauとsoftbankを持ってる友人に頼んで、メールヘッダなどを調査したらこんな感じ↓

■ Docomo
----------------------------------------------------------------------------------
From ****@docomo.ne.jp Wed May 26 17:54:11 2010
Return-Path: <****@docomo.ne.jp>
X-Original-To: ***@igreks.jp
Delivered-To: ***@igreks.jp
Received: from localhost (localhost.localdomain [127.0.0.1])
by ***.igreks.jp (Postfix) with ESMTP id 5DE338B41B2
for <***@igreks.jp>; Wed, 26 May 2010 17:54:11 +0900 (JST)
X-Virus-Scanned: amavisd-new at igreks.jp
Received: from ***.igreks.jp ([127.0.0.1])
by localhost (igreks.jp [127.0.0.1]) (amavisd-new, port ****)
with ESMTP id kkpANBxiUasa for <****@igreks.jp>;
Wed, 26 May 2010 17:54:11 +0900 (JST)
Received: from docomo.ne.jp (mail108.docomo.ne.jp [203.138.203.8])
by ***.igreks.jp (Postfix) with ESMTP id 3E6618B40B0
for <***@igreks.jp>; Wed, 26 May 2010 17:54:11 +0900 (JST)
Date: Wed, 26 May 2010 17:54:09 +0900 (JST)
From: ****@docomo.ne.jp
To: ***@igreks.jp
Subject: =?iso-2022-jp?B?GyRCMSsbKEI=?=
Message-ID:
MIME-Version: 1.0
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: 7bit

$BK\J8(B
----------------------------------------------------------------------------------


■ au
----------------------------------------------------------------------------------
From ****@ezweb.ne.jp Wed May 26 18:24:29 2010
Return-Path: <****@ezweb.ne.jp>
X-Original-To: ***@igreks.jp
Delivered-To: ***@igreks.jp
Received: from localhost (localhost.localdomain [127.0.0.1])
by ***.igreks.jp (Postfix) with ESMTP id F1F818B41B2
for <***@igreks.jp>; Wed, 26 May 2010 18:24:28 +0900 (JST)
X-Virus-Scanned: amavisd-new at igreks.jp
Received: from ***.igreks.jp ([127.0.0.1])
by localhost (igreks.jp [127.0.0.1]) (amavisd-new, port ****)
with ESMTP id CBSY6-Rv+mf6 for <***@igreks.jp>;
Wed, 26 May 2010 18:24:28 +0900 (JST)
Received: from ezweb.ne.jp (nx3oBP07-06.ezweb.ne.jp [59.135.39.240])
by ***.igreks.jp (Postfix) with ESMTP id D7FC88B40B0
for <***@igreks.jp>; Wed, 26 May 2010 18:24:28 +0900 (JST)
Received: from nxev04mp06 (localhost [127.0.0.1])
by nxev04mp06.ezweb.ne.jp (EZweb Mail) with SMTP id 955ED5BC640B4
for <***@igreks.jp>; Wed, 26 May 2010 18:24:28 +0900 (JST)
From: ****@ezweb.ne.jp
To: ***@igreks.jp
Subject: =?iso-2022-jp?B?GyRCMSsbKEI=?=
Message-ID: <2010052618242860697200007c12@nxev04mp06.ezweb.ne.jp>
Date: Wed, 26 May 2010 18:24:28 +0900
Mime-Version: 1.0
Content-Type: text/plain; charset="iso-2022-jp"
Content-Transfer-Encoding: 7bit

$B%6!<%6!<(B
----------------------------------------------------------------------------------


■ Softbank
----------------------------------------------------------------------------------
From ****@softbank.ne.jp Wed May 26 18:24:36 2010
Return-Path: <****@softbank.ne.jp>
X-Original-To: ***@igreks.jp
Delivered-To: ***@igreks.jp
Received: from localhost (localhost.localdomain [127.0.0.1])
by ***.igreks.jp (Postfix) with ESMTP id 975BD8B41B2
for <***@igreks.jp>; Wed, 26 May 2010 18:24:36 +0900 (JST)
X-Virus-Scanned: amavisd-new at igreks.jp
Received: from ***.igreks.jp ([127.0.0.1])
by localhost (igreks.jp [127.0.0.1]) (amavisd-new, port ****)
with ESMTP id HQrmiDMGcT3n for <***@igreks.jp>;
Wed, 26 May 2010 18:24:36 +0900 (JST)
Received: from mmrts049p01c.softbank.ne.jp (mmrts049p01c.softbank.ne.jp [123.108.236.27])
by ***.igreks.jp (Postfix) with SMTP id 6D9898B40B0
for <***@igreks.jp>; Wed, 26 May 2010 18:24:36 +0900 (JST)
Subject: =?ISO-2022-JP?B?GyRCJCYkcyQzGyhC?=
Mime-Version: 1.0
Content-Type:text/plain;charset=ISO-2022-JP
Content-Transfer-Encoding:7bit
Date: Wed, 26 May 2010 18:24:35 +0900
Message-ID: <20100526182435672870.2aea@0016E68F5982>
From: <****@softbank.ne.jp>
To: ***@igreks.jp
Sender:****@softbank.ne.jp
X-Priority: 3

$B%b%j%b%j(B
----------------------------------------------------------------------------------


仕様としては、

・DBはMySQL
・空メの送信先は「reg_kara_mail@igreks.jp」とする
・空メの件名に名前を指定できる。
・登録済みの場合はエラーメールを返す。
・空メ本文にメルマガIDをあらかじめ記載しておく。
・メルマガIDが認識できない場合もエラーメールを返す。
・読者データテーブル名は仮に「user_'メルマガID'」、文字コードはUTF8とする。

これを受けて、スクリプトはざっとこんな感じに。
※おおまかな流れだけで、細かい点は割愛してます


■ 空メール登録処理用CGI(karame.cgi)
-----------------------------------------------------------------------------------
#!/usr/bin/perl

package main;

use strict;
use CGI;
use DBI;
use Unicode::Japanese;

require './lib/get.pl'; #各種データ取得用ライブラリ(詳細割愛)
require './lib/proc.pl'; #各種処理用ライブラリ(詳細割愛)
require './lib/db.pl'; #DB接続、各種SQL実行用ライブラリ(詳細割愛)
require './lib/start.pl'; #メール配信用ライブラリ(詳細割愛)

&db::open(); #DB接続

my $sys_msg = ''; #エラー返信用メッセージ
my ($email, $name, $id);

###### メールから標準入力をパース
my $grep = '[\d\w-.+]+\@[\d\w-]+(\.[\d\w-]+)+'; #メールアドレスの正規表現

while(<>){
 if($_ =~ /^From.*?($grep)/i || $_ =~ /^Return-Path:.*?<($grep)>/i){
  $email = $1; #メールアドレス
 }
 if($_ =~ /^Subject:\s*(.+)$/i){
  $name .= $1; #名前
 }
 if($_ =~ /^\s*(=\?ISO-2022-JP.+\?=)\s*$/i){ #件名が途中で改行されてる時のため
  $name .= $1;
 }
 if($_ =~ /^mid:(.+)$/){
  $id = $1; #メルマガID
 }
}
if($email){
 if($id){
  my $stg = &get::setting_data($id); #メルマガ設定データ取得
  if($stg){
  ####### 読者テーブル内重複チェック
  &db::query("
   SELECT * FROM user_$id WHERE email='$email'
  ");
  my $href = $db::sth->fetchrow_hashref();
  if($href){
   #すでに登録済みの場合
   $sys_msg = <<EOM;
    送信いただいたメールアドレス「$email」はすでに登録済みです。
    このまま次回の配信をお待ちください。
EOM
   $sys_msg =~ s/\t//g;
   #メール返信処理
   &start::return_mail(
    '空メール登録処理エラー',
    $sys_msg,
   );
   exit;
  }
  if($name){
   #名前の入力があった場合
   $name = Unicode::Japanese->new(
    &proc::base64decode($name), #Base64デコード処理
    'jis'
   )->utf8; #JISからUTF8に変換
  }
  ######## 新規登録
  &db::query("
   INSERT IGNORE INTO user_$$pd{'id'} (
    no,  #オートインクリメント
    email,
    name,
    ・
    ・
    ・
   )
   VALUES(
    '',
    '$email',
    '$name',
    ・
    ・
    ・
   )
  ");
 }
 else{
  $sys_msg = <<EOM;
   登録しようとしたメールマガジンは存在しないか、すでに廃刊されています。
   お手数ですが、詳しくは発行者までお問い合わせください。
EOM
 }
}
else{
 $sys_msg = <<EOM;
  送信いただいた情報に不備があり登録できませんでした。
  空メール本文の内容は変更せずに送信してください。
  何度もこのエラーメールが返信される場合は、お手数ですが発行者までお問い合わせください。
EOM
}

########## エラー返信処理
if($sys_msg){
 &start::return_mail(
  '空メール登録処理エラー',
  $sys_msg,
 );
}
&db::close(); #DB切断

exit;

-----------------------------------------------------------------------------------



■Postfixエイリアス設定ファイル(/etc/aliase)に以下を追加
-----------------------------------------------------------------------------------
reg_kara_mail:  "|cd 'karame.cgiがあるディレクトリの絶対パス'; ./karame.cgi"
-----------------------------------------------------------------------------------

■リスタート
-----------------------------------------------------------------------------------
# newaliases

# /etc/init.d/postfix restart
-----------------------------------------------------------------------------------


■以下のようなメールを「reg_kara_mail@igreks.jp」宛に送る

 件名:自分の名前もしくは空白
 本文:mid:melmagaID

以上。


※レンタルサーバなどでエイリアスが設定できない場合は、後からcronなどでPOPアクセスして処理するなどの方法もある。(参考→http://www.igreks.jp/dev/2009/06/pop.html
この場合は返信メールが即時配信されない。


※ディズニーモバイルとかwilcomは調べてないけど、まあだいたい同じだべってことで。
結論から言うと、親プロセスの「wait;」の直後にもう一度MySQL接続処理を呼ぶ。
※2010/7/23追記:さらに各プロセスの頭でMySQL接続処理を呼ぶ。
ただそれだけ。


■MySQL接続用(db.pl)
--------------------------------------------------------------------------
package db;

use strict;

sub open{
my $dbs = "DBI:mysql:$dbname:$dbhost";
our $dbh = DBI->connect($dbs,$dbuser,$dbpass);
if(!$dbh){ die 'MySQL connection error!'; }
}

sub query{
my ($sql) = @_;
our $sth = $db::dbh->prepare($sql) || die $db::dbh->errstr();
my $exec = $sth->execute() || die $sth->errstr();
return $exec;
}

sub close{
$db::sth->finish();
$db::dbh->disconnect();
}

1;
--------------------------------------------------------------------------


■よくあるforkの処理
--------------------------------------------------------------------------
#!/usr/bin/perl

use strict;
use DBI;

require 'db.pl'; ######## 上のファイル読み込み
&db::open(); ######### 最初のMySQL接続

(親プロセス処理)

my $pid;
FORK: {
 if($pid = fork) {
  &db::open(); ###### ←【ここで単発接続】
  (親プロセス処理)

  close(STDOUT);
  wait;
  &db::open();  ###### ←【ここで再接続】これはfork終了後の処理用
 }
 elsif (defined $pid) {
  &db::open(); ###### ←【ここで単発接続】
  close(STDOUT);

  (子プロセス処理)

  exitしなくても勝手にexitされる?
 }
 elsif($! =~ /No more process/){
  sleep 5;
  redo FORK;
 }
 else{
  die 'Fork is not supported!';
 }
} # End Of Label:FORK


(親プロセス処理再開)

&db::close(); ######### 正規のMySQL接続切断
exit;
--------------------------------------------------------------------------



今まで、子プロセス内処理が終われば、全体の処理も終わるような構造のスクリプトばっかりだったので、なかなか気づかなかった。
ログも「mysql server has gone away...」ってしか出ないし。
標準出力を切ってるからブラウザからのデバックだけだと気がつかず、メルマガの空メール処理実装しようとして初めて気がついた。

一般的には、子プロセス内に入ったら、改めて子プロセス専用のコネクションを作らないと、子プロセスの終了時にMySQLコネクションが切断されてしまうらしい。
(つまり、親と同じコネクションを使っていると、親のコネクションも切れる)
全然しらんかった。


でも、なぜか子プロセスの最初で「&db::open();」してもダメだった。
perlのforkは親の変数が子に全部コピーされるから(同じデータベースハンドル、またはステートメントハンドルを使っている)なのかな?


わざわざ、子プロセス専用のSQL発行サブルーチン作るのも非効率だし、どうしようかなと悩んだ挙句こうなった。


時々、MilkyStepで即時配信メールが送れなくなるのはコレのせいだったりして。


※2010/7/23追記:
子プロセスの処理中に親プロセスが終了しても、子プロセスのMySQL接続が切れる。
また、親が終了する前に子のプロセスIDを使い始めると親のMySQL接続が切れる。
原因はよくわからんが、親・子それぞれの頭で接続処理を単発で入れたら上手く行った。

※2011/1/31追記
これに関しては下記も参照のこと。
http://www.igreks.jp/dev/2012/01/lost-connection-to-mysql-server-during-query.html
前記事(http://www.igreks.jp/dev/2010/01/mysql.html)のようにALTER TABLEを使って更新するのもいいのだが、できれば構造のテンプレートに合わせて一発でコピーしたい。

ただ単にテーブルの構造とデータをコピーする場合、普通は、

---------------------------------------------------------------------------------------
CREATE TABLE sample_new LIKE sample_original
INSERT INTO sample_new SELECT * FROM sample_original
---------------------------------------------------------------------------------------


しかし、新しい構造のテーブルにあわせて、構造を変更しながらデータをコピーするといった方法が見つからないため、スクリプトの処理を合わせて実現してみる。
あんまりスマートじゃないけど・・・

まず、最新の構造のテンプレートを作っておく。


■最新のテーブル構造のテンプレート(tbl.pl)
---------------------------------------------------------------------------------------

package tbl;

sub conf{
my %tbl = (

'table_a' => {
'column' => '
id INT AUTO_INCREMENT PRIMARY KEY,
column_1 VARCHAR(36),
column_2 VARCHAR(255),
column_3 VARCHAR(255)
',
'option' => '
ENGINE = MyISAM DEFAULT CHARSET = utf8
'
},

'table_b' => {
'column' => '
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
column_1 INT,
column_2 VARCHAR(255),
column_3 VARCHAR(255)
',
'option' => '
ENGINE = MyISAM DEFAULT CHARSET = utf8
'
},



);
return \%tbl;
}
1;

---------------------------------------------------------------------------------------


テンプレートを読み込んで新しいテーブルを一時作成し、
そのテーブルに既存のテーブルのデータをインポートする。

■更新処理(tbl_update.pl)
---------------------------------------------------------------------------------------
#!/usr/bin/perl

package::main;

my $dbh;



# この辺でデータベースへの接続処理(詳細割愛)

print "Content-type: text/html\n\n";

print "<p>データベースの再構築を開始します</p>\n";

# 既存のテーブル名をリストで取得
my $sth = $dbh->prepare("SHOW TABLES LIKE 'hoge_%'") || die $dbh->errstr();
$sth->execute();
my @hoge_tbl = ();
while(my @tbl = $sth->fetchrow_array()){
push(@hoge_tbl,$tbl[0]);
}

# テンプレート読み込み
require "./tbl.pl";
my $tbl = &tbl::conf(); # ※リファレンスで受け取り

foreach(@hoge_tbl){

# 既存テーブルのカラム名をリストで取得
my @cols = ();
my $sth = $dbh->prepare("DESCRIBE $_") || die $dbh->errstr();
$sth->execute();
while(my $href = $sth->fetchrow_hashref()){
push(@cols,$href->{'Field'});  # unshiftしちゃだめよ
}
my $cols = join(',',@cols);

#####################################
# テーブル名の末尾に個別にIDとかが付いてなければこの処理はいらない
$_ =~ /hoge_(.+?)(_.+?)?$/;
my $name = $1;
######################################

# 新しい構造の空テーブルを「pre_***」という名前で一旦作成
my $sth = $dbh->prepare("
CREATE TABLE IF NOT EXISTS pre_$_ (
$$tbl{$name}{'column'}
) $$tbl{$name}{'option'}
") || die $dbh->errstr();
$sth->execute();

# 既存のテーブルのデータを、カラム名を明示的に指定して挿入
# これをしないとカラムの数が合いませんよ!と怒られる
my $sth = $dbh->prepare("INSERT INTO pre_$_ ($cols) SELECT * FROM $_"); # ※1
$sth->execute();

# 既存のテーブル削除
my $sth = $dbh->prepare("DROP TABLE IF EXISTS $_");
$sth->execute();

# 新しいテーブルのリネーム("pre_"をとる)
my $sth = $dbh->prepare("ALTER TABLE pre_$_ RENAME $_");
$sth->execute();

}

print "<p>データベースの再構築が完了しました。</p>\n";

exit;
---------------------------------------------------------------------------------------


以上。

新しいテーブルにカラムが追加されてる場合は問題ないが、カラムが削除された場合は、多分※1のところでエラーになる・・・

こういうの一発でできるSQLないのかなぁ。

SQLだけでこういう処理をする方法が調べてもわからないので、スクリプト側でチェックしてみる。

メイン
------------------------------------------------------------------------------------------
#!/usr/bin/perl -w

use strict;
use DBI;
require './db.pl';

&db::open();

 my $table = 'table_name';
 ##追加カラム情報セット
 my @add = (
  ['field1' ,'INT' ],
  ['field2' ,'VARCHAR(255)' ],
  ['field3' ,'DATE' ],
  ['field4' ,'INT', ],
  ['field5' ,'VARCHAR(255)' ],
  ['field6' ,'VARCHAR(255)' ],
  ['field7' ,'INT' ]
 );
 my $cols = '';
 ##現在のテーブル情報を取得
 &db::query("DESCRIBE $table");
 while(my $href = $db::sth->fetchrow_hashref()){
  ##カラム名をセット
  $cols .= '<>'.$href->{'Field'}.'<>';
 }
 ##無ければ追加
 foreach(@add){
  if($cols !~ /<>$_[0]<>/){
    &db::query("ALTER TABLE $table ADD $_[0] $_[1]");
  }
 }

&db::close();
exit;
---------------------------------------------------------------------------------------



DB接続(db.pl)
---------------------------------------------------------------------------------------
package db;

use strict;

sub open{
my $dbs = "DBI:mysql:dbname:dbhost";
our $dbh = DBI->connect($dbs,dbuser,dbpass);
if(!$dbh){
die "データベースに接続できません";
}
}

sub query{
my ($sql) = @_;
our $sth = $db::dbh->prepare($sql) || die $db::dbh->errstr();
my $exec = $sth->execute || die $sth->errstr();
return $exec;
}

sub close{
$db::sth->finish;
$db::dbh->disconnect;
}

1;
---------------------------------------------------------------------------------------