月別アーカイブ: 2012年5月

jqGridのsetFrozenColumnsのイベント処理とその後のDOM構造について

javascriptをベースとしてエクセルのような機能を表現できるjqGridで最近では列固定、行固定することができるようです。

そのこと(setFrozenColumns)についてはこちらを参考にさせていただきました。

さて、setFrozenColumnsメソッドを呼ぶとのDOM構造が変わってしまいます。

それによって独自に拡張した部分の挙動がおかしくなりました。
正しく動作させるためにはsetFrozenColumns後に作られたDOMにあわせて拡張する必要があります。

そのためには以下の2つのことが必要ですが、ドキュメントにはなかったのでメモします。
※注意:この記事を書いた時点のバージョンjqGrid4.3.2におけるものであり、今後仕様が変更される可能性が高いです。

1:setFrozenColumnsが完了したというイベントを取る必要がある。

ソースを読むとどうやら、 “jqGridAfterGridComplete.setFrozenColumns”というイベントを発行しているようです。ですので

[javascript]
$grid.jqGrid(‘setFrozenColumns’);
$grid.on(‘jqGridAfterGridComplete.setFrozenColumns’, function(e){//処理})[/javascript]

とすればsetFrozenColumnsの完了イベントを受け取れます。

2:DOM変更後のテーブルを読み解く

DOM変更後は4つのテーブルに分かれます。

まず上下にテーブルが2つずつ。そしてそれぞれの左側に重なるようにテーブルが1つずつできます。このうちスクロールされるのが緑の部分というわけです。
上の二つのテーブルはクラスが”ui-jqgrid-htable’です。下の二つのテーブルには”ui-jqgrid-btable”という名前のクラスがつきます。ですので、

[javascript]hTables = $gird.find(‘table.ui-jqgrid-htable’);
bTables = $grid.find("table.ui-jqgrid-btable’);
$red_table =  $(hTables[0])
$blue_table = $(hTables[1])
$green_table = $(bTables[0])
$yellow_table = $(bTables[1])[/javascript]

とすればそれぞれのテーブルを取得できます。

Ruby on Rails 3.2 acts_as_tree から acts_as_nested_listへ乗り換えました

acts_as_treeからacts_as_nested_listへの乗り換えについては下記のリンクに書かれています。

Converting Acts_As_Tree to Awesome_Nested_Set

あーなんだ、簡単そうだ。
と思ったらこれが非常に大変でした。

アプリケーションを作る前だったら乗り換えるのは確かにこの手順を踏めばあっという間です。
しかし、アプリケーションをある程度実装している段階では乗換えの前に作戦を立てたほうがよさそうです。

まず、大前提としてacts_as_treeのほうは各カラムにparent_idを設定することでツリー構造を実現するという古典的な方法を使っていて、acts_as_nested_listのほうは入れ子集合モデルを応用しているものになります。

この違いによっていくつかのクリティカルの仕様変更を余儀なくされます。

■違い1:acts_as_treeはルートを2つ以上もてる、acts_as_nested_list:ルートは1つしかもてない

実社会でツリー構造をしているもののなかで、一番上がひとつのものは少ないのではないでしょうか。組織でも社長が一人とは限らないし、パソコンのファイルとフォルダをみても、一番上をたどっていったらフォルダがひとつなんてことはありません。acts_as_treeはparent_idをnilにすればすぐにルートになれたので、この点は単純で融通が利きました。

■違い2:acts_as_treeに順番は存在しないのに対し、acts_as_nested_listには順番が存在する。

nested_listという名前からもわかるように、acts_as_nested_listには何番目の子供かという情報があります。一方acts_as_treeにはその情報がないので、順番を設定したい場合はacts_as_listなどを追加で使う必要がありました。私も前バージョンの実装時にはこの二つをつかってツリー構造を定義していたわけです。

■違い3:acts_as_treeはdestroy時、childrenを消さないのに対し、acts_as_nested_listはchildrenを消す

厳密に言うとdescendantsを消します。acts_as_treeにはdescendantsメソッドはないので、すべての下位treeを取得する場合childrenを再帰ループさせる必要がありました。入れ子集合モデルではまさにこの点が有効で、SQLをひとつだけ発行すれば関連する親や子供を取得できることにあります。acts_as_nested_listに乗り換えた後は単にtree.destroyとするだけで子供も全部消してくれます。その際には子供のdestroyは呼ばずにdeleteで消している点も注意すべきポイントです。

さて私の行っていた実装でこれらのことがどのように影響したかということと、どのように解決したかをお話します。

■違い1に対して

どうしてもルートを2つ持ちたかったわけです。そのため空のツリーを一番上に追加するようにしました。そして、acts_as_nested_listにはいくつかルートに関するメソッドがありますが、それらを次のようにオーバーライドしました。

[ror]
def move_to_root
self.move_to_child_of(self.root)
end
def child?
!self.root?
end
def root?
self.depth <= 1 #depthを使っていなければlevel
end
[/ror]

このようにすると一番初めに強制的に作られたルートはずーっとルートのままになります。move_to_rootを呼ぶと実際にはrootの子供として設定されます。そして、root?やchild?もルートのすぐ下の子供をrootと認識するようになります。ただしrootメソッドだけは返すツリーはひとつと決まっていますのでそれは本来のrootを返すままになっています。わかりやすくなったかは微妙ですが、使いやすくなったことは確かです。

■違い2に対して

結論から言うとacts_as_nested_list + acts_as_listを併用して使うということに落ち着きました。事情としては私の作ったUIにあわせるため、併用したほうがよさそうだったからです。

まず最大の利点としてはツリー構造を表現する歳にSQLを単純にできたことでした。普通の考えであれば一番上のtreeを取得しそれにchildrenでループさせ、さらにchildrenがあれば、、、と再帰ループさせてツリーを表現します。しかし、それにはどうしてもchildrenを取得するためのSQLが発行されてしまいます。しかし順番が正しくてそれぞれの深度さえわかれば(acts_as_nested_listではoptionでdepthを保存しておけます)ツリー構造を表現できるので、

[ror]@trees = Tree.order(‘position’).all(id)[1..-1][/ror]

とし、view側で

[ror]<% @trees.each do |tree| %>
<li class="depth_<%=tree.depth%>"><%= tree.name %></li>
<% end %>[/ror]

とし、cssでマージンを設定すればよいです。若干冗長ではありますが、scssを用いて

[ror]$margin: 20px;
@for $i from 0 through 40 {
li.depth_#{($i+1)} { margin-left: $margin * $i}
}[/ror]

とすればだーっと作成してくれます

厄介だったのはacts_as_listでのpositionとacts_as_nested_listにて設定されている何番目の子供かという情報を完全につじつまを合わせる必要があることです。いくつかのロジックを組むことで一応実現できましたが、まだvalidationをするにはいたっていません。
実装しているUIとしては、ツリーを「上、下、右、左」に移動することで階層構造を変更することができるようなものが必要でした。たとえば、
ツリー1
└ツリー2
└ツリー3
ツリー4
とあるとして、「ツリー3を左へ」とすると
ツリー1
└ツリー2
ツリー3
ツリー4
となってほしいというような仕様になります。

この場合ツリー構造の視点から考えると「ツリー3をルート」の子供にするということがいえます。そうしたければacts_as_nested_listのmove_to_child_ofメソッドを呼べばすみます。しかしmove_to_child_toメソッドは最後の子供として追加してしまうためツリー3はツリー4の弟になってしまいます。何番目の子供にするのかを指定しなければなりません。acts_as_nested_listにはmove_to_child_with_indexメソッドというのがあって、それを実現できます。「上へ」や「下へ」とする場合は兄弟関係が変わります。その場合はpositionも変わります。ツリー構造の変更時にはpositiontと何番目の子供かという情報につじつまを合わせるようにコードを変更する必要がありました。これはデータとしては冗長的です。しかしそうしないことで、データの取り出す時にその取り出し方が冗長的であるならば、どっちかをとるしかありません。

■違い3について

acts_as_nested_listでは親に対しdestroyするとその関連する子供も消すというのは、とても利にかなっていると思えました。実際必要なコードも少なくなり、非常に有効に思えました。しかしdependent destroyがあるとちょっと厄介です。

私の場合はtreeは抽象クラスとしてpolymorphicにしています。つまりtreeはツリー構造だけを実装し、実際にはそのツリーに必ずひとつくっついている別のモデルがあるわけです。こうすることでアプリケーションにあるいくつかのツリー構造をしたオブジェクトを一括で管理することができます。(実はこの点は後悔する部分があります。polymorphicはデータベース対して使ってしまうとそのテーブルだけ肥大しやすくなるからです。委譲にしとけばよかったかなー。と。)

問題はacts_as_nested_listのdestroyメソッドを呼ぶとdescendantsに対しdeleteメソッドを呼ぶことにあります。そうなるとdependent destroyとしているオブジェクトが亡霊のようにデータベースに残ることになります。コレを回避するためにはbefore_destroyを定義して消そうとしているオブジェクトのdescendantsを走査し、あらかじめ関連オブジェクトを消しておく必要があります。

[ror]before_destroy :destroy_tree_items
belongs_to :tree_item, :polymorphic => true, dependent: :destroy
self.descendants.each do |tree|
tree.tree_item.destroy
end[/ror]

実際には消してしまいたいヤツと消したくないやつとあったのでもう少し複雑ですが、まこんなところです。

現段階でのファイルを張っておきます。特異なコードなので誰の役に立つかわかりませんが、。そのうちプラグイン化したいものです。

Ruby on Rails 3.2.0: has_many belongs_to association で結合している場合、includeオプションを使うとよい

AR.findなどでSQLを発行する際にincludeオプションを付けると
冗長なSQLが改善されることは知っていました。
しかし
posts - posts_writers - writers - people
というようにwritersが実際にはpeopleであるような多態性を持たせたアソシエーションの場合、
Postモデルからどのようにpeopleをincludeしておけばよいか悩んだのでメモ。
結論としてはモデル側のアソシエーション定義で
includeオプションを付けるとよいことがわかりました。 
[ror]
class Post < ActiveRecord::Base
has_many: writers,<strong> :include=>person</strong>
has_many: viewers,<strong> :include=>person</strong>
end

class Writer < ActiveRecord::Base
belongs_to :post
belongs_to :person
end

class Viewer< ActiveRecord::Base
belongs_to :post
belongs_to :person
end
[/ror]

このようにすることでコントローラー側で

[ror]
@post = Post.includes(:viewers).find(params[:project_id])
[/ror]

としたときにあらかじめ関連付けられたpeopleのレコードを読み込んでくれる

[sql]
SELECT "people".* FROM "people" WHERE "people"."id"
IN (13885, 13886, 13887, 13888, 13889, 13890, 13891, 13892)
ORDER BY updated_at
[/sql]

そうするとview側で

[ror]<% @post.viewers.each do | viewer| %><%= viewer.person.name %><% end %>[/ror]

としても都度SQLを発行することはないです。

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文の括弧の量がものすごく多いのは気になりますが、パフォーマンスの面では問題ないようです。