2013年06月

bootstrap の固定ナビゲーションとページ内アンカー問題

twitter-bootstrap の固定ナビゲーションバー(navbar-fixed-top)を使っている時に、ページ内アンカーでジャンプすると、固定ナビゲーションバーの太さの分だけ上部が隠れてしまう問題に遭遇したのでメモっておきます。

CSS はこんなかんじ。

.topbar {
   height: 40px;
   position: fixed;
   top: 0;
   left: 0;
   right: 0;
   z-index: 10000;
   overflow: visible;
}

body {
  padding-top: 40px;
}
ぐぐると色々な記事にヒットするのですが、どうやらまだオフィシャルに解決方法を提供していないようですね。StackOverflow の記事で見つけた css だけで解決する方法など色々と試したところ、最終的には github.com/twitter/bootstrap の Issue になっていたこちらのページの以下の方法が役に立ちました。

https://github.com/twitter/bootstrap/issues/1768#issuecomment-13306753
<head>
  <script>
    var shiftWindow = function() { scrollBy(0, -50) };
    window.addEventListener("hashchange", shiftWindow);
    function load() { if (window.location.hash) shiftWindow(); }
  </script>
</head>
<body onload="
load()"> 

この javascript コードにより、ページ内アンカーリンクをクリックした場合、およびアンカー付きのURLに直接飛んで来た場合に、ナビゲーションバーに表示が隠れることがなくなりました。動作的には、飛んだ後に scrollBy で微調整スクロールしているかんじですね。

これで完成か、と思ったのですが、試してみたところ chrome, firefox では動作するのですが、safari でうまく動作しないんですよね。safari のバージョンは 6.0.3 でした。

どうも、ページ内アンカーリンクをクリックしてジャンプする時の scrollBy 的な動作と、この javascript で書いている scrollBy の動作が競合しておかしな動きになっているような気配でした。試しに
<head>
  <script>
    var shiftWindow = function() { setTimeout("scrollBy(0, -50)", 200) };
    window.addEventListener("hashchange", shiftWindow);
    function load() { if (window.location.hash) shiftWindow(); }
  </script>
</head>
<body onload="load()">  

と setTimeout を付けて少し待たせてから scrollBy するようにするときちんと動作しました。じゃあ、それで解決?というわけではなく、この待ち時間のために、ちょっと待ってからピコッ!と移動するような動きになって UX 的に大変いけていないのがなんとかしたい。

最終的には jquery で次のように書くことで、期待通りの動作をしてくれるようになりました。
$(function() {
  $(window).hashchange(function(){
    var pos = $("a[name='" + location.hash.substr(1) + "']").offset().top;
    // $('html, body').animate({ scrollTop: pos - 40 }, 0); // does not jump well on page load on safari
    // $('html, body').animate({ scrollTop: pos - 40 }, 1); // works, but have to wait 1ms
    window.scrollTo(0, pos - 40); // worked on safari, chrome, firefox well
  });
  // trigger the event on page load.
  if (location.hash) { $(window).hashchange(); }
});

scrollBy (相対座標) ではなく scrollTo (絶対座標) の関数を使うことで、safari でも期待通りに動くようになるようです。その場合、アンカー先の絶対座標を取得する必要があるわけですが、素の javascript でそれをやろうとすると、element の親の親の親の親 .... と辿って全ての offsetTop 値を足し合わせる必要があって大変辛いので、その辺をすでにやってくれている jquery の offset() メソッドを使って書いています。

jquery なのだから、window.scrollTo() ではなく、jquery の animate() メソッドを使おうとしたのですが、そちらだと 1ms (かそれ以上)の待ち時間を追加しないとまた safari でまともに動作してくれず、setTimeout の時と同じくピコッ!と移動するような動きになってしまって嫌だったので、window.scrollTo をそのまま使うことにして落ち着きました。

ブラウザによる挙動の違いとか全く腑に落ちていないので何かツッコミあればぜひ。

ヒカリエのシアターのアラートTwitter Bot @orb_bot を導入しました

ORB Twitter Bot 作りました! => @orb_bot 

