オープンソースこねこね

Webプログラミングなどについてあれこれ。

複数のmysqldプロセスを起動するinit.dスクリプト

一つのサーバで複数のmysqldプロセスを起動させたいときがあります。 この用途のために標準でmysqld_multiというコマンドがあるのですが、

mysqld_multi — 複数のMySQL サーバ管理

このコマンドによるmysqldの起動、停止処理は即時レスポンスを返してしまって、プロセスが完全に立ち上がるのを待ってくれません。 というわけで、起動処理をちゃんと待ってレスポンスを返すような起動スクリプトが欲しかったのでinitd.dスクリプトを自作しました。

以下のスクリプトファイルを/etc/init.d/配下に実行権限つきでおいてやってください。

設定ファイルは

  • /etc/my.multi.foo.cnf
  • /etc/my.multi.bar.cnf

のようにmy.multi.#name#.cnfという形式で複数用意し、datadirpid-fileの設定などをそれぞれユニークにした設定を記述します。(MySQLのドキュメントにもあるように同じデータを複数プロセスで読み書きするのはできません)

あとは

$ sudo /etc/init.d/mysqld_multi start

を実行すれば

mysqld.foo を起動中:                                 [  OK  ]
mysqld.bar を起動中:                                 [  OK  ]

のように複数のプロセスが起動してくれます。

MySQL server version for the right syntax to use near 'delimiter ... というエラーで少しハマった件

というMigrationプログラムを自作したのですが、このプログラムでMySQLのトリガを作成するSQLを実行したら、

mysql server version for the right syntax to use near 'delimiter ...

というエラーがでて動作しませんでした。PHPからPDOでMySQLに接続してSQLを実行しているだけなんですが。

で、ちょっと調べたら、トリガ作成の時に使用してるdelimiterというコマンドが問題で、これはSQLではなくて、mysqlのクライアント/usr/bin/mysqlが用意しているコマンドとのこと。は~。どうりで動かんわけだ。

とはいえdelimiterを使えないのはちょっと困るので、SQLの実行をPDOの代わりにmysqlコマンドで行うことができるようにPHPMigrateをアップデートしました。

https://github.com/kohkimakimoto/phpmigrate

以下のように設定してやれば、DBへの接続はPDOの代わりにmysqlコマンドを使用するになります。これでdelimiterも使えるようになります。

MigrationConfig::set('mysql_command_enable',    true);
MigrationConfig::set('mysql_command_cli',       "/usr/bin/mysql");
MigrationConfig::set('mysql_command_tmpsqldir', "/tmp");
MigrationConfig::set('mysql_command_host',      "localhost");
MigrationConfig::set('mysql_command_user',      "user");
MigrationConfig::set('mysql_command_password',  "password");
MigrationConfig::set('mysql_command_database',  "yourdatabase");
MigrationConfig::set('mysql_command_options',   "--default-character-set=utf8");

参考リンク

PHPMigrate - PHPでマイグレーションツールを作った

デプロイツールに引き続き、今度はPHPでMigrationツールを作りましたので、記事を書きます。

PHPMigrate - https://github.com/kohkimakimoto/phpmigrate

フレームワークとMigration

最近のWebアプリケーションフレームワークを使えば、まあだいたいMigrationの機能ついてくるんですよね。でもフレームワークに組み込まれているものだと、Migrationを実行するためにアプリを実行環境にデプロイしないとならなかったりします。Migrationとメインのアプリのコードがひとつのプロジェクトとしてフレームワークで規定された構造にパッケージされているので、Migrationのコードだけデプロイするというのはなかなか難しいものがあります。

Migrationで変更するDBのスキーマは実運用上だいたいカラムの追加やテーブルの追加がほとんどで、カラムにデフォルト値などを適切に設定しておけば、アプリのコードのデプロイより先行してDBのスキーマを変更しても問題ない。

けれどスキーマ変更より先にコードがデプロイされると、存在しないテーブルやカラムを参照してしまいエラーを起こしてしまいます。

デプロイ作業的には

DBスキーマ変更(Migration)→アプリのコードをデプロイ→キャッシュ削除などの後処理。

という順序でいきたいのだけど

アプリのコードをデプロイ→DBスキーマ変更(Migration)→キャッシュ削除などの後処理。

というようになってしまいます。これを避けるためには、Migrationだけを行う専用の実行環境を用意したりと、ちょっと工夫したりしなければならない。

フレームワークにMigrationが組み込まれているとDBの接続情報をアプリとMigrationで一元管理ができて、いいこともあるのだけど、個人的にはMigrationはフレームワークに対して、もうちょっと疎結合に構成されていたほうがいいんじゃないかという考えに至りました。

PHPMigrate

そういうわけで、すごくシンプルなMigrationツールをつくりました。

