RailsアプリでElasticsearchを扱うならchewyがおすすめ

Twitterでツイートしたり、表参道.rbの懇親会では紹介していたけど、ブログに書いていなかったので今更ながらまとめておきます。

github.com

Chewyの利点

READMEを一通り読んでもらうと 最高に便利なのが分かる とは思うのですが、それだと身も蓋もないので個人的に良いと思ってる機能を3つだけ紹介します。

  1. 複数のストラテジ
  2. ゼロダウンタイムのインデックス更新に対応したrakeタスク
  3. Named scopes

1. 複数のストラテジ

Chewyにはデフォルトで複数のストラテジが用意されてあります。

  • :atomic: 一括でインデックスを登録する
  • :urgent: 1つずつインデックスを登録する
  • :bypass: インデックスに登録しない
  • :active_job: 非同期処理でインデックスに登録する( :sidekiq などを直接使う事も可能)
  • ...など

これらのストラテジは最初から良い感じに設定してあります。

また、一時的にストラテジを切り替えることもできます。

Chewy.strategy(:bypass) do
  City.popular.map(&:do_some_update_action!)
end

2. ゼロダウンタイムのインデックス更新に対応したrakeタスク

READMEから説明を引用して紹介します。

Performs zero-downtime reindexing as described here. So the rake task creates a new index with unique suffix and then simply aliases it to the common index name. The previous index is deleted afterwards (see Chewy::Index.reset! for more details).

要は、「新しいインデックスを作って、エイリアスで切り替えるとゼロダウンタイムで更新できる」というもので、これに対応したrakeタスクが標準で用意されています。 しかもrakeタスクは最初から並列実行に対応しています。

あと、フィールドに updated_at が含まれていれば chewy:sync のタスクでDBとElasticsearchの同期を簡単に行うことができます。 基本的には使わないのですが、何かしらの理由でデータ不整合が起きた時に簡単に修正できて便利です。

参考: https://github.com/toptal/chewy/blob/v5.0.0/lib/chewy/type/mapping.rb#L192

3. Named scopes

ChewyではActiveRecordのようにScopeを定義することができます。

class UsersIndex < Chewy::Index
  def self.by_name(name)
    query(match: { name: name })
  end
end

UsersIndex.limit(10).by_name('Martin')
#=> <UsersIndex::Query {..., :body=>{:size=>10, :query=>{:match=>{:name=>"Martin"}}}}>

これは検索機能のように複数の条件を組み合わせるときに便利です。

class UsersIndex < Chewy::Index
  def self.by_name(name)
    # 検索フォームに未入力の場合を考慮
    return all if name.blank?

    query(match: { name: name })
  end

  def self.in_followers(user)
    query(match: { follower_ids: user.follower_ids })
  end
end

UsersIndex.by_name(params.dig(:q, :name)).in_followers(current_user)
#=> <UsersIndex::Query {..., :body=>{:query=>{:bool=>{:must=>[{:match=>{:name=>"Martin"}}, {:match=>{:follower_ids=>[1, 2]}}]}}}}>

まとめ

RailsでElasticsearchを使う場合、elasticsearch-railsを選ぶ人が多いとは思いますが、是非Chewyも検討してみてください。

私は去年の11月にchewyを見つけたけど、READMEを読んでelasticsearch-railsからchewyへの乗り換えを決めました。

「privateメソッドのテストについての考え方」を読んで #yochiyochirb

highwide.hatenablog.com

を読んで、「自分なら設計を変えて、publicにしてからテストを書くなー」と思ったので、考え方・直し方の一例としてブログを書く。

元の設計

元記事のスライドの途中に出てくるコードはこんな感じで、バッチ処理などでよくある設計。

module Tasks
  module Hoge
    class Sender
      def self.execute
        data = aggregate_data
        processed_data = process_data(data)
        send_s3(processed_data)
      end
    end
  end
end

コードの臭い

個人的なリファクタリング原則で「引数が1つのメソッドは、その引数のインスタンスメソッドに書き換えられる」がある。*1

あと、元コードだと「データ収集、加工、s3に置く」のが1メソッドになっていてテストし辛いので、そこも分ける。

module Tasks
  module Hoge
    class Sender
      def self.execute
        instance.process_data.send_s3
      end

      def self.instance
        data = aggregate_data
        new(data)
      end

      def self.aggregate_data
        # データを集める処理
      end

      def initialize(data)
        @data = data
      end
      attr_reader :data

      def process_data
        processed = data.group_by(&:first)
          .sort
          .map { |key, value| [values[0], values[1]] }

        # Immutableの方がメンテしやすいので、新しいインスタンスを返す設計にしてある
        Sender.new(processed)
      end

      def send_s3
        # s3に置く処理
      end
    end
  end
