Sequelのカラム名にはシンボルを使うこと

http://sequel.rubyforge.org/rdoc/files/README_rdoc.html

Column references in Sequel

Sequel expects column names to be specified using symbols.

はまったのでメモ。


条件に沿って"field1", "field2"のようなフィールドにアクセスするときに
間違ってもStringでSequelのwhereとかselectとかに渡してはいけない。


そのまま「文字列」として処理されてしまう。

mysql> select * from foo;
+------+------------+-----------------+
| id   | field1     | field2          |
+------+------------+-----------------+
|    1 | fooooo!!!! | FOOOOOOO!??!?!? |
+------+------------+-----------------+
1 row in set (0.00 sec)
$ sequel mysql2://user:password@localhost/hoge

2.0.0-p247 :001 > puts DB[:foo].select("field1").sql
SELECT 'field1' FROM `foo`
 => nil
# ↑field1はバッククオートではなくシングルクオート、つまり文字列!

2.0.0-p247 :002 > puts DB[:foo].select("field1").all
{:field1=>"field1"}
 => nil
# ↑得られる結果も当然そのまま!

2.0.0-p247 :003 > puts DB[:foo].select(:field1).sql
SELECT `field1` FROM `foo`
 => nil

2.0.0-p247 :004 > puts DB[:foo].select(:field1).all
{:field1=>"fooooo!!!!"}
 => nil
# ↑本当はこうしないといけなかった!

2.0.0-p247 :005 > num = 1
 => 1

2.0.0-p247 :006 > puts DB[:foo].select(:"field#{num}").sql
SELECT `field1` FROM `foo`
 => nil

2.0.0-p247 :007 > puts DB[:foo].select(:"field#{num}").all
{:field1=>"fooooo!!!!"}
 => nil
# ↑動的に作る場合ももちろんシンボルにすること!

Oj.dumpでシンボルを吐く時は:mode => compatする

Ojを知ったきっかけ

Padrinoでスケルトンを作るとGemfileにojというgemが入っている。
Optimized JSONの略らしい。


標準ライブラリとなったjsonよりも早いらしい。


JSONの替わりに使ったOJが速い - Qiita


自分も、開発しているサービスで使われているJSONデータを使ってやってみたら、
確かにOjの方が早かった。

https://gist.github.com/rightgo09/6492700

Ojのdump mode

で、改めて触ってたら、デフォルトでOj.dumpでJSONを吐くと、
シンボルはすべて":hoge"のようになることが分かったのでメモ。


もしそれが意図しておらず、JavaScriptでコロンは不要なら、

:mode => :compat

が必要である。

Oj.dump({ :hoge => :fuga })
#=> {":hoge":":fuga"}

Oj.dump({ :hoge => :fuga }, :mode => :compat)
#=> {"hoge":"fuga"}


他のmodeは

  • :strict
    • JSONと同じ型だけをdumpできる
      • FalseClass, NilClass, TrueClass, Hash, Array, Numeric, String
    • これら以外のRubyの型が含まれているデータをdumpしようとするとTypeError Exceptionが投げられる
  • :null
    • :strict modeでExceptionが投げられるデータがnullになって、例外が投げられなくなる
  • :object
    • オブジェクトを渡すと、オブジェクトのインスタンス変数をdumpしてくれる
    • これがパフォーマンス的に最高のモードらしい
  • :compat
    • compatはcompatibilityの略
    • 渡されたObjectがto_hashかto_jsonを呼べればそれを使う。なければOjがよしなにしてくれる


また、default_optionsを最初に設定しておくこともできる。

Oj.default_options = {:mode => :compat}

thinでsleepして止まった話

先日、スマートフォンアプリの実装の開発を助けるために、以下のようなAPIをつくった。

# app.rb
require "sinatra"

# ロックファイルを作る
get "/lock" do
  FileUtils.touch("/tmp/lock")
  "ok"
end