https://github.com/kohkimakimoto/phpmigrate

使い方

2013/04/22 追記

複数のDBに対してMigrationできるように修正したので、この記事の記述とは設定の書き方が変わりました。READMEを参考にしてください。

プログラムはmigrate.phpというPHPファイル一つだけなので、これをMigrationのタスクを管理するディレクトリにダウンロードします。ダウンロードしたら、エディタで開きます。ファイルの上の方にDBの接続情報を記述する箇所があるので、適宜環境にあわせて修正してください。

MigrationConfig::set('database_dsn',         'mysql:dbname=yourdatabase;host=localhost');
MigrationConfig::set('database_user',        'user');
MigrationConfig::set('database_password',    'password');
MigrationConfig::set('schema_version_table', 'schema_version');  # migarationの版を管理するテーブル名

修正したら

php migrate.php create [任意のマイグレーションタスク名]

というコマンドを打つと、

[タイムスタンプ]_[任意のマイグレーションタスク名].php

PHPファイルが生成されます。

<?php
/**
 * Migration class.
 */
class Dummy
{
  public function preUp()
  {
      // add the pre-migration code here
  }

  public function postUp()
  {
      // add the post-migration code here
  }

  public function preDown()
  {
      // add the pre-migration code here
  }

  public function postDown()
  {
      // add the post-migration code here
  }

  /**
   * Return the SQL statements for the Up migration
   *
   * @return string The SQL string to execute for the Up migration.
   */
  public function getUpSQL()
  {
    return "";
  }

  /**
   * Return the SQL statements for the Down migration
   *
   * @return string The SQL string to execute for the Down migration.
   */
  public function getDownSQL()
  {
     return "";
  }

}

そして、getUpSQLメソッドの戻り値にテーブル追加のSQL、getDownSQLにそれをもとに戻すSQLが戻されるようにコードを修正します。以下のような感じです。

  public function getUpSQL()
  {
     return <<<END

CREATE TABLE `sample` (
  `id` INT UNSIGNED NOT NULL,
  PRIMARY KEY (`id`) )
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COLLATE = utf8_bin;

END;
  }


  public function getDownSQL()
  {
     return <<<END

DROP TABLE `sample`;

END;
  }

テーブルの変更内容はSQLを直接書きます。SelectやInsertを抽象化してくれるORMは好きなのですが、テーブル定義を行うDDLのようなSQLRDBMSの実装の影響も強く受けやすいと思っているので、無理に抽象化しない方針で設計しました。このほうが細かい調整がしやすいし、DBが最終的にどういう定義になるのかがわかりやすいと思っています。

あとは、以下のコマンドでmigrationを実行します

php migrate.php migrate

これで、変更が実行されます(上記の例だとsampleというテーブルが作成される)。そしてMigrationの版管理をするschema_versionというテーブルが自動で作成されてそこにファイルのプリフィクスだったタイムスタンプが登録されます。

戻す場合は

php migrate.php down

で戻せます。また他のコマンドとして

php migrate.php up      # 複数あるマイグレーションタスクで1件だけ進ませる。
php migrate.php status  # 現在の状態の表示。現在の版と、未実施のマイグレーションの一覧を表示。

があります。

またDB定義を書くのに、プログラムであるmigrate.phpを直接修正したくない場合は適当なPHPファイルを作って以下のようにmigrate.phpを読み込めば、定義情報を外出しすることもできます。

<?php
require 'migrate.php';

MigrationConfig::set('database_dsn',      'mysql:dbname=yourdatabase;host=localhost_modify');
MigrationConfig::set('database_user',     'user_modify');
MigrationConfig::set('database_password', 'password_modify');
MigrationConfig::set('schema_version_table',  'schema_version_modify');

Migration::main();

Adminer - PHPの1ファイルを置くだけで簡単に使えるDB管理ツール

これ。

http://www.adminer.org/

あんまり使っている人をみたことないのだけど、個人的に最近よく使っていて気に入っているツールなので紹介。Adminer(アドミナーって読み方でいいのか?)はPHPのDB管理ツールで、なんと1ファイルをWebサーバに置くだけで動作します。超楽ちん。機能的にも申し分ない。

条件を指定してのデータの参照や、SQLの実行はもちろん、一覧表示されたデータから該当のカラムをダブルクリックすれば直接データ編集ができる。その際ページの読み込みもない。CSVの出力もできる。

画面のデザインも変えられて、公式サイトにあるなかでは

https://raw.github.com/vrana/adminer/master/designs/pilot/adminer.css

が一番すき。画面を広く使える。

LAMPのWebアプリを開発してるけど、管理ページを作ってる時間はない場合は、とりあえずこれ入れてみるとかでもいいかもしれません。もちろん開発時のデータ確認とかに限定してつかってもいいですし。

