はじめに
HanamiというRubyフレームワークが話題にあがっているようです。
今年のRuby Kaigi 2017でもAnton Davydov氏が発表されるようです。
Hanami – New Ruby Web Framework
今回はそんなHanamiのチュートリアルをやってみたいと思います。
公式ドキュメントはこちらになります。
HANAMI
日本語の「花見」から来ているのでしょう、TOPページは桜ですかね?
紹介
GUIDEの部分を和訳しているだけです。
(和訳はもしかすると誤りがあるかもしれませんが。。。その場合はコメントをいただけると幸いです。)
Hanamiとは?
Hanamiは、多くのマイクロライブラリーで構成された、RubyのMVCフレームワークです。
シンプルで安定したAPIと最小限のDSLを持ち、あまりにも多くの責任と魔法のように複雑なクラスを越え、プレーンなオブジェクトの使用を優先します。
明確な責任を持つ単純なオブジェクトを使用することにより、より定型的なコードになります。 Hanamiは、基本的な実装を維持しながら、余分な箇所を軽減する方法を提供します。
Hanamiを選ぶ理由
Hanamiを選ぶ理由は3つあります。
軽量
花見のコードは比較的短いです。実装に関係なく、すべてのWebアプリケーションが必要とすることのみに関心があります。
Hanamiにはいくつかのオプションモジュールが付属しており、他のライブラリも簡単に組み込むことができます。
アーキテクチャとして
あなたが「Rails Way」に反していると感じたことがあるなら、あなたは花見を良いと思うでしょう。
Hanamiはコントローラの動作をクラスベースに保ち、独立してテストしやすくしています。
また、Hanamiはユースケースオブジェクト(別名:
interactors
)にアプリケーション・ロジックを書くことをお勧めします。
ビューはテンプレートから分離されているため、内部のロジックを十分に包含してテストすることができます。
ちなみにHanamiはクリーンアーキテクチャに影響を受けているとのことです。
スレッドセーフ
スレッドの使用は、アプリケーションのパフォーマンスを向上させるのに最適です。
スレッドセーフなコードを書くのは難しいことではありませんし、Hanami(フレームワーク全体かその一部かに関わらず)は実行時スレッドセーフです。
チュートリアルのための準備
下記リンクのチュートリアルを実施していきます。
Getting Started
作成するものは、下記のような「本棚アプリ」になります。
「はじめに」にあたる部分のこの一言は良いですね。
But without change, there is no challenge and without challenge, there is no growth.
チュートリアルを進める上で必要な環境としては、
- Ruby 2.3以上
- SQLite 3以上
になります。
まずはGemのインストールです。
Gitihubリポジトリはこちらです。
1 | $ gem install hanami |
プロジェクトを作成します。
デフォルトのDBはSQLiteになるようです。
1 | $ hanami new bookshelf |
いろいろファイルが生成されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | bookshelf ├ apps ├ config ├ db ├ lib ├ public ├ spec ├ .env.development ├ .env.test ├ .gitignore ├ .hanamirc ├ config.ru ├ Gemfile ├ Gemfile.lock └ Rakefile |
Gemfile
を見てみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | source 'https://rubygems.org' gem 'rake' gem 'hanami', '~> 1.0' gem 'hanami-model', '~> 1.0' gem 'sqlite3' group :development do # Code reloading # See: http://hanamirb.org/guides/projects/code-reloading gem 'shotgun' end group :test, :development do gem 'dotenv', '~> 2.0' end group :test do gem 'minitest' gem 'capybara' end group :production do # gem 'puma' end |
テストフレームワークは
minitest
なのに、
spec
ディレクトリが生成されているんですね。
(これは注釈がありましたが、
minitest
でも
RSpec
でもどちらでもいけるそうです。)
これらのGemをインストールしてサーバーを起動します。
サーバーは2300番ポートで起動します。
1 | $ bundle install |
1 2 3 4 | $ bundle exec hanami server INFO WEBrick 1.3.1 INFO ruby 2.4.1 (2017-03-22) [x86_64-darwin14] INFO WEBrick::HTTPServer#start: pid=39631 port=2300 |
これで
http://localhost:2300
にアクセスすると、下記のページが表示されます。
実践
はじめてのテスト
テストをする前に、テスト環境のDBをセットアップしておきます。
1 | $ HANAMI_ENV=test bundle exec hanami db prepare |
これで
db
ディレクトリにSQLiteのファイルができます。
Hanamiは
BDD
(
Behavior Driven Development
)を推奨しています。
実際にチュートリアルを実施する際には、テストコードから書いていますが、本記事ではテストコードは割愛させていただきます。
途中まで載せていたのですが、文章が冗長になってしまったので、泣く泣く削除しました。
もしテストコードもご覧になる場合は、下記のGithubリンクにてお願いします。
https://github.com/naoki85/bookshelf/tree/master/spec
(一応日本語でコメントもいれています。)
まずはルーティングを定義します。
1 | root to: 'home#index' |
ここはRailsと変わりません。
Homeコントローラーのindexメソッドを作成。。。と思ったら、コントローラーの定義の仕方が違いました。
コントローラーはまず、
apps/web/controllers
の下に
home
ディレクトリを作成して、その中に
index.rb
というファイルを作成します。
1 2 3 4 5 6 7 8 | module Web::Controllers::Home class Index include Web::Action def call(params) end end end |
モジュールで名前空間を切って、Railsでいうアクションがクラスになっています。
include Web::Action
は現状、決まり文句でよさそうです。
def call(params)
はマジックメソッドみたいなもので、このメソッドがないと呼ばれません。
params
にGETやPOSTなどで渡されたパラメータが格納されます。
次にビューですが、基本的にレンダリングするだけであれば、
templates
以下に命名規則に従ってファイルを作成することで、そのテンプレートを表示してくれます。
1 | <h1>Bookshelf</h1> |
ここまでで、テストは通りますが、
views
以下にビューコントローラーのようなファイルを作成しておきます。
今回は特にビューにロジックはないため、空のファイルですが、ヘルパー的なものを使用する場合やケースバイケースでテンプレートを変える場合などにお世話になりそうです。
1 2 3 4 5 | module Web::Views::Home class Index include Web::View end end |
こちらも名前空間を切って、決まり文句の
include Web::View
を記載しておきます。
テストを実行して、通ればOKです。
新しいアクションを生成
今度は本の一覧を表示するアクションを追加していきます。
Hanami Generatorでアクションを生成
rails generate
のようなコマンドがHanamiにもあります。
指定したファイル群を生成してくれます。
今回は
books#index
を生成します。
1 2 3 4 5 6 7 | $ bundle exec hanami generate action web books#index create spec/web/controllers/books/index_spec.rb create apps/web/controllers/books/index.rb create apps/web/views/books/index.rb create apps/web/templates/books/index.html.erb create spec/web/views/books/index_spec.rb insert apps/web/config/routes.rb |
テストコードも含めて生成されます。
routes.rb
にも自動で追記されています。
ただ、Railsのようにテンプレートの中身まで勝手に作ってくれないようなので、実際に描画されるHTMLは記載する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <h1>Bookshelf</h1> <h2>All books</h2> <div id="books"> <div class="book"> <h3>Patterns of Enterprise Application Architecture</h3> <p>by <strong>Martin Fowler</strong></p> </div> <div class="book"> <h3>Test Driven Development</h3> <p>by <strong>Kent Beck</strong></p> </div> </div> |
これでテストは通ると思います。
共通部分をLayoutにまとめる
templates/home/index.html.erb
と
templates/books/index.html.erb
の両方に
<h1>Bookshelf</h1>
が含まれています。
これは共通部分として、共通のテンプレートに記載します。
共通テンプレートは
templates/application.html.erb
になります。
1 2 3 4 5 6 7 8 9 10 11 12 | <!DOCTYPE html> <html> <head> - <title>Web</title> + <title>Bookshelf</title> <%= favicon %> </head> <body> + <h1>Bookshelf</h1> <%= yield %> </body> </html> |
これで共通部分に移せたので、先ほどの2つのファイルから
h1
タグを消しておきます。
モデルの作成
Hanamiのモデルには
entity
と
repository
の2種類があります。
厳密な区分は違うと思いますが、私の解釈としては、
-
entity
…SQLに依存しない状態でオブジェクトを管理する場合など -
repository
…テーブルにレコードを作成、更新、削除する場合など(SQLを発行)
SQLを発行するモデルと、DBとは関係なく状態を管理できるモデルという認識です。
実際に使ってみたいと思います。
hanami generate model
で、マイグレーションファイルを含め、モデルに関連するファイルを生成できます。
1 2 3 4 5 6 | $ bundle exec hanami generate model book create lib/bookshelf/entities/book.rb create lib/bookshelf/repositories/book_repository.rb create db/migrations/20170911043801_create_books.rb create spec/bookshelf/entities/book_spec.rb create spec/bookshelf/repositories/book_repository_spec.rb |
apps
ではなく、
libs
以下にモデルが作成されました。
ビジネスロジックは
libs
以下で管理し、
apps
以下に複数のマイクロサービスを管理するという考えです。
これはHanamiの大きな特徴の一つです。
マイグレーションファイルは下記のように、
title
と
author
カラムを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | Hanami::Model.migration do change do create_table :books do primary_key :id column :title, String, null: false column :author, String, null: false column :created_at, DateTime, null: false column :updated_at, DateTime, null: false end end end |
マイグレーションを実行します。
1 2 | $ bundle exec hanami db prepare $ HANAMI_ENV=test bundle exec hanami db prepare |
色々メッセージが出ると思いますが、下記のような状態になるかと思います。
(確認にはDB Browser for SQLiteを使用しました。)
生成された
entity
、
repository
のファイルはそれぞれ下記になります。
lib
ディレクトリ以下に作成されており、ビジネスロジックは分けるという考えが伺えます。
1 2 | class Book < Hanami::Entity end |
1 2 | class BookRepository < Hanami::Repository end |
それぞれ、HanamiのEntityとRepositoryを継承しており、これだけで最低限の動作ができるようです。
(RailsのActiveRecordみたいな感じですね。)
ちょっとしたコードの確認は
hanami console
でできます。
色々いじってみたので、まとめます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | $ bundle exec hanami console >> book = Book.new(title: 'title No.1') => #<Book:0x007f89436fa468 @attributes={:title=>"title No.1"}> >> book.save # saveなどのメソッドはないので、エラーになります。 # テーブルからレコードを取得する場合はRepositoryのインスタンスからやります。 >> repository = BookRepository.new => #<BookRepository relations=[:books]> # 全件取得(まだレコードはない) >> repository.all SELECT `id`, `title`, `author`, `created_at`, `updated_at` FROM `books` ORDER BY `books`.`id` => [] # レコード作成 # 戻り値はBookオブジェクト >> book = repository.create(title: 'TDD', author: 'Kent Beck') INSERT INTO `books` (`title`, `author`, `created_at`, `updated_at`) VALUES ('TDD', 'Kent Beck', '2017-09-11 06:20:12.065528', '2017-09-11 06:20:12.065528') SELECT `id`, `title`, `author`, `created_at`, `updated_at` FROM `books` WHERE ( `id` IN (2)) ORDER BY `books`.`id` => #<Book:0x007f8943694550 @attributes={:id=>2, :title=>"TDD", :author=>"Kent Beck", :created_at=>2017-09-11 06:20:12 UTC, :updated_at=>2017-09-11 06:20:12 UTC}> # Bookオブジェクトなので、値が取得できる >> book.id => 2 # この値を使って再取得 >> repository.find(book.id) SELECT `id`, `title`, `author`, `created_at`, `updated_at` FROM `books` WHERE (`books`.`id` = 2) ORDER BY `books`.`id` => #<Book:0x007f8943675da8 @attributes={:id=>2, :title=>"TDD", :author=>"Kent Beck", :created_at=>2017-09-11 06:20:12 UTC, :updated_at=>2017-09-11 06:20:12 UTC}> |
Indexの修正
本の一覧表示である
index
アクションを修正してDBから引っ張ってくるようにします。
まずはコントローラーです。
1 2 3 4 5 6 7 8 9 10 11 | module Web::Controllers::Books class Index include Web::Action + expose :books def call(params) + @books = BookRepository.new.all end end end |
@books = BookRepository.new.all
で
books
テーブルからレコードを全件取得してインスタンス変数にセットします。
ただ、
views
、および
templates
に渡すためにはexposeで指定する必要があります。
これで
books
変数を
views
および
templates
にて使用することができるようになりました。
templates/books/index.html.erb
を下記のように書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <h2>All books</h2> <% if books.any? %> <div id="books"> <% books.each do |book| %> <div class="book"> <h2><%= book.title %></h2> <p><%= book.author %></p> </div> <% end %> </div> <% else %> <p class="placeholder">There are no books yet.</p> <% end %> |
これで、DBから取得した値の一覧画面ができました。
新規登録画面と登録処理
登録フォームと登録処理を作成していきます。
hanami generateでbooks#newを生成
また、
hanami generate
コマンドで生成します。
1 2 3 4 5 6 7 | $ bundle exec hanami generate action web books#new create spec/web/controllers/books/new_spec.rb create apps/web/controllers/books/new.rb create apps/web/views/books/new.rb create apps/web/templates/books/new.html.erb create spec/web/views/books/new_spec.rb insert apps/web/config/routes.rb |
先ほどと同様、ルーティングへの追記や必要なファイルは全て生成されています。
ただ、テンプレートは作成する必要があります。
今回は入力フォームが必要なので、HanamiのFormHelpersを使用します。
ここはRailsを初めとしたWebフレームワークに触ったことがあれば、理解しやすいかと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <h2>Add book</h2> <%= form_for :book, '/books' do div class: 'input' do label :title text_field :title end div class: 'input' do label :author text_field :author end div class: 'controls' do submit 'Create Book' end end %> |
hanami generateでbooks#createを生成
実際に新規登録処理をするbooks#createを生成します。
1 2 3 4 5 6 7 | $ bundle exec hanami generate action web books#create create spec/web/controllers/books/create_spec.rb create apps/web/controllers/books/create.rb create apps/web/views/books/create.rb create apps/web/templates/books/create.html.erb create spec/web/views/books/create_spec.rb insert apps/web/config/routes.rb |
ちなみに
create
のルーティングはPOSTメソッドとして解釈されて追記されます。
1 | post '/books', to: 'books#create' |
ただ、コントローラーは修正の必要があるので、追記します。
1 2 3 4 5 6 7 8 9 10 11 | module Web::Controllers::Books class Create include Web::Action def call(params) BookRepository.new.create(params[:book]) redirect_to '/books' end end end |
かなり簡潔ですが、後ほどバリデーションを追加した際に、登録失敗した場合の処理も記載します。
パラメーターのバリデーション
バリデーションですが、モデルに書くものかと思いきや、Hanamiではコントローラー内の
params
で実施できるようです。
Parameters
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | module Web::Controllers::Books class Create include Web::Action params do required(:book).schema do required(:title).filled(:str?) required(:author).filled(:str?) end end def call(params) if params.valid? BookRepository.new.create(params[:book]) redirect_to routes.books_path else self.status = 422 end end end end |
require
で型を判定しています。で存在確認、
filled
実際に動かしてみると分かりますが、
params
を
valid?
したタイミングで、下記のようなハッシュが含まれるようになります。
1 2 | # 無効なパラメーターだった場合 { valid?: false, error_messages: ['Title must be filled', 'Author must be filled'] } |
valid?
が
false
だった場合は、HTTPステータスコードの422番をセットして返します。
普通であれば、再度
new.html.erb
をレンダリングするよう指示するところですが、それはコントローラーではなく、ビュー側の領分になります。
1 2 3 4 5 6 | module Web::Views::Books class Create include Web::View template 'books/new' end end |
これでもしパラメーターが無効であっても、
new.html.erb
をレンダリングできます。
最後にエラーメッセージを表示する部分を追記します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | <h2>Add book</h2> <% unless params.valid? %> <div class="errors"> <h3>There was a problem with your submission</h3> <ul> <% params.error_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> # 省略 |
ここまでで、下図のような画面になります。
ルーティングの修正
HanamiにもRESTなルーティングを生成できるresourcesが存在するため、そちらで修正しておきます。
この使い方はRailsとほとんど同じですね。
1 2 3 4 5 | -post '/books', to: 'books#create' -get '/books/new', to: 'books#new' -get '/books', to: 'books#index' root to: 'home#index' +resources :books, only: [:index, :new, :create] |
さいごに
チュートリアルが終わった後、CSSにMaterializeを使用してみてスタイリングしてみました。
ただ、
Assets
まわりはよく分からなく、CDNでもドツボにはまってしまったので、フォントなどは使えていません。
チュートリアル終了〜上記の画面までの差分はこちらになります。
https://github.com/naoki85/bookshelf/commit/7a589c97560d9144bb1a0138241b949793fb8b06
本家のチュートリアルと合わせて、こちらの記事も一緒に読み進めながらやってみました。おかげさまで内容を理解しながら完了できました。本当に助かりました。