LoginSignup
16
9

More than 5 years have passed since last update.

SQLアンチパターンon Rails Ⅰ部(論理設計)

Last updated at Posted at 2018-03-24

はじめに

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)。

16
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
9