Rails 3.2.0 で複雑な条件での検索:conditionはオブジェクトとして配列にし最後にwhereする: 簡潔編

前回の投稿からリファクタリングを進めていて、Module: Arel::Predicationsクラスというものにたどり着きました。matches, eq以外にもたくさんのメソッドが用意されています。

■それらを使って「属性の条件を格納した配列を返すメソッド」を短くできました。

↓リファクタリング前
[ror]
def ors_by_pattribute
pattribute_table = Pattribute.arel_table
@result = @result.joins(:pattributes).group('pattributes_people.person_id')
pattribute_ids = @pattributes.collect {|x| x.to_s}
ors = []
pattribute_ids.each do | pattribute_id |
ors << pattribute_table[:id].eq(pattribute_id)
end
ors
end
[/ror]

↓リファクタリング後

[ror]
pattribute_table = Pattribute.arel_table
@result = @result.joins(:pattributes).group('pattributes_people.person_id')
[pattribute_table[:id].in_any(@pattributes)]
[/ror]

Rails 3.2.0 で複雑な条件での検索:conditionはオブジェクトとして配列にし最後にwhereする

Rails3ではSQLを扱うためにArelというヤツを使っています。
Rails2は使ったことがないですが、ActiveRecordのインターフェイスとして2と3では似ているようです。

そのためArelを使ってどのようなことができるのか、情報が少ないうえに新旧の情報が入り混じっているためになかなか調べるのが大変でした。

「ちょっと複雑なSQLを作ろうとすると直接SQLを書かなくてはいけなくなる」という内容の記事もよく見かけました。
デフォルトでは難しそうだと感じた私はプラグインを探してみて、 meta_search、その進化版のRansack、またSqueelも試してみました。

プラグインを使うと「簡単!」と思えるところと、逆に「難しい!」と感じるところとありました。
「SQLを直接かくか、、」と何度も思いましたが、ぐっとこらえてなんとかスマートに複雑な検索を実現できないかを試行錯誤しました。

■やりたいこと→人物の名前・メール・属性(性別など)から複数の条件で検索したい

・人物にはfirst_nameとlast_nameがあります。
Person(first_name, last_name)
・メールはusersテーブルに格納されていてPersonがuserをbelongs_toです。
belongs_to :user (email)
・属性はpattributesテーブルに格納されていてPersonとpattributesはhas_and_belongs_to_manyアソシエーションになっています。そしてアソシエーションテーブルとしてpattributes_peopleテーブルがあります。
has_and_belongs_to_many :pattributes

条件は
・keywordを入力しfirst_name, last_name, emailのいずれかにマッチ(LIKE)する
・pattributesを選択し、関連付けられているものにマッチする
・それらの条件の片方か、もしくは両方をAND検索する

ようするに
「名前かemailに「tom」とつく「男性」を検索する」
とかいうようにしたいわけです。

■問題点:条件ごとにコードを書くと、条件が増えるたびにコードが増える。
この場合は「キーワードのみ」「属性のみ」「キーワードと属性」の3通りに分けなければなりません。

■方針:条件を追加していって最後に検索をかけるようにする。

■試行錯誤1:
Person.whereの戻り値はActiveRecord::Relationなので下記のようにかけます。

@result = Person.where(条件1)
@result = @result.where(条件2)

こうすると条件を追加していくことができます。
しかしこの方法だと条件がANDになり今回の場合にそぐいません。

■試行錯誤2:
ORにするために次のように書きました。

@result = Person.where(条件1.or(条件2))

たしかにORになったのですが、これだと一行で書かなければならなくなるのでフリダシにもどります。

■試行錯誤3:ここがポイント!
この「条件1」とか「条件2」自体をオブジェクトとして変数に保存できないだろうかと考えました。
「条件」にあたる部分の書き方はいろいろあるのですが、下記のような書き方があります。

