Practice of Programming

プログラム とか Linuxとかの話題

IO::Pty::Easy が簡単・便利

ちょっと遊んでいたら見つけたのですが、これ簡単ですね。

use IO::Pty::Easy;
my $pty = IO::Pty::Easy->new;

$pty->spawn("mysql -u root");
while ($pty->is_active) {
  while (my $o = $pty->read(1)) {
    print $o;
  }
  if (my $sql = <>) {
      $pty->write($sql);
  } else {
      $pty->write("exit\n");
      last;
  }
}

こんな感じにするだけで、perl から mysql の shell が叩けるよ。
いや、別に誰も嬉しくないけどさ。


top も叩ける。

use IO::Pty::Easy;
my $pty = IO::Pty::Easy->new;

$pty->spawn("top");
while ($pty->is_active) {
    while (my $o = $pty->read(2)) {
	print $o;
    }
}

いや、別に誰も嬉しくないけどさ。


余談ですが、batch 処理でtopを使うなら、

% top -b

を使うと良いですよ。


Expect 使って書くような処理を書いてみる。

use IO::Pty::Easy;
my $pty = IO::Pty::Easy->new;

$pty->spawn("htpasswd -c test_passwd ktat");
while ($pty->is_active) {
    $|= 1;
    while (my $o = $pty->read(1)) {
	print $o;
	if ($o =~m{password:}) {
	    my $passwd;
	    {
		chomp($passwd = <>);
		$passwd or redo;
	    }
	    $pty->write($passwd . "\n");
	}
    }
}

使いどころはあるんじゃないかな。


vi でファイルを作って、perlで実行とか。

use IO::Pty::Easy;
my $pty = IO::Pty::Easy->new;

$pty->spawn("vi test_pty.pl");
my @operation = (
                 chr(0x1b) . 'V',
                 'G',
                 'x',
                 'i',
                 '#!/usr/bin/perl' . "\n",
                 'print "Hello, World!\n"' . "\n",
                 chr(0x1b),
                 ":wq\n",
                );

while (@operation) {
  $pty->write(shift @operation);
}

while ($pty->is_active) {
  while (my $o = $pty->read(1)) {
    # print $o;
  }
}

$pty->spawn('perl test_pty.pl');

while (my $o = $pty->read(1)) {
    print $o;
}

SQL の Query Builder についてのとりとめのない話

誰か、ココが嫌だよQuery Builder的なエントリを書いてくれないかな。

使いどころ

  • 動的に条件を組み立てるところ(whereに与えるカラムが変わったりする)

使わなかったら、複雑な条件を動的に組み立てる場合は、sprintfでSQLを組み立てるみたいなことになって、それは劣化Query Builderを自作しているのと変わらないんじゃないですかね。


生のSQLも別に嫌いじゃないし、普通に書いてるけど、コードにはあんまり書かないです。

Query Builder のパフォーマンス

パフォーマンスについては、クエリを数個作る程度のことが多少遅くても、ボトルネックにはならない。
1/7500秒で処理できていたのが、1/2500秒かかるようになったとしても、困る人はあんまりいない。

Query Builder は余計なことするからなー

しません。


DBIx::Class はリレーションを設定していると、while で回して、foreign key で数珠つなぎにやってたら、千回くらいクエリ投げちゃった♪
みたいなことはあり得るかも知れませんけど。SQL::Abstract自体は、SQLを組み立てるだけです。
それは、Query Builderの問題ではなくて、ORマッパーの問題です。

SQL::Abstract::Moreは普通にSQL書くのとあまり変わらない

join はちょっと微妙なところはありますが。また、where 部分とかはどっかで組み立てた結果をそこに当てることになること場合は、ぱっと見分かりませんけどね。where の順番とかはハッシュで渡すと変わってしまいますが、配列リファレンスで一つずつ条件を渡せば、順番通りになります。

