ADZ 學習筆記

Ruby/Rails, Startup, Life

rails 筆記 - 實作包裝 business logic in gem (Db Migration & Test)

| Comments

繼上篇 移植 business logic model 到 gem 後 也把 model spec 都 copy 過來,不過因為之前提到的 devise, simple_enum 的執行要到 rails initialize 階段才會載入,所以我們應該要在 spec_helper.rb 把不足的東西先處理好,但是在這之前,我想先處理 migration 後在一併把 spec_helper 設定好:

A. Migration

再開始實作 shared migration 前,還沒什麼想法,直到找到這篇文章:Leave your migrations in your Rails engines

簡單來說,夠過 Rails::Engine 能讓使用的 application 增加 db/migration 應該要讀取的 path,讓你的 app 擁有以下功能:

  1. 輸入 rake your_engine_name:install:migrations 會自動 copy migrations file 到你的 app 內。
  2. 輸入 rake db:migrate 也會包含 gem 內的 migrations。

但我的需求是,有多個平台會共用一個 database,所以我們只需要其中一個 project 來跑 migration,所以我們做了以下判斷,將 primary_migration_repo 留給使用的專案來設定:

lib/gem_name/engine.rb
module GemName
  class Engine < ::Rails::Engine
    initializer :prepend_migrations do |app|
      if app.config.respond_to?(:primary_migration_repo) && app.config.primary_migration_repo == true
        config.paths["db/migrate"].expanded.each do |prepend_path|
          app.config.paths["db/migrate"].unshift(prepend_path)
        end
      end
    end
  end
end

然後要 require

lib/gem_name.rb
require 'gem_name/engine' if defined?(Rails)

負責跑 migration 的專案 application.rb 需要設定 primary_migration_repo=true

/config/application.rb
# ... (略)

module Project
  class Application < Rails::Application
    # ... (略)

    config.primary_migration_repo = true
  end
end

最後,把原本專案內的 migration 以相同的目錄結構丟到 gem 內,在專案內 run rake db:migrate 成功了,專案內讀取的到 gem 的 migration file。

B. Model Test

完成了 model, migration,剩下我們要移植 model spec 到 gem 裡,在跑 spec 前,我們應該要完成三件事情:

1. 設定 Test Database

因為不想再開台 mysql 來測,於是直接使用 sqlite,於是再 gem_name.gem_spec 加入 sqlite3

gem_name.gem_spec
Gem::Specification.new do |spec|
  # .. (略)

  spec.add_development_dependency 'sqlite3'
  # .. (略)

end

2. Run migration

這裡,我有看到兩種 run migration 的方式:

為了簡單方便,直接使用第二種方式:先在 app 跑完 migraion,然後輸入 rake db:schema:dump 再把 schema.rb copy 到 gem 的 spec 資料夾下,然後寫一隻 support/settings.rb 來負責 database configuration 和跑 migration 等等事情:

spec/support/settings.rb
require 'active_record'
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"

require 'schema' # 載入 schema

並在 spec_helper 內設定 require settings

spec/spec_helper.rb
require 'bundler/setup'
Bundler.setup
require 'support/settings'

RSpec.configure do |config|
    # .. (略)

end

3. 想辦法讓 devise, simple_enum 在 ActiveRecord::Base 上被混入。

一開始想說 devise, simple_enum 把 mix 動作設定再 initializer 階段,應該有其用意,我是否也需要模擬一個 rails application 才做得到? 於是我在 simple_enum 的 spec 找到這段 support。

https://github.com/lwe/simple_enum/blob/master/spec/support/active_record_support.rb#L26

原來直接 mix 就好了,於是我把 setting.rb 改成

spec/support/settings.rb
require 'active_record'
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"

require 'schema' # 載入 schema


# Extend libraries

require 'devise'
require 'devise/orm/active_record' # 這支檔案會 mix devise modules 到 activerecord

require 'simple_enum'

# 手動 mix simple_enum 到 activerecord

ActiveRecord::Base.send :extend, SimpleEnum::Attribute
ActiveRecord::Base.send :extend, SimpleEnum::Translation

require 'gem_name' # require all gem


GemName.load_platform :api # 載入 API platform models

這裡即使沒有 rails app context 還是可以手動將 devise, simple_enum 的一些 module mixin ActiveRecord::Base,然後 run rspc 全部綠燈,完成 :)

後記:

1. 尚未解決的問題

不過目前 platform 那段程式,只要呼叫 load_platform 就會 mix modules 到 model 中沒辦法 rollback 或 reset,有試著找過暴力 overwrite 的方式,不過好像行不太通,如果有人知道請告訴我。

2. Rails::Railtie 和 Rails::Engine

一開始有點疑惑這兩個有什麼不一樣,看了一些文件還是覺得有點模糊,不過實測上發現,migration path 寫在 Rails::Railtie 裡面會找不到 app,推論是 Engine 比 Railtie 能獲得更多 application context。

相關連結:

看完很多程式碼後才發現,原來有 rails plugin 的 template 可以用,早知道一開始就不用那麼辛苦了,果然看官方資料還是最快的。

相關文章

Comments

comments powered by Disqus