Newest Posts

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にはいくつかルートに関するメソッドがありますが、それらを次のようにオーバーライドしました。

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

このようにすると一番初めに強制的に作られたルートはずーっとルートのままになります。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を保存しておけます)ツリー構造を表現できるので、

@trees = Tree.order('position').all(id)[1..-1]

とし、view側で

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

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

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

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

 

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

この場合ツリー構造の視点から考えると「ツリー3をツリー1」の子供にするということがいえます。そうしたければ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を走査し、あらかじめ関連オブジェクトを消しておく必要があります。

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

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

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

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オプションを付けるとよいことがわかりました。 
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

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

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

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

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

そうするとview側で

<% @post.viewers.each do | viewer| %><%= viewer.person.name %><% end %>

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

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

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

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

↓リファクタリング前
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

↓リファクタリング後

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

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同士を結合できます。
これはいけそうです。

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

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

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

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

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

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

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

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

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

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

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

■検索のメソッド

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

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

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

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

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

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

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

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エラーがココを通ってしまうので、どうなんだろう。。