カテゴリー: BackEnd

RubyのHanamiチュートリアルをやってみた

はじめに

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リポジトリはこちらです。

$ gem install hanami

プロジェクトを作成します。
デフォルトのDBはSQLiteになるようです。

$ hanami new bookshelf

いろいろファイルが生成されます。

bookshelf
├ apps
├ config
├ db
├ lib
├ public
├ spec
├ .env.development
├ .env.test
├ .gitignore
├ .hanamirc
├ config.ru
├ Gemfile
├ Gemfile.lock
└ Rakefile

Gemfileを見てみます。

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番ポートで起動します。

$ bundle install
$ 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をセットアップしておきます。

$ HANAMI_ENV=test bundle exec hanami db prepare

これでdbディレクトリにSQLiteのファイルができます。

HanamiはBDDBehavior Driven Development)を推奨しています。
実際にチュートリアルを実施する際には、テストコードから書いていますが、本記事ではテストコードは割愛させていただきます。
途中まで載せていたのですが、文章が冗長になってしまったので、泣く泣く削除しました。

もしテストコードもご覧になる場合は、下記のGithubリンクにてお願いします。
https://github.com/naoki85/bookshelf/tree/master/spec
(一応日本語でコメントもいれています。)

まずはルーティングを定義します。

root to: 'home#index'

ここはRailsと変わりません。
Homeコントローラーのindexメソッドを作成。。。と思ったら、コントローラーの定義の仕方が違いました。

コントローラーはまず、apps/web/controllersの下にhomeディレクトリを作成して、その中にindex.rbというファイルを作成します。

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以下に命名規則に従ってファイルを作成することで、そのテンプレートを表示してくれます。

<h1>Bookshelf</h1>

ここまでで、テストは通りますが、views以下にビューコントローラーのようなファイルを作成しておきます。
今回は特にビューにロジックはないため、空のファイルですが、ヘルパー的なものを使用する場合やケースバイケースでテンプレートを変える場合などにお世話になりそうです。

module Web::Views::Home
  class Index
    include Web::View
  end
end

こちらも名前空間を切って、決まり文句のinclude Web::Viewを記載しておきます。

テストを実行して、通ればOKです。

新しいアクションを生成

今度は本の一覧を表示するアクションを追加していきます。

Hanami Generatorでアクションを生成

rails generateのようなコマンドがHanamiにもあります。
指定したファイル群を生成してくれます。
今回はbooks#indexを生成します。

$ 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は記載する必要があります。

<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.erbtemplates/books/index.html.erbの両方に<h1>Bookshelf</h1>が含まれています。
これは共通部分として、共通のテンプレートに記載します。
共通テンプレートはtemplates/application.html.erbになります。

<!DOCTYPE html>
<html>
  <head>
-   <title>Web</title>
+   <title>Bookshelf</title>
    <%= favicon %>
  </head>
  <body>
+   <h1>Bookshelf</h1>
    <%= yield %>
  </body>
</html>

これで共通部分に移せたので、先ほどの2つのファイルからh1タグを消しておきます。

モデルの作成

Hanamiのモデルにはentityrepositoryの2種類があります。

厳密な区分は違うと思いますが、私の解釈としては、

  • entity…SQLに依存しない状態でオブジェクトを管理する場合など
  • repository…テーブルにレコードを作成、更新、削除する場合など(SQLを発行)

SQLを発行するモデルと、DBとは関係なく状態を管理できるモデルという認識です。

実際に使ってみたいと思います。
hanami generate modelで、マイグレーションファイルを含め、モデルに関連するファイルを生成できます。

$ 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の大きな特徴の一つです。

マイグレーションファイルは下記のように、titleauthorカラムを追加します。

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

マイグレーションを実行します。

$ bundle exec hanami db prepare
$ HANAMI_ENV=test bundle exec hanami db prepare

色々メッセージが出ると思いますが、下記のような状態になるかと思います。
(確認にはDB Browser for SQLiteを使用しました。)

生成されたentityrepositoryのファイルはそれぞれ下記になります。
libディレクトリ以下に作成されており、ビジネスロジックは分けるという考えが伺えます。

class Book < Hanami::Entity
end
class BookRepository < Hanami::Repository
end

それぞれ、HanamiのEntityとRepositoryを継承しており、これだけで最低限の動作ができるようです。
(RailsのActiveRecordみたいな感じですね。)

ちょっとしたコードの確認は`hanami console`でできます。
色々いじってみたので、まとめます。

$ 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から引っ張ってくるようにします。

まずはコントローラーです。

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`を下記のように書き換えます。

<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コマンドで生成します。

$ 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フレームワークに触ったことがあれば、理解しやすいかと思います。

<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を生成します。

$ 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メソッドとして解釈されて追記されます。

post '/books', to: 'books#create'

ただ、コントローラーは修正の必要があるので、追記します。

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

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`で型を判定しています。
実際に動かしてみると分かりますが、paramsvalid?したタイミングで、下記のようなハッシュが含まれるようになります。

# 無効なパラメーターだった場合
{ valid?: false, error_messages: ['Title must be filled', 'Author must be filled'] }

valid?falseだった場合は、HTTPステータスコードの422番をセットして返します。

普通であれば、再度new.html.erbをレンダリングするよう指示するところですが、それはコントローラーではなく、ビュー側の領分になります。

module Web::Views::Books
  class Create
    include Web::View
    template 'books/new'
  end
end

これでもしパラメーターが無効であっても、new.html.erbをレンダリングできます。

最後にエラーメッセージを表示する部分を追記します。

<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とほとんど同じですね。

-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

naoki85

コメントを見る

  • 本家のチュートリアルと合わせて、こちらの記事も一緒に読み進めながらやってみました。おかげさまで内容を理解しながら完了できました。本当に助かりました。

シェア
執筆者:
naoki85
タグ: Ruby

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前