コードは手抜きだけど https://github.com/sonots/orb-rush-timer-twitter-bot に置きました。

発端: 


後記:かぶった!!ww







ヒカリエのシアターのアラート機能を導入しました

hackmylife: ヒカリエのシアターのアラート機能 http://hackmylife.net/archives/7935324.html

早速導入したので :D の人でチャンネル知りたい人は連絡ください

ありがたやーありがたやー

rails4 でバルクアップデート

こんにちは @sonots です。

rails4 で バルクインサートおよび、バルクアップデート(MySQL only) クエリを ActiveRecord から投げるには activerecord-import gem を使えます。rails4-rc2 でも動きましたのでご報告。

前準備

Gemfile に
gem 'activerecord-import'
を追加して
$ bundle
だけですね。

バルクインサート

ActiveRecord を使って以下のように create すると
10.times do |i|
  Book.create! :name => "book #{i}", :author => "author #{i}"
end
こんなかんじに個別に INSERT クエリが走って遅いですよね。
 INSERT INTO `books` (`author`, `created_at`, `name`, `updated_at`) VALUES ('author 0', '2013-06-22 08:47:44', 'book 0', '2013-06-22 08:47:44')
 INSERT INTO `books` (`author`, `created_at`, `name`, `updated_at`) VALUES ('author 1', '2013-06-22 08:47:44', 'book 1', '2013-06-22 08:47:44')
以下10個分 ….

そこでバルクインサートですよ。10個まとめてバルクインサートするにはこんなかんじで。
books = []
10.times do |i|
  books << Book.new(:name => "book #{i}", :author => "author #{i}")
end
Book.import books
実際のクエリはこんなかんじに1つの INSERT クエリになりますね。
  INSERT INTO `books` (`id`,`name`,`author`,`created_at`,`updated_at`) VALUES (NULL,'book 0','author 0','2013-06-22 09:23:10','2013-06-22 09:23:10'),(NULL,'book 1','author 1','2013-06-22 09:23:10','2013-06-22 09:23:10'),(NULL,'book 2','author 2','2013-06-22 09:23:10','2013-06-22 09:23:10'),(NULL,'book 3','author 3','2013-06-22 09:23:10','2013-06-22 09:23:10'),(NULL,'book 4','author 4','2013-06-22 09:23:10','2013-06-22 09:23:10'),(NULL,'book 5','author 5','2013-06-22 09:23:10','2013-06-22 09:23:10'),(NULL,'book 6','author 6','2013-06-22 09:23:10','2013-06-22 09:23:10'),(NULL,'book 7','author 7','2013-06-22 09:23:10','2013-06-22 09:23:10'),(NULL,'book 8','author 8','2013-06-22 09:23:10','2013-06-22 09:23:10'),(NULL,'book 9','author 9','2013-06-22 09:23:10','2013-06-22 09:23:10')

10個だと対した問題ではないかもしれませんが、1000個ぐらいになったらバルクインサートしたい所ですね。Benchmark 結果を見るには、InnoDB Table の場合、ActiveRecord の validation が有効な状態で4倍、validation が無効な状態で20倍スピードアップとのこと。

バルクアップデート

で、MySQL で使えるバルクアップデートですが、以下のように使います。name カラムを一括更新してみます。注意点は、Book.import の第一引数は ActiveRecord::Relation ではなく Array なので #to_a してあげないといけない所ですかね。
books = Book.all
books.each do |book|
  book.name = "updated #{book.name}"