# ロックファイルが存在する限りループ(1秒間隔でチェック、最大30秒)
get "/wait" do
  cnt = 0
  while File.exist?("/tmp/lock")
    # 1秒待つ
    sleep 1
    cnt += 1
    # 30秒を超えたら
    if cnt >= 30
      halt 400
    end
  end
  "ok"
end

# ロックファイル削除
get "/unlock" do
  FileUtils.rm("/tmp/lock")
  "ok"
end
# config.ru
require "./app"
run Sinatra::Application

で、これをthinで起動した。

$ bundle exec thin start

クライアントとしては、/lockにアクセスしたあと、/waitでアクセスを維持しつつ、/unlockでロックファイルを削除して、/waitの返却値を期待する、みたいな感じ。(あくまで実験的な実装)


で、実際にやってみたら、/lock→/waitで待っている間に/unlockにアクセスしてもなんの返答もない。
しばらく待って、/waitの30秒が経つのと同時に/unlockのレスポンスが返ってきた。


そりゃそうか。
プロセスは1つしかないので、出口が詰まるとみんな詰まる。

async_sinatra

ググったら、GitHub - raggi/async_sinatra: A plugin for Sinatra to provide a DSL extension for using Thin for asynchronous responsesというgemをみつけた。
Thin, Rainbows, Zbateryを使っている場合に有効らしい。
使い方はいつものgetやpostと書くルーティングにaをつけて、agetやapostにするだけである。

# app.rb
require "sinatra"
require "sinatra/async"
register Sinatra::Async

aget "/wait" do
  # 時間がかかる処理
end


一応内部の実装とthinを見てみて、最低限の処理を抽出すると以下のような感じだった。

# app.rb
require "sinatra"

get "/wait" do
  EM.next_tick do
    cnt = 0
    # 1秒間隔でチェック
    EM.add_periodic_timer(1) do
      cnt += 1
      if cnt >= 30
        request.env['async.callback'].call([200, {'Content-Type' => 'text/plain'}, ['ok']])
      end
    end
  end

  throw :async
end

最後に:asyncをぶん投げるのが肝のようだ。
あとは環境変数env['async.callback']にいつもの3つを渡せばよい。

reelというWebサーバを知った

GitHub - celluloid/reel: UNMAINTAINED: See celluloid/celluloid#779 - Celluloid::IO-powered web server
Celluloidという非同期の処理をするライブラリを使って作られているらしい。

Sinatraの1.4.4のChangesに入ってたので知った。

Rackと協調するには別途reel-rackというgemが必要。

# Gemfile
source 'https://rubygems.org'

