acts as section map(better nested setで組織図を書く)
「better nested setで組織図を書きたい」
前からやりたいと思っていた手軽にできる組織図表。
ついに完成しました。
ライセンスはMITです。
まずbetter nested setを使用したモデルを作成します。
./script/plugin source svn://rubyforge.org/var/svn/betternestedset/trunk ./script/plugin install betternestedset
モデルはSectionという名前にすることにして、使用するカラムはnameだけにします。
better nested setにはparent_id、lft、rgtという:integerなカラムが必要です。
class CreateSections < ActiveRecord::Migration def self.up create_table :sections do |t| t.column :name, :string, :null=>false t.column :parent_id, :integer t.column :lft, :integer t.column :rgt, :integer end end def self.down drop_table :sections end end
app/models/section.rbの中は
class Section < ActiveRecord::Base acts_as_nested_set end
これだけです。
データを作成し、適当な組織図のツリーを作ってみました。
% ./script/runner 'Section.create(:name=>"本社")' % ./script/runner 'Section.create(:name=>"東北支社").move_to_child_of Section.find_by_name("本社")' % ./script/runner 'Section.create(:name=>"札幌支社").move_to_child_of Section.find_by_name("本社")' % ./script/runner 'Section.create(:name=>"山形営業所").move_to_child_of Section.find_by_name("東北支社")' % ./script/runner 'Section.create(:name=>"福島営業所").move_to_child_of Section.find_by_name("東北支社")' % ./script/runner 'Section.create(:name=>"盛岡営業所").move_to_child_of Section.find_by_name("東北支社")' % ./script/runner 'Section.create(:name=>"函館営業所").move_to_child_of Section.find_by_name("札幌支社")' % ./script/runner 'Section.create(:name=>"釧路営業所").move_to_child_of Section.find_by_name("札幌支社")' % ./script/runner 'Section.create(:name=>"旭川営業所").move_to_child_of Section.find_by_name("札幌支社")'
DBの中を見ると、
# select * from sections; id | name | parent_id | lft | rgt ----+------------+-----------+-----+----- 1 | 本社 | | 1 | 18 2 | 東北支社 | 1 | 2 | 9 3 | 札幌支社 | 1 | 10 | 17 4 | 山形営業所 | 2 | 3 | 4 5 | 福島営業所 | 2 | 5 | 6 6 | 盛岡営業所 | 2 | 7 | 8 7 | 函館営業所 | 3 | 11 | 12 8 | 釧路営業所 | 3 | 13 | 14 9 | 旭川営業所 | 3 | 15 | 16 (9 rows)
こんな感じになっています。ツリーになっているのはがんばって読めばわかります。
acts as section mapのインストール
そして、acts as section mapをインストールします。
./script/plugin install http://xibbar.net/svn/rails/plugins/trunk/acts_as_section_map/
使い方
モデルには一行追加する必要が有ります。
class Section < ActiveRecord::Base acts_as_nested_set acts_as_section_map end
acts_as_section_mapはacts_as_nested_setの下に書かなければいけません。
ビューのindex.rhtmlはこれだけです。
<% section_map(:section) do |table| %> <%=table.name%> <% end %>
これだけで組織図が表示されます。
ブロックの中が個々のtdタグの中身になります。
:sectionというのはクラス名です。
Sectionと書くのはよろしくないと思って、Rails流に
シンボルでオブジェクト名を与えることにしました。
追加されるメソッド
acts_as_section_map一行で以下のメソッドがモデルに追加されます。
クラスメソッド
- set_depth(全ての:depthカラムに深さをセットします。)
- leaves(葉の全て:葉というのは枝の先端部分のことです。)
- table(モデルを2次元配列にしたものです。)
- table2(さらにtableを表示しやすいように変形したものです。)
インスタンスメソッド
- left?(枝の一番左側かどうか)
- right?(枝の一番右側かどうか)
- move_to_last_child_of(node)(ある親の枝の一番下側(右とも言う)にぶら下げます)
- up 一つ上に順番をあげる
- down 一つ下に順番を下げる
- leaves ぶら下がっている全ての葉
- colspan tableの中のtrで使うcolspan
- rowspan tableの中のtdで使うrowspan
今のところ、left?とupとかが混在しているのですが、
整理していきたいと思っています。
なぜこのようになっているかというと、Treeというのものは
上が頂点になるようになっているのですが、
組織図は左が頂点の方が表現しやすいためです。
tableやtrにstyleやclassなどを与えたい
ビューでsection_mapメソッドにオプションでstyleやクラスなどを渡すことができます。
<% section_map(:section,:table=>{:cellspacing=>"0",:style=>"border-collapse:collapse;"}, :td=>{:valign=>"top",:style=>"border:1px solid #666666;color:navy;font-size:10px"}) do |section| %> <%=section.name%> <% end %>
以上のような感じでHashの入れ子としてstyleやcellspacingやclassなどを指定することができます。
関連テーブルを使う
sectionにuserがぶら下がっている場合も簡単です。
section_idを持っているUserモデルを作成します。
Userのmigrationファイル
class CreateUsers < ActiveRecord::Migration def self.up create_table :users do |t| t.column :name, :string, :null=>false t.column :section_id, :integer end end def self.down drop_table :users end end
app/models/user.rb
class User < ActiveRecord::Base belongs_to :section end
Sectionモデルもhas_manyを追加します。
app/models/section.rb
class Section < ActiveRecord::Base acts_as_nested_set acts_as_section_map has_many :users end
Userのデータを突っ込みます。
% ./script/runner 'Section.find_by_name("福島営業所").users << User.create(:name=>"xibbar")' % ./script/runner 'Section.find_by_name("東北支社").users << User.create(:name=>"yuichi_katahira")' % ./script/runner 'Section.find_by_name("福島営業所").users << User.create(:name=>"Doggie")' % ./script/runner 'Section.find_by_name("東北支社").users << User.create(:name=>"monyakata")' % ./script/runner 'Section.find_by_name("山形営業所").users << User.create(:name=>"takedasoft")' % ./script/runner 'Section.find_by_name("東北支社").users << User.create(:name=>"h_mori")'
ビューも修正します。
<% section_map(:section,:table=>{:cellspacing=>"0",:style=>"border-collapse:collapse;"}, :td=>{:valign=>"top",:style=>"border:1px solid #666666;color:navy;font-size:10px"}) do |section| %> <%=section.name%> <ul> <% section.users.each do |user|%> <li style="color:green"><%=user.name%></li> <%end%> </ul> <% end %>
ツリーを構築するのが遅い場合
ちょっとしたツリーだったら表示するのが早いのですが、
100個を超えるような組織図を構築する場合は極端に遅くなります。
そういう場合はmigrateファイルに
class AddDepthToSections < ActiveRecord::Migration def self.up add_column :sections,:depth,:integer Section.set_depth end def self.down remove_column :sections,:depth end end
を書いて実行し、depthというカラムを追加してください。
.set_depthでは現状の値を修正してくれます。(levelを呼び出して上書き)
これは何かというとのろさの原因はsectionが
自分自身の深さを知るためにselect count(*)していることにあります。
(levelメソッドを呼んでいる)
これをカラムとして持つとツリーの構築の早さが圧倒的に違うことにあります。
(つまりdepthカラム)
depthカラムがある場合は組織図に変化があった場合に
自動的にdepthカラムの中身も変更するようにプラグインを作ってあります。(再起動が必要)
ベンチマークの結果です。(120のツリーを構築した場合)
% ./script/performance/benchmarker 100 'Section.find(:all).map(&:level)' user system total real #1 6.180000 0.530000 6.710000 ( 20.693455)
20秒かかっていたものが、rgt、lft、parent_idにインデックスを張って、
% ./script/performance/benchmarker 100 'Section.find(:all).map(&:level)' user system total real #1 5.790000 0.470000 6.260000 ( 10.331682)
10秒になり、depthカラムを追加することで、
% ./script/performance/benchmarker 100 'Section.find(:all).map(&:depth)' user system total real #1 0.610000 0.010000 0.620000 ( 0.784886)
0.78秒になりました。