end
Book.import books.to_a, :on_duplicate_key_update => [:name]
ON DUPLICATE KEY UPDATE を使ったバルクアップデートクエリを投げてくれます。
 INSERT INTO `books` (`id`,`name`,`author`,`created_at`,`updated_at`) VALUES (1,'updated book 0','author 0','2013-06-22 09:24:16','2013-06-22 09:24:16'),(2,'updated book 1','author 1','2013-06-22 09:24:16','2013-06-22 09:24:16'),(3,'updated book 2','author 2','2013-06-22 09:24:16','2013-06-22 09:24:16'),(4,'updated book 3','author 3','2013-06-22 09:24:16','2013-06-22 09:24:16'),(5,'updated book 4','author 4','2013-06-22 09:24:16','2013-06-22 09:24:16'),(6,'updated book 5','author 5','2013-06-22 09:24:16','2013-06-22 09:24:16'),(7,'updated book 6','author 6','2013-06-22 09:24:16','2013-06-22 09:24:16'),(8,'updated book 7','author 7','2013-06-22 09:24:16','2013-06-22 09:24:16'),(9,'updated book 8','author 8','2013-06-22 09:24:16','2013-06-22 09:24:16'),(10,'updated book 9','author 9','2013-06-22 09:24:16','2013-06-22 09:24:16') ON DUPLICATE KEY UPDATE `books`.`name`=VALUES(`name`),`books`.`updated_at`=VALUES(`updated_at`)

1度のバルクアップデートの数を絞る

さっきの例だと Book.all としていたので、全てのレコードを1度にバルクアップデートすることになります。あまりにもレコード数が多いとクエリサイズが巨大になってしまうので 1000 個ぐらいに抑えたいですよね。そういうときは ActiveRecord の #find_in_batches を使って細かくわけてクエリを投げましょう。今回はサンプルなので 2個ずつバルクアップデートしてみます。
Book.all.find_in_batches(:batch_size => 2) do |books|
  books.each do |book|
    book.name = "updated #{book.name}"
  end
  Book.import books.to_a, :on_duplicate_key_update => [:name]
end
実際のクエリはこんなかんじになりますね。
  SELECT `books`.* FROM `books` ORDER BY `books`.`id` ASC LIMIT 2
  INSERT INTO `books` (`id`,`name`,`created_at`,`updated_at`) VALUES (1,'updated updated book 0','2013-06-22 09:13:04','2013-06-22 09:13:04'),(2,'updated updated book 1','2013-06-22 09:13:04','2013-06-22 09:13:04') ON DUPLICATE KEY UPDATE `books`.`name`=VALUES(`name`),`books`.`updated_at`=VALUES(`updated_at`)
  SELECT `books`.* FROM `books` WHERE (`books`.`id` > 2) ORDER BY `books`.`id` ASC LIMIT 2
  INSERT INTO `books` (`id`,`name`,`created_at`,`updated_at`) VALUES (3,'updated updated book 2','2013-06-22 09:13:04','2013-06-22 09:13:04'),(4,'updated updated book 3','2013-06-22 09:13:04','2013-06-22 09:13:04') ON DUPLICATE KEY UPDATE `books`.`name`=VALUES(`name`),`books`.`updated_at`=VALUES(`updated_at`)
以下10個分 ….

カラムを絞る

ActiveRecord オブジェクトの配列を渡すと、全てのカラムのパラメータがバルクアップデートのクエリに載ってしまうようです。今回は :name カラムだけを更新したいのだからそこだけクエリに載せたいですよね。カラムを絞るには次のようにします。
books = Book.all
books.each do |book|
  book.name = "updated #{book.name}"
end
columns = [:id, :name] # 注意: レコード指定のためにプライマリーキー :id も指定する
values = books.map {|book| [book.id, book.name] }
Book.import columns, values, :on_duplicate_key_update => [:name]
クエリはこんなかんじです。:author 属性が減っていて少し節約ですね :D
  INSERT INTO `books` (`id`,`name`,`created_at`,`updated_at`) VALUES (1,'updated book 0','2013-06-22 09:29:48','2013-06-22 09:29:48'),(2,'updated book 1','2013-06-22 09:29:48','2013-06-22 09:29:48'),(3,'updated book 2','2013-06-22 09:29:48','2013-06-22 09:29:48'),(4,'updated book 3','2013-06-22 09:29:48','2013-06-22 09:29:48'),(5,'updated book 4','2013-06-22 09:29:48','2013-06-22 09:29:48'),(6,'updated book 5','2013-06-22 09:29:48','2013-06-22 09:29:48'),(7,'updated book 6','2013-06-22 09:29:48','2013-06-22 09:29:48'),(8,'updated book 7','2013-06-22 09:29:48','2013-06-22 09:29:48'),(9,'updated book 8','2013-06-22 09:29:48','2013-06-22 09:29:48'),(10,'updated book 9','2013-06-22 09:29:48','2013-06-22 09:29:48') ON DUPLICATE KEY UPDATE `books`.`name`=VALUES(`name`),`books`.`updated_at`=VALUES(`updated_at`)

