勉強にはよいけど、実戦では使ってほしくない事例

SQLで木と階層構造のデータを扱う――入れ子集合モデルっていうのに、はてなブックマークが160もついていた。

別にRDBを否定する気はない。MySQLとかPostgresとかが簡単に入手できて誰でも操作できる以上、SQL文は開発者にとっての「読み書き算盤」、みたいな基礎知識になっている、といってもいいだろう。

でも、自分の所のプロジェクトではSQL文でぐだぐだ書くのは禁止している。

  • ロジックがコード本体とSQLとに分散し、追いにくい。
  • 複雑なSQL文はテーブル構造の変更に弱い。
  • 読みやすいコードは実現可能だが、読みやすいSQLは実現不可能なことが多い。SQL文の意味を説明する資料が別途必要。仕様変更の際の手間が増える。

Railsだと

ちなみに木構造Railsで扱う場合は acts_as_tree を使う。

SQLで木と階層構造のデータを扱う――入れ子集合モデルと同じようなことを書いてみた。まずはテーブル定義。acts_as_treeを使う場合、自己参照外部キーとしてparent_idが必要となる。

class CreateEmployees < ActiveRecord::Migration
  def self.up
    create_table :employees do |t|
      t.column :name, :string
      t.column :parent_id, :integer
    end
  end

  def self.down
    drop_table :employees
  end
end

Employeeモデル

class Employee < ActiveRecord::Base
  acts_as_tree :order => :name

# rootはacts_as_treeの組込みメソッドで定義されている

# 子供なしノード一覧を取り出す
  def self.leaves
    ret = []
    self.find(:all).each {|e| ret << e if e.children.count == 0}
    ret
  end

# あるノードの深さを計算する
  def level
    parent ? parent.level + 1 : 0
  end

# 木全体の高さを計算する
  def self.hight
    max = 0
    Employee.find(:all).each {|e| max = e.level if max < e.level}
    max
  end
end

単体テストで使うテストデータ。

adachi:
  id: 1
  name: Adachi

igari:
  id: 2
  name: Igari
  parent_id: 1

ueda:
  id: 3
  name: Ueda
  parent_id: 1

ezaki:
  id: 4
  name: Ezaki
  parent_id: 3

ohgami:
  id: 5
  name: Ohgami
  parent_id: 3

kato:
  id: 6
  name: Kato
  parent_id: 3

kijima:
  id: 7
  name: Kijima
  parent_id: 4

単体テスト

class EmployeeTest < Test::Unit::TestCase
  fixtures :employees

# まずはノード定義がちゃんとできているか確認
  def test_read
    employee = employees(:adachi)
    assert_equal 2, employee.children.count

    employee = employees(:ueda)
    assert_equal 3, employee.children.count

    employee = employees(:ezaki)
    assert_equal 1, employee.children.count

    assert_equal employees(:adachi), Employee.root
  end

# 子なしノード一覧の確認
  def test_leaves
    assert_equal [employees(:igari), employees(:ohgami), employees(:kato), employees(:kijima)], Employee.leaves
  end

# ノードの深さ確認
  def test_level
    assert_equal 0, employees(:adachi).level
    assert_equal 1, employees(:igari).level
    assert_equal 1, employees(:ueda).level
    assert_equal 2, employees(:ezaki).level
    assert_equal 2, employees(:ohgami).level
    assert_equal 2, employees(:kato).level
    assert_equal 3, employees(:kijima).level
  end

# 高さ確認
  def test_hight
    assert_equal 3, Employee.hight
  end
end

データ操作ロジックはあくまでもmodelで記述する。SQL文は使わない。これにより可読性と保守性があがる。

木構造を表示する部分はmodelでやるべきかどうかは微妙。たぶん、別の場所。そしてSQLでやるべきでないことは確実。