person_table = Person.arel_table
condition = person_table[:first_name].matches( “%#{keyword}%”)

このconditionはクラスを調べると[Arel::Nodes::Matches]であることがわかります。
ちなみにmatchesでなくてeqをつかうと[Arel::Nodes::Equality]クラスが返ります。

このconditionはオブジェクトなので変数として保存しやすいです。
そしてorやandでcondition同士を結合できます。
これはいけそうです。

■キーワードの条件を格納した配列を返すメソッド

[ror]
def ors_by_keyword
person_table = Person.arel_table
user_table = User.arel_table
#@result = @result.joins(:user)
@result = @result.joins(‘INNER JOIN "users" ON "users"."id" = "people"."user_id"’)
keywords = @keyword.strip.split(/[\s]+/)
ors = []
keywords.each do |keyword|
k = "%#{keyword}%"
ors &lt;&lt; person_table[:first_name].matches(k)
ors &lt;&lt; person_table[:last_name].matches(k)
ors &lt;&lt; user_table[:email].matches(k)
end
ors
end
[/ror]

まず、Person.arel_tableでpeopleテーブルのArel::Tableオブジェクトを用意しています。
同様にUserに関しても用意します。

クエリでPersonに結び付けられたuserも参照することになるので、joinsを呼び出してjoin文を設定します。
ここで使われている@resultはあらかじめ用意しておいたActiveRecord::Relationオブジェクトです。

次にキーワードを空白で区切って複数のキーワードに分割します。
[“山田 タロウ”]だとしたら[“山田”, “タロウ”]という配列なるわけです。

そのキーワード分だけループします。
ループ内ではArel::Nodes::Matchesを必要な分だけ作成してorsという配列に格納します。

そして最終的にその配列を返します。
この配列にはArel::Nodes::Matchesが格納されているわけです。

■同様に属性の条件を格納した配列を返すメソッド(簡潔にしたコード

[ror]
def ors_by_pattribute
pattribute_table = Pattribute.arel_table
@result = @result.joins(:pattributes).group(‘pattributes_people.person_id’)
pattribute_ids = @pattributes.collect {|x| x.to_s}
ors = []
pattribute_ids.each do | pattribute_id |
ors << pattribute_table[:id].eq(pattribute_id)
end
ors
end
[/ror]

キーワードのときとやっていることはほとんど同じです。

@pattributesの中はpattributesテーブルのidがFixnumで格納されています。

■検索のメソッド

[ror]
def search
ands = []
@result = Person.includes(:user)
ands << self.ors_by_keyword if @keyword.present?
ands << self.ors_by_pattribute if @pattributes.present?
conditions = generate_and_condition(ands)
@result = @result.where(conditions)
end
[/ror]

まず条件を格納するandsという配列を用意します。

@resultにはPerson.includes(:user)を実行してActiveRecord::Relationオブジェクトを格納しておきます。

次にキーワードがあればその条件の配列をandsに追加します。
同様に属性があればその条件の配列をandsに追加します。

conditionsには後述しますがwhereに渡すconditionsが格納されます。

■配列に格納した条件をconditionに展開します。

[ror]
def generate_and_condition(ands)
conditions = nil
ands.each do | ors |
condition = generate_or_condition(ors)
conditions = conditions ? conditions.and(condition) : condition
end
conditions
end

def generate_or_condition(ors)
condition = nil
ors.each do | c |
condition = condition ? condition.or(c) : c
end
condition
end
[/ror]

andsは[[条件,条件…],[条件,条件]]のように入れ子の配列になっているはずです。

この上位の階層がANDになり下位の階層がORの条件になります。
つまり[[条件 OR 条件 OR…] AND [条件 OR 条件 OR…]]という感じです。

今回の場合は [キーワードの条件] AND [属性の条件]となるわけです。

上記二つのメソッド generate_and_condition と generate_or_conditionはandsを走査させてandやorメソッドを使い、conditionsに条件を合体させているわけです。

このwhereメソッドの引数とするconditionsはArel::Nodesパッケージのさまざまなオブジェクトの塊になっています。

■最終的なSQLは下記のようになります。

SELECT “people”.* FROM “people” INNER JOIN “pattributes_people” ON “pattributes_people”.”person_id” = “people”.”id” INNER JOIN “pattributes” ON “pattributes”.”id” = “pattributes_people”.”pattribute_id” INNER JOIN “users” ON “users”.”id” = “people”.”user_id” WHERE ((((((“people”.”first_name” LIKE ‘%goro%’ OR “people”.”last_name” LIKE ‘%goro%’) OR “users”.”email” LIKE ‘%goro%’) OR “people”.”first_name” LIKE ‘%simane%’) OR “people”.”last_name” LIKE ‘%simane%’) OR “users”.”email” LIKE ‘%simane%’) AND (“pattributes”.”id” = 455 OR “pattributes”.”id” = 456)) GROUP BY pattributes_people.person_id ORDER BY updated_at LIMIT 3 OFFSET 0

■結論。

このような手順を踏むことで、条件の追加を容易にします。WHERE文の括弧の量がものすごく多いのは気になりますが、パフォーマンスの面では問題ないようです。

続、親子でロタウィルスにかかりました。

昨日の夜から、肋骨の奥(胃の上あたり)がよじれるように痛み出し、断続的にそれが繰り返されるような感じです。いまは落ち着いています。

前にも、食中毒っぽいときに一晩はき続けた翌日になった症状に似ています。今回のは以前よりひどいです。

どうやら「内臓の筋肉痛」というもののようです。
内臓に筋肉はないらしいですが、吐くなどを繰り返すときに普段使われていない内側の筋肉が使われ続けます。
それによってその筋肉が痛むことがあるみたいです。

とても独特なつらさです。なんともたとえづらい。。
内側の筋肉にはバンテリンがぬれないのでどうすることもできず。。
食べると胃が動くので痛みが増します。

下痢も1週間くらい続くらしいですし、いつになったら元気になれるのやら。。

親子でロタウィルスにかかりました。

先週から1歳半になる息子の体調がわるくなり、下痢と嘔吐を繰り返すようになりました。はじめは風邪だったのが、ロタウィルスにかかったようです。それがわかったのがつい先日です。かかりつけの小児科ではなく別の小児科に行って初めてわかりました。その日の夜、私も吐き気が止まらなくなりました。

あーうつったな。と。

息子をだっこしているときに何度も息子が吐いていたので、それを被っています。まず息子の体を拭き、服を着替えさせ、落ち着かせてから自分の始末をしていたのです。親としてはその順序になるかと思うので、うつるのは必至の状態でしたね。

ただかかりつけの小児科ではやくそれを診察してくれれば、気をつけようもあったと思うし、はっきりわからなくてもそういう注意でもしてくれればよかったのですが。。

しらべてみるとロタウィルスの検査には血液検査をするらしいのですが、最近ではラピッドテストという簡易検査ができるようです。別の小児科ではそれを使って診察中に結果がでました。肛門にめんぼうをさして検体を採取し液体につけることで、ちょうど妊娠検査薬のような形で検査ができるというものです(たぶんそんなかんじです)。
おそらくかかりつけではそれを導入していなかったのでしょう。

ロタウィルスは5歳までにほぼ全員がかかる病気なのだそうです。お子さんがいらっしゃるかたはほぼ通る道です。

もし疑わしい症状が出た場合はノロウィルスなのかロタウィルスなのか簡易に検査できるものがありますので、それを使える小児科に相談することをお勧めします。ただ分かったからといって薬でロタウィルスを直接殺すということにはならないようです。他の風邪と同じように症状を和らげながら体に抗体ができるのを待つことになります。特に脱水症状には気をつけましょう。だからといっていろいろ知らない状態では他の人間(親も含めて)にうつさないようにすることはできません。

ポイントしてはいくつかあります。※感染症情報センターから抜粋

  • 患者の便や嘔吐物には大量のウイルスが含まれていますので、その処理には十分注意する必要があります。また、下痢の症状がなくなった後も、患者の便にはしばらくウイルスの排出が続くと考えられますので、症状が治まっても安心はできません。汚物を処理する際には使い捨ての手袋を使用し、用便後や調理前の手洗いを徹底しましょう。
  • 殺菌には熱湯あるいは0.05から0.1%の次亜塩素酸ナトリウムを使用します。アルコールや逆性石鹸にはあまり殺菌効果はありません。
  • 調理器具、おもちゃ、衣類、タオル等は熱湯(85℃以上)で1分以上の加熱が有効です。
  • 市販の塩素系漂白剤(通常は5から10%次亜塩素酸ナトリウム)なら50倍から100倍に薄めて使用します(例えば、原液10ミリリットルを1リットルの水で薄める)。
  • 汚物の処理方法
  1. 患者の便や嘔吐物を処理するときは、使い捨ての手袋とマスクを着用する。
  2. 便や嘔吐物はペーパータオル等で取り除き、ビニール袋に入れる。
  3. 残った便や嘔吐物の上にペーパータオルをかぶせ、その上から50倍から100倍に薄めた市販の塩素系漂白剤を十分浸るように注ぎ、汚染場所を広げないようにペーパータオルでよく拭く。
  4. ウイルスは乾燥すると空気中に漂い、これが口に入って感染することがあるので、便や嘔吐物を乾燥させないことが重要。
アルコール消毒が効かないっていう部分がちょっと驚きでした。アルコール消毒は無敵だと思っていたので。
小児科にあるおもちゃなどは定期的にアルコール消毒をしているらしいですが、それだとロタウィルスは殺せないということを意味します。うちの息子は小児科にいくと毎回そのおもちゃであそんでいます。。
これらの事プラス下記のことも注意が必要です。
•感染から体に抗体ができるまで一ヶ月くらいかかる。
注意しないとまたロタウィルスにかかりますよということです。
今は息子も私もだいぶ落ち着きました。私の方はまだ下痢がひどく、頭痛がします。。
イクメン(と呼ばれることには抵抗がありますが) の皆様。
仕事に支障が出ます(出ました)ので、子供の病気の知識もつけましょう。

 

 

Ruby on Rails 3.2.0 ActiveRecordを継承したクラスのサブクラスでtable_nameが指定できない:解決編

前回の記事で紹介した問題ですが、解決策を見つけたので書きます。

何のことはないです。継承がダメなら委譲ってことで無理やり。。

まず継承はあきらめて普通にサブクラス(だったやつ)はActiveRecord::Baseを継承します。

class B < ActiveRecord::Base
end

でスーパークラス(だったやつ)はmoduleにします。

module A
end

それをBでincludeします。

class B < ActiveRecord::Base
include A
self.table_name = “b”
end

で、belongs_toとかの設定をAにメソッド定義します。

module A
def self.initialize_me(active_record)
active_record.belongs_to :hoge
#…
end
end

最後にBから呼び出します。

class B < ActiveRecord::Base
include A
self.table_name = “b”
A.initialize_me(self)
end

あとは普通のMix-inを使って共通メソッドなりを定義していくだけです。
そもそも私、継承よりも委譲派だった。

ややこしや。

Ruby on Rails 3.2.0 ActiveRecordを継承したクラスのサブクラスでtable_nameが指定できない

3.1はできるらしいですが、3.2ではできないらしいです。

class A<< ActiveRecord::Base
end

class B << A
table_name = “bs”
end

とすればBで使われるテーブルはbsになるはずですが、superクラスの設定のまま。

調べてみると同様の現象に困っている人がいました。

これが私の勘違いでなければオライリーの「エンタープライズRails」にあるポリモーフィズムの代わりに継承を使うという部分(P.128)が実現不可能になるということではないでしょうか。

前述の記事でも話されているように「何で継承なんてしたいの?」と言っている人もいますが、オライリーの本に書いてあるようなことをしたいわけですよ。他にもいろいろと継承の多重継承にはメリットがあります。
ActiveRecordは継承の多重継承するなということなのか、またはRailsのバグなのか。。

とにかく別の方法でやりたいことを実現するように方向転換を余儀なくされました。

いくら設計してもこういうことがあるからなぁ。。

 

追記(同日)解決編

Ruby on Rails 3.2.0 でcucumber書いていたらDEPRECATION WARNINGがでた。

DEPRECATION WARNING: Passing the format in the template name is deprecated. Please pass render with :formats => [:html] instead. (called from realtime at /home/ubuntu/.rvm/rubies/ruby-1.9.2-p290/lib/ruby/1.9.1/benchmark.rb:310)

と出て困った。

render :file => “#{Rails.root}/public/404.html”, :status => 404, :layout => false

を、

render :file => “#{Rails.root}/public/404”

にしたらでなくなりました。

Ruby on Rail 3.2.0 + cancanで意図しないActiveRecord::RecordNotFoundが起こる件

たとえばpeople_controllerのshowで

def show
@person = Person.find(params[:id])
respond_to do |format|
format.html
end
rescue ActiveRecord::RecordNotFound
back_to_index
end

とか書いてあるとき、対象IDのPersonが見つからなければrescue文を通るはず。

RSpecのテストでも
Person.should_receive(:find).and_raise(ActiveRecord::RecordNotFound)
のように書いて試したらきちんとrescue文を通りました。

一安心したところ実際にブラウザで試してみると画面に ActiveRecord::RecordNotFound例外が表示されます。

??なぜ

ログを見るとcancanがそれ以前にfindメソッドを呼んでる。どうやらココで例外が起こってしまっているようです。

当然といえば当然の結果。こちらによると作者は404エラーを起こすべきと考えているようです。正しいように思います。
どうしても修正するにはプラグインの挙動をいじらないと。。

プラグインの中で起こるエラーについて捕捉するにはapplication_controller.rbの中でrescue_fromを書くのがよいけれど、

rescue_from(ActiveRecord::RecordNotFound) {
#処理
}

これだとすべてActiveRecord::RecordNotFoundエラーがココを通ってしまうので、どうなんだろう。。

Cucumber+CapybaraでXPathを用いてセレクトボックスで選択できる値があるかどうかを調べる

<select id=”user_role” name=”user[role]”><option value=”admin”>admin</option>…</select>

とかあるときに選択できる値を持つoptionタグががあることをテストするためにXPathを使いました。正直XPathは避けていたがそれほど難しくはなさそうです。

ならば /^roleのセレクトボックスにadminが存在するはずだ$/ do
page.should have_selector(:xpath,’//select[@id=”user_role”]/option[@value=”admin”]’)
end

こちらを参考にしました。

Viewのテストには重宝しそうです。

Ruby on Rails 3.2.0 RSpec 2.8.0でActiveRecord::RecordInvalidのテストでargumentの数が違うと怒られる

以下のようにActiveRecord::RecordInvalidのテストをしようと思いました。

let(:person) { mock_model(Person).as_null_object }
it{
Person.should_receive(:find).and_return(person)
person.should_receive(:update_attributes!).and_raise(ActiveRecord::RecordInvalid)

}

が、

ArgumentError:
wrong number of arguments (0 for 1)

と怒られます。なかなか情報がなかったのですが、RecordInvalidクラスがコンストラクタでnewをするときにActiveRecordを引数としてとるらしく、それがないので怒られたようです。下記のようにするとよいです。

person.should_receive(:update_attributes!).and_raise(ActiveRecord::RecordInvalid.new(person))

エラー表示はどのタイミングでエラーが起きたのかというのをもっと詳細に知らせてほしいものです。テストをするといろいろなことが知れて、そういったよい面もありますね。