updated_at を更新しない

updated_at も更新する必要がないとか、updated_at 分のクエリサイズも減らしたいとかいう時は、:timestamps => false オプションを使えます。
Book.import columns, values, :on_duplicate_key_update => [:name], :timestamps => false
大分スッキリしてうれしいですね :D
  INSERT INTO `books` (`id`,`name`) VALUES (1,'updated book 1'),(2,'updated book 2'),(3,'updated book 3'),(4,'updated book 4'),(5,'updated book 5'),(6,'updated book 6'),(7,'updated book 7'),(8,'updated book 8'),(9,'updated book 9'),(10,'updated book 10') ON DUPLICATE KEY UPDATE `books`.`name`=VALUES(`name`)

validation をオフ

デフォルトでは ActiveRecord モデルの validation を使いますが、validation を切ってしまえば Benchmark 結果のいうように、さらなるスピードアップが図れます。:validate => false オプションを使用します。
Book.import columns, values, :on_duplicate_key_update => [:name], :validate => false

まとめ

まとめのテンプレート的にはこんなかんじになりますかね。
columns = [:id, :name]
Book.all.find_in_batches(:batch_size => 1000) do |books|
  books.each do |book|
    book.name = "updated #{book.name}"
  end
  values = books.map {|book| [book.id, book.name] }
  Book.import columns, values, :on_duplicate_key_update => [:name], :timestamps => false, :validate => false
end

こんなかんじで MySQL のバルクアップデートを効率的に使えば20倍スピードアップできるので積極的に使っていきましょう!参考: Benchmark

あれ、rails4 関係なかった。まぁ、動きましたよ、ということで ^^;

それでは!

RubyKaigi 2013に参加&LTしてきた #rubykaigi

こんにちは @sonots です。5/30 - 6/1 の3日間 RubyKaigi 2013 に参加してきたので何か書いておきます!
d252399e5e9393c33ed0101f0a93a70e

実は、初 RubyKaigi でございまして、というのも Ruby を本気で使い始めたのは一昨年の RubyKaigi の時期ぐらいからで(その前は組み込みエンジニアでした。C++/Objective-C バリバリでしたね)、そのときは参加できなくて、去年は開催されなかった、ということで念願の初 RubyKaigi って感じですね。再開催してくださった @kakutani さんをはじめとする運営の方々ありがとうございます!

で、満を持しての RubyKaigi なんですが、待ってました感が溜まっていて絶対何か話したい!と思っていたので LT に申し込みました。無事採択されて発表してきましたので、資料とustreamへのリンクを載せておきます!このブログで一番書いている Haikanko の話を大舞台でやってまいりました in English :D

Ustream (こちらの2番目です) => http://www.ustream.tv/recorded/33573319 
 


で、RubyKaigiの感想なんですが、

印象に残った発表(初日)

初日の発表の中では以下の3本セットが印象に残りましたね。 
RubyMotion、JRuby、CRuby (MRI) それぞれでコンパイラの中間表現がでてきて、それぞれの処理系での違いとかを、1日のうちに続けて聞けたからこそ自分の中で比較ができて良かったです。流石のうまいスケジューリングでした!
とくに、普段は RubyMotion、JRuby の中の人から直接話をきける機会がないので、話がきけてホント良かったですねー

印象に残った発表(二日目)

