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]

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

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

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です