はじめに
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を使うと良さそう。
- ancestry(経路列挙モデル)
- awesome_nested_set (入れ子集合モデル)
- closure_tree(閉包テーブルモデル)
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
が使えても嬉しくない -
id
とbug_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
を使えば、BugStatus
のbug_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)。