RailsからActiveRecordを切り離してみた

2020年11月27日

こんにちは。
CTOの徳江です。

今日は私の経験をお送りしたいと思います。


エピローグ


とある受託案件 社内向けRailsアプリ(シンプルなMonolith)のリリース目前にて、
「このシステム、セキュリティの関係でDBを直繋ぎ禁止になった! API経由にできない?」

エヘヘ(●´∀`).。o○(やっべ、たのしそ🤤 やってみちゃぉ🙃)


というわけで、既存のRailsアプリを、
  1. API+DB側rails (security-group などで厳しめに保護)
  2. フロント側rails (rails 既存資産を生かすため)
に分離しました。
※ どちらも内省で、自由に改修可能
※ フロント部分は完成しており、ルーティング含め手を入れたくない → 最短でRailsで行く

※ 成功を保証するものではありません。ご了承を😜

レシピ:1.API+DB側rails


全テーブル分をgrapeでcrud作っちゃいます。

module GrapeApi
    class Base < Grape::API
      ActiveRecord::Base.connection.tables.map(&:classify).map(&:safe_constantize)
      ApplicationRecord.descendants.each do |model|
        entity = ResourcesEntityFactory.create(model)
        resource model.table_name.to_s, desc: model.model_name.human do
          get do
            authenticate!
            scope = search_scope(model, params)
            params.each { |key, value| scope = scope.where(key.to_sym => value) if model.new.attributes.keys.include? key }
            entity.represent scope
          end
          get '/:id' {...}
          post {...}
          put '/:id' {...}
          delete '/:id' {...}

          # *1 fields, associations, scopes
          get '/schema' do
            authenticate!
            {
              fields: model.attribute_types,
              associations: model.reflect_on_all_associations.map(&:name),
              scopes: scopes(model)
            }
          end

          # *2 association
          namespace '/:id' do
            model.reflect_on_all_associations.each do |assoc|
              resource assoc.name.to_sym do
                get '' do
                  authenticate!
                  Array.wrap(model.find(params[:id]).send(assoc.name))
                end
              end
            end
          end
        end
      end
    end
end


  • 隠し味
    • ransack 検索機能の柔軟化
    • kaminari 検索機能の柔軟化
    • 追加API: *1 モデル情報として、field一覧、association一覧、scope一覧を返す
    • 追加API: *2 associationを返す
    • token認証系(秘密)


レシピ: 2. フロント側rails


ここからが本領、ですね。
めっちゃ難しかった(= 楽しかった🙃)

  • ActiveRecord → ActiveModel + her gem
コチラ参考に諸所検討し、herにしました。
- そこそこコミュニティが元気で更新あり
- かゆいところに手が届く
- 同時の「scope」代替機能

実装も以下のように結構簡単
class ApplicationRecord < ActiveRecord::Base
class ApplicationRecord
  include Her::Model
  include ActiveModel::Serialization

  parse_root_in_json false, format: :active_model_serializers
  request_new_object_on_build true
  include_root_in_json true
最後の3行は、apiと合わせるためのおまじないですね。

※ 昔使ったActiveResourceは、最近元気ない & エラー多くてRails6にはマッチせず😭

サービス起動時などで意図せずDB接続が発生。
DBのadapter階層で撲滅 🙉

  • ActiveRecorde系
    • そのままでOK🎉 
      • AR accept_nested_attribute_for ← 一括コミットに大事 🎉🎉🎉
      • ARのscopeの遅延実行 🎉
    • それっぽく独自実装💪
      • save(validate: false, callback: false, context: 'aaaa')
      • find: raiseしてくれないので、find! を作って置き換え
      • オブジェクトキャッシュ -> request_store gem
      • polymorphic 関連
    • ApplicationRecordへコピー実装 😅
      • enum
      • has_secure_token
    • 今回はスルー 🙈
      • ActiveRecordのincludes, preload、eager_load
      • i18n: APIとサーバーで二重実装
      • find_each, find_in_batches
    • transaction系
      • 多くはaccept_nested_attribute_forで一括コミット
      • ↑で処理できない範囲は…専用APIが必要
        • → apiのrequest-responseをリダイレクトするようなclientを作って対応

  • gem系
    • そのままでOK🎉
      • simple_form
      • kaminari -> application_recordでAPIにそのまま流す 
      • paranoid, acts_as_list
      • papertrail (API側に託す)
      • Draper(decorator)
    • 大玉: her用adapter自作
      • devise
        デフォルトで、activerecord用とmongoid用のadapterがあります。
        → her用のadapterを、模倣しながら自作(ちょっと怖いので非公開🙊)
        参考: dynamoid-devise, devise-activeresource
      • ransack
        ここはやばかったですね😭(非公開)
        極力、シンプルにher型のAPIに変換して投げるように工夫(かなり不十分)
    • 今回はスルー🙈
      • activerecord-import
      • kaminari の ページネーションURL


というわけで、


どうにかなりました。
大分趣味的パズルですが、
どこかの誰かの助けになれば幸いです 😉
コメント