end

この構造にすると、データ収集・加工・s3アップロードをそれぞれテストしやすくなる。

まとめ

単にpublicに変えるわけじゃなくて、設計を見直した結果publicになるものかなと思ってます。

*1:個人的によく使う原則なんだけど、何か名前あったりするのかな

Railsアプリでrakeタスクのログを見やすくする

rakeタスクのログを調べやすくするために、ActiveSupport::TaggedLoggingを使って読みやすくする方法のメモ。

# lib/rake_logger_rails.rb

module RakeLoggerRails
  # rakeタスクでログを出力するとき、自動的にタグ付けを行います。
  #
  #   task foo: :environment do
  #     logger.info('hello')  # Logs "[RAKE] [foo] hello"
  #   end
  def execute(*)
    if Rails.logger
      Rails.logger.tagged('RAKE', name) { super }
    else
      super
    end
  end
end
Rake::Task.prepend(RakeLoggerRails)

def logger
  Rails.logger
end

Rakefile の中で上のファイルを読み込む。

# Rakefile

require_relative 'config/application'
require 'rake_logger_rails'

Rails.application.load_tasks

これで、タスク内では logger を簡単に使える。

task foo: :environment do
  logger.info 'hello' #=> [RAKE] [foo] hello
end

Gitで更新頻度の高いファイルを見つける方法

というツイートを見かけて、ブログの下書きに眠っていたこの記事を公開した。

$ git log --name-only --oneline | grep -v ' ' | sort | uniq -c | sort -n
(...略)
  10 .rubocop_todo.yml
  10 README.md
  10 app/models/authentication.rb
  11 app/views/dashboard/show.html.slim
  11 config/environments/production.rb
  13 config/application.rb
  13 spec/rails_helper.rb
  14 circle.yml
  14 config/initializers/omniauth.rb
  14 db/schema.rb
  24 .rubocop.yml
  31 config/routes.rb
  68 Gemfile
 132 Gemfile.lock

可視化はしてないけど、どのファイルが変更されるのかは分かる。

上の出力で「bundle updateしてるけど、機能作ってない個人Railsアプリ」ってのが分かる。

自分のプロジェクト管理に関する考えのまとめ

ここ最近は色々とあってプロジェクト管理、スクラム開発について勉強していたので、それを整理するためにまとめた。

あたりを読んで勉強したので、スクラムに関する理解はそれなりになっているはず。

ただ、評価や役割に関するところは自分の好みで少し脚色があるので、これが「スクラム開発なのか!」みたいに勘違いしないように注意。

ソフトウェア開発の前提(スクラムはあまり関係ない)

「なぜソフトウェア開発をするのか?」という前提の話を最初に書いておく。 多くの企業の目的は サービス(製品)を開発し、ビジネスで利益を上げるため になるかなと思う。*1

また、1週間で計画に使える時間は最大でも 40時間/人 です。 残業しないと終わらないような計画を立てている時点で「プロジェクト管理として失敗してる」ので、マネジメントしてる人は勉強し直した方が良いです。*2

スクラム開発の役割

スクラム開発の書籍だと決まったように「プロダクトオーナー」「スクラムマスター」「開発者」「顧客」あたりの役割が出てきますが、責任が不明瞭だったので自分なりの理解をまとめた。

プロダクトオーナー(PO)

プロダクトの機能、実装する優先度、中長期の計画を立てる責任がある。

  • 顧客の要望を聞いて、ストーリーとしてプロダクトバックログにまとめる
  • 開発者と相談してUI/UX、見積もり、優先度を決める
  • イテレーションで開発する範囲を決める
  • 優先度を決めるため、市場の調査、KGI/KPIを把握しておく必要がある
    • ただし、自分でSQLを書く必要までは無い
    • 開発者や顧客に協力してもらっても構わない
  • どのストーリーが収入増加(支出減少)につながるかを気にする
    • ビジネスに近いところを判断するので、コスト感覚を少しは身につけた方が良い

スクラムマスター(SM)

他メンバーの動きを計測し、みんなが「スクラムらしい振る舞い」をできるように促す責任がある。

  • 他メンバーを教育するため、スクラム開発のプロセスを熟知している必要がある
  • プロダクトバックログ、スプリントバックログが整理されているか気にする
    • PO(開発者)に整理することを促す
  • デイリースクラムで問題点が無いか気にする
    • 顧客からの割り込み作業が無いか
    • スプリントバックログの進捗に問題ないか
  • ふりかえりの準備として、バーンダウンチャート、残業時間、コミット状況、テストの失敗率などを見える化する
    • これらを見える状態にして、問題に気づきやすい環境を作る
  • スクラムイベントのファシリテーター役をするための準備

