はじめに

SQLアンチパターンをRailsの文脈で読み解いていきます。
この記事では、Ⅰ部 データベース論理設計のアンチパターン(1~8章)を取り上げます。

リンク

本文

1. ジェイウォーク(信号無視)

class CreateProducts < ActiveRecord::Migration[5.1]
  def change
    create_table :products do |t|
      t.string :name, null: false
      t.string :account_ids
    end
  end
end
name account_ids
"Visual TurboBuilder" "12,34"

みたいなことは誰もしませんよね。

class Product < ApplicationRecord
  has_many :product_accounts
  has_many :accounts, through: :product_accounts
end

class ProductAccount < ApplicationRecord
  belongs_to :product
  belongs_to :account
end

交差テーブル作ってhas_manyするだけ。

2. ナイーブツリー(素朴な木)

隣接リストは遅いという話。
この辺りのgemを使うと良さそう。

3. ID リクワイアド(とりあえず ID)

主張は大きく2つに分かれると思います。

  • 自然キーや複合キーも使おう
  • 代理キー使うにしても、idではなくもう少しわかりやすい名前をつけましょう
class CreateBugs < ActiveRecord::Migration[5.1]
  def change
    create_table :bugs, id: false do |t|
      ...
    end
  end
end

id: falseすれば、代理キーを作らないようにしたり、

class CreateBugs < ActiveRecord::Migration[5.1]
  def change
    create_table :bugs, primary_key: :bug_id do |t|
      ...
    end
  end
end

のようにして、代理キーの名前を変えることも可能です。
またその場合、以下のようにして、モデルの方でも使う主キーを明示的に書く必要があります。

class Bug < ApplicationRecord
  self.primary_key = 'bug_id'
end

composite_primary_keysというgemを使えば、複合主キーも扱えるようになるようです。

一応このように色々レールを外れる手段は用意されていますが、そこまでしてレールを逸脱するような旨味は感じられませんでした。

  • どうせSQLなんてそこまで書かないからUSINGが使えても嬉しくない
  • idbug_idでそこまでわかりやすさが変わるとも思えない。寧ろそれで主キーと外部キーの区別がついてわかりやすいのでは?

私はそれより、とりあえずtimestampが気になります。

4. キーレスエントリ(外部キー嫌い)

foreign_key: trueを書きましょう。

class CreateBugs < ActiveRecord::Migration[5.1]
  def change
    create_table :bugs do |t|
    end
  end
end

class CreateBugStatuses < ActiveRecord::Migration[5.1]
  def change
    create_table :bug_statuses do |t|
      t.references :bug, foreign_key: true, index: { unique: true }
    end
  end
end
class BugStatus < ApplicationRecord
  belongs_to :bug, optional: true
end

のようにoptional: trueを使えば、BugStatusbug_idはnull許容となります。BugStatus.createでレコード作成できます。
optional: trueを書かないと、BugStatus.createはエラー。
ただし、dependent: nullifyとして、belongs_toしているbugを削除すれば、bug_idはnullにできる。)
nullも非許容にしたければ、null: falseとしておきましょう。

5. EAV(エンティティ・アトリビュート・バリュー)

class CreateIssueAttributes < ActiveRecord::Migration[5.1]
  def change
    create_table :issue_attributes do |t|
      t.references :issue, foreign_key: true
      t.string :attr_name, null: false
      t.string :attr_value
    end

    add_index :issue_attributes, [:issue_id, :attr_name], unique: true
  end
end

こんなこと誰もしませんよね。(2回目)
素直にカラムを追加するなどして対処しましょう。

6. ポリモーフィック関連

本文ではアンチパターンを用いても良い場合として、 ORMを用いる場合というのが挙げられています。
少なくとも、

しかし、フレームワークを用いずに、手書きでポリモーフィック関連を実装するのは「車輪の再発明(Reinvesting the Wheel)」アンチパターン[BMMM98]です。無駄が多く、リスクの高い行為だと言わざるを得ません。 
(本文から引用)

よりは遥かにましなようです。
フレームワークとしてサポートしているので、使えばいいと私は思います。
(SQLを書こうとすると辛みがあるかもしれない?)

また、本書によると参照を逆にするとのことですが、あまりうまく書ける方法が思い浮かびませんでした。
例えば、Railsガイドのポリモーフィック関連の例は以下の通り。

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end

class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end

class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

それを本のやり方に従い、ポリモーフィック関連を使わないように書き換えてみます。
まず、交差テーブルを使う方法だと、下のようになります。

class Picture < ApplicationRecord
  has_one :employee_picture
  has_one :employee, through: :employee_picture
  has_one :product_picture
  has_one :product, through: :product_picture

  def imageable
    employee || product
  end

  # employee && product == falseとなることをvalidationで保証する必要があるが割愛
end

class EmployeePicture < ApplicationRecord
  belongs_to :employee
  belongs_to :picture
end

class Employee < ApplicationRecord
  has_many :employee_pictures
  has_many :pictures, through: :employee_pictures
end

class ProductPicture < ApplicationRecord
  belongs_to :product
  belongs_to :picture
end

class Product < ApplicationRecord
  has_many :product_pictures
  has_many :pictures, through: :product_pictures
end

また、共通の親テーブルだと下のようになります。

class Picture < ApplicationRecord
  belongs_to :imageable
  has_one :employee, through: :imageable
  has_one :product, through: :imageable

  def imageable
    employee || product
  end
end

class Imageable < ApplicationRecord
  has_one :picture
  has_one :employee
  has_one :product

  # employee && product == falseとなることをvalidationで保証する必要があるが割愛
end

class Employee < ApplicationRecord
  belongs_to :imageable
end

class Product < ApplicationRecord
  belongs_to :imageable
end

この二つなら共通の親テーブルの方が好きです。

7. マルチカラムアトリビュート(複数列属性)

タグを最大3つまで持つので、tag_id1, tag_id2, tag_id3とカラムを作るみたいな話。
こんなこと誰もしませんよね。(3回目)
やりたいことの本質は1章のジェイウォークのそれとかわらないので、解決策も同じ。
テーブル増やして、has_many

8. メタデータトリブル(メタデータ大増殖)

レコードが増えたからといって、log_2017, log_2018みたいにテーブルを分割してはいけない、という話。
解決策としては、DBの機能であるパーティショニングを使って、水平分割or垂直分割することになる。
DBによって様々だと思われる。
例えば、PostgreSQLだと、Rails+PostgreSQLのパーティショニングを制覇する(翻訳)が参考になるかもしれない。

すみません。まだ大量のレコードを経験したことがないのでわかりません。

おわり

こういう記事書きたいなと思っていて、いざ書き始めたが、悲しいことに知見が足りませんでした(特に8)。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.