あと、このAdminer自体の開発は普通に複数のファイルで開発されていて最後に1ファイルにマージして提供されているようです。この1ファイルにマージするプログラムにちょっと興味あり。自分もツール系Webアプリを作って1ファイルにして他でも導入しやすくしてみたいなあー。

 

 

機械学習 - PHPとMeCabとMySQLでベイジアンフィルタを実装してみた。

PHPMeCabMySQLを使ってベイジアンフィルタを実装してみました。ひとまず動くようにはなったので公開します。機械学習によってテキストドキュメントの自動分類ができます。

高校数学すら忘れてしまっていたので、ついでに条件付き確率などを基礎から勉強してみたが、残念ながらあんまり理解していないです。

さて、実装したベイジアンフィルタですが、機械学習やカテゴリ推定のアルゴリズム部分はPHPで書いて、学習データはMySQLに保存するようにしてあります。ソースはgithubに置きました

https://github.com/kohkimakimoto/BayesClassifier 

使い方など

あとで書きます。。。

あと、カテゴリ推定のサンプルなども書く。。。

2012/04/20 - 追記

サービス作ってみました。

http://kohkimakimoto.hatenablog.com/entry/2012/04/20/125341

参考にしたサイトなど

http://gihyo.jp/dev/serial/01/machine-learning/0003

上記のサイトの他に、以前購入したWeb DB Press総集編に収録されている伊藤直也氏による記事も参考にしました(Vol56のアルゴリズムの連載記事)。というかアルゴリズムの部分は、ほとんどこの記事にあったPerlのコードをPHPに置き換えた感じです。

 

iPhoneの絵文字をMySQLにInsertしようとすると文字列がぶった切れる件

半日くらいハマったので、メモしておきます。

PHPMySQLを使ってWebアプリ開発しているのですが、この環境にiPhoneから絵文字入りのテキストデータをPOSTすると、絵文字を入力した後ろの所からバッサリと文字列が削除されてしまうという現象が起きていました。結論からいうとiPhoneの絵文字がUTF8の4バイト文字なのでMySQLに登録できなかったわけなのですが、その調査作業手順をまとめたメモです。

まずサーバやアプリのログを追ってみたのですが、よくわからず。そもそもサーバまでちゃんとデータが届いているのか調べるためtcpdumpしてみました。

# tcpdump -nX port 80 -s0 -i eth0 -w hogehoge.dump

のようにコマンドを実行すると、パケットを監視して内容をファイルにダンプするようになる。この状態でiPhoneから

abc[絵文字]abc

のようなテスト用テキストをPOSTします。これでTCPのダンプが取れます。続いて、取得したダンプファイルをローカルPCにダウンロードしてWiresharkというソフトを使って内容を確認します。

f:id:kohkimakimoto:20120203191741p:plain

HTTP POSTリクエストのところをクリックして詳細を見ます。POSTで送信されるデータは「Line-based text data: application/x-www-form-urlencoded」というセクションに書かれているのでココを見てみます。すると投稿データは以下のようになっているのがわかりました。

text=abc%F0%9F%98%A3abc

どうやら絵文字データちゃんとサーバまで届いているようです。HTTPのプロトコル上では「%F0%9F%98%A3」と表現されているのが絵文字の箇所。で、これはURLエンコーディングされているので元のデータは

F09F98A3

という4バイトデータということがわかります。ここで、おや?と感じる。いわゆる携帯(ガラケー)の絵文字はキャリアごとに仕様は異なるのですが、DBに保存するときUTF8の3バイト文字表現に変換して登録していました。UTF8の4バイト文字とはあんまり馴染みがない。

UTF8、4バイト、MySQL。ここいらをキーワードにしてぐぐる先生で検索。

http://www.mysql.gr.jp/mysqlml/mysql/msg/13823

はい。結構古い記事なのですがこれがドンピシャなようです。これによると「...4バイトのUTF-8の文字以降の全ての文字列が削除された上で格納される」らしいです。はい、そうなってました。よし、もうちょっと調べる。

http://nippondanji.blogspot.com/2010/04/mysqlmysql-554.html

によるとutf8mb4という文字コードを指定すれば4バイトまで入れられるっぽい。

とはいえ、そのまま4バイトデータを入れてもこれをガラケー向けに表示するには変換しなければならなかったり、現在動作中のDBの文字コードを途中で変えるのも何が起こるかよくわからんので、ひとまず4バイト文字は全部削除する方向で対応しました。

PHPで書くとこんな感じのコードです

$textwithout4byte = preg_replace('/[\xF0-\xF7][\x80-\xBF][\x80-\xBF][\x80-\xBF]/', '', $textwith4byte);

これで絵文字は削除されてしまうが、少なくとも絵文字から後ろがごっそり削除されるようなことにはならなくなりました。めでたしめでたし?