読者です 読者をやめる 読者になる 読者になる

クラウドワークス エンジニアブログ

日本最大級のクラウドソーシング「クラウドワークス」の開発の裏側をお届けするエンジニアブログ

Rails Engineを使って管理画面を作ろうとした話、とその後

初めまして、3月からクラウドワークスの開発Div.にjoinした山本です。
好きなお酒は芋焼酎で割り方はお湯割一択です!

さて、私は入社してから社内の業務支援系のシステムの新規開発に携わってます。
今回の内容は社内ツール開発時に管理画面をRails Engineで作った時の話をしたいと思います。
(筆者は今回初めてRails Engineを触ったので、生暖かい目で読んでいただけると助かります)

本記事はRails Guidesの内容を参考にさせてもらってますがまとめとして残そうと思います。

そもそもRails Engineってどんな機能?

アプリケーションのミニチュアのようなものであり、ホストアプリケーションに機能を提供します。
full/mountableの2種類がありrails plugin new するときにそれぞれ --full(省略可)--mountable オプションをつけて作成します。
--mountableオプションをつけるとRailsアプリのスケルトン構成を自動的に作ってくれます。

今回は(慣れている作りで)比較的時間をかけずに実装したいということで--mountableオプションをつけて作ることにしました。

注)ここより先は読みやすいように
--mountableオプションを使ったRails Engine → Mountable Engine
として説明していきます。

Mountable Engineの特徴

ホストアプリケーション内に別アプリを入れ子して機能単位で管理できる仕組みという認識です。
認証機能を提供するdeviseや管理画面を分離する機能を提供するRailsAdminなどが有名かと思います。

特徴としては

  • Railsアプリと同等のスケルトン構造である。
  • ホストアプリケーションにマウントすることで容易に機能の拡張ができる。
  • 名前空間分離されているためホストアプリケーションとの衝突を回避できる。
  • ダミーのテスト用アプリケーションがデフォルトでtest/dummyに配置されており個別でテスト等の管理ができる。

今回の経緯

  • 管理画面の機能を設計から任された。
  • やるなら何か新しいことにチャレンジしてみよう。
  • あれこれ探しているうちにRails Engineを使って管理画面を作っているエントリを見つける。*1
  • これでいこう!

はい、今思い返すと圧倒的に安易な考えですね。
というわけで管理画面を作った訳ですが、結論から言うと今回の仕様の関係で幾つかの問題があり、導入はできませんでした。
振返りの意味も含めて以下でまとめたいと思います。

注)ここより先は読みやすいように
ホストアプリケーション → MainApp
Mountable Engine側のアプリケーション → EngineApp で書いていきます

今回導入できなかった理由について

  • 管理画面でやりたいことがMainAppのマスタやユーザ管理が主だったためEngineAppに対する依存が大きくなりそうだった。
  • そのため、EngineAppの独立性を保ち続けるのが辛い。
  • EngineApp側からのMainApp側に依存する形で試作を作ってしまったので個別にテストが書けなかった。
  • どこまでコストがかかるか想定できず、それ以上踏み込んでいけなかった。

