はじめに
Advent Calenderのコメント欄には、「JSのお話」と予告しておきながら、当日Rubyネタに変わる……。
あるあるの話ですね(笑)
さて、Rubyでアプリケーション作る場合、みなさんやっぱりRailsなんですかね?それともSinatra?
なんですか、またフレームワーク宗教戦争ですか?
そうなんです。Ruby以外の言語でも、Railsライク、Sinatraライクって出てきて、飽きたんですよ(笑)
私も、担当サービスではPadrinoを使用していますが、
新たにアプリケーションを作る場合、何でもRails!という風にはしたくない派です。
Railsは、パフォーマンス面でどうしてもチューニング(特にActiveRecord)が必要になります。
そして、config/
下のファイルも多いなぁという印象です。
今回紹介したいのは、Hanami。(旧Lotus。1-2-3じゃないよ。でも、一太郎派だったよ。)
http://hanamirb.org/
意外とQiitaの記事が多くなかったので、特徴を紹介しようと思います。
Hanamiの特徴
思想
Railsは、「アプリケーションが扱うDBは1つである」の通り、作りがどうしても巨大な**Monolith(一枚岩)**になってしまいます。小さいアプリケーションを作りたくても、最初からでかいです。
一方で、HanamiもMonolith Firstではあるのですが、
「MVP(実用最小限の製品)」「Microservices」という考え方を基に、Containerを組み合わせて、
1つのアプリケーションは小さく、そして継続的に機能拡張していける特徴があります。
後発のフレームワークなので、最近の開発手法にも合っている気がします。
特徴を一言でいうならば、
- Rails…オールインワン
- Sinatra…シンプル
- Hanami…スケーラブル
という感じでしょうか。
Railsも、Engineを使えば、ごりっとmodular化はできますが、なかなか強引な感じですし、
Sinatraも、modularとして開発可能ですが、機能としては少なく、自分で実装する部分は多いと思います。
Hanamiは、ある程度の機能を持ちながら、拡張を意識して作られています。
主な特徴
ディレクトリ構造
まず、
$ hanami new sample_app
を実行してできる中身は、こんな感じです。
$ tree -L 1 sample_app
sample_app/
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── apps
├── config
├── config.ru
├── db
├── lib
├── spec
└── vendor
1つのContainer
はこんな感じです。
$ tree sample_app/apps/web
sample_app/apps/web
├── application.rb
├── config
│ └── routes.rb
├── controllers
├── public
│ ├── javascripts
│ └── stylesheets
├── templates
│ └── application.html.erb
└── views
└── application_layout.rb
一方でデータソースを扱う部分(model
)は、ライブラリとなります。
$ tree sample_app/lib
sample_app/lib
├── bookshelf
│ ├── entities
│ └── repositories
├── bookshelf.rb
└── config
└── mapping.rb
もし、フロントのWebアプリケーションを作成した後に、管理ツールが欲しくなった場合は、こうなります。
$ cd sample_app
$ bundle exec hanami generate app admin
$ tree -L 2 sample_app/apps/
sample_app/apps/
├── admin
│ ├── application.rb
│ ├── config
│ ├── controllers
│ ├── public
│ ├── templates
│ └── views
└── web
├── application.rb
├── config
├── controllers
├── public
├── templates
└── views
Railsも、頑張ってディレクトリ構造を変えて、複数アプリをmountするようなこともできますが、
Hanamiは、始めからその想定で作られています。
本当に小さいサイズで済んでしまうアプリケーションの場合は、プロジェクト作成時に--arch=app
を付けて、Railsっぽいディレクトリ構成でも作成可能です。
$ hanami new tiny_sample_app --arch=app
$ tree -L 1 tiny_sample_app
tiny_sample_app/
├── Gemfile
├── Rakefile
├── app
├── config
├── config.ru
├── db
├── lib
├── public
└── spec
$ tree -L tiny_sample_app/app
tiny_sample_app/app
├── controllers
├── templates
│ └── application.html.erb
└── views
└── application_layout.rb
Model
entity
とrepository
に分かれています。
この辺は、DDDの思想を受け、ちょっとJavaっぽいところもあるかもしれません。
class Book
include Hanami::Entity
attributes :title
end
# .persist(entity) – Create or update an entity
# .create(entity) – Create a record for the given entity
# .update(entity) – Update the record corresponding to the given entity
# .delete(entity) – Delete the record corresponding to the given entity
# .fetch(raw) – Fetch raw datasets for the given raw query string (eg. SQL)
# .execute(raw) – Execute raw command (eg. SQL)
# .all - Fetch all the entities from the collection
# .find - Fetch an entity from the collection by its ID
# .first - Fetch the first entity from the collection
# .last - Fetch the last entity from the collection
# .clear - Delete all the records from the collection
# .query - Fabricates a query object
class BookRepository
include Hanami::Repository
def self.raw_all
fetch("SELECT * FROM books")
end
def self.find_all_titles
fetch("SELECT title FROM books").map do |book|
book[:title]
end
end
def self.max_price
result = 0
fetch("SELECT price FROM books") do |book|
result = book[:price] if book[:price] > result
end
result
end
end
Action側で呼び出す時には、こうなります。
module Web::Controllers::Books
class Index
include Web::Action
expose :books
def call(params)
@books = BookRepository.all
end
end
end
さらに、Modelごとに、どのデータソース(File/Memory/SQL)からデータを取ってくるかを選べます。
Memoryはまだadapter
の種類が少ないですが、こんな感じでRedisやMemcachedに対応するような動きが出てくるのは容易に想像できますし、拡張もしやすいです。
Hanami::Model.configure do
##
# Database adapter
#
# Available choices:
#
# * File System adapter
# adapter type: :file_system, uri: 'file:///db/bookshelf_development'
#
# * Memory adapter
# adapter type: :memory, uri: 'memory://localhost/bookshelf_development'
#
# * SQL adapter
# adapter type: :sql, uri: 'sqlite://db/bookshelf_development.sqlite3'
# adapter type: :sql, uri: 'postgres://localhost/bookshelf_development'
# adapter type: :sql, uri: 'mysql://localhost/bookshelf_development'
#
adapter type: :sql, uri: ENV['BOOKSHELF_DATABASE_URL']
# ...
end.load!
View
私的にはMojaviの記憶がよみがえりますが、view
層があります。
どういった表示にするか、template
の組み合わせをここで決めます。
module Web::Views::Dashboard
class Index
include Web::View
end
end
Router
Rackベースで書こうと思えば、こんな書き方もできます。
get '/proc', to: ->(env) { [200, {}, ['Hello from Janami!']] }
get '/action', to: "home#index"
get '/middleware', to: Middleware
get '/rack-app', to: RackApp.new
get '/rails', to: ActionControllerSubclass.action(:new)
その他
migration
、helper
、mailer
もあるので、Rails、Sinatraユーザであれば、問題なく使えると思います。
パフォーマンスの違い
鵜呑みは良くないですが、かるーくSinatraの倍のパフォーマンスは出ている気がします。
https://github.com/luislavena/bench-micro
http://blog.ragnarson.com/2015/03/23/lotus-performance-tested-against-sinatra.html
さいごに
まだ、2014年6月出たばかりで、2015年12月現在v0.5.0です。
ますます、今後に期待です!
参考
http://hanamirb.org/guides/
https://speakerdeck.com/jodosha/lotus-for-rails-developers
https://speakerdeck.com/jodosha/lotus-rubyday-2015
https://blog.codeship.com/a-survey-of-non-rails-frameworks-in-ruby-cuba-sinatra-padrino-lotus/
https://speakerdeck.com/yyagi/about-lotus-framework
https://speakerdeck.com/takai/microservices-in-action