開発者

正しい見積もりと、コミットメントした機能の実装に責任を持つ。

  • 見積もりが大きくブレないようにする
    • 不安なら事前にスパイクを打つストーリーを積む
    • 残業してる時点で、見積もりダメなので反省してください(余計な人件費がかかります)
  • 完了条件の曖昧なストーリーを受け入れない
  • コミットメントした機能の実装は 絶対に終わらせる
    • どうしても無理なら、早めにPOに相談する
    • 責任を果たしていないので、評価が下がるのを覚悟する

顧客

PO、SM、開発者以外の全てが顧客です。運営メンバー、営業、上司などを顧客として見た方が良い。

  • スプリントレビューで製品の質をチェックする
    • 「リリース判断可能な成果物」をレビューして「リリース判断」をする
  • 要望をPOに伝えることができる
    • 開発者に要望を直接伝えてはいけない
  • ストーリーの優先度について要望を挙げることができる
    • ただし、最終決定権はPOが持っている

スクラム開発チームの外側

スクラム開発チームの外側の人の役割について。

上司

サービス(製品)の売上目標に責任を持っていて、開発・営業の各チームリーダーから報告を受ける人。

  • POと連携して、売上の上がる施策を考える
  • POの判断と売上を分析し、POを評価する
  • 営業から挙がった施策をPOに伝える
  • 開発から挙がった施策を営業に伝える

エンジニアリングマネージャー(EM)

最近流行っているので、エンジニアリングマネージャー(EM)にしてみた。 別に上司が評価できるなら、EMを置く必要はないと思う。

  • スクラムマスターをフォローする
    • チーム全体のスクラムの知識が少ないとうまく回らないため
  • 開発者の見積もり、実装状況を計測して評価する(以下は評価の例)
    • 残業なし + 実装完了 => +2点
    • 残業あり + 実装完了 => +1点
    • 残業なし + 実装なし => +0点
    • 残業あり + 実装なし => -1点
  • 開発者のフォロー
    • 技術的な知見の共有
  • エンジニアと1on1して、目指すキャリアにあった役職を与える
  • 役職が埋まらなかったら、どうにかして調達する

まとめ

顧客により良い製品を届けるため、POは責任を持ってプロダクトバックログを作る。

開発者は「見積もり」と「コミットメントした範囲の実装」に責任を持つ。 見積もり通りに実装を終わらせたら、次イテレーションの準備をしても良いし、早めに帰っても良いと思う。

スクラムマスターは業務がうまく回るように見える化、改善を促す。 メンバーが自発的に改善し始めて、SMが仕事をサボっても問題ないのが理想だと思う。

スクラム開発チーム外の役職者(もしくはエンジニアリングマネージャー)は開発チームがうまく回るようにフォロー、育成する環境を用意する責任がある。

*1:一部の企業には、世界平和やより良い未来のために開発してる人はいるかも

*2:ただ、スタートアップの企業などで40時間を守っている場合じゃないことはある

rubyのflat_mapをちゃんと理解した

2019年の最初だし、ブログでも書いてみようという試み。

事の発端のツイート

この例は「flat_map使っているし、flatten要らないよね」って意図だったけど、@kamipoさんや@hanachin_さんからbarがネストした配列を返すときは必要というリプライを頂いた。

flat_mapよく分からなくなったので試した

実際に試してみて、flat_mapがmap+flatten(1)相当だと理解できた。

require 'rspec'

RSpec.describe 'flat_map' do
  it 'mapしてflatten(1)と同じ' do
    nums = [1, 2, 3]
    # [num, [num*2, num**2]] になる配列
    expected = [1, [2, 1], 2, [4, 4], 3, [6, 9]]

    x = nums.map { |n| [n, [n * 2, n**2]] }.flatten(1)
    expect(x).to eq expected

    y = nums.flat_map { |n| [n, [n * 2, n**2]] }
    expect(y).to eq expected

    z = nums.map { |n| [n, [n * 2, n**2]] }.flatten
    expect(z).to eq expected
    # Failure/Error: expect(z).to eq expected
    #
    #   expected: [1, [2, 1], 2, [4, 4], 3, [6, 9]]
    #        got: [1, 2, 1, 2, 4, 4, 3, 6, 9]
  end
end

RSpec::Core::Runner.invoke

