Hanami(旧:Lotus) - MicroservicesのためのRubyフレームワーク


はじめに

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

entityrepositoryに分かれています。
この辺は、DDDの思想を受け、ちょっとJavaっぽいところもあるかもしれません。

entity
class Book
  include Hanami::Entity
  attributes :title
end
repository
# .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側で呼び出す時には、こうなります。

controller
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の組み合わせをここで決めます。

view
module Web::Views::Dashboard
  class Index
    include Web::View
  end
end

Router

Rackベースで書こうと思えば、こんな書き方もできます。

router
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)

その他

migrationhelpermailerもあるので、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