my ($s, @binds) = $sql->select                  # select
  (
   '-columns' => [                              # x.id, y.id, count(*) as cnt
		  'x.id',
		  'y.id',
		  'count(*) as cnt',
		 ],
   '-from' => [                                 # from
	       '-join',
	       'table1|x',                      # table1 as x
	       'x.id=y.table1_id,x.col1=y.col1',# on x.id = y.table1_id and x.col1 = y.col1  <- ここだけ、順番が違う
	       'table2|y'                       # inner join table2 as y  
	      ],
   '-where' => {                                 # where
		'x.type' => '1',                 # x.type = 1 and date = '2011-12-05' and y.name = 'name'
		'date'   => '2011-12-05',
		'y.name' => 'name'
	       },
   '-group_by' => [                      # group by (x.id, y.id)
		   'x.id',
		   'y.id'
		  ],
   '-limit' => 3                         # limit 3
  );

運用者の立場

というわけでもないけど、DBIx::QueryLogで吐き出されるSQLを見て、index使われてないからこのカラムもwhereに入れてとか、それパーティショニング使われてないから、このカラムは必ず入れてみたいなのは、Query Builder に何を使おうが別に問題なくできる。
デバッグでも、基本的にQueryLogを見て、組み立て部分間違ってないかなぁというのをチェックするとかが多い。
本番にいきなり投入されて、なんか遅くなっちゃったんだけど…とかだったらご残念としか言いようがないけど。


まぁ、この辺は、Query Builderと関係ないレイヤーの話なんじゃないかと思ったりします。


※その話でいうと、SQL::Abstract::Moreで吐き出すSQLが整形されていないのがみにくいのは結構見づらいので、整形するコードでも書こうかなぁと思ったり、思わなかったりする。

Teng::Plugin::SearchBySQLAbstractMore リリース

ケースによりますが、最近のプロジェクトだと、searchメソッドじゃ足りないことが多かった(50%くらい)ので、SQL::Abstract::Moreをクエリービルダーに使えるようにしました。
Teng::Plugin::SearchBySQLAbstractMore

リリースしたらblog書こうと思ってたら、リリースしては改変を繰り返してしまい、0.06っていう…どんだけバージョン上げてんだかorz
気を取りなおして…。

Tengのクエリービルダーとして、SQL::Abstract::Moreを使えるようになります。
これの利点としては、

  • group by 可能
  • having とかも書ける
  • (x = 1 or y = 2) のような別カラムの or が書きやすい
  • 複雑な join も書ける
  • SQL::Abstractに慣れてる人は書きやすい & SQL::Abstractを素で使うよりは全然楽

という、感じです。
デメリットとしては、

  • クエリが整形されてないのでデバッグ時に見づらい
  • SQL::Makerより2倍以上遅い
                    Rate sql_abstract_more      sql_abstract         sql_maker
sql_abstract_more 2692/s                --              -21%              -64%
sql_abstract      3422/s               27%                --              -55%
sql_maker         7566/s              181%              121%                --

ベンチマークのコードは、https://gist.github.com/1566118
とはいえ、実際問題としてはこれがボトルネックになることはないでしょう。


Teng::Plugin::SearchBySQLAbstractMore には、以下の5つのプラグインがあります(最初3つだったんだけどな...)。

  • Teng::Plugin::SearchBySQLAbstractMore
  • Teng::Plugin::SearchBySQLAbstractMore::Pager
  • Teng::Plugin::SearchBySQLAbstractMore::Pager::MySQLFoundRows
  • Teng::Plugin::SearchBySQLAbstractMore::Pager::Count
  • Teng::Plugin::SearchBySQLAbstractMore::Pager::CountOrMySQLFoundRows

で、普通にload_pluginすれば、それぞれ、

  • search_by_sql_abstrat_more
  • search_by_sql_abstrat_more_with_pager
  • search_by_sql_abstrat_more_with_pager (CALC_FOUND_ROWS を使うやつ)
  • search_by_sql_abstrat_more_with_pager (count(*) を使うやつ -- group by や having を使ってると微妙かも知れない)
  • search_by_sql_abstrat_more_with_pager (count(*) or CALC_FOUND_ROWS(group by or havingがある時のみ)を使う)

が生えます。Teng の search と一応互換性があるはず。Tengのsearch関連のテストをいくつか通しています。