まとめ

flat_map使っても、制限なしにflatになるわけではない。

参考ページ

「現場で使えるRuby on Rails5速習実践ガイド」の感想 #現場rails

なかなか読む時間が取れなくて積み本になっていたのを週末に読んだので、その感想です。

現場で使える Ruby on Rails 5速習実践ガイド(特典PDF付き)

現場で使える Ruby on Rails 5速習実践ガイド(特典PDF付き)

ちなみに、読み終わった直後の感想はこれ。

全体の感想

Twitterにツイートしてあるように、良い本だと思う。 Railsの全体像を丁寧に説明してある本なので、Railsを触り始めたばかりの人とか、Railsをもっと知りたい人にはとてもおすすめできる本です。

あと、本を読んでいて、いくつも「あ、これ仕事でよくあるやつだ」みたいなのが書かれていて、実践的な印象を受けました。

各章の感想

ここからは各章で思ったことを書いていきます。

他の方の感想がいくつかWeb上にあったけど、みんな褒めていたので、このブログでは少し辛口のフィードバックもつけておきました 🍛🔥

絵文字は以下の分類でつけてます。

  • ⭐️よかったところ
  • 👻誤植、typo
  • 📝個人的に気になったところ

1. RailsのためのRuby入門

  • ⭐️p.002 Rubyの基本的なところから説明してあるし、Rails初心者でも読みやすそう
  • 👻p.036 のコード例はいくつか間違っている
    • メソッド最後の後置ifはnilを返すケースがあり、意図した挙動にならない
    • given_name と family_name が逆?
    • 実行例は "浦島太郎" だけど、コード例だと "浦島 太郎" と空白が入る
def name(full: true, with_age: true)
  n = if full
        "#{given_name} #{family_name}"
      else
        given_name
      end
  n << "(#{age})" if with_age
end
  • ⭐️p.050 eachからmapへの説明が丁寧だ

2. Railsアプリケーションをのぞいてみよう

  • ⭐️p.052 Windowsまで考慮しててすごい
  • ⭐️p.055 rbenvを入れるあたり、実践的で良い
  • ⭐️p.079 ルーティングからの流れを図で説明してて、良い本だ

3. タスク管理アプリケーションを作ろう

  • ⭐️slim、bootstrapを使っていて実践的だなーと思った

4. 現実の複雑さに対応する

  • ⭐️p.128 マイグレーションの名前の付け方。初心者は嵌りそうなやつ。
  • ⭐️p.129 migrate:redo を紹介されてる。そうそう、大事なやつ。
  • ⭐️p.130 DB制約とバリデーション大事。
  • ⭐️p.145 コールバックの説明が分かりやすい

5. テストをはじめよう

  • ⭐️p.188 RSpec、Capybara、FactoryBot の組み合わせは実践的で良いと思った
  • 📝p.198 factory :user で email が固定値になっていて微妙
    • ユニーク制約がかかっているカラムは sequence を使うべき

6. Railsの全体像を理解する

  • ⭐️p.226 Railsで開発していく中で必要そうな技術の用語が並んでいて良い
  • 📝p.238 多言語対応の説明してあるけど、コード例はコントローラーに日本語書いてあって中途半端
    • ただ、初心者向けの書籍で完全な多言語対応するのも限度あって難しそう
    • 「多言語化しようとして序盤はlocale使うけど、途中で諦めてコントローラーに日本語書いてる案件」は多いし、ある意味では実践的?
  • 📝p.249 「ルーティングで発生した例外」に対して言及しているのに、それに対する対処方法がないのモヤっとした
    • rambulance の紹介とかあっても良いのでは
  • ⭐️p.250 ロガーの話してるの良い
  • ⭐️p.253 セキュリティに関して丁寧に説明していて良い。新人に読ませたい
  • ⭐️p.278 5.2 で導入されて色々な議論を生んだ Credentials だ 🔑
  • 📝p.284 そして、一切触れられなかった ActionCable さん...

