Edited at

複数Railsアプリケーションから同一DB(テーブル)を参照する場合にRails Engineを活用してmodelのソースを共有する

More than 1 year has passed since last update.

色々あって、表題を実装しました。

もしこのようなことをやろうと思ってる方の1つの手段として参考になれば幸いです。


前提条件


RubyやらRailsのバージョン関係

Ruby:2.2.3

Rails:4.2.6


システム構成案

アプリケーションとDBの関係は以下(あくまで例です)

スクリーンショット 2016-06-15 9.36.31.png


やりたいこと

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を軽く実装します。


rails_engine_sample/app/models/rails_engine_sample/customer.rb

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で作成するにあたり、モデルの定義は以下のようになるでしょう。


rails_engine_sample/app/models/rails_engine_sample/customer.rb

module RailsEngineSample

class Customer < ActiveRecord::Base
def hoge
'hoge'
end
...(実装)
end
end

なので上位のアプリケーションで使用するとすれば


sale_app/config/routes.rb

Rails.application.routes.draw do

resources :customers
end


sale_app/app/controllers/customers_controller.rb

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化したという背景もあります)


rails_engine_sample/lib/rails_engine_sample.rb

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へのアソシエーションは以下のように一手間かかりますが書けます。


sale_app/app/models/sale.rb

class Sale

belongs_to :customer, class_name: :'RailsEngineSample::Customer'
...
end

とclass_nameを指定してあげれば問題なく参照が可能です。では逆の場合はどうしたかというとさらに一手間かけます。まずはpluginで拡張できるようにmodelの定義を以下のように変更します。


rails_engine_sample/app/models/rails_engine_sample/concerns/customer_active_recordable.rb

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



rails_engine_sample/app/models/rails_engine_sample/customer.rb

module RailsEngineSample

class Customer < ActiveRecord::Base
include RailsEngineSample::Concerns::CustomerActiveRecordable
end
end

こんな風に実態をActiveSupport::Concernで外に出します。で上位側でこのmodelを拡張したい場合は


sale_app/app/models/rails_engine_sample/customer.rb

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を外したかったので、


rails_engine_sample/lib/rails_engine_sample.rb

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