Tengと同様の使い方で引数を渡す場合は、Tengの引数 -> SQL::Abstract::Moreに渡せるように変換しているのですが、
SQL::Abstract::More と同様の引数も取ることが出来ます。
その場合は、第一引数を table名の代わりにハッシュリファレンスを渡します。

 $teng->search({
    -columns => [qw/col1 col2 col3/],
    -from => 'table',
    -where => {
        col1 => {'>' => "1"},
    },
 });

この渡し方であれば、SQL::Abstract::Moreと全く同じ使い方です。
SQL::Abstract::Moreの使い方については、SQLを組み立てるものという記事に若干書いてます。


search、search_with_pager というメソッド名で使いたい場合は、

 YourClass->load_plugin('Teng::Plugin::SearchBySQLAbstractMore', {
    alias => 'search_by_sql_abstract_more' => 'search',
 });
 YourClass->load_plugin('Teng::Plugin::SearchBySQLAbstractMore::Pager', {
    alias => 'search_by_sql_abstract_more_with_pager' => 'search_with_pager',
 });
 YourClass->install_sql_abstract_more(pager => 'Pager'); # or pager => 'Pager::MySQLFoundRows'

のようにすれば、良いかと思います。メンドクサイよっていう人は、

 use Teng::Plugin::SearchBySQLAbstractMore;
 YourClass->install_sql_abstract_more(alias => 'search', pager => 'Pager'); # or pager => 'Pager::MySQLFoundRows' / 'Pager::Count'

とすると、

  1. YourClass に search を生やす
  2. YourClass に search_with_pager を生やす

※aliasって、ドキュメントに書かれてないので、ずっと残る物なのかは知らないです。


Teng::search を置き換えることもできます。

 use Teng::Plugin::SearchBySQLAbstractMore;
 YourClass->install_sql_abstract_more(pager => 'Pager', replace => 1);

とすると、

  1. Teng の search メソッドを search_by_sql_abstract_more で置き換え
  2. search_with_pager を生やす

Teng::search を置き換えるのはあんまりよろしくない気がするので、気になる方は使わない方が良いと思います。
ていうか、いらない気がしますね。無くすかも知れません。

DBにOracleを使ってるんだよ、とか言う場合は、

YourClass->sql_abstract_more_new_option(sql_dialect => 'Oracle');

のように、渡すことが出来ます(Oracleないので確かめてないけど)。このnew_optionに渡した引数は、そのままSQL::Abstract::Moreのnew時に渡されます。
詳しくはそちらのドキュメントを読んで頂ければと思います。

Tengのsearchだと表現できないけど、SQLの条件を文字列で組み立てるのはちょっと…
っていう方は使ってみて頂ければと思います。


追記: DBIx::QueryLogを使う場合、以下のコードを書いておいたほうが良いです。

%DBIx::QueryLog::SKIP_PKG_MAP = (
      'Teng::Plugin::SearchBySQLAbstractMore' => 1,
      'Teng::Plugin::SearchBySQLAbstractMore::Pager' => 1, # ここは使うPagerプラグインを適当に
      %DBIx::QueryLog::SKIP_PKG_MAP
);

ここに書かれたpackageは、無視して、その先までたどってログに行数を出してくれます。

xaicron さんの以下の記事に書かれています。
http://blog.livedoor.jp/xaicron/archives/51554520.html

Text::Parts 0.08 をリリース

先日リリースしました。
https://metacpan.org/module/Text::Parts

write_files の性能がちょい上がってます。それだけ。

Text::Parts::Partオブジェクトを作るときに、file open & seek しているのですが、write_filesの場合は、頭からファイルをreadして書きだすだけなので、ファイルのopenとseekが分けたパーツの分無駄なので、やめるようにしました。

ので、3Gを40分割で2分くらいかかっていたのが、1分40秒くらいですむようになりました。

Text::Parts 0.07 をリリース

Text::Parts 0.07
0.06も上げたのですが、微妙なところがあって、すぐに0.07をリリースしてしまった。

変わったところ。

Text::Partsにwrite_filesメソッド追加。

  $s->write_files('file%d.csv', num => 4);

みたいな感じ。
# 0.06で追加したけど、0.07でインターフェースを変えてしまいました

Text::Parts::Partにwrite_fileメソッド追加。

 foreach my $part ($s->split(num => 4)) {
    $part->write_file("filename" . $i++ . ".csv");
 }

