どんなときに有効?
- 1つのテーブルの中に更新/参照頻度が高いカラムと低いカラムが混在している
- 1つのmodelに大量のビジネスロジックがあって可読性が低い
カラムを抽出するメリット
- memcache などキャッシュを使っている場合に無駄が減る
- 参照頻度の高いデータだけキャッシュされるべき
- 更新頻度の多い情報のサイズは小さいほうが良い
- DBのメモリになるべく乗るように
例
User modelの中に、更新/参照頻度が低い profile 関係の情報がある
- zipcode
- prefecture_code
- city_code
- address
User model
$ rails g scaffold user name:string mail:string zipcode:string prefecture_code:integer city_code:integer address:string
db/schema.rb
create_table "users", force: :cascade do |t|
t.string "name"
t.string "mail"
t.string "zipcode"
t.integer "prefecture_code"
t.integer "city_code"
t.string "address"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
テーブルを分割する
1. Profile モデル作成
$ rails g model profile zipcode:string prefecture_code:integer city_code:integer address:string user_id:integer
2. Profile モデルと、Userモデルを関連付け
profile.rb
class Profile < ActiveRecord::Base
belongs_to :user
end
Profile model を delegate することで、既存コードの修正が不要に
user.rb
class User < ActiveRecord::Base
# delegateすることで、Profile モデルの中身を User から取得できる
delegate :zipcode, :prefecture_code, :city_code, :address, to: :profile
has_one :profile
end
3. マイグレーションを作成
- Profileテーブルを作る
- Profileテーブルに移行する情報をUserモデルからコピー
- Userテーブルから、Profileテーブルに移ったカラムを削除
db/migrate/20160619032503_create_profiles.rb
class CreateProfiles < ActiveRecord::Migration
def up
# Profileテーブルを作る
create_table :profiles do |t|
t.string :zipcode
t.integer :prefecture_code
t.integer :city_code
t.string :address
t.integer :user_id
t.timestamps null: false
end
# Profileテーブルに移行する情報をUserモデルからコピー
User.all.each do |user|
Profile.create(
user: user,
zipcode: user.zipcode,
prefecture_code: user.prefecture_code,
city_code: user.city_code,
address: user.address
)
end
# Userテーブルから、Profileテーブルに移ったカラムを削除
remove_column :users, :zipcode
remove_column :users, :prefecture_code
remove_column :users, :city_code
remove_column :users, :address
end
def down
add_column :users, :zipcode
add_column :users, :prefecture_code
add_column :users, :city_code
add_column :users, :address
# Profileモデルの情報をUserモデルに移行
Profile.each do |profile|
profile.user.update(
zipcode: profile.zipcode,
prefecture_code: profile.prefecture_code,
city_code: profile.city_code,
address: profile.address
)
end
drop_table :profiles
end
end
Profile情報が必要な時だけ、Profileテーブルを参照するようになった
irb(main):010:0> User.all.first.zipcode
User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
Profile Load (0.1ms) SELECT "profiles".* FROM "profiles" WHERE "profiles"."user_id" = ? LIMIT 1 [["user_id", 1]]
=> "419-4284"
irb(main):011:0> User.all.first.name
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1
=> "木村 優衣"
この後やるべきこと
- 必然性があまりないのに、Profileの情報を参照している部分を修正する
code
参考
-
プライマリキーを使った1:1関連でカラム数の多いテーブルを分割する - Hidden in Plain Sight
- 必ず1対1であれば、わざわざ関連付けなくても、同じ値のプライマリキーを使うという方法
- ソーシャルゲームのためのMySQL入門その2 - Technology of DeNA