色々あって、表題を実装しました。
もしこのようなことをやろうと思ってる方の1つの手段として参考になれば幸いです。
前提条件
RubyやらRailsのバージョン関係
Ruby:2.2.3
Rails:4.2.6
システム構成案
やりたいこと
Railsのmodel部分を共有したい。これにつきます。
方法は色々あると思いますが、自身がやったのは
「共通となるModelをRails EngineにてPlugin化し、各アプリケーションでそれを共有/使用する」
です。他にもgitでごにょごにょするとか色々あると思います。
共通modelのplugin作成
共通となるmodelはRails Engineでpluginとして作成します。
※ここは以降の本題で使用するための前準備なので読み飛ばしても結構です。
rails plugin new rails_engine_sample --skip-bundle --mountable -T --dummy-path=spec/dummy
これでまずはpluginの雛形を作ってしまいます。ちなみに上記はRspecを使用するために書いたオプションがあるので、Rspec使わない人は --mountable
以降はいりません。
今回の例だと取引先のmodelを共通化するのでそのmodelを作ります。
rails g model customer
本来であればこの取引先のmodelの属性をmigrateファイルに追加しますが、ここで追加すると問題があるので今は作成しません。
まずは取引先のmodelを軽く実装します。
module RailsEngineSample
class Customer < ActiveRecord::Base
def hoge
'hoge'
end
...(実装)
end
end
後は上位側のアプリケーションのGemfileに以下を追加してbundle install
すれば追加でき、上記のmodelが上位側のアプリケーションで使用できるということです。
gem 'rails_engine_sample', path: '../rails_engine_sample' #or 'https://github.com/chimame/rails_engine_sample.git'
実装上の注意点
本記事の本題はここから
migrate(DB変更)はどうする?
先ほど、generatorを実行するとmodelsやspec(or test)の下にcustomerモデルに関連するファイルができますが、厄介なのがmigrateです。その理由を以下に記載します。
今回の構成上でplugin内でmigrateを作成した場合のmigrate実行アプリケーションはどこでしょうか?
今回はこのpluginが複数のアプリケーションで使用されることを想定しています。pluginが管理しているモデルのmigrateはpluginで書きたくなりますが、実際にそのmigrateが実行されるのは上位のアプリケーションです。上位のアプリケーションでpluginのmigrateを実行するには
rake rails_engine_sample:install:migrations
を叩き、plugin内のmigrateをアプリケーションにコピーしてきてからrake db:migrate
を実行するわけですが、
複数アプリケーションで実行すればファイル名が異なり、同一内容のmigrateにも関わらず、各アプリケーションで実行されてしまうことになります。
それはできないので自身がとった内容は
「Railsのmigrate機能は使わず、migrate管理専用のプロジェクトを作成する」
cookpad開発ブログでも紹介されている『Ridgepole』を使用することにしました。
こうすることによって、migrate管理を一元化することで上記の問題をクリアです。
なのでplugin側で作成されるmigrate関係のファイルは捨てましょう。
pathとurlヘルパーにpluginのモデルを使用するとmodule(plugin)名が含まれてしまう
pluginで作成するにあたり、モデルの定義は以下のようになるでしょう。
module RailsEngineSample
class Customer < ActiveRecord::Base
def hoge
'hoge'
end
...(実装)
end
end
なので上位のアプリケーションで使用するとすれば
Rails.application.routes.draw do
resources :customers
end
class CustomersController < ActionController::Base
def edit
@customer = RailsEngineSample::Customer.find(params[:id])
end
end
編集画面を開ける場合は、上記のようなコードで編集画面に@customer
を渡すわけですが、その@customer
はView側でform_forヘルパー等に渡されることがあります。
<%= form_for @customer do |f| %>
...(入力コントロール等諸々)
<% end %>
するとRailsは
undefined method `rails_engine_sample_customer_path' for ...
のエラーを吐くと思います。これはヘルパー内でmodule名を含めたpathヘルパーを呼んでるためです。module名をroutesに含めれば問題ありませんが、そうしたくない場合は、plugin内で以下を記述します。(自分の場合は既存アプリケーションから一部をplugin化したという背景もあります)
module RailsEngineSample
def self.use_relative_model_naming?
true
end
end
これでmodule名を含めたpathヘルパーではなくなるため、処理できるようになると思います。もしcontrollerやviewもpluginに含める場合はこのような問題は発生しないでしょう。
上位アプリケーションにてpluginのmodelにアソシエーションの追加等の拡張を行いたい
pluginには各アプリケーションで共通となるmodelを持っていますが、上位側でもmodelを保持するでしょうし、pluginのmodelともアソシエーションも定義したいでしょう。上位アプリケーションのmodelからpluginのmodelへのアソシエーションは以下のように一手間かかりますが書けます。
class Sale
belongs_to :customer, class_name: :'RailsEngineSample::Customer'
...
end
とclass_nameを指定してあげれば問題なく参照が可能です。では逆の場合はどうしたかというとさらに一手間かけます。まずはpluginで拡張できるようにmodelの定義を以下のように変更します。
module RailsEngineSample
module Concerns
module CustomerActiveRecordable
extend ActiveSupport::Concern
included do |klass|
has_many :payment_terms
scope :by_fuga, ->(fuga){where(fuga: fuga)}
def hoge
'hoge'
end
..(実装)
end
end
end
end
module RailsEngineSample
class Customer < ActiveRecord::Base
include RailsEngineSample::Concerns::CustomerActiveRecordable
end
end
こんな風に実態をActiveSupport::Concernで外に出します。で上位側でこのmodelを拡張したい場合は
module RailsEngineSample
class Customer < ActiveRecord::Base
include RailsEngineSample::Concerns::CustomerActiveRecordable
has_many :sales
#Overrideする場合は普通に再定義
def hoge
'hoge2'
end
end
end
これでplugin内で定義されたmodelの拡張も思いのままです。普通にclassを再オープンするとplugin内の定義が失う(正確には見えなくなる)ので、上記の方法だと失うことなく拡張が可能です。
上記3点(特に1番目と3番目)が自分中でなかなか困ったとこでしたが、すっきりした感じにはなったと思っております。
番外編:plugin管理のテーブル名にprefixを付けたくない
これは正直番外編です。
少し触れましたが、自身の場合は既存のアプリケーションから一部をpluing化した背景があり、テーブルは既に存在し、本番運用されています。
plugin管理のテーブルは<plugin名>_<モデル名(複数形)>
になるわけですが、先頭のprefixを外したかったので、
module RailsEngineSample
def self.use_relative_model_naming?
true
end
def self.table_name_prefix
end
end
table_name_prefix
メソッドにて、付与するprefixを無しにすれば問題ありません。
安定の誤字脱字は後に修正するとして、もっといい方法や指摘等あればコメントや編集リクエスト下さい。
参考程度に作ったリポジトリを貼っておきます。
https://github.com/chimame/rails_engine_sample
https://github.com/chimame/rails_engine_use_app_sample