Text::Parts::Partにallメソッド追加

 foreach my $part ($s->split(num => 4)) {
     my $all = $part->all; # $partの範囲全部
     # ...
 }

ちなみに、実装順番は、紹介順の逆でした。


Parallel::ForkManagerと一緒に使う例も書いていることだし、実際試してみよう、と思って試してみたのですが、
スピードはforkしても、しなくても一緒でした。無駄に並列に動いているのでCPUの損です!
同一ディスク上だから、スピード上がんないよってことかな。RAID上だったら、もしかしたら意味あるかもしれません。

追記: もちろん、ループ内での処理が非常に遅い(大量データをDBへinsertとか)なら並列実行に意味はあるかと思います。
ちなみに、3Gのファイルを40個に分割して書きだすのは、2分くらいかかりました(読み書きとも同一HDD)。

SQLを組み立てるもの

最近、Tengを使っていますが、それほど複雑でもないSQLを組み立てるのに、searchメソッドは使いにくいと思う時が割と多い(group by出来ないとか、x = ? or y = ? とか書きにくいとか)。たぶん、方針として、シンプルなSQL以外は、search_by_sqlをっていうことなのだろうと思います。


ですが、SQLを文字列として組み立てるのは、割とめんどくさいので、SQLを組み立てるモジュールを使って、search_by_sqlに渡しています。


O/Rマッパーで、SQLのクエリを組み立てるものが組み込まれてますが、Tengで使われているのは、SQL::MakerDBIx::Classだと、SQL::Abstract。Tengのsearchメソッドでは、SQL::Makerのselect メソッドを使っていますので、出来ることは、selectメソッドに出来ることに限られます。SQL::Makerをフルで使う場合は、>query_buildersql_builderからnew_selectとか使えばSQL::Maker::Selectのオブジェクト取れますので、それを使ってごにょごにょできます。

SQL::Maker

SQL::Makerはコードが読みやすくてすっきりしています。ただ、andで書く場合や、一つのカラムについてのorを書く場合は良いんですが、x = ? OR y = ? とかが書くのが、ちょっとめんどくさいと思う(もっと簡単にかけますかね?)

  use SQL::Maker;
  my $q = SQL::Maker::Select->new(driver => 'mysql');
  my $c1 = SQL::Maker::Condition->new;
  $c1->add(hoge => 1);
  my $c2 = SQL::Maker::Condition->new;
  $c2->add(fuga => 2);
  my $c3 = SQL::Maker::Condition->new;
  $c3->add(xxx => 2);

  $q->add_from('hoge')->set_where(($c1 | $c2) & $c3)->add_select('id');
  my $sql = $q->as_sql;
  my @binds = $q->bind;

  print $sql, "\n";
  print join ", ",@binds;

結果。

SELECT id
FROM hoge
WHERE (((hoge = ?)) OR ((fuga = ?))) AND ((xxx = ?))
1, 2, 2

コードを見たときに条件の組み立てが分かりやすいのはいいとは思います。後、結果のSQLが改行されていて見やすい。


自分で、SQL::Maker::Selectオブジェクトを使ってSQLを作る場合に、order by, limit, offsetなどの処理を、selectメソッドと同じような引数で同じような処理をしたくても、その辺の処理は select メソッド内で書かれていて外から呼べないので、自前で書くか SQL::Makerからコピペする必要があります。

SQL::Abstract

SQL::Abstractだと、

  use SQL::Abstract;
  my $sql = new SQL::Abstract;
  my ($s, @binds) =$sql->select('hoge', ['id'], {-or => [{hoge => 1}, {fuga => 2}], xxx => 2});
  print $s, "\n";
  print join ", ", @binds;

結果。

SELECT id FROM hoge WHERE ( ( ( hoge = ? OR fuga = ? ) AND xxx = ? ) )
1, 2, 2

ただ、SQL::Abstractも、joinとかしようと思うと、割とめんどくさいなぁって気分になるというか、書き方を毎度忘れる。
limit使う場合は、SQL::Maker::Abstract::Limit とかも覚えられない。


ちなみに、SQL::Abstractについては、zigorouさんが、2年前に記事を書かれています。
http://perl-users.jp/articles/advent-calendar/2009/hacker/06.html