7. 機能を追加してみよう

  • ⭐️p.291 確認画面について言及しているの良い
  • 📝p.292 ransack は簡易的な実装としては良いけど、Elasticsearch とかのちゃんとした対応について言及して欲しかった
    • ransack を入れた後、パフォーマンスの問題が出て誰かが直す事に...
    • ただ、 ransack で検索条件を絞る話を書いてあるのはすごく良い。大事なので。
  • 📝p.299 TaskMailer#creation_email のメソッド名にある email が冗長な気がする
    • とはいえ、他に良いメソッド名は思い浮かばない
    • メーラー周りの命名はいつも悩むけど、未だに良い命名規則を見つけられていない
  • 📝MailerPreview は紹介されていなかった
    • htmlメールの見栄えを確認できて便利なやつなので、ぜひ改訂版では1ページぐらい紹介してあげて欲しい
  • 📝p.304 text/html のテストが全く同じだけど、htmlの方は「タグの有無」くらいテストした方が良いのでは?
    • html_body の中で content_type みてるし、要らないのかもしれない
  • ⭐️p.306 ActiveStorage の説明が最低限はあった
    • purge や with_attached_image の説明がないのは少し残念
  • ⭐️p.312 CSVのインポート・エクスポート。顧客から「Excelでみたい」って言われたときに作るやつですね
  • 📝p.312 CSVのエクスポートは件数増えると重くなるので、普通にpostしない方が良い
    • CSVエクスポートの要求を受けつけて、非同期処理でCSVを作って、作り終わったら画面にダウンロードリンクを出す
  • 📝p.316 自分ならコントローラーに import アクションを作らず、 Tasks::Importings#create とかにするなー
  • 👻p.327 set(wait:1.week) は他コードに合わせてスペース入れた方が良い

8. RailsJavaScript

  • ⭐️p.340 Turbolinks の良い点、ハマりどころが丁寧に書かれていてすごく良い
    • Turbolinks はやれば出来る子なんです
    • みんなすぐ無効にするけど
  • ⭐️p.347 Webpacker のメリット、デメリットを紹介してるのも良いですねー

9. 複数人でRailsアプリケーションを開発する

  • ⭐️p.354 実際の仕事の流れを追体験できて良いなと思った
  • 📝p.362 database.yml をGit管理しない場合、CIの設定やデプロイフローが複雑になってしまう
    • 個人的には database.yml をGit管理しておき、何か変えたい場合は DATABASE_URL で調整する方が好きかな
  • ⭐️p.366 git-bisect に言及してるの最高に良い!
  • 📝p.367 rebaseは英語で、マージがカタカナなのが少し気になった
    • Gitの書籍だと「リベース/マージ」とカタカナ統一な気がする
    • ただ、仕事だとrebase/マージって書いてる人が多い印象
  • ⭐️p.378 マイグレーションの話が丁寧
  • 📝p.382 ridgepole を紹介しても良いかも、と思った
  • 👻p.386 Order. find_each と半角スペースが入っていた
  • ⭐️p.387 スキーマキャッシュの説明があるの良い

10. Railsアプリケーションと長く付き合うために

  • ⭐️p.392 バージョンアップ大事
  • 👻p.393 bundleupdate スペースが無い
  • 📝p.403 自分なら Articles::PublicationsController を作るかな
    • とはいえ、このコードも早すぎる最適化っぽさはある
# app/controllers/articles/publications_controller.rb
module Articles
  class PublicationsController < AppliationController
    def create
      @article = Article.find(params[:article_id])
      publication = Articles::Publication.new(article: @article)

      if publication.save
        redirect_to @article, notice: '記事を公開しました'
      else
        message = publication.errors.full_messages.first
        redirect_to @article, alert: message
      end
    end
  end
end

# app/models/articles/publication.rb
module Articles
  class Publication
    include ActiveModel::Model
    attr_accessor :article

    validate :must_be_draft

    def save
      return false if invalid?

      set_expires_at! if article.limited?
      update(status: 'published', published_at: Time.current)
    end

    private

    def must_be_draft
      errors.add(:base, '下書きの記事のみ公開にできます') unless article.status == 'draft'
    end

    def set_expires_at!
      article.expires_at = (article.published_at + 3.months).end_of_month
    end
  end
end
  • 📝p.414 upload_image ってアクションを持つコントローラー?
    • どんなルーティングを想定しているのかよく分からなかった
  • ⭐️p.416 ApplicationController に実装して、後から Module に抽出するの実践感あって良い
  • 👻p.444 ルーティングとコントローラの実装が不一致
    • たぶん GET /order_csvs.csv がエクスポートで、 POST /order_csvs がインポートかな
    • そうなるとルーティングが only: %i(index create) になるのが正しそう
resources :order_csvs, only: %i(index update)

class OrderCsvsController < ApplicationController
  def index
  end

  def create
  end
end
  • ⭐️p.450 最後の「使うべきか議論の余地が大きいメソッド」で笑った。〆が良い

最後に

良い本だなーと思ったので、気になったところやtypoに気づいたところはフィードバックのためにもブログ書いた。 このブログが著者の目にとまって、何かしら本に反映されたら嬉しい。