自分がインフラサイドのエンジニアということもあって、@mirakui さんの High Performance Rails - Issei Naruta (ustream) は大変楽しく拝聴させていただきました。
cookpad さんが GCを止めてる、というのは良く聞いていた話でしたが、実際どうやって止めるのか調べてなかったので(^^; 今回の話で聞けてありがたかったです。
routes が遅い、とかその他諸々も参考になりました!まぁでも、自分が運用するのは perl 環境だったりするんですけどね(ごにょごにょ

あと、3日目の発表とあわせてしまいますが、
この辺りの Fat Model 問題に対するお話シリーズが良かったですね。それぞれやり方は違いますが、共通で持っている課題は同じで、今熱い論点だと思います。
とくに Patterns の発表は、あのパターンを Haikanko の Fat になっている所に適用すればクリーンに出来るな、というアイデアも沸いてきたのでありがたかったです。

印象に残った発表(三日目)

今度は2日目の発表とあわせてしまいますが、
といった非同期処理関連のお話シリーズがよかったですね。ただ、自分の知識不足の所もあったので、Goroutine とか別言語でのアイデアも含めて体系的に学んでおかないとあかんな!と思いました。

あとは、ruby コミュニティでも仲良くさせていただいている @tkawa さんの REST の話。時間の都合上 @tkawa さんの提唱しているすべてのパターンについては聞けませんでしたが、今度改めて全パターン聞きたいですね ^^

そして、もちろん Fluentd talk ですね!
ログデータは一般的にそれぞれスキーマが違う&カラムを増やしたくなる、というのがあって、fluentd の場合ログの保存先に NoSQL である mongo を使うこと(人)が多いのですが、@kuenishi さんの発表で riak もアリだな!と思いました。NoSQL なのに紹介されていた mohair を使うとSQLクエリで集計をかけられる、ということでSQL好きエンジニアには大変便利っぽいですね!

そして、@tagomoris さんの発表されていた norikra がすばらしい!fluentd クラスタを流れているデータに対するクエリ、stream query、を発行できるというものです。
これでもう集計処理をするための fluent-plugin-なんちゃらcounter のようなオレオレプラグインからはおさらばできます!
自分の手元でもオレオレプラグインが増えてきていて、いっその事 select ほんにゃら的な文法で集計処理を定義できる fluent-plugin 作ったろうか!と思ったことがあったのですが、これはまさしくそれに対する解というか、それ以上の解なので本当にありがたい!
「No more restart fluentd. No more private plugins. No more fat fluentd configurations!」ということで、きたワー、という感じでした!

スポンサーについて

RubyKaigi はスポンサー企業がすごい充実していてすばらしかったです!Microsoft 社の無限ドリンクありがたかったですし、Heroku 弁当は大変美味でございました。ありがとうございました!

あと、RICOH 社の電子ホワイトボード、初めて触ったのですが面白かったですね^^ #半ステマ
575637_10101812436015548_1987378931_n.jpg

ただちょっと苦言を呈しますが、弊社の名前を冠したネットワークが切れやすかったり、初日 github.com 見れなかったりしたのが残念でしたね。いちおう言い訳をしておくと、DeNA は提供までしかやっていないので、決して DeNA のネットワーク力が弱いということではございません!なのですが、あれだけ SSID で社名を主張するなら、ネットワークエンジニアも派遣して責任もって構築するべきだったのでは?と思わないではないですね! 

追記:ネットワーク番長を してくださった@koiwaさんには大変感謝です!DeNAの人はみんなお礼を言うべき!

PS. 個人的な話

個人的な話ですが DeNAとRICOH が一緒にスポンサー紹介されていたのを見て、ちょっとぐっときました。はい、個人的な話です…
2013-05-30 10.26.02.jpg

まとめ
日本の東京にいて自宅からちょっと通うだけで、Rails, RubyMotion, JRuby, Sinatra の中の人といった、海外に行かないと話をきけないようなエンジニアの話を聞ける機会を得られて本当に幸運だったと思います。サイコーでした!@kakutani さんをはじめとする運営の方々の手腕が光っていました :D

本当におつかれさまでした!ありがとうございました!
A Ruby and Fluentd committer working at DeNA. 記事本文および記事中のコード片は引用および特記あるものを除いてすべて修正BSDライセンスとします。 #ruby #fluentd #growthforecast #haikanko #yohoushi #specinfra #serverspec #focuslight
はてぶ人気エントリー