こんにちは。 Ruby エンジニアの水野です。
みなさまは「 Ruby の Web フレームワーク」というと何を思い浮かべますか?
恐らくほとんどの方が Ruby on Rails
を第一に思い浮かべると思います。
続いてジャンルが異なる質問になりますが、「春」といえば何を思い浮かべますか? さまざまな答えがあると思いますが、僕は花が好きですので「桜」を思い浮かべます。 そして「桜」といえば「花見」!
上記を理由に、名前に惹かれて好きになった Hanami
の特徴を少しお話をしていきます。
Hanami とは
Hanami | The web, with simplicity
比較的新しめにリリースされた Ruby の Web フレームワークです。
DDD (ドメイン駆動設計) をベースに構成されているので、長期的にメンテナンスをすることに向いているフレームワークになります。
個人的に Hanami の Web ページのファーストビューが大好きですw
以降は、以下の Docker 環境内で試して僕が感じた Hanami の特徴を書いていきます!
楽をしたかったのでお試しで触る程度の予定ですので、敢えて Docker 環境内で完結させています。
ARG RUBY_VERSION FROM ruby:$RUBY_VERSION ENV LANG=C.UTF-8 RUN apt-get update -qq \ && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ libpq-dev \ vim \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && truncate -s 0 /var/log/*log WORKDIR /app RUN gem update --system \ && rm /usr/local/lib/ruby/gems/*/specifications/default/bundler-*.gemspec \ && gem install hanami \ && hanami new . --database=mysql --template=slim \ && bundle install
version: '3' services: hanami: build: context: . dockerfile: ./Dockerfile args: RUBY_VERSION: '2.6.6' command: 'bundle exec hanami server --host 0.0.0.0 -p 2300' ports: - 2300:2300 stdin_open: true tty: true mysql: image: mysql:5.7.30 ports: - 3306:3306 environment: MYSQL_DATABASE: 'app_development' MYSQL_USER: 'app_user' MYSQL_PASSWORD: 'app_password' MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
コンテナ起動
適当なディレクトリを作って
mkdir hanami-test && cd hanami-test
ファイルを用意して
[~/workspace/hanami-test] > ls Dockerfile docker-compose.yml
コンテナを起動して
docker-compose up -d
http://localhost:2300
で接続すると・・・
画面が表示されたので準備完了です!
いざ Hanami
の世界へ!
Hanami のコマンド
Rails ライクなコマンドになっていますので、 Rails を知っていれば容易に使えそうな印象です。
例えば画面に表示されたコマンドも Rails エンジニアの方でしたらなんとなくわかるのではないでしょうか?
もっとも、英語を読めば何が処理されるかわかりますが、コンテナの中に入ってコマンドを実行してみましょう。
docker-compose exec hanami bash
bundle exec hanami generate action web 'home#index' --url=/
root@a6ffd49554f8:/app# bundle exec hanami generate action web 'home#index' --url=/ create apps/web/controllers/home/index.rb create apps/web/views/home/index.rb create apps/web/templates/home/index.html.slim create spec/web/controllers/home/index_spec.rb create spec/web/views/home/index_spec.rb insert apps/web/config/routes.rb
何やらたくさんファイルが作られましたが、 Action 関連を生成してくれます。
Rails と同様な Generator コマンド機能の認識で良さそうですね。
生成されたファイルを見ていきましょう。
テストに関しては省略します。
Action (Controller)
Hanami では Controller は Action 毎にファイルが必要です。
module Web module Controllers module Home class Index include Web::Action def call(params) end end end end end
ネストが深くなってしまうのが気になるので、実際は以下のようにスコープ演算子で書くのかなと思います。
module Web::Controllers::Home class Index include Web::Action def call(params) end end end
Hanami のコンセプトとして、 Modularity
があり、継承を使わずにモジュールで mix-in を多用しています。
何が Controller で何が Action なのか、わかりやすくなっていますね!
View
Hanami では View は HTML テンプレートとクラスで構成されています。
以下の2つのファイルが該当します。
apps/web/views/home/index.rb apps/web/templates/home/index.html.slim
今回は slim をテンプレートに指定していますが、もちろん erb や haml も使えます。
テンプレートには何も書かれていなかったので、クラスを見ていきましょう。
module Web module Views module Home class Index include Web::View end end end end
Controller と同様ですね。
ネストが気になるので以下に変更します。
module Web::Views::Home class Index include Web::View end end
HTML テンプレートとクラスは 1 対 1 に対応しているようなので、実際に書いて試してみましょう。
今回は試しませんが、 1 対 1 で対応しているため、他のクラスのメソッドを呼び出そうとするとエラーになるようです。
module Web::Views::Home class Index include Web::View def title "Hello World" end end end
h1 = title
http://localhost:2300
で接続してみましょう。
表示されましたね!
このように、クラスに定義したメソッドを HTML テンプレートから呼び出すことができます。
Rails で例えると Helper や ActiveDecorator などに値しますが、デフォルトでスコープが制限されているのが非常にわかりやすいと思います。
Routing
Hanami は Rack アプリケーションなので、基本的に Rails と同様です。
もちろん異なる箇所もありますので、気になる方は下記をご覧になってみてください。
- Routing: Basic Usage | Hanami Guides
# Configure your routes here # See: https://guides.hanamirb.org/routing/overview # # Example: # get '/hello', to: ->(env) { [200, {}, ['Hello from Hanami!']] } get '/', to: 'home#index'
最終行が Generator コマンドによって登録された Routing です。
前述の通り、 Rack アプリケーションなので #call
メソッドを持つオブジェクトを渡すと実行してくれます。
正確には違いますので、詳細は下記をご覧になってみてください。
File: SPEC — Documentation for rack/rack (master)
A Rack application is a Ruby object (not a class) that responds to call. It takes exactly one argument, the environment and returns an Array of exactly three values: The status, the headers, and the body.
ひとつ上のコメントを外し、 http://localhost:2300/hello
で接続してみましょう。
Hello from Hanami!
と表示されるはずです。
Proc
オブジェクトなので、 #call
メソッドを持っています。
表示されましたね!
Model
まだ Model が登場していませんでしたね。
Rails と比べると全然違いますので、特に目立つ点だと思います。
Generator コマンドで Model を生成してみましょう。
bundle exec hanami generate model flower
root@a6ffd49554f8:/app# bundle exec hanami generate model flower create lib/app/entities/flower.rb create lib/app/repositories/flower_repository.rb create db/migrations/20200611064756_create_flowers.rb create spec/app/entities/flower_spec.rb create spec/app/repositories/flower_repository_spec.rb
Hanami では Repository
と Entity
で Model が 2 種類に分離されています。
Migration
ファイルも作成されましたね。
見ていきましょう。
Migration
Hanami::Model.migration do change do create_table :flowers do primary_key :id column :created_at, DateTime, null: false column :updated_at, DateTime, null: false end end end
構造は違えど、基本的に Rails と同様のようですね。 同じ感覚でカラムを追加できそうですので、お花たちに名前をつけてあげましょう。
Hanami::Model.migration do change do create_table :flowers do primary_key :id column :name, String, null: false, unique: true column :created_at, DateTime, null: false column :updated_at, DateTime, null: false end end end
では Migration してみましょう! ですが、その前に使用するデータベースの設定をする必要があります。 環境変数 DATABASE_URL を変更しましょう。 ホスト名は Docker コンテナのサービス名を指定すれば名前解決してくれます。
# Define ENV variables for development environment DATABASE_URL="mysql2://app_user:app_password@mysql:3306/app_development"
データベースはイメージの起動時に作成済みなので、下記コマンドで Migration します。
bundle exec hanami db migrate
root@a6ffd49554f8:/app# bundle exec hanami db migrate [hanami] [INFO] (0.000433s) SET @@wait_timeout = 2147483 [hanami] [INFO] (0.000299s) SET SQL_AUTO_IS_NULL=0 [hanami] [ERROR] Mysql2::Error: Table 'app_development.schema_migrations' doesn't exist: SELECT NULL AS `nil` FROM `schema_migrations` LIMIT 1 [hanami] [INFO] (0.019519s) CREATE TABLE `schema_migrations` (`filename` varchar(255) PRIMARY KEY) [hanami] [ERROR] Mysql2::Error: Table 'app_development.schema_info' doesn't exist: SELECT NULL AS `nil` FROM `schema_info` LIMIT 1 [hanami] [INFO] (0.005298s) SELECT `filename` FROM `schema_migrations` ORDER BY `filename` [hanami] [INFO] Begin applying migration 20200611064756_create_flowers.rb, direction: up [hanami] [INFO] (0.025800s) CREATE TABLE `flowers` (`id` integer PRIMARY KEY AUTO_INCREMENT, `name` varchar(255) NOT NULL UNIQUE, `created_at` datetime NOT NULL, `updated_at` datetime NOT NULL) [hanami] [INFO] (0.001341s) INSERT INTO `schema_migrations` (`filename`) VALUES ('20200611064756_create_flowers.rb') [hanami] [INFO] Finished applying migration 20200611064756_create_flowers.rb, direction: up, took 0.029871 seconds
これで flowers テーブルが作成されました!
mysql> show tables; +---------------------------+ | Tables_in_app_development | +---------------------------+ | flowers | | schema_migrations | +---------------------------+ 2 rows in set (0.01 sec)
Entity
class Flower < Hanami::Entity end
Hanami は Entity
で RDBMS などのテーブルやレコードを Ruby のオブジェクトとして扱えます。
イミュータブルで、インスタンス化するときにしか値を入力することができません。
bundle exec hanami console
irb(main):001:0> flower = Flower.new(name: "Sakura") => #<Flower:0x000055d1015a27d8 @attributes={:name=>"Sakura"}> irb(main):002:0> flower.name => "Sakura" irb(main):003:0> flower.name = "Ume" Traceback (most recent call last): 16: from /usr/local/bundle/gems/bundler-2.1.4/lib/bundler/cli.rb:30:in `dispatch' 15: from /usr/local/bundle/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor.rb:399:in `dispatch' 14: from /usr/local/bundle/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command' 13: from /usr/local/bundle/gems/bundler-2.1.4/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run' 12: from /usr/local/bundle/gems/bundler-2.1.4/lib/bundler/cli.rb:476:in `exec' 11: from /usr/local/bundle/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:28:in `run' 10: from /usr/local/bundle/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `kernel_load' 9: from /usr/local/bundle/gems/bundler-2.1.4/lib/bundler/cli/exec.rb:63:in `load' 8: from /usr/local/bundle/bin/hanami:23:in `<top (required)>' 7: from /usr/local/bundle/bin/hanami:23:in `load' 6: from /usr/local/bundle/gems/hanami-1.3.3/bin/hanami:6:in `<top (required)>' 5: from /usr/local/bundle/gems/hanami-cli-0.3.1/lib/hanami/cli.rb:57:in `call' 4: from /usr/local/bundle/gems/hanami-1.3.3/lib/hanami/cli/commands/command.rb:85:in `call' 3: from /usr/local/bundle/gems/hanami-1.3.3/lib/hanami/cli/commands/console.rb:51:in `call' 2: from (irb):3 1: from /usr/local/bundle/gems/hanami-model-1.3.2/lib/hanami/entity.rb:145:in `method_missing' NoMethodError (undefined method `name=' for #<Flower:0x000055d1015a27d8 @attributes={:name=>"Sakura"}>)
Entity は基本はこれだけです。
あくまで入れ物として認識しておくのが良いでしょう。
ではどうやって値を代入するのでしょうか?
答えは Repository にあります。
※RDBMS 以外で使用する場合は、 Custom Schema
の設定が必須になります。
Entities: Custom Schema | Hanami Guides
Repository
class FlowerRepository < Hanami::Repository end
Hanami は Repository
でデータベースのテーブルに対して CRUD 操作を行います。
テーブルのレコードを更新し Entity を返すなど、 RDBMS と Entity の仲介役を担っています。
実際にレコードを作成してみましょう。
bundle exec hanami console
irb(main):001:0> FlowerRepository.new.create(name: "Yaezakura") [app] [INFO] [2020-06-11 08:29:27 +0000] (0.003513s) INSERT INTO `flowers` (`name`, `created_at`, `updated_at`) VALUES ('Yaezakura', '2020-06-11 08:29:27', '2020-06-11 08:29:27') [app] [INFO] [2020-06-11 08:29:27 +0000] (0.001222s) SELECT `id`, `name`, `created_at`, `updated_at` FROM `flowers` WHERE (`id` IN (2)) ORDER BY `flowers`.`id` => #<Flower:0x000055cc10c53118 @attributes={:id=>1, :name=>"Yaezakura", :created_at=>2020-06-11 08:29:27 UTC, :updated_at=>2020-06-11 08:29:27 UTC}>
レコードを作成してから Entity が返却されていることがわかりますね。
もちろんレコードを取得することもできます。
irb(main):002:0> FlowerRepository.new.find(1) [app] [INFO] [2020-06-11 08:36:53 +0000] (0.000934s) SELECT `id`, `name`, `created_at`, `updated_at` FROM `flowers` WHERE (`flowers`.`id` = 1) ORDER BY `flowers`.`id` => #<Flower:0x000055cc1113e150 @attributes={:id=>1, :name=>"Yaezakura", :created_at=>2020-06-11 08:29:27 UTC, :updated_at=>2020-06-11 08:29:27 UTC}>
次にメソッドを定義してみましょう。 Hanami では複雑なクエリはメソッド内で完結させることを推奨しています。
class FlowerRepository < Hanami::Repository def find_by_name(name) flowers.where(name: name).first end end
実行してみます。
bundle exec hanami console
irb(main):001:0> flower_repository = FlowerRepository.new => #<FlowerRepository relations=[:flowers]> irb(main):002:0> flower_repository.find_by_name("Yaezakura") [app] [INFO] [2020-06-11 08:47:10 +0000] (0.002044s) SELECT `id`, `name`, `created_at`, `updated_at` FROM `flowers` WHERE (`name` = 'Yaezakura') ORDER BY `flowers`.`id` => #<Flower:0x000055f00786dda8 @attributes={:id=>1, :name=>"Yaezakura", :created_at=>2020-06-11 08:29:27 UTC, :updated_at=>2020-06-11 08:29:27 UTC}>
メソッドの定義で flowers
という定義した覚えがないオブジェクトがでてきましたね。
わからないままは不安です。
何者なのか確認しましょう。
irb(main):003:0> flower_repository.flowers => #<ROM::Relation::Composite name=flowers dataset=#<Sequel::Mysql2::Dataset: "SELECT `id`, `name`, `created_at`, `updated_at` FROM `flowers` ORDER BY `flowers`.`id`">>
ROM::Relation::Composite
のインスタンスでした。
Hanami は ROM (Ruby Object Mapper) でデータベースのテーブルにアクセスしているようですね。
最後に
Interactor
や Validations
など、まだまだ言及したいことはありますが、メインとなる特徴のお話をさせていただきました。
興味が湧いた方はぜひ一度 Hanami を使ってみてください!