酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

Rails 5.0.0.beta1の新機能紹介の公式ブログ記事を読んでみた

Rails公式ブログの Rails 5 beta1の新機能についてのブログ記事『Riding Rails: Rails 5.0.0.beta1: Action Cable, API mode, Rails command』を英語の勉強がてら読んでみました!


Action Cable

- Action Cableは、Websocketをハンドリングするためのフレームワーク
- チャットや、Notificationを簡単に実装することができる

チャットアプリのサンプルソース

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# アプリの作成
$ rails _5.0.0.beta1.1_ new action_cable_sample --skip-spring --skip-bundle
$ cd action_cable_sample

# bundle install の実行
bundle install --jobs=4 --path=vendor/bundle

# Message の scaffold を実行
$ rails g scaffold messages content:text

# マイグレーションを実行
$ rails db:migrate

# Chat チャネル with Speakアクション を作成
$ rails g channel chat speak

# 非同期でブロードキャストするための Jobを作成
$ rails g job message_broadcast

ルーティングを修正。

1
2
3
4
5
6
7
8
# config/routes.rb
Rails.application.routes.draw do
  # ↓ 以下を追加
  root to: 'messages#index'

  # Serve websocket cable requests in-process
  mount ActionCable.server => '/cable'
end

JS側で、ActionCableを有効にします。

1
2
3
# app/assets/javascripts/cable.coffee
@App ||= {}
App.cable = ActionCable.createConsumer()

クライアントサイド(CoffeeScript)のSpeakアクションを修正。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# app/assets/javascripts/channels/chat.coffee
  received: (data) ->
    # メッセージを受け取ったら #messages に付け足す
    $('#messages').append data['message']

  speak: (message) ->
    # formでsubmitしたメッセージをサーバに送信
    @perform 'speak', message: message

# リターンキーを押したら、メッセージが送信される
$(document).on 'keypress', '[data-behavior~=chat_speaker]', (event) ->
  if event.keyCode is 13
    App.chat.speak event.target.value
    event.target.value = ''
    event.preventDefault()

サーバーサイドの ActionCableのクラス CatChannel を修正。

1
2
3
4
5
6
7
8
9
10
11
12
13
# ActionCableのクラス
class ChatChannel < ApplicationCable::Channel
  # 配信する際の名前。 chat.coffee内の「ChatChannel」と対応(?)
  def subscribed
    # ↓ 以下をコメントアウトする
    stream_from 'chat_channel'
  end

  # クライアントから送られた message を テーブルに保存
  def speak(data)
    Message.create! content: data['message']
  end
end

Messageをテーブルに保存したら、非同期でJobを起動。

1
2
3
4
5
# Messageを管理するクラス
class Message < ApplicationRecord
  # 作成後のコミットが完了したら ブロードキャストする
  after_create_commit { MessageBroadcastJob.perform_later self }
end

保存されたメッセージを、MessageBroadcastJobで非同期でブロードキャスト。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 非同期でクライアントにメッセージを送るためのJob
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  # 部分テンプレートをrenderする
  def perform(message)
    ActionCable.server.broadcast 'chat_channel', message: _render_message(message)
  end

  private

  # 部分テンプレートをrenderする
  def _render_message(message)
    ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
  end
end

チャット風にメッセージを表示。

1
2
3
4
5
6
7
8
9
10
11
<!-- app/views/messages/index.html.erb -->
<h1>Chat</h1>

<div id="messages">
  <%= render @messages %>
</div>

<form>
  <label>Send Message:</label><br>
  <input type="text" data-behavior="chat_speaker">
</form>

あと部分テンプレート追加。キャッシュも普通に使う事ができます!

1
2
3
4
5
6
<!-- app/views/messages/_message.html.erb -->
<% cache message do %>
  <div class="message">
    <p><%= message.content %></p>
  </div>
<% end %>

あとはいつもどおり rails s で起動するとpumaが立ち上がります。

感想

以下、使ってみて感じたことです。

  • サーバ/クライアントサイド両方の処理を意識する必要がある
  • Viewのキャッシュなど、Railsの今までの知識を有効活用できる
  • 一時的に接続が失敗したり、エラー時のリカバリ処理等、実際に作る場合はいろいろ考慮が必要そう
  • Basecamp 3では本番で使われているけど、ネットの知見がたまるまで少し様子見が吉かも

関連リンク

API Mode

BackendとしてAPIを作るのに特化したrails プロジェクトを rails new backend --api 等で簡単に作成できる。

このAPI Modeとは直接の関係はないですが、『jsonapi-resources』というプロジェクトがあるようです。 こちらもサンプルソースを見た限りはなかなか面白そう。上手く使いこなせれば、楽ができそうです!

も少し詳しく調べてみました => Rails 5.0.0.beta2 APIモードについて調べてみた

ActiveRecord Attributes

