RailsでModelをDBから分割・整理する
それなりの規模のRailsプロジェクトではよくFat Model問題とかFat Controller問題が課題になってくる。
で、Fat Model問題については、Concernsなどに振る舞いを切り出したり、そもそも「それって1つのテーブルに収める必要ある?」というものを分割するなどの対策がある。
後者については、http://kenn.hatenablog.com/entry/2014/03/05/081525 この記事がわかりやすいサンプルとしてUserモデルからProfileモデルをテーブルごと切り出す例を挙げている。
今僕が関わっているプロジェクトでも上記例のように姓名などはProfileに切り出してあり、その他も住所関連の情報をAddressに、「退会理由」みたいなめったにつかわない情報をExtraInfoに、といったように意味合いと使用頻度を考慮していくつか分割する設計になっているものがあるんだけど、このような立ち位置のモデルが増えてくると、app/modelsの直下にフラットにモデルを配置していることがキツくなってくる。
ということで、userに関するモデルはネームスペース切ってまとめたので、そのときの手順をProfileを例にして以下にメモ。
テーブル名を変更
'profiles'というテーブル名だったけど、MySQLなどのRDBMSではuserとの関連が見えたほうがわかりやすいので、'user_profiles'という名前に変えた。これによって'admin'の'profiles'が欲しくなっても怖くないぜ、という効果もあるかもないかも。
usersディレクトリをmodels下に掘る
app/models/users/profile.rb
という按配になる。
userディレクトリにしたいキモチが少しあるけど、userモデルがあるので同名のmoduleは作れないので、usersで良しとする。
ProfileモデルをUsers module配下に
こんな感じですな。
module Users class Profile < ActiveRecord::Base self.table_name = 'user_profiles' ~snip~
self.table_nameの指定が必要になるけど、僕はあまり気にしない。気になる人は、module名とテーブル名のprefixのルールを厳格に管理できるのならActiveRecord::Baseを継承したクラスを作ってごにょごにょしてもいいかもしれないし、僕もそのうちそうするかもしれない。
Userモデルの関連を修正
app/models/user.rb
class User < ActiveRecord::Base ~snip~ has_one :profile, dependent: :destroy, foreign_key: 'id', class_name: 'Users::Profile' ~snip~
こんな感じで、class_nameを指定する必要がでてくるけど、大した問題じゃあない。
テスト(Spec)ファイルを移動・修正
app側と同じく、spec/models/usersディレクトリを掘って、そこにprofile_spec.rbを移動。
修正は冒頭のクラス指定箇所くらい。
RSpec.describe Users::Profile, type: :model do
Factoryファイルを移動・修正
Rspecの場合。
spec/factories/usersディレクトリを切って、そこに移動する。
で、class指定をする。
FactoryGirl.define do factory :user_profile, class: Users::Profile do ~snip~
localeファイルの移動・修正
僕は日本語対応しかしないプロジェクトでも表記ゆれ防止とかテスタビリティのこととか考えるとI18nを利用したほうがいいと思っているので、この作業も必須になる。
これも同じようにconfig/locales/models/usersディレクトリを切ってprofile/ja.ymlを配置する(localファイルは好きな場所に置けるのでシステム的な意味はない。)
で、ネームスペースをどう表現するか、なんだけど以下のようになる。
Before
ja: activerecord: models: profile: プロフィール
After
ja: activerecord: models: users/profile: プロフィール
ということで/でネームスペース(module名)とモデル名を区切ればいい。
使う時も以下のような感じで問題ない。
I18n.t('activerecord.attributes.users/profile.last_name')
こんな感じにしておけば、Userに関連するモデルがどこにあるのかわかりやすくなるし、使う分にはuser.proflieと言った具合にこれまでと何の変わりもないし、自分的には良いこと尽くめだと思うんですけどどうですかね。
Railsでhttpsでアクセスしてもrequest.urlがhttpを返してきてしまう場合
ロードバランサーでSSL/TLSのデコードを行う場合、ブラウザのアドレスバー的にはhttpsでもアプリからみるとhttpなのでrequest.urlとかimage_urlとかがhttpで返してきて都合が悪かったりする場合の話。
このあたりはRailsというかRackの処理になっていて、たぶんここで決まるんだと思う。
https://github.com/rack/rack/blob/master/lib/rack/request.rb#L66
で、冒頭のケースは、最初のif文を抜けちゃうケースだと思うんだけど、2つめの条件である、HTTP_X_FORWARDED_SSLで引っ掛ければうまくいきそうだ。
なのでapplication_controllerとかのbefore_actionでrequest.env['HTTP_X_FORWARDED_SSL'] = 'on'とかやっとけば後続の処理はhttpsとして捉えてくれる。
そういや、AWSのRoute53だとこういうこと起きないなと思ったら、AWSのRoute53でデコードするとHTTP_X_FORWARDED_SSL: onつけてくれてたからだね。
Capybaraで不定期にElementNotFound(特にCircleCIで)
何事もall or nothingにしたがる人は置いといて、ぼくはせっせと効果的な範囲でテストを書いていきたいと思っていますが、そんなぼくの心を知らずしてか、Capybaraは不定期に以下のようなエラーを吐く。何故か特にCircleCIで。
Capybara::ElementNotFound:
Unable to find css "#some_selector"
Bootsrapを使っているときに顕著なんだけど、cssのtransitionなどのエフェクトが完了するまえにCapybaraが先走ってしまうことがあるので不定期に起こってしまう、というシナリオ。
であれば、テストのときはエフェクト切ればいいじゃない。
http://stackoverflow.com/questions/14488836/capybara-2-having-trouble-interacting-with-bootstrap-modals
application.html.erb
~snip~ <%= javascript_tag '$.fx.off = true;' if Rails.env.test? %> <%= stylesheet_link_tag 'disable-transition' if Rails.env.test? %> </head> ~snip~ <% if Rails.env.test? %> <script> $(".fade").removeClass("fade"); </script> <% end %> </body>
disable-transition.css.scss
div, a, span, footer, header { -webkit-transition: none !important; -moz-transition: none !important; -ms-transition: none !important; -o-transition: none !important; transition: none !important; } .modal { display: none !important; } .modal.in { display: block !important; } .modal-backdrop { display: none !important; }
これで発生率がぐっと減るはずだっちゃ。
railsでhttpヘッダのを確認する
意外とゴリッと出すしかなかった。
request.headers.sort.map { |k, v| logger.warn "#{k}:#{v}" }
ゴリっていう音がでるほどじゃないか。
active_decoratorとoctopusは相性が悪い
master-slaveの振り分けを行いたくてoctopusを導入した。
正直octopusはやりたいことに比べてtoo much感がプンプンしているんだけど、他のgemは導入に手間取ったのでoctopusにした。
そしたら、あるとき
undefined method to_a' for classOctopus::RelationProxy'
というエラーがでるか所がいくつかあることに気づいた。
調べてみると、どうもactive_decoratorが絡んでいるもよう。細部を見ると以下のコードが原因っぽい。
https://github.com/amatsuda/active_decorator/blob/master/lib/active_decorator/decorator.rb#L26
ただ、詳細まで追ってないので確信はないけど、これはどちらかというとoctopusがactiverecordをうまくラップできてないせいなんじゃないかと思う。
とはいえ、octopus以外の選択肢が取りづらい状況ではあったのでdraperに移行した。
draperはdecorateしたいタイミングで明示的にdecorateするので上記のようなことは起きない。ちょっとめんどくさいけど。
というだけのお話。。。
Rails4でID以外をプライマリーキーにする
かっこつけて「なんでもIDついているのがレイルズウェイです!うぇーい!」とか言っているやつがいるんですよ〜。
なぁーにぃ!?やっちまったな!!
男はだまって
id: false!!
add_index!!
def change create_table :foo_tables, id: false do |t| t.integer :foo, null: false t.timestamps end add_index :foo_tables, :foo, unique: true end
- +
| Field | Type | Null | Key | Default | Extra |
- +
| foo | int(11) | NO | PRI | NULL | |
結局、プライマリーキーなんてのは、NULL制約+UNIQ制約なんだってな!
これはMySQLでしか確認してないけど!
モデルにも対応が必要だ!
class FooTable < ActiveRecord::Base self.primary_key = :foo ~snip~ end
こんだけだ。
FactoryGirlとかの対応?
求めれば周りが右往左往して
世話を焼いてくれる
そんなふうにまだ考えてやがるんだ
臆面もなく・・・!
大人は質問に答えたりしない それが基本だ・・・!
RailsでSTIなfactoryの書き方
Vehicle <- Car
Vehicle <- Bike
みたいな関連があるときのFactoryの書き方。
こんな感じに書くとDRYだし、親モデルのテストにも使える。
FactoryGirl.define do factory :vehicle, class: 'Vehicle' do ~共通的な項目~ end factory :car, parent: :vehicle, class: 'Car' do ~独自項目~ end factory :bike, parent: :vehicle, class: 'Bike' do ~独自項目~ end end
class指定はなぜか必須のよう。ファイルはもちろんバラバラでも問題ない。