gem 'sinatra'
gem 'reel'
gem 'reel-rack'
$ bundle exec reel-rack config.ru
I, [2013-10-27T22:45:44.178102 #10300]  INFO -- : A Reel good HTTP server! (Codename "Garbo")
I, [2013-10-27T22:45:44.178174 #10300]  INFO -- : Listening on http://0.0.0.0:3000


Celluloidベースというのが魅力なのか、webmachine-rubyと相性がいいのが良いのか、
いまひとつよくわからなかった。。。

daemonとか再起動とかもまだできないみたいである。

名前

セルロイド…合成樹脂の名前。アニメの「セル画」の「セル」らしい。
セルロイド - Wikipedia
リール…映写機の回るやつ。

Sinatraでrake routesその2

いま流行りっぽいYARDのSinatra用gem yard-sinatra | RubyGems.org | your community gem host を使うと
簡単にルーティングを出力できるみたい。
↓の"Other use cases"を見た。
GitHub - rkh/yard-sinatra: Display sinatra routes in yard documentation.

Gemfile
source 'http://rubygems.org'

gem 'rake'
gem 'sinatra'

group :development do
  gem 'yard'
  gem 'yard-sinatra'
end
Rakefile
task "routes" do
  require "yard/sinatra"
  YARD::Registry.load ["./app.rb"], true
  YARD::Sinatra.routes.each do |route|
    puts route.http_verb, route.http_path, route.file, route.docstring
  end
end

YARD::Registry.loadは1回読み込むとキャッシュが使われて2回め以降動かなくなるので
trueをつけて毎回parseするようにした。

app.rb

サンプルシナトラ

require "sinatra"

get "/" do
  "foooooooooo!"
end

get "/bar" do
  "baaaaaaaaaaaar!"
end

post "/bar" do
  "POST baaaaaaaaaaaar!"
end

put "/bar" do
  "PUT baaaaaaaaaaaar!"
end

get "/baz/:id" do
  "baz: #{params[:id]}"
end
rake routes
$ bundle exec rake routes
GET
/
app.rb

GET
/bar
app.rb

POST
/bar
app.rb

PUT
/bar
app.rb

GET
/baz/:id
app.rb

もっとRailsっぽく

Rakefileをこうしてみる。

task "routes" do
  require "yard/sinatra"
  YARD::Registry.load ["./app.rb"], true
  routes = []
  YARD::Sinatra.routes.each do |route|
    routes << [route.http_verb, route.http_path, route.file]
  end
  max_verb_len = routes.map{|r|r[0].length}.max
  max_path_len = routes.map{|r|r[1].length}.max
  routes.each do |r|
    puts "%-#{max_verb_len}s %-#{max_path_len}s %s" % r
  end
end
$ bundle exec rake routes
GET  /        app.rb
GET  /bar     app.rb
POST /bar     app.rb
PUT  /bar     app.rb
GET  /baz/:id app.rb

いいね!

Sequel3.48でgraph使ったらdeprecated出たので対応した

通常のjoin

Sequelを使って、単純にAテーブルとBテーブルをjoinすると、同名のカラムが存在する場合、右側の方が有効になる。

mysql> select * from a;
+----+------+-------------+
| id | z_id | name        |
+----+------+-------------+
|  1 |  123 | Yamada Taro |
+----+------+-------------+
mysql> select * from b;
+----+------+---------------+
| id | z_id | name          |
+----+------+---------------+
|  1 |  123 | Suzuki Hanako |
+----+------+---------------+
require "sequel"
DB = Sequel.connect("mysql2://user:password@localhost/foo")

class A < Sequel::Model(DB[:a])
end
class B < Sequel::Model(DB[:b])
end

p A.join(B, :z_id => :z_id).sql
#=> "SELECT * FROM `a` INNER JOIN `b` ON (`b`.`z_id` = `a`.`z_id`)"

p A.join(B, :z_id => :z_id).first
# Bの"Suzuki Hanako"が格納されてしまう
#=> [#<A @values={:id=>1, :z_id=>123, :name=>"Suzuki Hanako"}>]

graphを使う

このときjoinの代わりにgraphメソッドを使うと、AもBも両方使えるようになる。

p A.graph(B, :z_id => :z_id).sql
#=> "SELECT `a`.`id`, `a`.`z_id`, `a`.`name`, `b`.`id` AS `b_id`, `b`.`z_id` AS `b_z_id`, `b`.`name` AS `b_name` FROM `a` LEFT OUTER JOIN `b` ON (`b`.`z_id` = `a`.`z_id`)"

p A.graph(B, :z_id => :z_id).first
#=> {:a=>#<A @values={:id=>1, :z_id=>123, :name=>"Yamada Taro"}>, :b=>#<B @values={:id=>1, :z_id=>123, :name=>"Suzuki Hanako"}>}

が、同時にdeprecation warningも出てきた。

SEQUEL DEPRECATION WARNING: Dataset#graph_each is deprecated and will be removed in Sequel 4.0.
Load the graph_each extension if you want to continue using it.

使っていたバージョンは3.48。3系の最後。
4系でも使うためには、いまからextensionに切り出された方を使うのがいいんだろう。

extensionでgraphを使う

DB.extension(:graph_each)

1行追加して終わり。
警告も出なくなった。