SQL::Abstract::More

で、SQL::Abstract::Moreというのを見つけました。
SQL::Abstract::Moreは、SQL::Abstractのサブクラスですが、hash一つで、割と綺麗に書ける気がする。

use SQL::Abstract::More;

my $sql = new SQL::Abstract::More;
my ($s, @binds) = $sql->select
    (
     -columns => ['*'],
      -from    => 'test',
      -where   => {
                   'hoge' => {'!=' => undef},
                   'fuga' => {'not in' => [1,2,3,4]},
                   'bar' => [
			     222
			    ],
                   '-or' => [
                             {xxx => 1},
                             {yyy => 1}
                            ],
                  },
      '-group_by' => [
                      'group1',
                      'group2'
                     ],
      '-limit' => 3
     );
  print $s, "\n";
  print join ", ", @binds;

結果

SELECT * FROM test WHERE ( ( ( xxx = ? OR yyy = ? ) AND bar = ? AND fuga NOT IN ( ?, ?, ?, ? ) AND hoge IS NOT NULL ) ) GROUP BY group1, group2  LIMIT ? OFFSET ?
1,1,222,1,2,3,4,3,0

joinとかも、慣れたら覚えられるレベル。

use Data::Dumper;
print Dumper($s, \@binds);

use SQL::Abstract::More;
my $sql = new SQL::Abstract::More;
my ($s, @binds) = $sql->select
  (
   '-columns' => [
		  'x.id',
		  'y.id',
		  'count(*) as cnt',
		 ],
   '-where' => {
		'x.type' => '1',
		'date'   => '2011-12-05',
		'y.name' => 'name'
	       },
   '-from' => [
	       '-join',
	       'table1|x',              # from table1 as x
	       'x.id=y.table1_id', # on x.id = y.table1_id
	       'table2|y'              # inner join table2 as y
	      ],
   '-group_by' => [
		   'x.id',
		   'y.id'
		  ],
   '-limit' => 3
  );
  print $s, "\n";
  print join ", ", @binds;

こんな感じ。

SELECT x.id, y.id, count(*) as cnt FROM table1 AS x INNER JOIN table2 AS y ON ( x.id = y.table1_id ) WHERE ( ( date = ? AND x.type = ? AND y.name = ? ) ) GROUP BY x.id, y.id  LIMIT ? OFFSET ?
2011-12-05,1,name,3,0

fromのところを以下のようにすると LEFT JOIN。

   '-from' => [
	       '-join',
	       'table1|x',
	       '=>{x.id=y.table1_id}',
	       'tablr2|y'
	      ],

以下のようにjoinを書ける。

         '<=>' => '%s INNER JOIN %s ON %s',
          '=>' => '%s LEFT OUTER JOIN %s ON %s',
         '<='  => '%s RIGHT JOIN %s ON %s',
         '=='  => '%s NATURAL JOIN %s',

割と良さそうなので、Teng用に既存のメソッドと似た感じに使えるようにプラグインを書いてみた。まだ、テストはしていません。
Teng::Plugin::SearchBySQLAbstractMore

[Linux][vim] samba上のファイルをvimで編集する時のtimestampの問題を回避する設定

どうも、samba上でvimでファイルを編集していると、変更してないのに変更されていると文句を言ってくる。

WARNING: The file has been changed since reading it!!!
警告: 読込んだ後にファイルに変更がありました!!!

こいつです。


ググってみたところ、sambaのtimestampまわりの問題のようなんだけど、
バグ報告はあるものの、だいぶ前に既に直っているとかで、closeされちゃったりしてて、なんかいまいちよくわからず。
回避できずにだいぶ放置してたんだけど…。
やっぱ、なんとかしようと思い、vimのマニュアルを読んでいると以下のものが使えそうな気がした。

  • set autoread
    • 変更を検知して読み直す
  • autocmd
    • イベントに関連付けてコマンドを実行できる
  • checktime
    • timestampのチェック

これらを利用して以下のように設定。

autocmd BufWritePost * sleep 1
autocmd BufWritePost * checktime
set autoread

これで、一応回避できました。
sleep 1 を入れないと、どうもダメでした。

※本当は、samba側でなんとかしたいんだけども