はい、「作る前に気付こうね(#^ω^)ピキピキ」な内容です。。。一番の要因としては
仕組みの理解が浅くMainAppとEngineAppを相互依存する形で作ってしまったため、つらくなることが容易に想像できたことでした。
以下、Rails Guidesより

アプリケーションは いかなる場合も エンジンよりも優先されます。ある環境において、 最終的な決定権を持つのはアプリケーション自身です。エンジンはアプリケーションの動作を大幅に変更するものではなく、 アプリケーションを単に拡張するものです。

あくまでMainAppに機能を拡張するものであり、EngineApp側に責任を押し付けすぎるというのは本末転倒でした。。。
まとめると完全に理解が足りてなかった。

さらに気づいたこと

反省点

  • 今回は社内ツールの新規開発なのにそもそもRails Engineとして切出しを検討するのは早すぎた。(まだその段階ではなかった。)
  • 知見不足のために、スピード感を持って柔軟に対応ができなかった。
  • 検討する時間を十分に取れないのに、半ば思いつきで走ってしまった。

やってよかった点

  • パワフルな機能なのでこの先いろいろ所で使えそうな希望が見えた。
  • 個人的にはあまり触らない機能を知る取っ掛かりができた。
  • 何より知らないことに取り組むのはモチベーションにつながる。

その後

Mountable Engineでの管理画面はできませんでしたが、何かしら作ってみたいということで
Rails Guidesを参考に掲示板(bulletin_board)の機能の作りました。

まずはアプリケーションの雛形とusersを作成。

rails new main_app --skip-bundle
cd main_app
bundle install --path vendor/bundle
bundle exec rails g scaffold user name:string
bundle exec rake db:create
bundle exec rake db:migrate
bundle exec rails s

動作確認ができたら早速mountable engineをアプリ内にplugin newコマンドで作成します。

$ bundle exec rails plugin new bulletin_board --mountable --skip-bundle

デフォルトではTest::Unitを使う仕組み(engine_app直下にtest/dummy)が作成されますが、rspecを利用したい場合はオプションをつけて作成します。
(-T: Test::Unitを使う仕組みをskipする --dummy-path: rspecで使うdummy_appを作成)
また、MainApp直下が気に入らない場合は以下のように指定のディレクトリに作成も可能です。

$ bundle exec rails plugin new vendor/engines/bulletin_board --mountable --skip-bundle -T --dummy-path=spec/dummy

ディレクトリの構成を見てみると以下のようになってます。

f:id:moti245:20160516202845p:plain

基本的にはrails new で作成したものと同じ構成になっています。

engineでも通常のアプリと同じようにscaffoldが使えるのでboard(掲示板)Classを作ります。

$ cd bulletin_board
$ bundle exec rails g scaffold board title:string

作成されたモデルを見てみるとClass名がBulletinBoard::Boardとなっていると思います。
MainApp側との衝突を避けるためにnamespaceが自動で付加さるようです。

$ bundle exec rake db:create
$ bundle exec rake db:migrate
== 20160516094903 CreateBulletinBoardBoards: migrating ========================
-- create_table(:bulletin_board_boards)
   -> 0.0011s
== 20160516094903 CreateBulletinBoardBoards: migrated (0.0011s) ===============

作成されたマイグレーションにはbulletin_boardのscopeが付いていることが確認できます。

ここまで出来た段階でbulletin_boardがちゃんと動くか動作を確認してみます。
Engineではtest/dummyに移動することでrails sが使えます。

$ cd test/dummy
$ bundle exec rails s

http://localhost:3000/bulletin_board/boards/

f:id:moti245:20160517112726p:plain:w300

問題なく見れますね。
次に掲示板のコメント機能を作ります。

$ cd ../../
$ bundle exec rails g model comment text:string board_id:integer
$ bundle exec rake db:migrate
== 20160516100103 CreateBulletinBoardComments: migrating ======================
-- create_table(:bulletin_board_comments)
   -> 0.0010s
== 20160516100103 CreateBulletinBoardComments: migrated (0.0011s) =============

ここまででEngine側の作り方はある程度イメージができたかと思います。
ここから先はいつも通りの作り方なので省略します。
出来上がりは下のような感じ。

f:id:moti245:20160517113119p:plain:w300

掲示板にコメントを追加していくイメージです。

EngineAppができたので次にMainApp側から使えるように以下を追記してエンジンをマウントします。

Gemfile

gem 'bulletin_board', path: 'bulletin_board'

config/routes.rb

mount BulletinBoard::Engine, at: 'bulletin_board'

更にDBを共有するためMainApp側にEngineApp側で作成したマイグレーションファイルをコピーします。

cd main_app/
$ bundle exec rake bulletin_board:install:migrations
Copied migration 20160515141241_create_bulletin_board_boards.bulletin_board.rb from bulletin_board
Copied migration 20160515141242_create_bulletin_board_comments.bulletin_board.rb from bulletin_board
$
$ bundle exec rake db:migrate
== 20160515141241 CreateBulletinBoardBoards: migrating ========================
-- create_table(:bulletin_board_boards)
   -> 0.0010s
== 20160515141241 CreateBulletinBoardBoards: migrated (0.0011s) ===============

== 20160515141242 CreateBulletinBoardComments: migrating ======================
-- create_table(:bulletin_board_comments)
   -> 0.0006s
== 20160515141242 CreateBulletinBoardComments: migrated (0.0006s) =============

ここまでで準備は終わりです。
最後にuserの詳細画面からEngineApp側の掲示板へ遷移できるようにしてみます。

app/views/users/show.html.erb

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @user.name %>
</p>

+ <%= link_to 'BulletinBoard', bulletin_board.boards_path %> |
<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>

それとは逆にEngineAppからMainAppへの遷移も以下のようにpathを指定すれば可能です。

bulletin_board/app/views/bulletin_board/boards/index.html.erb

<p id="notice"><%= notice %></p>

<h1>Listing Boards</h1>

-- 省略 --

<br>
+ <%= link_to 'Back MainApp', main_app.root_path %>
<%= link_to 'New Board', new_board_path %>

使ってみて良かったと思う点

  • 同リポジトリ内で完結していていて名前空間で分けられているためMainApp側のマニフェストファイル等を使える。
  • 疎結合な作りを強制的に考えることができる。
  • テストが個別でできるので保守性が上がる。

これから試すこと

  • rspecでテストを書く。(今回ここまでやろうとしてましたが、間に合いませんでした。。。)
  • デプロイ時にテストと静的解析ができるようにCIとの連携を考える。
  • Apiを提供するタイプの切出しを模索してみる。

最後に

いかがでしたでしょうか。私の感想としては思ったよりも柔軟にできて、いろいろな場面での利用ができそうな印象を持ちました。
学習コストは多少ありますが、モノリシックなアプリケーションや、複数アプリケーションで汎用的な機能をコピーして使いまわしているなどの場面で威力を発揮してくれるはずです。
「あれ?これは。。。」となった時は是非試してみて下さい。

この記事を見て少しでも興味を持って頂ければ幸いです。

クラウドソーシングのクラウドワークスではチャレンジングな環境でサービスと一緒に成長していきたいエンジニアを募集しています!

www.wantedly.com

© 2016 CrowdWorks, Inc., All rights reserved.