DBに保存している値の型と、ActiveRecordで取り出した時の型を分けることができる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 小数点付きの型でDBに保存
# db/schema.rb
create_table :store_listings, force: true do |t|
  t.decimal :price_in_cents
end

#### Before ####
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
end

# 小数点付きで値を返す
store_listing = StoreListing.new(price_in_cents: '10.1')
store_listing.price_in_cents # => BigDecimal.new(10.1) 

#### After ####
class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :integer # 整数に設定した場合
end

# 整数で値を返す
store_listing = StoreListing.new(price_in_cents: '10.1')
store_listing.price_in_cents # => 10

配列や、レンジの状態で値を受け取る事もできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 配列やレンジを設定する事もできる
class MyModel < ActiveRecord::Base
  attribute :my_string, :string
  attribute :my_int_array, :integer, array: true
  attribute :my_float_range, :float, range: true
end

model = MyModel.new(
my_string: "string",
my_int_array: ["1", "2", "3"],
my_float_range: "[1,3.5]",
)
model.attributes
# =>
# {
#    my_string: "string",
#    my_int_array: [1, 2, 3],
#    my_float_range: 1.0..3.5
# }

さらに、独自の型を定義することもできます。これはいいかも!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MoneyType < ActiveRecord::Type::Integer
  def type_cast(value)
    if value.include?('$')
      price_in_dollars = value.gsub(/\$/, '').to_f
      price_in_dollars * 100
    else
      value.to_i
    end
  end
end

# クラスに設定
class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, MoneyType.new
end

# 変換した結果を返す
store_listing = StoreListing.new(price_in_cents: '$10.00')
store_listing.price_in_cents # => 1000

実用性も高いし、これは地味にかなりありがたい機能。

ちなみに、こんな使い方もできるそうで。。。夢が広がりんぐ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Money < Struct.new(:amount, :currency)
end

class MoneyType < Type::Value
  def initialize(currency_converter)
    @currency_converter = currency_converter
  end

  def type_cast_for_database(value)
    value_in_bitcoins = currency_converter.convert_to_bitcoins(value)
    value_in_bitcoins.amount
  end
end

class Product < ActiveRecord::Base
  currency_converter = ConversionRatesFromTheInternet.new
  attribute :price_in_bitcoins, MoneyType.new(currency_converter)
end

Product.where(price_in_bitcoins: Money.new(5, "USD"))
# => SELECT * FROM products WHERE price_in_bitcoins = 0.02230

Product.where(price_in_bitcoins: Money.new(5, "GBP"))
# => SELECT * FROM products WHERE price_in_bitcoins = 0.03412

ActiveRecord::Attributes::ClassMethods

belongs_to が必須に

belongs_to で association を定義した際には、対応するデータがあることが必須になりました。 対応するデータがない場合はエラーとなるため、エラーにしないためには次のように optional: true を追加する必要があります。

1
2
3
class User
  belongs_to :organization, optional: true
end

render をどこからでも呼び出せる

rake タスクや、バックグラウンドのジョブキューなどでも #render を行う事ができるようになりました。

1
ApplicationController.render _render_options_

assings オプションなどを使うことで、インスタンス変数をテンプレートに渡すこともできます。

Ruby 2.2.2 以上をサポート

本番環境で動いている場合は、Railsのバージョンアップよりも、Rubyのバージョンアップを先やる必要があるかも。

パフォーマンス改善について

Ruby 2.2.2以上や、Rails 5を使うことで、GCの改善や、メモリ消費量の削減といったパフォーマンスの改善の恩恵を受ける事ができる。 以下のコミットはパフォマンスの改善に関するコミットである。

Turbolinks 3

Turbolinksを新しく書きなおしたバージョン。最大の特徴は、テンプレートの部分的な書き換えに対応していること。

- data-turbolinks-permanent => ページ間で保持されることでより高速に動作。サイドバーとかに使えそう
- data-turbolinks-temporary => ページ間で切り替わっていく要素につける

その他変更点

- rake db:migrate => rails db:migrate に変更
- テストの failures 情報が見やすくなる
- 抽象クラスの ApplicationRecord をベースにするようになりました
- ActiveRecord::Relation#in_batches を扱いやすくしてメモリの使いすぎを防ぐ

CHANGELOG

(補足) Turbolinks 5

ネイティブのiOSとAndroidのラッパー実装として、Turbolinks5を新しく作っているらしいです。まだどんな実装なのかはわかりませんが、新しい試みで面白そう。

turbolinks/turbolinks: Turbolinks makes navigating your web application faster

(補足) Ruby 2.2.2以上を使用

Rails 5からは Ruby 2.2.2以上を利用します

Special Thanks

変更来歴

  • (2016-01-31 22:00) 新規作成
  • (2016-02-08 21:50) APIモードの記述を修正
  • (2016-02-09 08:25) Change Logをbeta2にバージョンアップ
  • (2016-02-12 20:30) belongs_toやrender anywhereについて追記

おすすめの書籍