Quantcast
Channel: Ruby on Railsの記事一覧|TechRacho by BPS株式会社
Viewing all 1381 articles
Browse latest View live

Rails: メールをActive Recordのコールバックで送信しないこと(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: メールをActive Recordのコールバックで送信しないこと(翻訳)

Railsアプリケーションで何かとやってみたくなることのひとつといえば、メール送信でしょう。

モデルのインスタンスが変更または作成されたときにメールを送信するというのが、よくあるパターンです。

次のようにしないこと

モデルのコールバックにメール送信を仕込む。

class BookReview < ApplicationRecord
  after_create :send_email_to_author

  private

  def send_email_to_author
    AuthorMailer.
      with(author: author).
      review_notification.
      deliver_now
  end
end

次のようにすること

メールをコントローラで送信する。

class BookReviewsController < ApplicationController
  def create
    BookReview.create(comment_params)
    AuthorMailer.
      with(author: author).
      review_notification.
      deliver_now
  end
end

コールバックで送信すべきでない理由

後でコードに触るときに理解しやすくするためであり、後で自分がびっくりしないためです。

上のコード例で考えてみましょう。本にブックレビューを付けるときに、その本の著者に必ずしもメールを送る必要があるとは限りません。1つ目のサンプルコードでは、ブックレビューを1つ作成する副作用としてメールが発射されてしまいます。

場合によってはrails consoleなどで何か操作するときに、ユーザーが著者にメールを送信せずに本のレビューを作成する必要が生じるかもしれません。コールバックをスキップするメソッドを駆使する方法も一応可能ですが、そこから先は泥沼です。

「やることリスト」をコントローラのアクションの中に置いてそこで全部見えるようにしておく方が、明確かつ手続き的です。この場合、ブックレビューの作成の後に行う別の操作としてメール送信を書いておけば、後でコードを見返したときに意図がずっと明確になります。

それだけではありません。無関係な機能をデバッグするために、モデルのコールバックたちをもれなくチェックして回るのは、認知に大きな負荷がかかってつらい作業になります。自分の頭で把握しておかなければならないコンテキストがぐっと増えてしまうからです。

コールバックで送信する理由があるとすれば

ドキュメントではいつも、メール送信をモデルのコールバックで行うことは「Rails Way」であるとみなされて上のようなコード例が付いていたりします。そして、シンプルなケースであれば実際にはこれといった問題は生じません。しかし、やがてアプリケーションが複雑になってくれば、このアプローチのつらみだけが前面に出てくるようになるのです。

「モデルはファットに、コントローラは薄くすることを目指すべき」とよく言われることもあって、私たちは多くの機能をモデル層に盛り込むようになります。これはもちろん一般的にはよいアドバイスではあります。しかしこれはどちらかというと、コールバックを量産する副作用を使いまくってもよいということではなく、アプリケーションの操作を明確にすることでコントローラ層から複雑さを排除しようという話です。

自分の感覚では、モデル変更に伴ってメールを送信するぐらいであれば、コールバックベースの抽象化が混乱するほど複雑にはならないと思います。大事なのは、コントローラのアクションが呼び出されると、アプリケーションのユーザーに重要なことが2つ発生することを明確に示すことです。

私は、コントローラのメソッドが複雑になってきたときには、それらをコールバックに押し込めるよりも素のRubyの「Service Object」に移す方が好みです。

関連記事

Rails: :before_validationコールバックの逸脱した用法を改善する(翻訳)

Rails: Active Recordのコールバックを避けて「Domain Event」を使おう(翻訳)


週刊Railsウォッチ(20190917-1/2前編)Sidekiq 6.0がリリース、銀座Rails#13と「出張!Railsウォッチ」、るびま0060号、ロックイン回避の落とし穴ほか

$
0
0

こんにちは、hachi8833です。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

⚓週刊Railsウォッチ「公開つっつき会」第15回のお知らせ(無料)

第15回目公開つっつき会は、10月5日(木)19:30〜にBPS会議スペースにて開催されます。皆さまのお気軽なご参加をお待ちしております🙇

⚓Rails: 先週の改修(Rails公式ニュースより)

公式の更新情報がなかったので、今回も6-0-stableを中心に見繕いました。

⚓(6.0)insert allなどでクエリキャッシュをクリアするようにした

insertinsert_allupsertupsert_allではクエリキャッシュをクリアするようになった。
Eugene Kenny
changelogより

# activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L154
+     def exec_insert_all(sql, name) # :nodoc:
+        exec_query(sql, name)
+     end
# activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb#L9
        def included(base) #:nodoc:
          dirties_query_cache base, :insert, :update, :delete, :truncate, :truncate_tables,
-           :rollback_to_savepoint, :rollback_db_transaction
+           :rollback_to_savepoint, :rollback_db_transaction, :exec_insert_all

          base.set_callback :checkout, :after, :configure_query_cache!
          base.set_callback :checkin, :after, :disable_query_cache!
        end
# activerecord/lib/active_record/insert_all.rb#L23
    def execute
      message = +"#{model} "
      message << "Bulk " if inserts.many?
      message << (on_duplicate == :update ? "Upsert" : "Insert")
-     connection.exec_query to_sql, message
+     connection.exec_insert_all to_sql, message
    end

つっつきボイス:「#37142 のupsert_allのバグIssue↓に対する修正対応という感じですね」

# 37142より
ActiveRecord::Base.connection.enable_query_cache!
#=> true
User.create(name: "Fred")
#=> #<User id: 1, name: "Fred", created_at: "2019-09-06 02:24:38", updated_at: "2019-09-06 02:24:38">
u = User.first   # ここでクエリキャッシュができる
#=> #<User id: 1, name: "Fred", created_at: "2019-09-06 02:24:38", updated_at: "2019-09-06 02:24:38">
User.upsert_all([{id: u.id, name: "Amy", created_at: u.created_at, updated_at: Time.now}])
#=> #<ActiveRecord::Result:0x00007f9b16b439e0 @columns=[], @rows=[], @hash_rows=nil, @column_types={}>
User.first.inspect
#=> #<User id: 1, name: "Fred", created_at: "2019-09-06 02:24:38", updated_at: "2019-09

再現手順を実行するとActive Recordがクエリキャッシュを読み込むため古い結果が返され、クエリが発生しない。このクエリキャッシュはdatabase_statementsでクエリが変更された場合に自動的にデータのキャッシュをクリアするようセットアップされるが、新しいupsert/insert系メソッドではそうなっていない。
#37142より大意

⚓(6.0)エンドレスRangeでinclude?を呼ぶと落ちる問題を修正

# activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb#L11
    def include?(value)
-     if first.is_a?(TimeWithZone)
+     if self.begin.is_a?(TimeWithZone)
        cover?(value)
-     elsif last.is_a?(TimeWithZone)
+     elsif self.end.is_a?(TimeWithZone)
        cover?(value)
      else
        super
      end
    end

つっつきボイス:「endless Rangeでは #first / #lastではなく #begin / #end を使うべきという話みたいなのだけど、この辺のドキュメント、Rubyの日本語リファレンスマニュアルの方だと違いが書かれていなくて、英語のRDocの方には書かれているという差があるみたい」「😳

後で見てみると、日本語リファレンスマニュアルにはendless Rangeについての記述自体がありませんでした。
Ruby 2.6.4のPryでやってみると↓、エンドレスRangeのlastはエラーになり、endだとエラーになりませんでした。なお...でも同じです。

» (1..).last
RangeError: cannot get the last element of endless range
from (pry):24:in `last'
» (1..).end
»

⚓(6.0、5.2.3)app/にREADME.mdを置くとdevelopment環境でエラーになる問題を修正

# railties/lib/rails/application.rb#L350
    def watchable_args #:nodoc:
      files, dirs = config.watchable_files.dup, config.watchable_dirs.dup

      ActiveSupport::Dependencies.autoload_paths.each do |path|
-       dirs[path.to_s] = [:rb]
+       File.file?(path) ? files << path.to_s : dirs[path.to_s] = [:rb]
      end

つっつきボイス:「6.0と5.2.3のLinuxのdevelopment環境の場合に起きたそうです」「autoload_pathsにディレクトリパスではなくファイルパスを書いているとダメだったのを、ファイルパスでも正常に動くようにした模様」

⚓(master)classやmoduleもActiveJobの#perform引数に渡せるようにした

# activejob/lib/active_job/serializers/module_serializer.rb
+# frozen_string_literal: true
+
+module ActiveJob
+  module Serializers
+    class ModuleSerializer < ObjectSerializer # :nodoc:
+      def serialize(constant)
+        super("value" => constant.name)
+      end
+
+      def deserialize(hash)
+        hash["value"].constantize
+      end
+
+      private
+        def klass
+          Module
+        end
+    end
+  end
+end

つっつきボイス:「Factory method的なクラスをActiveJobのperform時に渡せるようになった、という感じのようだ」

# 同PRより
class EmailJob < ApplicationJob
  queue_as :default
  def perform(template_class, *arguments)
    template_class.new(*arguments).send!
  end
end

module Email
  class FooTemplate ... end
  class BarTemplate ... end
end

EmailJob.perform_later(Email::FooTemplate, ...)
EmailJob.perform_later(Email::BarTemplate, ...)

参考: Factory Method パターン - Wikipedia

⚓(master)Rubyのキーワード引数変更に引き続き対応

先週(ウォッチ20190909)に続くキーワード引数周りの対応です。

# activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L480
-     def add_column(name, type, options)
+     def add_column(name, type, **options)
        name = name.to_s
        type = type.to_sym
        @adds << AddColumnDefinition.new(@td.new_column_definition(name, type, **options))
      end
    end
# activerecord/lib/active_record/migration/compatibility.rb#L153
-       def add_column(table_name, column_name, type, options = {})
+       def add_column(table_name, column_name, type, **options)
          if type == :primary_key
            type = :integer
            options[:primary_key] = true
          end
          super
        end

⚓Rails

⚓Sidekiq 6.0がリリース(Ruby Weeklyより)


sidekiq.orgより(CC BY-SA 4.0

「デーモン化をやめた」「ログ出力を一新」「Active Jobとの統合」が目に付きました。


つっつきボイス:「利用側としてはログフォーマッタが使えるようになったというくらいかな?: sidekiqctlバイナリがなくなった、とかも書いてあるので、セットアップスクリプト周りはプロジェクトによっては変更の必要があるのかもしれない」「おぉ」「公式のUpgrade noteの方が情報量ありそう↓」


以下は同アップグレードノートより:

ActiveJobでsidekiq_optionsを用いてSidekiqの機能や内部を直接設定できるようになった(retryサブシステムなど)。ActiveJobでうまく動かない一部の機能(unique jobなど)についてはネイティブのSidekiq::Worker APIが望ましい。

class MyJob < ActiveJob::Base
  queue_as :myqueue
  sidekiq_options retry: 10, backtrace: 20
  def perform(...)
  end
end
  • ログ出力が再設計されてフォーマッターやSidekiq付属のフォーマッタをpluggableにできるようになった
    • default: macOSの典型的な出力
    • heroku: Heroku実行時に特化した出力
    • json: インデックス検索用のJSONフォーマット(1行1ハッシュ)
  • 検出された環境に最適なフォーマッターが有効になる。明示的にログフォーマッターを設定することでオーバーライドもできる。詳しくはLogging Wiki参照。
Sidekiq.configure_server do |config|
  config.log_formatter = AcmeCorp::PlainLogFormatter.new
  # config.log_formatter = Sidekiq::Logger::Formatters::JSON.new
end
  • 以下を廃止: デーモン化、logfileやpidfileコマンド引数、sidekiqctlバイナリ
  • REDIS_PROVIDER変数は正しく使うこと
  • デフォルトのシャットダウン時間を8秒から25秒に延長
  • 以下はサポート対象外:
    • Rails 5より前
    • Ruby 2.5より前
    • Redis 4より前
  • Rails 6以降はZeitwerkモードでのみ動作する

⚓Stimulsjsってどう?

Stimulusjsのリポジトリは★7500超えで、issueやPRがほぼ残っていないのがびっくりです。


つっつきボイス:「StumulusjsはBasecampが作っていてRailsとの相性がよさそうなのが気になったので」「フロントエンドも全部Railsエンジニアが書くならStumulusjsは悪くない選択肢にも見えるけど、Railsエンジニアってフロントエンドは触りたくないと言っている人もちらほら見る(そもそもサーバーサイドで手一杯で手が回らないという話も)ので、潤沢なRailsエンジニアの供給を前提とするのが果たして妥当かどうか、という話になるのかもしれないですね」

⚓Rails 6ではDBのadvisory lockを無効にできる(RubyFlowより)

Rails 6ではdatabase.ymlでadvisory lockをオンオフできるそうです(デフォルトはtrue)。

production:
  adapter: postgresql
  advisory_locks: false

つっつきボイス:「migrationの時だけ参照される?ような説明になっているように見える: 無効にしたいシチュエーションがよくわからない🤔

参考: 13.3. 明示的ロック — Advisory lockは「勧告的ロック」と訳されてます
参考: 3.19 接続設定 — Rails アプリケーションを設定する - Rails ガイドadvisory_locks設定の説明

「ガイドによると、PostgreSQLのPgBouncer↓とかを使う時にもadvisory lockをオフにする必要があるかもしれないってありますね」

参考: PgBouncer - lightweight connection pooler for PostgreSQL

Rails開発者のためのPostgreSQLの便利技(翻訳)

⚓銀座Rails#13で「出張!Railsウォッチ」

9/12の銀座Rails#13でmorimorihogeさんが「出張!Railsウォッチ」で登壇しました。お題はRails 6のAction Textです。

以下のスライドではTechRachoの翻訳記事「ActiveRecord::FixtureSet」にも言及いただきました🙇


つっつきボイス:「今回は銀座Railsに参加できずでした😓」「報告記事上げます~」

後でmorimorihogeさんから伝え聞いた@a_matsudaさんのRailsパフォーマンス話の要点も興味深い内容でした😋

追記

なお、前回の銀座Rails#12で評判だった@jnchitoさんのライブコーディング動画が有料公開されました🎉

以下はその後のツイートです。

⚓その他Rails


unubo.comより

つっつきボイス:「Unuboがはてブでバズってたので」「ニフティクラウドC4SAみたいに滅びる可能性も高いので、あくまで練習用という感じがするのと、あとどこの誰が運用しているのかがサイト見てもよくわからなかった🤔

参考: NIFTYCloud-C4SA/support: ニフティクラウドC4SA ドキュメント・FAQ・質問等はこちらへ

ニフティクラウド C4SAは、2017年11月30日をもちまして、サービスを終了いたしました。 これまで長らくご愛顧賜り、誠にありがとうございました。
同リポジトリより

⚓Ruby

⚓るびま0060号リリース🎉


magazine.rubyist.netより


つっつきボイス:「るびまサイトがRSSフィードしていることについ昨日気づきましたので、Slackでフィードを受けるよう設定しました😅」「るびま、Rubyistは必読なので読んでない人はぜひ🎉

⚓スライド: Ruby 3のキーワード引数


同スライドより

上のスライドは今年のRubyKaigiより少し前に以下のissueに貼られているのを見つけました。最終的にJeremy Evansさんの案がベースになったようです。


つっつきボイス:「上のスライドは方針決めのためのもので、結局どうなるのか・どうすればいいのかをissueでまだ追いきれていないのですが😅、以下あたり↓が比較的まとまってそうです」「breaking changesなので、次のRubyKaigiでも話題になる気がしますね」「もしかするとmameさんがそのうちブログにまとめてくれるかもしれないとひっそり期待してます🙏

⚓RubyのUnboundMethod

この間のTokyo Rubyist Meetup@ベストマサフミさんが発表後にこれをテーマに即興LTをやったのを見て知りました。

参考: Ruby|UnboundMethodで遊んでみた - Qiita

その他Ruby

# 同記事より
# Defining sub_scorer which is used at multiple places
# and based on some condition it is updating scores
def sub_scorer(scores)
  ...
  scores << 50 if condition
  ...
end

# Defining final_scorer which may call sub_scorer
def final_scorer
  begin
    scores = [10, 20, 30].freeze
    sub_scorer(scores)
  rescue FrozenError => e
    # We can now gracefully handle Frozen object violations
    # based on the receiver
    if e.receiver == scores
      return "Can not modify scores"
    else
      return "Can not modify frozen objects"
    end
  end
end

final_scorer
#=> "Can not modify scores"

⚓その他

⚓書籍とか


つっつきボイス:「今度こそ技術書典行ってみたかったんですがまたしても都合がつかなくて😭」「技術書店、人が多いのが苦手過ぎて行けてないので自宅からOculus QuestとかでVR参加したいなあ」

⚓「ロックインの回避」にロックインされるな(Morning Cup Of Codingより)


martinfowler.comより

ロックインを避けようとするあまり:

  • 余分な工数がかかる
  • コストもかかる
  • 利便性が落ちる
  • さらに複雑になる
  • さらに別のロックインに陥る

つっつきボイス:「martinfowler.comの記事ですが書いたのはMartin Fowlerさんではありませんでした」「あるある: リスク管理で大事なのはリスクの洗い出しとコストを含めたトレードオフの認識であって、無限のリソースをかけてロックインを回避するというのは全くの悪手(国防方面や原発・プラント制御みたいなものだと例外はあるけど)」


前編は以上です。

バックナンバー(2019年度第3四半期)

週刊Railsウォッチ(20190910-2/2後編)buildersconと「20年後のソフトウェアテスト」、はてなブックマークがScalaに移行、「詳解PostgreSQL」、Go 1.13ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Morning Cup Of Coding

morningcupofcoding_banner_captured

Rails API: ActiveRecord::Migration(翻訳)

$
0
0

概要

MITライセンスに基づいて翻訳・公開いたします。

Rails API: ActiveRecord::Migration(翻訳)

複数の物理データベースで使われるスキーマの成長をマイグレーションで管理できます。マイグレーションは、新しい機能を使うためにローカルデータベースにフィールドを追加するときによく起こる問題を解決する方法のひとつですが、変更点を他の開発者やproductionサーバーにプッシュする方法までは関知しません。マイグレーションを用いることで、自己完結するクラスの変換方法を(Gitなどの)バージョン管理システムにチェックインして記述し、別のデータベースに対して1バージョン前、2バージョン前、5バージョン前のものを実行できるようになります。

シンプルなマイグレーションの例を以下に示します。

class AddSsl < ActiveRecord::Migration[5.0]
  def up
    add_column :accounts, :ssl_enabled, :boolean, default: true
  end

  def down
    remove_column :accounts, :ssl_enabled
  end
end

上のマイグレーションは、accountsテーブルに:booleanフラグを追加し、マイグレーションが巻き戻されるときには削除します。また、あらゆるマイグレーションでマイグレーションの実装や削除を行うのに必要な変換を記述するupメソッドとdownメソッドの使い方も示されています。これらのメソッドは、add_columnremove_columnのようなマイグレーション固有のメソッドはもちろん、変換で必要なデータを生成する通常のRubyコードも含められます。

データの初期化が必要となる、さらに複雑なマイグレーションの例を以下に示します。

class AddSystemSettings < ActiveRecord::Migration[5.0]
  def up
    create_table :system_settings do |t|
      t.string  :name
      t.string  :label
      t.text    :value
      t.string  :type
      t.integer :position
    end

    SystemSetting.create  name:  'notice',
                          label: 'Use notice?',
                          value: 1
  end

  def down
    drop_table :system_settings
  end
end

このマイグレーションでは最初にsystem_settingsテーブルを追加し、続いて、そのテーブルが依存しているActive Recordを用いる最初の行を作成します。より高度なcreate_tableも用いて、テーブルの完全なスキーマを指定する構文を1つのブロック呼び出しで指定できます。

注: このAPIドキュメントには上述のSystemSetting.createのようにマイグレーション内でモデルを呼び出すコード例がいくつかありますが、そのモデルが将来削除またはリネームされるとdb:migratedb:migrate:resetが失敗する可能性があるので、マイグレーション内でのモデル呼び出しは避けるべきという指摘がBPS Webチームでありました。以下の2つの記事もご覧ください。
参考: How to use models in your migrations (without killing kittens) - The blog of makandra

[Rails 3] 失敗しないmigrationを書こう

利用可能な変換

作成用メソッド

create_join_table (table_1, table_2, options)
最初の2つの引数の辞書式順序(lexical order: いわゆるアルファベット順)を持つjoinテーブルを1つ作成する。詳しくはConnectionAdapters::SchemaStatements#create_join_tableを参照。
create_table(name, options)
nameという名前のテーブルを1つ作成して、そのテーブルオブジェクトをブロックで利用可能にしてカラムを足せるようにする。add_columnと同じフォーマットに従う。上述の例を参照。オプションハッシュは、テーブル作成の定義にappendされる”DEFAULT CHARSET=UTF-8″などのフラグメントに用いられる。
add_column(table_name, column_name, type, options)
table_nameと呼ばれるテーブルにcolumn_nameと呼ばれる新しいカラムを1つ追加する。typeには:string:text:integer:float:decimal:datetime:timestamp:time:date:binary:booleanのいずれかを指定する。デフォルト値は、{ default: 11 }などのoptionsハッシュを渡すことで指定できる。:limit:null(`{ limit: 50, null: false }など)といったその他のオプションについて詳しくは、ActiveRecord::ConnectionAdapters::TableDefinition#columnを参照。
add_foreign_key(from_table, to_table, options)
新しい外部キーを1つ追加する。from_tableはキーのカラムを持つテーブル、to_tableは、参照される主キーを含むテーブルを表す。
add_index(table_name, column_names, options)
指定のカラム名を持つ新しいインデックスを1つ追加する。:name:unique{ name: 'users_name_index', unique: true }など)、:order{ order: { name: :desc } }など)といったオプションもある。
add_reference(:table_name, :reference_name)
reference_name_id(デフォルトでinteger)という新しいカラムを1つ追加する。詳しくはConnectionAdapters::SchemaStatements#add_referenceを参照。
add_timestamps(table_name, options)
タイムスタンプ(created_atupdated_at)カラムをtable_nameに追加する。

変更用メソッド

change_column(table_name, column_name, type, options)
指定のカラムを、add_columnと同じパラメータを用いて別のタイプに変更する。
change_column_default (table_name, column_name, default_or_changes)
table_namedefault_or_changesで定義されたcolumn_nameのデフォルト値を設定する。:from:toを含むハッシュをdefault_or_changesとして渡すと、マイグレーションでこの変更をリバース可能にできる。
change_column_null (table_name, column_name, null, default = nil)
column_nameNOT NULL制約を設定または削除する。nullフラグは、値にNULLを使ってよいかどうかを指定する。詳しくはConnectionAdapters::SchemaStatements#change_column_nullを参照。
change_table(name, options)
nameというテーブルへのカラム変更(ALTER)を行える。このメソッドによって、tableオブジェクトをブロックで利用できるようにしてカラムやインデックスや外部キーを追加または削除できるようにします。
rename_column(table_name, column_name, new_column_name)
カラムをリネームします。タイプや内容は変更しません。
rename_index(table_name, old_name, new_name)
インデックスをリネームします。
rename_table(old_name, new_name)
テーブルをold_nameからnew_nameにリネームします。

削除用メソッド

drop_table(name)
nameというテーブルをDROPします。
drop_join_table(table_1, table_2, options)
引数で指定されたJOINテーブルをDROPします。
remove_column(table_name, column_name, type, options)
table_nameというテーブルからcolumn_nameというカラムを削除します。
remove_columns(table_name, *column_names)
指定のカラムをテーブル定義から削除します。
remove_foreign_key (from_table, to_table = nil, **options)
指定の外部キーをtable_nameというテーブルから削除します。
remove_index(table_name, column: column_names)
インデックスをcolumn_namesで指定して削除します。
remove_index(table_name, name: index_name)
インデックスをindex_nameで指定して削除します。
remove_reference (table_name, ref_name, options)
table_nameにある参照をref_nameで指定して削除します。
remove_timestamps (table_name, options)
タイムスタンプのカラム(created_atupdated_at)をテーブル定義から削除します。

リバースできない変換

変換によっては、破壊的な操作を行うためリバースできないものがあります。そのようなマイグレーションはdownメソッドでActiveRecord::IrreversibleMigration例外が発生します。

Railsの中でマイグレーションを実行する

Railsパッケージには、マイグレーションの作成や適用を支援するさまざまなツールがあります。

新しいマイグレーションの生成には以下のコマンドを使えます。

rails generate migration MyNewMigration

MyNewMigrationはマイグレーションの名前です。この場合ジェネレータによってdb/migrate/ディレクトリの下にタイムスタンプ_my_new_migration.rbというファイルが作成されます。このタイムスタンプは、マイグレーションが生成された日時をUTC形式で表したものです。

テーブルにフィールドを追加するマイグレーションを生成するための、特殊なショートカット構文が利用できます。

rails generate migration add_fieldname_to_tablename fieldname:string

上は以下のような内容のタイムスタンプ_add_fieldname_to_tablename.rbファイルを生成します。

class AddFieldnameToTablename < ActiveRecord::Migration[5.0]
  def change
    add_column :tablenames, :fieldname, :string
  end
end

現在設定されているデータベースへのマイグレーションを実行するには、rails db:migrateコマンドを使います。これによってペンディング中のマイグレーションがすべて実行されてデータベースが更新され、schema_migrationsテーブルが作成されます(後述の「schema_migrationsテーブルについて」を参照)。schema_migrationsテーブルが見当たらない場合は、db:schema:dumpコマンドも実行してdb/schema.rbファイルを更新し、データベース構造と整合させます。

データベースを以前のバージョンのマイグレーションにロールバックするには、rails db:rollback VERSION=Xを使います。このXはダウングレード先のバージョンです。直近のいくつかのマイグレーションにロールバックしたい場合は、STEPオプションを用いることもできます。rails db:rollback STEP=2を実行すると、直近の2つのマイグレーションをロールバックします。

複数のマイグレーションのいずれかでActiveRecord::IrreversibleMigration例外が発生すると、そのステップは失敗します。その場合は手動での作業が必要です。

サポートされるデータベース

現在マイグレーションをサポートしているのは、MySQL、PostgreSQL、SQLite、SQL Server、Oracleです(DB2を除くこれらすべてをサポートします)。

さまざまなマイグレーションの例

マイグレーションによってスキーマが変更されるとは限りません。たとえば、以下のようにデータの修正のみを行うマイグレーションもあります。

class RemoveEmptyTags < ActiveRecord::Migration[5.0]
  def up
    Tag.all.each { |tag| tag.destroy if tag.pages.empty? }
  end

  def down
    # not much we can do to restore deleted data
    raise ActiveRecord::IrreversibleMigration, "Can't recover the deleted tags"
  end
end

次のように、(downではなく)upの場合にカラムを削除するマイグレーションもあります。

class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[5.0]
  def up
    remove_column :items, :incomplete_items_count
    remove_column :items, :completed_items_count
  end

  def down
    add_column :items, :incomplete_items_count
    add_column :items, :completed_items_count
  end
end

次のように、マイグレーションで直接抽象化を行わず、SQLで何かを実行する必要が生じることもありえます。

class MakeJoinUnique < ActiveRecord::Migration[5.0]
  def up
    execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)"
  end

  def down
    execute "ALTER TABLE `pages_linked_pages` DROP INDEX `page_id_linked_page_id`"
  end
end

モデルのテーブルを変更した後でそのモデルを使う場合の注意

マイグレーションでカラムを1つ追加し、すぐにそれを使いたいことがあります。このような場合、以下の例のようにBase#reset_column_informationを呼んで、新しいカラムの追加後にそのモデルで最新のカラムデータを確実に利用できるようにする必要があります。

class AddPeopleSalary < ActiveRecord::Migration[5.0]
  def up
    add_column :people, :salary, :integer
    Person.reset_column_information
    Person.all.each do |p|
      p.update_attribute :salary, SalaryCalculator.compute(p)
    end
  end
end

コンソール出力を制御する

デフォルトのマイグレーションでは、実行中の操作をその都度コンソールに詳細出力し、各ステップに要した時間を示すベンチマークも出力に含めます。

出力を止めるには、ActiveRecord::Migration.verbose = falseを設定します。

say_with_timeメソッドを使えば、以下のようにメッセージやベンチマークに独自のメッセージを追加することもできます。

def up
  ...
  say_with_time "salaryを更新中..." do
    Person.all.each do |p|
      p.update_attribute :salary, SalaryCalculator.compute(p)
    end
  end
  ...
end

ブロックが完了すると、そのブロックのベンチマークとともに「salaryを更新中…」というフレーズが出力されます。

タイムスタンプ付きのマイグレーション

Railsがデフォルトで生成するマイグレーションファイルは以下のようになります。

20080717013526_your_migration_name.rb

ファイル名の冒頭は、生成時のタイムスタンプ(UTC)を表します。

この形式ではなく、ファイル名の冒頭に数値を使いたい場合は、application.rbで以下を設定することでマイグレーションのタイムスタンプをオフにできます。

config.active_record.timestamped_migrations = false

リバース可能なマイグレーション

リバース可能なマイグレーションとは、マイグレーションをdownで取り消す方法を自動認識するマイグレーションのことです。リバース可能なマイグレーションではupする方法を指定するだけで、downコマンドで実行すべき内容をマイグレーションシステムが認識してくれます。

リバース可能なマイグレーションを定義するには、以下のようにマイグレーションでchangeメソッドを定義します。

class TenderloveMigration < ActiveRecord::Migration[5.0]
  def change
    create_table(:horses) do |t|
      t.column :content, :text
      t.column :remind_at, :datetime
    end
  end
end

上のマイグレーションはupのときにはhorsesテーブルを作成し、downのときにhorsesテーブルをDROPする方法を自動認識します。

コマンドの中には、リバースできないものもあります。そのような場合にupとdownの方法を定義しておきたい場合は、従来と同様にupメソッドとdownメソッドを定義すべきです。

マイグレーションがdownするときにリバースできないコマンドでは、ActiveRecord::IrreversibleMigration例外が発生します。

リバース可能なコマンドリストについてはActiveRecord::Migration::CommandRecorderを参照してください。

トランザクショナルなマイグレーション

DDL(データ定義言語)のトランザクションをサポートするデータベースアダプタでは、すべてのマイグレーションが自動的に1つのトランザクション内にラップされます。クエリによってはトランザクションの中では実行できないものがありますが、そのような場合は次のようにしてトランザクションを自動的にオフにできます。

class ChangeEnum < ActiveRecord::Migration[5.0]
  disable_ddl_transaction!

  def up
    execute "ALTER TYPE model_size ADD VALUE 'new_value'"
  end
end

ただし、self.disable_ddl_transaction!を使うマイグレーションの内部であっても独自のトランザクションをオープンできることを忘れてはいけません。

関連記事

Rails API: `ActiveRecord::FixtureSet`(翻訳)

週刊Railsウォッチ(20190924-1/2前編)Railsのconcernsを考える、Netflixのヘキサゴナルアーキテクチャ、Railsのアロケーション削減ほか

$
0
0

こんにちは、hachi8833です。RailsチュートリアルのRails 6対応翻訳をひとまず終えました🍵。お目見えはもう少し先になると思います。


  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

⚓週刊Railsウォッチ「公開つっつき会」第15回のお知らせ(無料)

第15回目公開つっつき会は、10月5日(木)19:30〜にBPS会議スペースにて開催されます。Railsウォッチのコンテンツにいち早く触れるチャンスです!皆さまのお気軽なご参加をお待ちしております🙇

⚓Rails: 先週の改修(Rails公式ニュースより)

6-0-stableブランチが少し静かになったようなので、6.xマイルストーンなども覗いてみました。


つっつきボイス:「お、6.xなんてマイルストーンがあるし」「どう使い分けてるのかな?🤔」「6.1.0にも乗らないissueが6.xに乗るのかも」「新機能は6.xに入れるとか?」「そのうちやるToDo的なものも6.xに入ってるっぽいです😆

⚓(6.0)render :textrender :nothingオプションは削除完了と5.1アップグレードガイドに追記

ビューでrender :textを使っている場合は動かなくなる。MIMEタイプをtext/plainにしてテキストをレンダリングする新しいメソッドとしては今後render :plainを使うこと。
同様にrender :nothingも削除されるので、ヘッダーのみを含むレスポンスを送信するには今後headメソッドを使うこと。たとえばhead :okとするとbodyなしでレスポンス200を送信する。
同PRより大意


つっつきボイス:「render :textってもう動かなくなってたか😳」「ついに消えた」「そのことを忘れて古い方を使っちゃったりしますし😅」「今後はheadを使ってくれだそうです」「render :nothingってヘッダーだけ返してたのね」「renderしてないのにrenderを使うよりはheadの方がいいかも🤣」「たしかに🤣

参考: render — ActionController::Renderer
参考: head — ActionDispatch::Integration::RequestHelpers

なお、つっつき後にRailsガイドにも反映しました(#890

⚓はみ出し

「ところでrender :plainって、毎回plainの綴りで一瞬迷うんですよね😆」「そうそう、plainだっけplaneだっけみたいな😆」「調べるの面倒だし、どうせデバッグだからってついrender :textにしてみちゃったり😆」「plainとplaneって発音違ってたかな…?🤔

発音は同じでした↓。

参考: Plain vs. Plane: How to Choose the Right Word

そういえば映画『マイ・フェア・レディ』で発音の特訓に使われた『スペインの雨は主に平野に降る』という早口言葉もありました。

参考: マイ・フェア・レディ (映画) - Wikipedia

⚓(master)マイグレーションでDatabaseConfigオブジェクトを直接使うようにした

コネクション設定については、あちこちで直接引き回されているHashではなく、DatabaseConfigをもっと積極的に使おうという方針に移行しつつある。
ここではDatabaseTasksdatabases.rakeでデータベース設定オブジェクトを使うように変えている。以下はメモ。

  • DatabaseTaskscharset_currentcollation_currentのpublicメソッドのテストがなかったのでそれも足してある。
  • schema_up_to_date?の引数のうち重複してて紛らわしい部分を非推奨化した。うち1つ(environment)は以前使われていなかったが、現在はspec_namedb_config (DatatabaseConfig)オブジェクトを直接取れてしまう。
    同PRより大意
# activerecord/lib/active_record/tasks/database_tasks.rb#L342
-     def schema_up_to_date?(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary")
+     def schema_up_to_date?(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = nil, spec_name = nil)
+       db_config = resolve_configuration(configuration)
+
+       if environment || spec_name
+         ActiveSupport::Deprecation.warn("`environment` and `spec_name` will be removed as parameters in 6.2.0, you may now pass an ActiveRecord::DatabaseConfigurations::DatabaseConfig as `configuration` instead.")
+       end
+
+       spec_name ||= db_config.spec_name
+
        file ||= dump_filename(spec_name, format)

        return true unless File.exist?(file)

-       ActiveRecord::Base.establish_connection(configuration)
+       ActiveRecord::Base.establish_connection(db_config)
        return false unless ActiveRecord::InternalMetadata.table_exists?
        ActiveRecord::InternalMetadata[:schema_sha1] == schema_sha1(file)
      end

つっつきボイス:「冗長な書き方を整理する感じでしょうか?」「なるほど、わざわざ変換しなくても元オブジェクトを渡せばいいんじゃね?って話か」「わざわざhashにしなくてもええと」「もしかするとテストのときはhashの方がやりやすかったみたいなことが当時あったのかもしれませんけど☺」「割と修正範囲広いな〜」「必須引数を増やすとついこうなりがちですよね😆

「ところで元のコードはどういう経緯でhashを使う設計にしたんだろう?🤔」「永続化したかったとか?🤔

⚓(master)ActiveRecord::DatabaseConfigurationsto_hto_legacy_hashを非推奨化

# activerecord/lib/active_record/database_configurations.rb#L81
    def to_h
-     configs = configurations.reverse.inject({}) do |memo, db_config|
-       memo.merge(db_config.to_legacy_hash)
+     configurations.inject({}) do |memo, db_config|
+       memo.merge(db_config.env_name => db_config.configuration_hash.stringify_keys)
      end
-
-     Hash[configs.to_a.reverse]
    end
+   deprecate to_h: "You can use `ActiveRecord::Base.configurations.configs_for(env_name: 'env', spec_name: 'primary').configuration_hash` to get the configuration hashes."

つっつきボイス:「なぬ、Active Recordのto_hto_legacy_hashを非推奨化?」「あ、タイトルのドラフトはしょりすぎでした😅」『ActiveRecord::DatabaseConfigurationsの』が正しい😎

to_legacy_hash↓ワラタ😆」「移行パスのためっぽい匂いを感じる🌷

# activerecord/lib/active_record/database_configurations/database_config.rb#L41
-     def to_legacy_hash
-       { env_name => configuration_hash.stringify_keys }
-     end

「そういえばARにto_hってありましたっけ?手元でuser.to_hとかするとそんなのねえって言われる😭」「あ、ハッシュはattributesで取れるんだったか😆

参考: attributes — ActiveRecord::AttributeMethods
参考: RailsでActiveRecordのデータをhashにする方法 - Qiita

⚓(master)6.0でincludesとjoinsを併用するとjoinしたクエリの順序が変わる問題を修正

# activerecord/lib/active_record/associations/join_dependency.rb#L73
+     def base_klass
+       join_root.base_klass
+     end
+
# activerecord/lib/active_record/relation/query_methods.rb#L1103
      def build_joins(manager, joins, aliases)
        buckets = Hash.new { |h, k| h[k] = [] }
        unless left_outer_joins_values.empty?
          left_joins = valid_association_list(left_outer_joins_values.flatten)
          buckets[:stashed_join] << construct_join_dependency(left_joins, Arel::Nodes::OuterJoin)
        end

+       if joins.last.is_a?(ActiveRecord::Associations::JoinDependency)
+         buckets[:stashed_join] << joins.pop if joins.last.base_klass == klass
+       end
+
        joins.map! do |join|
          if join.is_a?(String)
            table.create_string_join(Arel.sql(join.strip)) unless join.blank?
          else
            join
          end
        end.compact_blank!.uniq!
        while joins.first.is_a?(Arel::Nodes::Join)
          join_node = joins.shift
          if join_node.is_a?(Arel::Nodes::StringJoin) && !buckets[:stashed_join].empty?
            buckets[:join_node] << join_node
          else
            buckets[:leading_join] << join_node
          end
        end
        joins.each do |join|
          case join
          when Hash, Symbol, Array
            buckets[:association_join] << join
          when ActiveRecord::Associations::JoinDependency
            buckets[:stashed_join] << join
          when Arel::Nodes::Join
            buckets[:join_node] << join
          else
            raise "unknown class: %s" % join.class.name
          end
        end
        build_join_query(manager, buckets, Arel::Nodes::InnerJoin, aliases)
      end

つっつきボイス:「JOINの順序維持問題は@kamipoさんが以前も似たようなことを言及してたのを見たような気がしますね」「そういえば既視感あると思ったら、以前Arelの話題で取り上げた#36805↓が上と近い問題でした(ウォッチ20190819)」

参考: Preserve user supplied joins order as much as possible by kamipo · Pull Request #36805 · rails/rails

「@783cafeのコミットメッセージ↓でも#36805に言及してますね」「こういうのは見つけてつぶしていくしかなさそう🙏

リレーションにeager_loadとstringのjoinsだけがある場合、#36805によってstringのjoinsが最初のjoinsとみなされ、従来と挙動が変わる。
joinの順序を従来どおりにするために、stringのjoinsより前にjoinのeager loadingを先にチェックする。
@783cafeより大意

「お〜、こういうふうにjoinsに生SQLを書く形でテストするのね↓」

# activerecord/test/cases/associations/inner_join_association_test.rb#L82
+ def test_eager_load_with_string_joins
+   string_join = <<~SQL
+     LEFT JOIN people agents_people ON agents_people.primary_contact_id = agents_people_2.id AND agents_people.id > agents_people_2.id
+   SQL
+
+   assert_equal 3, Person.eager_load(:agents).joins(string_join).count
+  end

参考: joins — ActiveRecord::QueryMethods

⚓(master)amatsudaさんのアロケーション削減

# activesupport/lib/active_support/parameter_filter.rb#L59
      def self.compile(filters, mask:)
        return lambda { |params| params.dup } if filters.empty?

-       strings, regexps, blocks = [], [], []
+       strings, regexps, blocks, deep_regexps, deep_strings = [], [], [], nil, nil

        filters.each do |item|
          case item
          when Proc
            blocks << item
          when Regexp
-           regexps << item
+           if item.to_s.include?("\\.")
+             (deep_regexps ||= []) << item
+           else
+             regexps << item
+           end
          else
-           strings << Regexp.escape(item.to_s)
+           s = Regexp.escape(item.to_s)
+           if s.include?("\\.")
+             (deep_strings ||= []) << s
+           else
+             strings << s
+           end
          end
        end

つっつきボイス:「お〜、この地道な修正はまさにこの間の銀座Rails#13でamatsudaさんが話してたトピックのひとつですよ😋」「😀」「このときのスライド見たいんですけどね…」「探したけど見つかりませんでした🥺

「ベンチマークをがっと出してメモリ消費量をリストアップして、『ここが多そう』と当たりをつけては修正してはまたベンチ回すという感じでメモリアロケーションを減らしているそうです💪」「すげぇ〜!」「@5c07e1aのコミットメッセージにつぶやきが↓」「苦労が偲ばれる🙇


@5c07e1aより

「@2db4c02の修正↓を見ると、mapよりeachの方がアロケーションが少ないということ?」「修正前はmapのブロックの中でまたmapを回してたけど、修正後はどちらもeachに変えてますね」「つまりメモリアロケーション的にはmapよりeachの方が有利ということか😳」「おぉ〜」「あくまでフレームワークの最適化としてですけどね☺

# actionpack/lib/action_dispatch/journey/gtg/transition_table.rb#L44
        def move(t, a)
          return [] if t.empty?

          regexps = []
+         strings = []

-         t.map { |s|
+         t.each { |s|
            if states = @regexp_states[s]
-             regexps.concat states.map { |re, v| re.match?(a) ? v : nil }
+             states.each { |re, v| regexps << v if re.match?(a) && !v.nil? }
            end

            if states = @string_states[s]
-             states[a]
+             strings << states[a] unless states[a].nil?
            end
-         }.compact.concat regexps
+         }
+         strings.concat regexps
        end

mapeachだとアロケーション周りの内部実装が違うのかな?」「mapの挙動からしてメモリコピーはやってるでしょうね: ビックリ付きのmap!だと破壊的なので違いますけど」「そうそう☺」「APIドキュメント↓によるとeachの戻り値はレシーバーだから確かにアロケーションは起きませんね」「mapは配列を返すから回すたびにアロケーションすると」

参考: map — Class: Array (Ruby 2.6.4)
参考: each — Class: Array (Ruby 2.6.4)

「お、上の修正前コードの外側のmapは生成した結果をわざわざ捨ててるし」「結果のarray使ってなかったのね😳」「eachでやれるところだったのにもったいない」「これは残念なパターン😆」「自分はmap大好きマンですけど、そのせいかたまにこんな感じでmapをうっかり繰り返しのためだけに使っちゃったりしますね😅

「そういえば以前この↓記事でeachより先にmap使おうって言ってましたね😆」「メモリアロケーションみたいな最適化は後回しにしたい派だしmap好きなの❤」「そういえば『最初から最適化するな』って普段からおっしゃってますよね☺」「自分はeachで書いてみる派かな〜😆」「この辺は人による😆

Ruby: `each`よりも`map`などのコレクションを積極的に使おう(社内勉強会)

「あと、mapcollectで書く方が集めるというイメージに合ってて好きですね😋

⚓Rails

⚓スライド: Netflixの「Surrounded by Microservices」


つっつきボイス:「これは今年6月にNetflixが発表したRails設計話で、スライドないかと思ったらありました😂

「やさいちさんのツイートのまとめがわかりやすいです↑」「ヘキサゴナルアーキテクチャ↓ですか!」「Netflixといえばカオスエンジニアリング💪」「強い」「Hanamiの話も出てきてる🌸

参考: Netflix 驚異的なトラブル対応 カオスエンジニアリングとは、何か?

「今Netflixのスライドを見てみると、割と写真中心ですね😆」「おしゃべりの背景用かな😆」「動画の方がメインかな😅

動画はよく編集されていて字幕も見やすくなっています😋

「ツイートに『優れたアーキテクチャの目的は意思決定を遅らせることである』とありますね」「気持ちわかる」「わかる😂」「YAGNIはやめとけと」

YAGNIを実践する(翻訳)

「EuRuKoってEuropean Ruby conferenceなのね」「conferenceなのにKo?」「ヨーロッパだとKで書く感じの地域が割とありますね(ドイツとかチェコとかロシアとか)」

そういえば、ヨーロッパ系の人はcompanyを「コンパニー」みたいに発音する傾向がどことなくある印象です。

EuRuKoのまとめ記事: EuRuKo 2019: A recap

既に来年のeuruko2020.orgのドメインもできてますね😋

⚓スライド『Concernsに関する懸念』でconcernsを考える


つっつきボイス:「ruby-jp Slackで見かけたスライドです」「@willnetさん!」

「concernsというと、だいたいよくない使われ方の方をよく見かけますね😆」「そうそう😆」「『関心事』とは果たして何ぞやと😆」「concernsはピュアなRubyのモジュールという印象」「実際そうですし」

「『初心者が取り扱うのは難しい』、ほんとそう!」「やめといた方がいい😆」「適切なconcernsってそうそう作れませんし、基本的にはconcernsってほぼ使わないな〜☺」「自分は割とconcerns好きなのでちょいちょい使ってますけど😆

「concernsをRailsで使う意味がどこまであるかですよね: concernsでメソッド生やしてもいいけど、includeしたときにconcernsで生やしたものが全部ちゃんと動くように設計できるのか?って思ったりしますし」「まあそれはありますね」「ちゃんと設計しておかないと、こっちのモデルにはconcerns生やすと動くけど、あっちのモデルでconcernsすると動かないとか、そういうものがきっと残る😇

「それで言うとRubyのEnumerableはeachを定義してincludeすれば必ず動くからキレイですよね✨」「たしかに!」「concernsもそんなふうに『このconcernsをincludeしていい条件はこれとこれだよ』というのが明らかになってないと気持ち悪い😓

参考: module Enumerable (Ruby 2.6.0)

「あと、ソースコードの行数を減らすためだけにconcernsを使っている残念なパターンも見かけますよね😆」「おぉ、ちょうどスライドにもその事例が↓😆」「Postsでしか使ってないモジュール😇」「これはマジでやめて欲しい😤」「それ普通にPostsに書けばええやんって思いますし」「ほんに」「でもやりたくなっちゃうとき、あるんですよね〜😅

「こういう形でconcernsを使われると困るのは、ファイルが分かれてしまうからどこでincludeされているかまで考えないといけなくなることがまず1つ」「うんうん」「2つ目は、こういうPostでしか使わないつもりのコードがconcernsになっていると、それを他の人が見て『お、concernsに入っているこれ使えるじゃん❤』って勝手に他で入れようとする事案が発生すること😇

「スライドにもあるけど、concerningで名前空間を分けるだけならわかる↓」「concerningってのがあるんですね😳」「concerningも一応同じように書けます😎

参考: concerning — Module::Concerning
参考: Rails 4.1.0 で追加された Module#concerning と関心事の分離 | TECHSCORE BLOG

「そうそう、rubocopのClassLengthで怒られないためにconcerns使うのヤメレと↓😆」「『行数多すぎ』ってヤツですね」

参考: Metrics/ClassLength — Metrics Cops - RuboCop: The Ruby Linter that Serves and Protects

「とはいうものの、concernsだと一見それっぽくできちゃったりするんですよね〜😅」「RubyMineの右クリックリファクタリング的な感じの単なるコピペになりがち😆」「たしかに意味変わらないんだけどそれをconcernsでやらないで〜って😭

「スライドに引用されているDHHのコードのconcernsの量がヤバい↓🤣」「🤣」「しかも# Depends on Readableとかがずらっと並んでるあたりconcerns地獄感ある😈


同ツイートより

「concernsによほどいい名前が付けられていれば別だけど、この書き方は好きじゃないな〜😅」「CopyableとかReadableとかって、ライブラリの機能ならともかく業務コードでその機能使う?って気持ちになりますし😆」「でもPrintableだったらありかな、というかこれは他でも使われてるちゃんとしたconcernsという気がする」「そんな感じしますね」

「あとconcernsは読み込み順序も絡んでくるのがマジ面倒」「あ〜前後関係ありますね!」「スライドにもあるけど、concernsは最終的に1つのクラスに集約されちゃうから大変😭」「😆」「methodsしてgrepするとヤバいことになってる😇」「メソッド名がかぶると死にますし😇」「名前空間を分ける、はホントに大事」

「DHHはこれをいいと言うけど、結局ここに行き着くという↓🤣」「わかる〜🤣」「バンドが解散する理由ってたいていこれですよね🤣

参考: 音楽性の違い - アンサイクロペディア

「フックをconcernsにする↓のもたまに見かけるし: コントローラのconcernsにbeforeなんちゃらを書いてincludeするヤツ、自分はあんまり読みやすいと思わない😆」「これがよほど特殊な計算で複数の場所で使うならconcernsもワンチャンありかなという気がしますけど、この例だと単にTime.zone.nowしてるだけだからconcernsにする意味がまったくないな〜って思いますし😆

「コントローラの認証のコードをconcernsにしてincludeするとかなら、わかる」「それならわかりますね☺」「ネストしたURLの末尾idチェックをconcernsにしてbefore_actionで動かすとかもやりますね」「concernsの中でのbefore_actionってリダイレクトしたときどうなるんだっけ?」「あ、before_actionはレンダリングするとリダイレクトしないでそこで終わっちゃうので、むしろきれい😆」「それはそれで、しれっと書かれたincludeにはじかれるという悲しいことが発生するという😆」「そう、だからやりすぎ注意😅

「これ↑もね、とてもよくわかる😆」「わかるわかる〜!😆」「そういえば例の肥大化したActiveRecordモデル記事↓でも言ってましたね」

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

「@willnetさんのスライドとてもためになる❤」「つらいconcernsをたくさん見てくるとわかってくる😭

「concernsディレクトリってデフォルトで作らなくてもいいのに、という気もするけど、たぶんここはDHHの思想なんだろうな…」「あれは要らないっすね〜😅」「悪い使い方を誘発しがちというか」「ゴミ置き場になりやすい」

「modelsディレクトリの直下のconcernsじゃなくて、そこからもうひとつ名前空間を掘ったところに置くconcernsなら、あっていいと思う」「そうそう!」「それなら『このconcernsはこの下でしか呼ばれない』ということが伝わるから😋」「それなら関心が分離されてる感ありますね」「逆にcontrollersの直下のconcernsに20個ぐらいあったりすると、その治安の維持は相当難しいと思うし😇」「そのconcernsはシステム全体を横断する関心事なのかと問い詰めたくなりますし😆

後でDHHがRails 4でconcernsを導入したときの記事を見つけました↓。

参考: Put chubby models on a diet with concerns – Signal v. Noise

⚓テストを不安定にさせる要因


つっつきボイス:「こちらもruby-jpで見かけました」

「CIはたまにこうやってコケる😆」「たまにコケるのやめて欲しい😭」「たまにコケたらrandom seedを固定して何回か回して…と😆」「時刻絡みでコケるとか多いっすね😆」「コケたらとりあえずもう1回回してみると😆

「必ずORDER BYを付けよう、とか」「あ〜それたまに忘れちゃう😅」「この辺ってDBMSによっても違うことがあって、MySQLだと(ORDER BYがなくても)経験的には入れた順で毎回同じ結果が返ってきたりするんですけど、ぽすぐれはそうとは限らないという」「ぽすぐれのご機嫌次第😆」「でもSQL的には本来ぽすぐれの方が正しい」

参考: MySQLでORDER BYをつけないときの並び順 - かみぽわーる — InnoDBかMyISAMかで違ってくるそうです

⚓GitHubがRails 6へのアップグレードを完了


つっつきボイス:「これもバズりましたね😋」「Publickeyに先越されましたorz」「わずか9日とあるけど、その前からやってたでしょきっと😆」「そのためにGitHubは2年半かけて独自forkを解消してましたね(ウォッチ20190603)↓」

⚓その他Rails


techplay.jpより

つっつきボイス:「ちょうど定員越えてる」「よさげなイベントですが、気づくのが遅かった😭」「マルチDBの話もやるのね」「でかいシステム扱ってる人はたいていマルチDBやりますね☺


以下はつっつき後に見つけました。


yasslab.jpより


前編は以上です。

バックナンバー(2019年度第3四半期)

週刊Railsウォッチ(20190918-2/2後編)RubyPrize 2019候補者発表、GoogleがTypeScript 3.5に熱烈フィードバック、日本語形態素分析kagomeほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSなど)です。

Rails公式ニュース

Publickey

publickey_banner_captured

RailsのI18nの書き方を調べたのでまとめました

$
0
0

概要

I18nとは

I18n (国際化・多言語化を意味する internationalization を短縮したもの)


Ruby on Rails 4アプリケーションプログラミング(山田祥寛、技術評論社)、p502より

こちらの図の通り、

  1. クライアントからページ要求をサーバが受け取る
  2. コントローラで使用するロケール(言語)を決定する
  3. ロケールに合致する辞書ファイルを選択する
  4. ERBテンプレートに辞書ファイルの内容を反映した結果をクライアントに返す

という順番の処理になります。

パブリックI18n API

translate
訳文を参照します
localize
DateオブジェクトやTimeオブジェクトを現地のフォーマットに変換します

上のメソッドにはそれぞれ#t#lという別名メソッドがあります。
本記事ではtranslateメソッドのみ紹介します。

使い方

通常

辞書ファイルの定義方法

辞書ファイルはconfig/localesフォルダ配下には YAML 形式、 もしくは Ruby スクリプトで記述します。
通常はYAML形式です。
一番上の階層で言語の種類を指定します。
その配下で様々な単語の組み合わせをYAMLの階層構造で記述します。

config/locales/ja.yml

ja:
  hello: こんにちは
  greeting: こんにちは、%{name} さん
  helpers:
    submit:
      create: 登録する
      submit: 保存する
      update: 更新する

config/locales/en.yml

en:
  hello: Hello!
  greeting: Hello, %{name}!
  helpers:
    submit: 
      create: Create
      submit: Save
      update: Update

参照方法

irb> I18n.t(:hello) 
=> "こんにちは"
irb> I18n.t('helpers.submitcreate') 
=> "登録する"

式展開での参照方法

irb> I18n.t( :greeting, name: '太郎')
=> "こんにちは、太郎"

ActiveRecord

config/locales/ja.yml

ja:
  activerecord:
    models:
      administrator: 管理者
    attributes:
      administrator:
        email: メールアドレス

models:の定義をmodelからmodel_name.humanで参照します。
attributes:の定義をmodelからhuman_attribute_name(:attr)で参照します。

irb> Adnimistrator.model_name.human
=> "管理者"
irb> Administrator.human_attribute_name(:email)
=> "メールアドレス"

translateメソッドでフルパスを書くことで参照することもできます。

irb> I18n.t('activerecord.models.administrator')
=> "管理者"

参考文献

週刊Railsウォッチ(20190930前編)知られざる7つの便利gem、Duration.buildにstringを渡せなくなる、Webpackerのpacksをマスターほか

$
0
0

こんにちは、hachi8833です。Google Translator Toolkitが12月4日にディスコンになるそうなので忘れないうちにファイルをGTTからダウンロードしておきました。


つっつきボイス:「終わっちゃうんだ😳」「GTTは自分も最近めっきり使ってませんでしたが、突然の終了宣言で😇」「見た感じユーザー数は少なそうではありますけど😆」「ローカライズに関心のある人ぐらいしかいないと思いますので、きっとそうです😆」「マイグレーションパスとかもないのかしら?」「終了までの間にデータを逃がすことはできるみたいですけど、マイグレーションパス的なのはない感じですね: たぶん今後はGoogle内部でしか使わないのかも🤔」「無料とはいえ他に類似の無料ツールがあんまりないから、これに頼ってた人は泣くしかなさそう: 他の商用ツールを使ってくれという感じですかね」「Googleのハシゴ外し伝説がまた1つ増えました😭」

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

週刊Railsウォッチ「公開つっつき会」第15回のお知らせ(無料)

第15回目公開つっつき会は、今週10月5日(木)19:30〜にBPS会議スペースにて開催されます。週刊Railsウォッチの記事にいち早く触れられるチャンスです!皆さまのお気軽なご参加をお待ちしております🙇。

Rails: 先週の改修(Rails公式ニュースより)

今回は公式の更新情報があったのでやりやすくて助かりました😋。

キャッシュストアから複数のキーを削除するActiveSupport::Cache::Store#delete_multiメソッドを追加

キャッシュからキーのarrayを削除するActiveSupport::Cache::Store#delete_multiを追加。
ほとんどの実装ではActiveSupport::Cache::Store#deleteを単にn回読んでいるが、Redisはキーのarray削除をサポートしている。従来のdeleteをn回呼び出していたのを1回の呼び出しにできるので、パフォーマンス向上が期待できる。
なおMemcachedはマルチdeleteをサポートしていない
同PRより大意

# activesupport/lib/active_support/cache.rb#L480 
+     def delete_multi(names, options = nil)
+       options = merged_options(options)
+       names.map! { |key| normalize_key(key, options) }
+
+       instrument :delete_multi, names do
+         delete_multi_entries(names, options)
+       end
+     end
...
+       def delete_multi_entries(entries, options)
+         entries.inject(0) do |sum, key|
+           if delete_entry(key, options)
+             sum + 1
+           else
+             sum
+           end
+         end
+       end

つっつきボイス:「キーのarrayを削除する機能が追加された❤️」「キャッシュエンジンがキーのarrayを一発削除する機能をサポートしているなら、それを使うほうが絶対速いですね👍」「イミュータブルだろうし」「Redisだと効くようになるとPRメッセージにありますね😋」「なるほど!」「大量の削除を大量のAPI呼び出し発行でやるのはしんどいですから😆」

Action Mailerにemail_address_with_nameを追加

現状のガイドではmail(to: %("#{@user.name}" <#{@user.email}>))のように自力でフォーマットするようアドバイスしている。
残念ながらnameがAaron "Tenderlove" Pattersonとか-_-" <3の場合は破綻する。さらに困るのは、メールは送信完了したと暗黙のうちに思わせてしまうこと。実際にはこれを理解できないメールサーバーに飲み込まれてしまう。
今回の変更で、email_address_with_nameメソッドでmail(to: email_address_with_name(@user.email, @user.name))のようにフォーマットを扱えるようになる。これは内容を正しくエスケープできるようにする手法のひとつとなる。
同PRより大意

# actionmailer/lib/action_mailer/base.rb#L593
+     def email_address_with_name(address, name)
+       Mail::Address.new.tap do |builder|
+         builder.address = address
+         builder.display_name = name
+       end.to_s
+     end
...
+   def email_address_with_name(address, name)
+     self.class.email_address_with_name(address, name)
+   end

つっつきボイス:「ここで行っているnameのエスケープというのが何が目的なのかわからなくて😅」「あ〜なるほど!: これはですね、メールのto:ヘッダーとかでは、以下みたいにdisplay nameの後ろに< >で囲んでメールアドレスを示すなどの仕様がメール関連のRFCに記載されているんですよ😎」「おぉ〜!」「MTAはこういう仕様を頼りにヘッダーから送信先メールアドレスを取り出します☺️」

mail(to: %("#{@user.name}" <#{@user.email}>))

「そして今のRailsではそれをメソッドとして実装できないので、RFCの仕様を知っている人が自力で実装することになっているんですけど、やっぱりこういうふうにメアドと表示名をエスケープしてくれるヘルパーメソッド↓があった方がいいよねということで今回足したんでしょうね☺️」「はぁ〜なるほど!」「表示名に<>"があったらエスケープするとかはどのみちやらないといけないので」

mail(to: email_address_with_name(@user.email, @user.name))

参考: 3.4 RFC2822(Internet Message Format)
参考: 3.4 RFC 2822 - Internet Message Format — 原文

「RFC 2822のこの辺の仕様ですね↓」「そういうことでしたか😳」「いにしえの昔からこの仕様です😎」


tools.ietf.orgよりRFC2822の3.4

「上のname-addrは[display-name] angle-addrだからdisplay-nameはオプション、そしてdisplay-name = phrase、phraseはこのRFCを探すと3.2.6phrase = 1*word / obs-phrase、そしてobs-phraseは4.1obs-phrase = word *(word / "." / CFWS)だからダブルクォートで囲む…という仕様」「こういう感じで仕様を辿るんですね😋」

「というような仕様を満たすためのemail_address_with_nameメソッドがRailsに足されたという話」「このemail_address_with_nameという名前がちょい長い気はしますけど😆、いいんじゃないでしょうか」「😆」

ActiveSupport::Duration.build()がNumericのみを受け取るように変更

今回の変更は、型が::Numericでない場合にActiveSupport::Duration.build(value)によってActiveSupport::Durationインスタンスが作成されることを防ぐ(でないとTypeErrorになる)。
この修正によって、#37012のさまざまな振る舞い(ActiveSupport::Durationが原因不明で失敗する、文字列からビルドしたdurationの比較結果がおかしくなる)が修正される。
同PRより大意


つっつきボイス:「Durationbuildがあるとは😳」「しかもstring入れられる!ひゃ〜😅」「数字を入れると秒として認識されると」「従来はsmall_duration_from_int < large_duration_from_stringという比較↓でエラーになる😇」「durationをintから作ったかstringで作ったかで違ってくると」「よく見つけたな〜😳」「知らずに踏みそうなバグ」「どうやってビルドしたかとか普通関知しないですし😆」「to_iしたらむしろそのせいで壊れたりしそう😆」

# Changelogより
# 修正前:
small_duration_from_string = ActiveSupport::Duration.build('9')
large_duration_from_string = ActiveSupport::Duration.build('100000000000000')
small_duration_from_int = ActiveSupport::Duration.build(9)

large_duration_from_string > small_duration_from_string
#=> false

small_duration_from_string == small_duration_from_int
#=> false

small_duration_from_int < large_duration_from_string
#=> ArgumentError (comparison of ActiveSupport::Duration::Scalar
#                    with ActiveSupport::Duration failed)

large_duration_from_string > small_duration_from_int
#=> ArgumentError (comparison of String with ActiveSupport::Duration failed)

# 修正後

small_duration_from_string = ActiveSupport::Duration.build('9')
#=> TypeError (can't build an ActiveSupport::Duration from a String)

「今回の修正を見ると、Numericしかビルドしないことにしたのか↓」「まあ文字列使わなければいいんでしょうけど」「でもこれはDurationを文字列でビルドしてた人にとってはbreaking changesになるから対応が必要になるでしょうね⛔️」「そうか!」「API側で文字列をto_iしてもいいような気もするけど、文字列のto_iって割と事故りやすいんですよね😅」「たしかに〜😆」「この#37013はもうマージされたんだなあ」

# activesupport/lib/active_support/duration.rb#L183
      def build(value)
+       unless value.is_a?(::Numeric)
+         raise TypeError, "can't build an #{self.name} from a #{value.class.name}"
+       end
+
        parts = {}
        remainder = value.to_f

        PARTS.each do |part|
          unless part == :seconds
            part_in_seconds = PARTS_IN_SECONDS[part]
            parts[part] = remainder.div(part_in_seconds)
            remainder = (remainder % part_in_seconds).round(9)
          end
        end
        parts[:seconds] = remainder
        new(value, parts)
      end

[Rails5] ActiveSupport::Durationの期間処理メソッド(1)演算、比較など

buildにはそもそもstring渡すなという話なんだろうな🤔」「buildがドキュメントでどう扱われているかを参照すべきでしょうね↓」

「以下のサンプルコード↓は載っているけど、yardのドキュメントに書かれてるわけではなかった😅」「つまりstringを渡せるとは書かれていないけど、渡しても動いてた😆」「内部でto_iしてくれてるよねきっと、みたいな気持ちになるのは、わかる気はする☺️」

# APIより
ActiveSupport::Duration.build(31556952).parts # => {:years=>1}
ActiveSupport::Duration.build(2716146).parts  # => {:months=>1, :days=>1}

watcherに渡すファイルやディレクトリの扱いを修正

現在のwatcherに渡されるautoloadパスがディレクトリになっている。evented watcherを使うと、これがListenにそのまま渡される可能性がある。しかしautoloadパスにはファイルが含まれているので、ファイルが渡されるとListenでエラーが発生する。このため、ファイルとディレクトリを正しく仕分ける必要がある。
#37011が修正される。
同PRより大意

# railties/lib/rails/application.rb#L350
    def watchable_args #:nodoc:
      files, dirs = config.watchable_files.dup, config.watchable_dirs.dup

      ActiveSupport::Dependencies.autoload_paths.each do |path|
-       dirs[path.to_s] = [:rb]
+       File.file?(path) ? files << path.to_s : dirs[path.to_s] = [:rb]
      end

      [files, dirs]
    end

つっつきボイス:「修正はわずかですけど、watcherっていうのがRailsにあるとは知りませんでした😅」「これだけ見ると、いわゆるファイル変更を監視するwatcherなんじゃないかな?🤔」「Railsのdevelopmentモードでよくやる、ファイルが更新されたらリロードするみたいな?」「gemでそういうのありましたけど、名前が出てこない😅」

guard gemでした↓。

「この修正ではevented watcherと言ってるので変更イベントの通知用でしょうね」「以下の記事に『ファイルの変更を検知しているのはapp/config/environments/development.rbfile_watcherの部分』とあるから、これのことか」「FileUpdateCheckerもあればEventedFileUpdateCheckerもあると😆」「ポーリングもやれるということでしょうね☺️」

参考: dockerでrails5環境構築 - Qiita
参考: Rails アプリケーションを設定する - Rails ガイド

config.file_watcher: config.reload_classes_only_on_changeがtrueの場合にファイルシステム上のファイル更新検出に使われるクラスを指定します。デフォルトのRailsではActiveSupport::FileUpdateChecker、およびActiveSupport::EventedFileUpdateChecker(これはlistenに依存します)が指定されます。カスタムクラスはこのActiveSupport::FileUpdateChecker APIに従わなければなりません。
Railsガイドより

ファイル更新通知について

「ファイル更新のトリガー通知って、内部的にはD-Bus使ったり、LinuxだとinotifyとかMacだとFile System Eventsみたいにいろいろあるんですけど、POSIXに仕様がないんですよ」「あ〜」「だからこういう機能は基本的にOS依存になります」「知らなかった〜😅」「OSによってやり方を変えるしかないんですね😅」

参考: D-Bus: DBusWatch
参考: Man page of INOTIFY
参考: macos - Is there a command like “watch” or “inotifywait” on the Mac? - Stack Overflow

「お、fswatch↓はクロスプラットフォームらしい?😳」「リポジトリの冒頭を見ると、macOSではFile System Events、BSD系だとkqueue、Linuxだとinotify、Windowsだとstat()ベースのバックエンドってあるから、内部で使い分けてますね😆」「へぇ〜!」「だからバックエンドのファイル更新イベントはやっぱりOS依存のAPIを使ってる😆」

レスポンスのVary: Acceptを再設定しないよう修正

#36213で、レスポンスにAcceptヘッダーを使うVary: Acceptヘッダーが追加された。このため、アプリでVaryヘッダーにAccept以外の値を設定した場合に問題が起きる。コントローラのアクションでこのヘッダーにどんな値を設定しても、_set_vary_headerメソッドが値を"Accept"に設定する。
このコミットによって、ヘッダーに"Accept"を設定する前に、ヘッダーに既に値が設定済みかどうかをチェックするようになる。このヘッダーが空欄の場合はヘッダーを設定する。このヘッダーに既に値がある場合、アプリケーションがこのヘッダーを扱っているという前提に立って_set_vary_headerは何もしない。
同PRより大意

# actionpack/lib/action_controller/metal/rendering.rb#L78
      def _set_vary_header
-       self.headers["Vary"] = "Accept" if request.should_apply_vary_header?
+       if self.headers["Vary"].blank? && request.should_apply_vary_header?
+         self.headers["Vary"] = "Accept"
+       end
      end

参考: Vary - HTTP | MDN


つっつきボイス:「Vary: Acceptヘッダーを二重設定してしまうケースがあったのを修正したと」

「ちょっと前にVaryヘッダーが追加されたときも話題にしましたね(ウォッチ20190805)」「う、それの続きのようです😅」「たしかキャッシュするかどうかの基準をどのヘッダーにするかをブラウザに通知する的なのがVaryヘッダーでしたね😆」「HTTPのヘッダーっていろいろありすぎて覚えきれない😅」「見慣れないヘッダーだったので何となく覚えてました☺️」

Varyヘッダーは、クライアント側というかリバースプロキシが使うヤツですね: リバースプロキシがどのヘッダーを見てキャッシュするかどうかを決めるとか」「おぉ〜」「たとえばVaryにUser Agentを指定すればUser Agentごとにキャッシュできるし、それに加えて別のヘッダーも指定すれば、その組み合わせによってキャッシュするしないを指定できると」「な〜るほど!😋」「でもCloudFrontではVaryヘッダーが効かないらしい、みたいな話を前回してた覚えがあります😎」

Rails

GitHub ActionsでRailsのCIを回す(RubyFlowより)


同記事より


つっつきボイス:「先週はmizchさんのGitHub Actions記事でしたが(ウォッチ20190925)、今週はこちらのやってみた記事を拾ってみました」「今なら普通に動きますしね☺️」「CircleCIから割とさくっと移行できるみたいですし☺️」

「Boring Railsってなかなかスゴい名前😆」「このサイトはどうやら最新記事以外は単行本に移しているようなので、次の記事が出たら見えなくなるかも😅」「なるほど😆」


boringrails.comより

Webpackerのpacksをマスターする

# 同記事より
app/javascript
├── admin
├── channels
├── login
└── packs
    ├── admin.js
    ├── application.js
    └── login.js

つっつきボイス:「1本目は少し前に取り上げた気がしますね😆」「あ、かぶってました(ウォッチ20190902)😅: 2本目がその続き的な記事です」「packファイルを太らせるな、とか書いてますね」

「うん、この記事はmanifest.jsonとかがどういうタイミングで参照されるとか、このファイルがダイジェスト付きになるとかが丁寧に説明されているっぽいのがいいですね👍」「お〜」「大事なのはjs/admin-67dd60bc5c69e9e06cc3.jsみたいなダイジェストを引き回してくれるところ」「webpack-dev-serverを動かせばこの辺の情報が出てくれるのね↓」「後でちょっと読んでおこう😋」「翻訳したいです😋」

# 同記事より
▶ ./bin/webpack-dev-server
ℹ 「wds」: Project is running at http://localhost:3035/
ℹ 「wds」: webpack output is served from /packs/
ℹ 「wds」: Content not from webpack is served from /Users/prathamesh/Projects/scratch/better_hn/public/packs
ℹ 「wds」: 404s will fallback to /index.html
ℹ 「wdm」: Hash: 5387bbdba96d7150c792
Version: webpack 4.39.2
Time: 2753ms
Built at: 09/24/2019 12:23:20 AM
                                     Asset       Size       Chunks             Chunk Names
          js/admin-67dd60bc5c69e9e06cc3.js    385 KiB        admin  [emitted]  admin
      js/admin-67dd60bc5c69e9e06cc3.js.map    434 KiB        admin  [emitted]  admin
    js/application-d351b587b51ad82444e4.js    505 KiB  application  [emitted]  application
js/application-d351b587b51ad82444e4.js.map    569 KiB  application  [emitted]  application
          js/login-1c7b2341998332589ec0.js    385 KiB        login  [emitted]  login
      js/login-1c7b2341998332589ec0.js.map    434 KiB        login  [emitted]  login
                             manifest.json  958 bytes               [emitted]

記事末尾のまとめ:

  • packファイルは最小限にし、必要なコードは他のファイルからインポートすること
  • app/javascript/packsにはpackファイル以外は置かないこと
  • それ以外についてはapp/javascriptの下は自由にやってよい
  • Webpackの出力でbundleのサイズに目を光らせておこう
  • packファイルは必要に応じて編成し、それらが提供する機能に依存するpackを管理する

Railsのerrors.added?errors.of_kind?


つっつきボイス:「ロケールに依存しない形でテストできるそうです」「お?added?っていうメソッドがあるのか😳」「こういうふうに、実際の生メッセージじゃなくてシンボルでメッセージを指定できるのね↓」「当然この方がいいですよね❤️」

# 同記事より
expect(john.errors.added?(:first_name, :too_short, count: 2)).to be_truthy

# または(predicateマッチャを使う場合)
expect(john.errors).to be_added(:first_name, :too_short, count: 2)

「マルチリンガルのメッセージを変えただけなのにバリデーションロジックのテストが落ちたら、それはテストがI18nから分離できていないということだからおかしいですしね」「そうそう😆」「本来はこういうふうにテストすべき」

「このメソッドを覚えてたら使うだろうけど、そのときに思い出せるかどうか😅」「ついしれっと日本語でテスト書いちゃいそうな気も😆」「自分で何度か使ってみたり、既に誰かが使っててくれたらいいんだけど」「あったら使いますし😋」

jnchitoさんの記事に、Rails 6のerrors.of_kind?ならcount: 2を書かなくていいと追記されていました。

Quoraより: Railsとworker killer


つっつきボイス:「サーバーの利用メモリがひたすら増加していくような挙動を観測したら?そりゃもうworker killerでしょう🤣」「🤣」「このQuoraで、Pumaにもworker killer↓があるって初めて知りました」「ありま〜す😆どれにもworker killerありま〜す」「Unicornでworker killer使うという話しか知りませんでした😅」「Passengerにも何リクエスト受けたら自滅するヤツとかありますよ」

「Quoraのリンク先の記事↓にもあるように、memory bloatだったりリークだったりいろいろ原因は考えられますけど、この辺はもうしょうがないんですよね🤣」「🤣」「Rubyに限りませんけど、動的にオブジェクトを生成するスクリプト言語だと、メモリにきれいに乗らないからエッジをまたぐみたいなことが起きたりしますし、GCしてもうまく寄ってくれないとかありますし」「ふむふむ」「それ以前にGCって重いからそうそうGCしませんし😆」

参考: What causes Ruby memory bloat? – Joyful Bikeshedding — (ウォッチ20190401でも取り上げました)


同記事より

「私のオレオレRailsアプリがworker killerなしでやっていけてるのは、単に作りがあまりに素朴だからなのかな?🤔」「そういうちっちゃなアプリならほぼ問題になりませんね☺️」「よかった😂」「問題になるのはメモリフラグメンテーションが大量に発生するようなケースですし、内部のメモリ効率が落ちるだけなので」「そういえば私のRailsアプリはモデル1個しかないし、一般ユーザーはモデルの読み出ししかやってませんし😅」「Active RecordやActive Modelがオブジェクトをぼこぼこ作ったり、スコープ外れて消えたり、ということが起きまくっていると、フラグメンテーションが起きやすくなります☺️」

「というわけでworker killerはみなさん使ってますので😆」「Rubyを使ってRailsのようにひたすら動的にメモリを確保するようなビッグアプリを動かしていれば、この辺は割とどうしようもありませんし☺️」

あまり知られてないけど便利なgem 7つ(Ruby Weeklyより)


つっつきボイス:「gemのリポジトリのリンクが記事にあまりなくて辿りにくいので、下にリストアップしてみます」

  1. traceroutes
  2. strong_migrations
  3. isolator
  4. test-profとruby-prof
  5. database_consistency
  6. attractor
  7. coverband

1. traceroutes

「amatsudaさんのgemだ!」「rake tracerouteで使われてないルーティングやアクションを見つけてくれる❤️」「お?記事はこれがRails 6で動かないからプルリクを送ったと書いてる↓」「でもtracerouteのREADMEにはRails 6対応って書いてあるけど?😆」「入れ違いかな?🤔」

2. strong_migrations

「strong_migrations、これは前にもウォッチに出てきたような↓🤔」「だいぶ前だったかな」

週刊Railsウォッチ(20170915)Ruby 2.4.2リリースで脆弱性修正、strong_migrations gemでマイグレーションチェック、書籍『Mastering PostgreSQL』ほか

「gemのREADMEに、マイグレーションでこういう潜在的に危険な操作↓を行ったときに警告してくれるとある」「これがデンジャラスオペレーションですよ😆」

  • カラムの削除
  • カラムにデフォルト値を追加する
  • 後からデータを入れる
  • インデックスをnon-concurrentに追加する
  • referenceを追加する
  • 外部キーを追加する
  • カラムの型を変更する
  • カラムをリネームする
  • テーブルをリネームする
  • forceオプションを付けてテーブルを作成する
  • change_column_nullでデフォルト値を使う
  • jsonカラムを追加する

「正しいマイグレーションのやり方もREADMEに書かれているし」「へ〜、ignored_columns↓って知らなかった!」「先にignored_columnで入れておけば、今動いているRailsワーカーが誤って参照するのを防げるということか〜!」「なかったことにしてくれる😂」「すげ〜こんなのあるんだ😳」「でデプロイしてから最後にsafety_assuredで確定すると」

# 同リポジトリより
# Good
class User < ApplicationRecord
  self.ignored_columns = ["some_column"]
end
# 同リポジトリより
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[6.0]
  def change
    safety_assured { remove_column :users, :some_column }
  end
end

参考: ignored_columns — ActiveRecord::ModelSchema::ClassMethods

「こうやってデフォルト値を足すときも間違えがち↓」

# 同リポジトリより
# Bad
class AddSomeColumnToUsers < ActiveRecord::Migration[6.0]
  def change
    add_column :users, :some_column, :text, default: "default_value"
  end
end

「strong_migrations gemでこの手の危険をはらむマイグレーション操作を警告してくれるのはありがたいですね😋」「こういうのは気を付けててもやっちゃいますし😅」「わかってても踏む😆」「いいgem!」「おや、既に自分もこのgemに★付けてた😆」

3. isolator

「isolatorか」「non-atomicなDBトランザクションを検出してくれると」「この例は確かにnon-atomicだわ↓」

# 同リポジトリより
# HTTP calls within transaction
User.transaction do
  user = User.new(user_params)
  user.save!
  # HTTP API call
  PaymentsService.charge!(user)
end

#=> raises Isolator::HTTPError

# background job
User.transaction do
  user.update!(confirmed_at: Time.now)
  UserMailer.successful_confirmation(user).deliver_later
end

#=> raises Isolator::BackgroundJobError

「こういうのを自動検出してくれるならいいですね❤️」「代わりに怒ってくれると😆」「当てにしすぎるわけにもいかないと思うけど、初歩的なミスを拾ってくれるならいいと思います😋」

4. test-profとruby-prof

「どのテストが遅いかをこの2つで検出すると」「test-profは記事ありました↓」

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

5. database_consistency

bundle exec database_consistencyでデータベースの一貫性をチェックしてくれるそうです」「ああ、こういうgemを作りたくなることってよくありますね: このgemは、Railsのバリデーターをすり抜けたデータが入ってないかどうかをチェックしてくれる」「おぉ〜😍」

「こういう感じのコードってよく自作しますよ、不安だから😆」「やりますね〜😆」「eachで回してvalid?メソッドをひたすら呼ぶバッチとか😆」「まさにそういうのをやってくれると」

「生SQLでデータをいじる人が出始めると、だいたい整合性がおかしくなるんですよ😭」「バリデーションをスキップする悪しきupdate系メソッドが使われちゃってたとかね😇」

「このgemは多分単純なことしかしてないし、消費税対応とかで生SQLでデータをいじった後に使いたい感じですね」「ただバリデーションに誰かがcallbackを書いていたりすると副作用でこいつがデータを破壊して回りそうではある🤣」「🤣」

6. attractor


同リポジトリより

「attractor gemはコードからこういうメトリックスレポート↑を出してくれるようですね」「complexityとかあるし、静的に解析してくれるツールのひとつらしき」「グラフの斜めの線より上にあるコードはリファクタリング候補、とか☺️」「こういうツールはたくさんありますね☺️」

7. coverband

「coverbandは知ってる!」「production環境で使われてないコードを検出できる」「使ってないビューとかも検出できると↓」「ただし実行されてないというだけで判断してるので、年1でしか動かさないバッチも使われてないとみなされちゃうかも😆」「でも機械的にそうしたコードを検出できるのはいいですね😋」「coverbandはいい👍」


同記事より

「こういうので、Ruby 2.5以降あたりを使ってもっといい感じにやれるのが確かありましたね」「名前思い出せないけどそういえばありました😅」「たしかRubyKaigiでもこれ↓とか、後もうひとつつぐらいこのテーマで発表されてたと思います☺️」


「この記事のgemのチョイスはいいと思います!❤️」「忘れがちだけど、入れておくとコードの健康が保てる系のgemということで☺️」

Form Objectでモデルを複数使う(Ruby Weeklyより)


つっつきボイス:「Form Objectを使うときって、こうやってモデルが複数出てくるときが多いですよね😆」「自分はForm Objectはキライじゃない派」「自分もForm Objectは好き😍」「つまり2人とも好きなんですね😆」

Form Objectよもやま話

「Form Objectをどう位置づけるかですよね: 世の中にはService Objectでやりたい人たちもいて、もちろんそれもわかるんですけど、Form Objectなら1リクエストを受けるフォームとみなせるじゃないですか」「そうそうっ😋」

「もしかするとForm Objectという言葉が好きになれないとかあったりするのかも🤔」「その気持もわかりますね」

「Form Objectといえば、kazzさんも好きな、Active Modelだけ継承した素のクラスでやる方法を、最近誰かのスライドで『Application Object』だったか『Application Model』って呼んでたのを思い出しました: 一般的な呼び方かどうかよくわからないので、そのスライド限定の呼び方かもしれませんけど」「正しい呼び方は知らないけど😆、そういえばそんな話もしましたね」

「それで言うと、Form Objectと呼んだときは、あくまでユーザーのリクエストとかAPIへのPOSTリクエストを扱うもの、というニュアンスが出ますよね: 逆にApplication Objectと呼ぶと、内部APIからもアクセスされるもの、というニュアンスになる」「そうそうっ😋」

「その意味で、外部のインターフェイスに強依存するならForm Objectと呼ぶ方がしっくりくる」「うんうん」「逆にたとえばバッチの中で所定のハッシュを作って、内部からもそのAPIを呼ぶんだったら、Application Objectの方がしっくりきますね〜」「それはフォームでもあるんですけどね🤣」「そうそう🤣: でもForm Objectを内部APIとして叩くのはちょっと違和感があるので、そういうものだったらApplication Objectと呼ぶ方がより汎用的で使い回しが効く感じはありますね☺️」

「どちらに重きを置くかで変わってくるんですね」「Form Objectの方が限定的です☺️」「ビュー寄りですし☺️」


その後社内でRailsdm 2018 Day4のこのスライド↓を教えてもらいました。ありがとうございます🙇

その他Rails

つっつきボイス:「今からActive Recordを全部追おうとすると大変だろうな〜😅」

後で買っちゃいました😋。


つっつきボイス:「GitLabでTerraformも動かせるんですね」「まあ何でも動かせますし😆」「『ベースになるジョブをincludeで別リポジトリから読み込む』とか、GitLab CIハック感ありますね〜😋」「BPS社内でも参考になりそう!」「もうやってる人いるかも😆」



つっつきボイス:「igaigaさんの『Railsの教科書』、これはいい本ですよぉ〜❤️」「おぉ〜」「書いてあるとおりにやればできる本😋」「carrierwave使ってますね」

「そういえば、この間Railsチュートリアル↓をRails 6版に更新翻訳したんですけど、旧チュートリアルで使われてたcarrierwaveがRails 6のActive Storageに切り替わってました」「そりゃそうでしょうね😆」「Rails 6でrails newするのに今どきcarrierwave使うとしたら理由が必要☺️」


railstutorial.jpより


前編は以上です。

バックナンバー(2019年度第3四半期)

週刊Railsウォッチ(20190925-2/2後編)AWS Lambdaの秘密鍵保存法、Rubyコミット歴史の動画、Rubyコードの最適化と式展開ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Railsでenum_helpを使ってBoolean型のチェックボックスを作る

$
0
0

enum_helpというgemをRailsに追加して、Boolean型のチェックボックスを作る方法を紹介します。

Rails 4.1.0のActiveRecordではご存知の通りEnumメソッドがサポートされていますが、i18nやsimple_formではうまく動きません。
このgemは、Enumの機能をi18nやsimple_formでうまく動くようにします。
同リポジトリのREADMEより抜粋・大意

手順

1. Projectを作る

rails newします。

rails new enum_test
cd enum_test

2. scaffolding

rails generate scaffold user name:string age:integer sex:boolean

3. enum_helpのgemを追加

Gemfileに下記を追加します。

# 追加
gem 'enum_help'

bundle installします。

bundle install

4. モデルの修正

Userのモデルにenumの定義とバリデーションを追加します。

  • enumはtruefalseで作ります。0と1ではありません。
  • バリデーションはenumの文字列にします。Symbolだと動かないので文字列にします。

app/model/user.rb

class User < ApplicationRecord
  enum sex: { male: true, female: false }
  validates :sex, inclusion: {in: ["male", "female"]}
end

5. i18n辞書ファイルを編集

config/locales/en.yml

en:
  hello: "Hello world"
  enums:
    user:
      sex:
        male: man
        female: woman

6. ビューの修正

_form.html.erbのcheckboxの第3引数(チェックされたときの値)、第4引数(チェックされてないときの値)を先ほどの文字列で設定します。

app/views/users/_form.html.erb

    <%= form.check_box :sex, {}, "male", "female" %>

こうすると下図の通りinputのvalueは”male”になっているはずです。

DBには0または1が保存されます。

<%= form_with(model: user, local: true) do |form| %>
  <% if user.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul>
      <% user.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div class="field">
    <%= form.label :age %>
    <%= form.number_field :age %>
  </div>

  <div class="field">
    <%= form.label :sex %>
    <%= form.check_box :sex, {}, "male", "female" %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

参照方法はi18n対応の参照方法に変更します。
<%= @user.sex %><%= @user.sex_i18n %>に変更します。

app/views/users/show.html.erb

<p id="notice"><%= notice %></p>

<p>
  <strong>Name:</strong>
  <%= @user.name %>
</p>

<p>
  <strong>Age:</strong>
  <%= @user.age %>
</p>

<p>
  <strong>Sex:</strong>
  <%= @user.sex_i18n %>
</p>

<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>

Userの参照ページに行くと下記の通りmanまたはwomanが表示されているはずです。
こうなれば完成です。

一覧(index.html.erb)は修正していないのでmale、femaleが表示されたままだと思います。
適宜修正頂ければと思います。

以上、ご参考になれば幸いです。

関連記事

RailsのI18nの書き方を調べたのでまとめました

Rails 5.1〜: ‘form_with’ APIドキュメント完全翻訳

Railsのリクエストのライフサイクルを理解する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。画像は元記事の引用です。

参考

以下のサイトで、Railsのリクエストの全ライフサイクルをビジュアル表示で追うことができます。本記事と合わせて参照することで理解が進むと思います。

rails-trace.chriszetter.comより

Railsのリクエストのライフサイクルを理解する(翻訳)

本記事は、私達がRailsConf 2019で行ったスピーチのまとめです。スライドはこちらでご覧いただけます。

Example Rails controller code

エディタでコントローラのファイルを開き、アクションメソッドにRubyコードを少々書いて、ブラウザでURLを叩けば、書いたコードは即動き出します。ほとんどのRails開発者は、こうしたワークフローについてそこそこ慣れ親しんでいるはずです。しかし、このしくみを深く考えてみたことはありますか?ブラウザのアドレスバーにURLを入力してからコントローラのメソッドが呼び出されるまでのしくみを説明できますか?そのメソッドを実際に呼び出しているのがどこかご存知でしょうか?

インターネッツの旅路

'Let's meet for lunch' text conversion

ランチに誰かを誘おうとしている状況を考えてみましょう。「Pastiniでどう?」というメッセージは同僚に送信するには十分ですが、その地域に不案内な知人には少々不親切です。普通なら図のようにレストランの住所も送ってあげるべきでしょう。これでタクシーに行き先を伝えるなりマップで調べるなりできるようになります。

コンピュータでもこれと似たようなことが言えます。ブラウザにドメイン名だけを入力した場合、ブラウザの最初の仕事はサーバーに接続することです。「skylight.io」のようなドメイン名は人間には覚えやすくても、コンピュータはそれだけではサーバーを見つけられません。サーバーへの到達方法を知るには、そうしたドメイン名をコンピュータネットワークのアドレス、すなわちIPアドレス(Internet Protocolアドレスの略です、念のため)に変換する必要があります。

うすうすお気づきかと思いますが、IPアドレスは34.194.84.73のような感じになります。コンピュータはこうしたIPアドレスを用いることで、インターネット上で互いに接続されたネットワークをたどって目的のサーバーにたどり着けるようになります。

DNS(ドメインネームシステム)は、コンピュータがドメイン名をIPアドレスに変換して正しいサーバーを見つけるためのものです。皆さんのコンピュータでdigというユーティリティですぐに試せます(なおオンライン版のdigもあります)

; <<>> DiG 9.10.6 <<>> skylight.io
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32689
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;skylight.io.           IN  A

;; ANSWER SECTION:
skylight.io.        59  IN  A   34.194.84.73

;; Query time: 34 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Mon Apr 29 14:50:34 PDT 2019
;; MSG SIZE  rcvd: 56

digの出力は何だか難しそうに見えますが、重要なのは「ANSWER SECTION」です。ここではskylight.io34.194.84.73というIPアドレスに解決されています。

DNSに登録されているドメイン名はIPアドレスに対応付けられています。ドメインを購入または所有した場合は、この対応付けを自分で行わなければなりません。さもないと、顧客はあなたのサーバーを見つけられないからです。

サーバーのIPアドレスがわかれば、ブラウザからサーバーに接続できるようになります。ブラウザからサーバーへの接続方法はなかなか興味深いものです。両者の接続は、電話を手に取って相手の電話番号に電話をかけているようなものと考えても構いません。

実はこの部分についても、telnetと呼ばれるツールで実際に「生の」接続を試すことができます。たとえば、telnet 34.194.84.73 80と入力することで、先ほどのサーバーへの接続をオープンにできます。80はデフォルトのHTTPポート番号です。

訳注: telnetはmacOSやLinuxの多くのディストリビューションにはデフォルトでインストールされていません(telnetは通信を暗号化できません)。
参考: telnetコマンド | Linux技術者認定試験 リナック | LPI-Japan

An example telnet session

接続に成功したら、何かメッセージを伝えなければなりませんが、どんなメッセージを伝えればよいのでしょうか?ブラウザとサーバーが相手のメッセージを理解するには、互いに「話す」言葉について合意しなければなりません。その言葉こそがHTTP(Hyper Text Transfer Protocl)と呼ばれるものです。ブラウザとサーバーは、どちらもHTTPを理解できます。

ここで行える最もシンプルなリクエストとして「skylight.io/hello」を送るには、「リクエストの種類: GET」「対象ホスト: skylight.io」「対象パス: /hello」を指定します。

GET /hello HTTP/1.1
Host: skylight.io

上をtelnetセッションに正しく入力すれば(リクエストの終わりを示すために、最後に空行を入力するのが肝心です)、サーバーから以下のようなレスポンスを受け取ります。

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11
Date: Thu, 25 Apr 2019 18:52:54 GMT

Hello World

上は、リクエストが成功したことを示しています。ヘッダー情報がいくつも続いた後、コントローラでレンダリングした「Hello World」というテキストが最後に表示されています。

HTTPは(バイナリプロトコルではない)平文テキストでできたプロトコルなので、人間が読んで理解することもデバッグすることもできます。HTTPは、ブラウザがWebページやアセットをリクエストしたり、フォームを送信したり、キャッシュを操作したり、圧縮したりといったさまざまな操作を体系的に提供します。

このブラウザとサーバーの間の通信は、電話線と同様、暗号化なしの状態です。

このリクエストは、相手側に届くまでにさまざまな場所を通過します。このカンファレンスのWi-Fiルーター、会場のコンベンションセンターのルーター、インターネットプロバイダ、サーバーをホスティングしている企業など、リクエストが正しい場所に届くまでに多くの中間ネットワークを経由します。つまり、その途中ではブラウザとサーバーのやりとりを盗み聞きする機会はいくらでもあるということです。しかしもちろん皆さんは盗み聞きなどされたくありませんよね?

ご心配なく。やりとりの内容は暗号化(encrypt)できます。暗号化されたやりとりも(暗号化されていないやりとりと)すべて同じところを通過しますので、「やりとりが行われている」こと自体は中間地点にいてもわかります。しかし途中にいる者がやりとりの内容を理解することはできません。メッセージを復号化(decrypt)するためのキーを持っているのは、やりとりしているサーバーとブラウザだけだからです。

これがHTTPSと呼ばれるプロトコルです(SはセキュアのSです)。HTTPSは、HTTPの単なる別種ではありません。上述のように平文テキストでやりとりをする点については同じですが、ブラウザはメッセージを送信する前にメッセージを暗号化し、サーバーはそれを復号化してからメッセージを解釈します。

暗号化と復号化は、ブラウザとサーバーの両方が秘密鍵(secret key)を用いて行います。ブラウザとサーバーの両者が暗号化通信に合意すれば、他の誰も内容を知るすべはありません。なお、通信経路のあらゆる中間地点で通信を見ることができる状況で、ブラウザとサーバーが暗号化と復号化に使う鍵を、鍵を奪われずにどうやって選んでいるのでしょうか?この高度なトピックについてはまたの機会にでも。

サーバー

Logos of web servers

さて、ブラウザが無事サーバーに接続して、特定のWebページをサーバーにリクエストしたとしましょう。そのレスポンスはどのように生成されるのでしょうか?

そもそもこれは何のサーバーなのかといえば、「Webサーバー」です。Webサーバーは、上述のとおり「HTTPを話す」サーバーです。apachenginxpassengerlighthttpdunicornpuma、果てはwebrickなどがWebサーバーの例です。Rubyで書かれたWebサーバーもありますし、C言語などで書かれたWebサーバーもあります。

Webサーバーの役割は、リクエストを「理解する」ことと、そのリクエストに対してどんなサービスを提供するかを決定することです。静的アセットを求めるシンプルなリクエストであれば、Webサーバーをそれ用に構成するだけで簡単に扱えるようになります。

たとえば、ブラウザが/assets/以下にあるものをリクエストしたら、常にアプリの/public/assetsフォルダ以下のものを返すようにWebサーバーを設定したいとします。リクエストされたアセットが存在すれば、サーバーはそれを(圧縮なしで)返し、存在しない場合は「404 not found」ページを返すべきです。

利用しているWebサーバーによっては、そのサーバー固有の設定言語や設定構文も使えたりします。たとえばNginxを使っているのであれば、nginx.confに以下のような記述があるでしょう。

location /assets {
  alias /var/myapp/public/assets;
  gzip_static on;
  gzip on;
  expires max;
  add_header Cache-Control public;
}

What about my blog?

しかしさらに凝ったことをやろうとすると、話は複雑になってきます。

たとえば、「ブラウザで/blogにアクセスしたら、直近のブログ記事10件をデータベースから取得して見た目を整え、コメントも表示し、HTMLヘッダーやフッターやナビゲーションバーを追加し、JavaScriptやCSSも追加してからレスポンスを返せ」とWebサーバーに指示したいとしましょう。

ここでWebサーバーの設定言語について説明しようとすると、いくら何でも話がややこしくなりすぎてしまうでしょう。しかし私たちにはRailsという味方がついているのですから、実際にWebサーバーに指示したいのは「リクエストを受け取ったら、そこから先はRailsに渡してお任せしろ」ということなのです。ところでWebサーバーとRailsはどのようにやりとりしているのでしょうか?

A few possible ways for the web server to communicate with Rails

Rubyでは、この種の情報をやりとりするための手法がいろいろ考えられます。RailsがブロックにWebサーバーを登録する方法もあれば、WebサーバーがRailsのメソッドを呼び出す方法もあります。Webサーバーはリクエストの情報をメソッドの引数という形で渡すことも、環境変数で渡すこともできます。グローバル変数で渡すことすら可能でしょう。さて、WebサーバーからRailsにリクエストの情報を渡したとき、そこにどのようなオブジェクトが存在するべきでしょうか?言い換えれば、RailsはどのようにしてWebサーバーに返信するのでしょうか?

上のどの方法も同じように利用可能ではありますが、重要なのは、やりとりの両側で同じ規約に合意することです。Rackはそのために誕生しました。Rackは、WebサーバーがRuby製Webフレームワークとやりとりする(あるいはその逆)ための統一APIを提供します。どんなRubyフレームワークであっても、Rackプロトコルを実装してRackの規約に沿うことで、上述のさまざまなWebサーバーとシームレスにやりとりできるようになります。

The web server talking to Rails

Rackは、ごくわずかな機能を実現するためのシンプルなRubyプロトコルであり規約です。WebサーバーはWebフレームワークに「そちらの担当分のリクエストが1件来たぞ、そうそう、パスとかHTTP verbとかヘッダーもあるのでよろしく」と通知する必要があります。WebフレームワークはWebサーバーに「ほいきた、こちらの処理は終わったので結果(ステータスコード、ヘッダー、body)をお返ししとく」と返信する必要があります。

Rackはこうしたやりとりを軽量かつフレームワーク非依存に扱うため、Rubyで可能な最もシンプルな方法を選択します。RackはWebフレームワークへの通知をメソッド呼び出しで行い、やりとりの詳細はメソッドの引数を経由させ、戻り値をメソッド呼び出しから返すことでWebフレームワークに返信します。

以上をコードで表すと次のような感じになります。

env = {
  'REQUEST_METHOD' => 'GET',
  'PATH_INFO' => '/hello',
  'HTTP_HOST' => 'skylight.io',
  # ...
}

status, headers, body = app.call(env)

status  # => 200
headers # => { 'Content-Type' => 'text/plain' }
body    # => ['Hello World']

最初に、Webサーバーはハッシュを1つ用意します。これは「envハッシュ」と呼ばれるのが通例です。envハッシュにはHTTPリクエストの情報がすべて含まれています。たとえばREQUEST_METHODにはHTTP verbが、PATH_INFOにはリクエストのパスが、HTTP_*には対応するヘッダーの値が含まれます。

一方、アプリやフレームワークは#callメソッドを1つ実装しなければなりません。Webサーバーは、envハッシュを唯一の引数としてこのメソッドを呼び出します。ここでは、envハッシュ内の情報に基づいてリクエストが処理されることと、正確に3つの要素を含むarray(「3つのタプル」とも言います)を1つ返すことが期待されます。

この3つの要素とは何だかおわかりでしょうか?

第1の要素はHTTPステータスコードです。200ならリクエストの成功を、404ならnot foundを表すといった具合です。

第2の要素は、Content-Typeなどのレスポンスヘッダーを含む1個のハッシュです。

最後に第3の要素は、レスポンスのbodyとなる1個の配列です。このbodyはきっと文字列だろうとお思いの方もいるかもしれませんが、実は違います!いくつかの技術的な理由によって、このbodyは「each可能なオブジェクト」、つまり文字列をyieldする#eachを実装しているオブジェクトなのです。シンプルな場合は、その中に文字列が1切れ入ったarrayを1個だけ返します。

Let's build a Rack app!

では実際に動くところをお目にかけましょう!Railsでやる前に、シンプルなRackアプリを1つ手作りしてみます。

# app.rb

class HelloWorld
  def call(env)
    if env['PATH_INFO'] == '/hello'
      [200, {'Content-Type' => 'text/plain'}, ['Hello World']]
    else
      [404, {'Content-Type' => 'text/plain'}, ['Not Found']]
    end
  end
end

これは、おそらく最もシンプルに構築できるRackアプリでしょう。継承をまったく使わず、#callを実装するシンプルなクラスが1つあるだけです。このコードはenvハッシュのリクエストパスを参照し、パスが正確に/helloとマッチすれば”Hello World”という平文レスポンスをレンダリングし、マッチしない場合は404 “Not Found”エラーレスポンスをレンダリングします。

さてアプリができましたが、このアプリをどう使えばよいのでしょうか?アプリに仕事をさせるにはどうしたらよいのでしょうか?Rackは、Webサーバーが実装できるプロトコルの1つに過ぎないことを思い出しましょう。つまりこのアプリを、Rackの言葉でやりとりできるWebサーバーと接続する必要があります。

ありがたいことに、rackという便利なgemがあります。これはRackの仕様を実装するお便利ユーティリティ集で、rackupと呼ばれるサンプルWebサーバーを備えています。rackupはconfig.ruと呼ばれる形式の設定ファイルを認識します。

# config.ru

require_relative 'app'

run HelloWorld.new

config.ruは基本的にRubyファイルですが、若干の設定DSLを含んでいます。ここでは先ほどのappファイルをrequire_relativeし、Hello Worldアプリのインスタンスを構成して、runというDSLメソッドでrackupサーバーに渡しています。

これで、config.ruファイルのあるディレクトリでrackupコマンドを実行して、サーバーをデフォルトの9292番ポートにアタッチできるようになりました。http://localhost:9292/helloを開けば「Hello World」と表示され、http://localhost:9292/watあたりを開けば「Not Found」エラーが表示されます。

今度は、http://localhost:9292/というルートパスからhttp://localhost:9292/helloへのリダイレクトを追加したいとしましょう。この場合は次のようにアプリを変更できます。

# app.rb

class HelloWorld
  def call(env)
    if env['PATH_INFO'] == '/'
      [301, {'Location' => '/hello'}, []]
    elsif env['PATH_INFO'] == '/hello'
      [200, {'Content-Type' => 'text/plain'}, ['Hello World']]
    else
      [404, {'Content-Type' => 'text/plain'}, ['Not Found']]
    end
  end
end

これでも一応動きますが、あまりスケールしません。ここに継ぎ足していくとなると、ifelsifelseendが延々チェインしていくことになります。リダイレクトはよく行われる操作でもあるので、アプリの他の場所でも使い回しが効くようにしたいものです。リダイレクト機能の実装をモジュラーかつ再利用可能かつコンポジション可能にできたらよさそうですね。

もちろんできます!

# app.rb

class Redirect
  def initialize(app, from:, to:)
    @app = app
    @from = from
    @to = to
  end

  def call(env)
    if env["PATH_INFO"] == @from
      [301, {"Location" => @to}, []]
    else
      @app.call(env)
    end
  end
end

class HelloWorld
  def call(env)
    if env["PATH_INFO"] == '/hello'
      [200, {"Content-Type" => "text/plain"}, ["Hello World!"]]
    else
      [404, {"Content-Type" => "text/plain"}, ["Not Found!"]]
    end
  end
end

これでHelloWorldクラスの部分を一切変更せずに済むようになりました。その代りに、単一の責務をこなす役割を担うRedirectというクラスを新たに追加しました。マッチするパスがあればリダイレクトレスポンスを発行して終了し、ない場合は、このクラスに渡しておいた次のアプリに委譲します。

config.ruを以下のように変更して、リダイレクトが効くようにします。

require_relative 'app'

run Redirect.new(
  HelloWorld.new,
  from: '/',
  to: '/hello'
)

上はHelloWorldアプリのインスタンスを構成し、それをRedirectアプリに渡します。

これでRackミドルウェアを実装できました!ミドルウェアはRackの仕様の技術的な一部ではありません。Webサーバーという視点からは、ここにはRedirectというアプリがひとつあるきりです。このアプリの#callメソッドの中で別のRackアプリが呼ばれますが、Webサーバーはそのことについて関知する必要はありません。

このミドルウェアのパターンはよく使われますので、config.ruにはそれ用のDSLキーワードがあります。

require_relative 'app'

use Redirect, from: '/', to: '/hello'

run HelloWorld.new

useキーワードを使えばネストがきれいさっぱりなくなります。やったね!

このミドルウェアパターンはきわめて強力です。余分なコードを書かなくても、rack gemのミドルウェアを数個追加するだけで、圧縮機能やHTTPキャッシュやHEADリクエストのハンドリングをアプリに追加できます。

require_relative 'app'

use Rack::Deflater
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag

use Redirect, from: '/', to: '/hello'

run HelloWorld.new

このようにしてとても機能的なアプリを構築できることがおわかりかと思います。

Rails 3より前のRack

ついにRailsまでたどり着きました!

言うまでもなく、RailsはRackを実装しています。自分のRailsアプリを覗いてみれば、以下の記述があるconfig.ruファイルが見つかるはずです。

require_relative 'config/environment'

run Rails.application

config.ruはrackup由来ではありますが、その他のWebサーバーやHerokuなどのサービスでも認識されるので、config.ruがデフォルトでRailsに含まれているのは便利です。

runキーワードには何らかのRackアプリを渡すことになっています。つまりRails.application#callに応答するRackアプリでもあるはずです!となればRailsコンソールで試してみましょう。

Trying it in the Rails console

仕様に沿ったenvハッシュをわざわざ手作りしなくても、rack gemのRack::MockRequest.env_forユーティリティメソッドでできます。このメソッドにURLをひとつ渡せば後は代わりにやってくれます。このRails.application.call呼び出しにこのenvハッシュを渡せば、ステータスコードとヘッダーとbodyが期待どおりタプルとして生成されます。見慣れたリクエストログをコンソールに出力することもできます。素晴らしい!

このRailsアプリのconfig.ruでひとつ気がつくのは、どこにもuseステートメントがない点です。Railsはミドルウェアを全然使っていないのでしょうか?そんなことはありません。実は、アプリに入っている全ミドルウェアをコマンド一発でおなじみconfig.ru構文の形で表示できます。

$ bin/rails middleware

use Rack::Sendfile
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
run Blorgh::Application.routes

これを見れば、Railsではcookieハンドリングなどの多くの機能を、ミドルウェアの形で実装していることがわかります。これはナイスな設計です。なにしろAPIサーバーを実装する場合は、不要なミドルウェアを削除するだけで済むのですから。ところでどうやって削除するのでしょうか?

useステートメントは、単にconfig.ruでアプリのビルドを便利にやれるようにするためのものであることを思い出しましょう。Webサーバーはアプリの最も外側の部分にしか注目していないのです。Railsのconfig/application.rbにも、ミドルウェアを管理するのに便利なものがあります。

# config/application.rb

require_relative 'boot'
require 'rails/all'

Bundler.require(*Rails.groups)

module Blorgh
  class Application < Rails::Application

    # cookieを無効にする
    config.middleware.delete ActionDispatch::Cookies
    config.middleware.delete ActionDispatch::Session::CookieStore
    config.middleware.delete ActionDispatch::Flash

    # 独自のミドルウェアを追加する
    config.middleware.use CaptchaEverywhere

  end
end

ついにアプリまでたどり着いた!

ここまでミドルウェアについて見てきましたが、肝心のアプリはどうなったのでしょう?bin/rails middlewareの出力を見ると、useがミドルウェア用、runがアプリ用とわかるので、どうやらBlorgh::Application.routesがそれに違いありません!

Our app!

Railsコンソールで同じテストを実行すると、Rails.application.callが無事Blorgh::Application.routesに置き換わっているかどうかを確認できます。すべて問題なく動いています。だとすると、このRackアプリとは一体何なのでしょう?そしてこのRackアプリはどこから来たのでしょう?

このRackアプリはリクエストURLに着目し、膨大なルーティング規則とマッチさせて、呼び出すべき正しいコントローラとアクションを検索します。Railsはこのconfig/routes.rbに基づいて、アプリを生成してくれます。

# config/routes.rb

Rails.application.routes.draw do
  resources :posts
end

ほとんどのRails開発者はresources DSLをとっくにご存知でしょう。resourcesは、多くのルールを一度に定義するショートハンドです。resourcesは最終的に7つのルーティングに展開されます。

# config/routes.rb

Rails.application.routes.draw do
  # resources :postsから以下ができる
  get '/posts' => 'posts#index'
  get '/posts/new' => 'posts#new'
  post '/posts' => 'posts#create'
  get '/posts/:id' => 'posts#show'
  get '/posts/:id/edit' => 'posts#edit'
  put '/posts/:id' => 'posts#update'
  delete '/posts/:id' => 'posts#destroy'
end

たとえばGETリクエストを/postsに送信するとPostsController#indexメソッドが呼び出され、PUTリクエストを/posts/:idに送信するとPostsController#updateメソッドが呼び出されます。

ところでこのposts#indexという文字列は一体何なのでしょう?PostsControllerindexアクションだということぐらいはわかります。Railsでこのコードを追ってみると、最終的にPostsController.action(:index)という形に展開されます。さてこれは一体?

Action Controllerのコードをうんとシンプルにしたものを以下に示します。

class ActionController::Base
  def self.action(name)
    ->(env) {
      request = ActionDispatch::Request.new(env)
      response = ActionDispatch::Response.new(request)
      controller = self.new(request, response)
      controller.process_action(name)
      response.to_a
    }
  end

  attr_reader :request, :response, :params

  def initialize(request, response)
    @request = request
    @response = response
    @params = request.params
  end

  def process_action(name)
    event = 'process_action.action_controller'

    payload = {
      controller: self.class.name,
      action: action_name,
      # ...
    }

    ActiveSupport::Notifications.instrument(event, payload) do
      self.send(name)
    end
  end
end

上の冒頭にはactionクラスメソッドがあり、lambdaを返すことがわかります。このlambdaはenvと呼ばれる引数を1つ受け取ります。このenvは何かというと、驚かないでください、ハッシュなのです!そしてlambdaが返すのは何とarrayなのです!さらにこのlambdaは#callに応答するのです!失礼、つまりこれがRackアプリです。隅から隅までRackアプリなのです。

最後にここまでの話をまとめて考えると、Rackアプリは以下のような姿のルーティングアプリであることが想像できるでしょう。

class BlorghRoutes
  def call(env)
    verb = env['REQUEST_METHOD']
    path = env['PATH_INFO']

    if verb == 'GET' && path == '/posts'
      PostsController.action(:index).call(env)
    elsif verb == 'GET' && path == '/posts/new'
      PostsController.action(:new).call(env)
    elsif verb == 'POST' && path == '/posts'
      PostsController.action(:create).call(env)
    elsif verb == 'GET' && path =~ %r(/posts/.+)
      PostsController.action(:show).call(env)
    elsif verb == 'GET' && path =~ %r(/posts/.+/edit)
      PostsController.action(:edit).call(env)
    elsif verb == 'PUT' && path =~ %r(/posts)
      PostsController.action(:update).call(env)
    elsif verb == 'DELETE' && path = %r(/posts/.+)
      PostsController.action(:destroy).call(env)
    else
      [404, {'Content-Type': 'text-plain', ...}, ['Not Found!']]
    end
  end
end

上のコードは、渡されたリクエストパスとHTTP verbを、自分のルーティング設定で定義されたルールとマッチさせ、コントローラで適切なRackアプリに委譲します。こういうコードを手作りしなくて済むのは、ひとえにRailsのおかげです!

ところで、Railsはこの対応付けを行うために、どうやってこれをルーティング設定から生成し、すべてのリクエストを効率よくルーティングするのでしょうか?これについては昨年のRailsConfでちょうどこの話題を扱ったセッションがあります↓ので、どうぞご覧ください。

これで「すべてはRackアプリである」ことがわかりました。私たちはこれらを自由に「選んで組み合わせられる」のです(訳注:「MIX & Match」は韓国ドラマのタイトルです)。ここでプロならではの技😆をいくつかお目にかけましょう。

1. Railsアプリの一部からRackアプリへのルーティングを次のように書けることをご存知ですか?

Rails.application.routes.draw do
  get '/hello' => HelloWorld.new
end

2. 私たちは既にlambdaについて学んだので、以下のようにインラインで書くことも可能です。

Rails.application.routes.draw do
  get '/hello' => ->(env) {
    [200, {'Content-Type': 'text/plain'}, ['Hello World!']
  }
end

3. 「これはひどい」と思う人もいるかもしれませんが、もう皆さんも先ほどこの機能を使っているのです。ルーターのredirect DSLはどんなつくりだと思います?驚かないでください!実はRackアプリを返すのです。

Rails.application.routes.draw do
  # redirect(...) はRackアプリを返す!
  get '/' => redirect('/hello')
end

4. その気になれば、Railsアプリの中でSinatraアプリをマウントすることすら可能です。まさかと思う人もいるかもしれませんが、SidekiqのWeb UIはSinatraで書かれています。つまり皆さんもRailsアプリの中でSinatraアプリを動かしたことがあるかもしれないのです。

Rails.application.routes.draw do
  mount Sidekiq::Web, at: '/sidekiq'
end

原文追記: Sidekiq 4.2以降のWeb UIは、外部依存性を削減するためにカスタムフレームワークに移行しました。もちろん相変わらずRackプロトコルを使っています。

この移行のプルリクは、今回Rackプロトコルについて学んだことを元に最小版のSinatraをビルドするのに必要なものを学ぶうえで、興味深い事例です(特に基本的なルーティングの取扱、ビューのレンダリング、リダイレクトなど)。

5. もちろん、逆にSinatraアプリの中でRailsアプリをマウントすることも可能ですが、ここは皆さんのご想像にお任せしましょう。

ここで学んだことを応用すれば、controller#actionという文字列を以下のように置き換えることすらできます。

Rails.application.routes.draw do
  get '/posts' => PostsController.action(:index)
  get '/posts/new' => PostsController.action(:new)
  post '/posts' => PostsController.action(:create)
  get '/posts/:id' => PostsController.action(:show)
  get '/posts/:id/edit' => PostsController.action(:edit)
  put '/posts/:id' => PostsController.action(:update)
  delete '/posts/:id' => PostsController.action(:destroy)
end

その気になれば、bin/rails middlewareの出力をconfig.ruファイルにコピペすることもできます。

なお、実際には皆さんのRailsアプリでこういうやんちゃをするのはおすすめしません。そんなことをすればオートローディングや一部のパフォーマンス最適化がバイパスされ、gemでミドルウェアを足すこともできなくなるのですから、Rails周りで将来変更が発生したときにつらくなるだけです。とはいえ、このようにしてすべてが組み立てられていることを理解するのは実にクールですよね。

Example Rails controller code

そういうわけで、やっとのことで出発地点であるコントローラアクションに戻ってまいりました。なになに、render plain...が、Rack仕様で要求されるレスポンスのタプルになるまでの道のりの話はどうなったかって?今日のところは残念ながら時間がありませんが、続きは「The Lifecycle of a Rails Response」的なセッションかブログ記事を出すと思いますのでご期待ください。

お知らせ: Skylightのしくみ

本記事では、フレームワークが魔法でも何でもないことを学んできました。フレームワークはあくまで単なるレイヤの1つであり、一貫して十分に定義されたプリミティブの上にそのレイヤがトッピングされたものに過ぎません。こうした規約(convention)を学ぶことは、Railsの使い方やあなたのスキルを他の開発者と共有するうえで有用ですが、何よりも、規約はコミュニティのすべての人々にツールを書く力をもたらしてくれます。

たとえば、私たちSkylightではリクエストの実行全体に要した時間を測定する手段を必要としています。ミドルウェアよりうまく測定をやれるのはどんな方法でしょうか?

$ bin/rails middleware

use Skylight::Middleware
use Rack::Sendfile
use ActionDispatch::Executor
...
run Blorgh::Application.routes

「設定より規約」は、単にRailsアプリを構築する方法論にとどまりません。Railsの規約があることで、Skylightは独自の設定を書かなくても顧客のWebアプリの詳細な情報を集められるのです。

当初、私たちはRailsがアクションをどのようにディスパッチしているかをActionController::Base#process_actionをシンプルにしたもので調べました。このメソッドの内部では、Railsがアクションをコントローラにディスパッチするときに組み込みのActiveSupport::Notificationsというinstrumentation(測定)システムを用いて、何らかの事象が発生していることをSkylightなどのライブラリに通知します。SkylightはこのAPIによって、顧客のWebアプリのエンドポイント名を設定なしで取得します。

A condensed Skylight trace

Skylightでは、平均レスポンス時間以外の情報も提供できるので、顧客のWebアプリのリクエスト全体について集約された情報をお届けします。私たちはActiveSupport::NotificationsやRailsコミュニティの規約を駆使して、RailsのテンプレートレンダリングやActive Recordの実行タイミングはもちろん、著名なHTTPライブラリやライブラリのキャッシュ状況、Mongoidのようなその他のデータベースライブラリについても、Rails規約に沿った詳細情報を提供します。

デフォルトでは顧客のWebアプリのリクエストの重要な部分のみを表示しますので、何が起こっているかを急いで調べることに集中できます。この例であれば、エンドポイントを高速化するにはおそらくAppSerializerを集中的にチェックするべきでしょう。Skylightは、顧客がより多くの情報を表示する必要が生じれば、顧客のWebアプリで用いられているすべてのRackミドルウェアなどのさまざまな情報も収集いたします。

An expanded Skylight trace

以上は、Skylightが提供するサービスの氷山の一角に過ぎません。ご興味がおありでしたら、こちらの採用情報をご覧ください!


Skylightを使ったことのない方は、Skylightを本番採用しているオープンソースアプリのリストをぜひご覧ください。Skylightを業務アプリでお使いになりたい方向けに、無料の30日間お試しサインアップをご用意しております。ご友人を紹介(訳注: ログインが必要)いただいた方には、ご友人ともどもSkylightクレジット50ドルのキャッシュバック特典がございます。

訳注: キャッシュバックについては以下に説明があります。
* Skylight | App and Account Management

関連記事

Webアプリの基礎とさまざまな実行環境を理解する#1(社内勉強会)


週刊Railsウォッチ(20191007前編)Ruby 2.6.5でセキュリティ修正、Arel.sqlがstable APIに、Puma 4.2、RailsのDomain ObjectとService Objectほか

$
0
0

こんにちは、hachi8833です。そういえば今夜からノーベル賞発表が始まりますね。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

今回は第15回公開つっつき会からお送りします。お集まりいただきありがとうございました!🙇

⚓臨時ニュース: Ruby 2.6.5などセキュリティ修正リリース

ウォッチ20190925で取り上げた#16136の文字化け問題も2.6.5で修正されています。

⚓Rails: 先週の改修(Rails公式ニュースより)

⚓(master)stat(2)の呼び出しを削減

File.file?やその他の述語(メソッド)はパーミッションについて同じstat(2)の呼び出し結果を利用できる。
同PRより大意


つっつきボイス:「nobuさんのRailsコミットを初めて見たような気がします」「stat(2)って何を指しているんでしたっけ?🤔」「manコマンドで2 statとセクション番号を指定するということだと思います☺」「そちらでしたか😅」「セクション2だからシステムコールですね↓」

参考: manコマンドについて詳しくまとめました 【Linuxコマンド集】

STAT(2)                     Linux Programmer's Manual                     STAT(2)

NAME
       stat, fstat, lstat, fstatat - get file status

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>
       #include <unistd.h>

       int stat(const char *pathname, struct stat *statbuf);
       int fstat(int fd, struct stat *statbuf);
       int lstat(const char *pathname, struct stat *statbuf);

       #include <fcntl.h>           /* Definition of AT_* constants */
       #include <sys/stat.h>

       int fstatat(int dirfd, const char *pathname, struct stat *statbuf,
                   int flags);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       lstat():
           /* glibc 2.19 and earlier */ _BSD_SOURCE
               || /* Since glibc 2.20 */ _DEFAULT_SOURCE
               || _XOPEN_SOURCE >= 500
               || /* Since glibc 2.10: */ _POSIX_C_SOURCE >= 200112L

       fstatat():
           Since glibc 2.10:
               _POSIX_C_SOURCE >= 200809L
           Before glibc 2.10:
               _ATFILE_SOURCE

「manのセクション番号、6が『ゲームやデモ』ってなってますし😆」「セクション番号っていつまでたっても覚えられなくて😅」「自分も2がシステムコール、ぐらいしか覚えてませんけど😆

参考: singleton method File.stat (Ruby 2.6.0)

rescueelseって書けるんですね↓😳」「書いてもよかったはず」「必ず実行するときは何でしたっけ」「ensureで〜す😆」「File.statはシステムコールのstat()を呼んで返しているだけですね☺

# actionpack/lib/action_dispatch/middleware/static.rb#L84
      def file_readable?(path)
-       file_path = File.join(@root, path.b)
-       File.file?(file_path) && File.readable?(file_path)
+       file_stat = File.stat(File.join(@root, path.b))
      rescue SystemCallError
+       false
+     else
+       file_stat.file? && file_stat.readable?
      end
# railties/lib/rails/commands/dbconsole/dbconsole_command.rb#L122
      def find_cmd_and_exec(commands, *args) # :doc:
        commands = Array(commands)
        dirs_on_path = ENV["PATH"].to_s.split(File::PATH_SEPARATOR)
        unless (ext = RbConfig::CONFIG["EXEEXT"]).empty?
          commands = commands.map { |cmd| "#{cmd}#{ext}" }
        end
        full_path_command = nil
        found = commands.detect do |cmd|
          dirs_on_path.detect do |path|
            full_path_command = File.join(path, cmd)
-           File.file?(full_path_command) && File.executable?(full_path_command)
+           begin
+             stat = File.stat(full_path_command)
+           rescue SystemCallError
+           else
+             stat.file? && stat.executable?
+           end
          end
        end

⚓(master)serializeメソッドでシンボルもstringにシリアライズするよう修正

model.some_string = :foo
model.save! # "foo" が永続化される

シンボルは永続化するときに上のようにstringにシリアライズされる。
このPRでは、シンボルをstringにシリアライズするImmutableStringクラスを更新して、ActiveModel::Attributeがこのシリアライズメソッドを呼び出してchanged_in_place?の戻り値を確定させる振る舞いと同じになるようにする。
この変更が行われる前は、以下のように”something”が変更されたと表示されていたが、変更後はcomment.something_changeがnilを返すようになる。
同PRより大意

comment = Comment.create! # (文字列の"something"フィールドがある)
# commentsテーブルかcomment.attributesの"something"フィールドで"anything"を永続化する
comment.update_column :something, :anything
comment.something  # or comment.attributes
comment.something_change
# 上は["anything", "anything"]になる
# `to`と`from`の値なのに同じになってしまう

つっつきボイス:「シンボルをシリアライズするときにstringにするということらしい」「前はシンボルの場合にto_sされていなかったと」

# activemodel/lib/active_model/type/immutable_string.rb#L10
      def serialize(value)
        case value
-       when ::Numeric, ActiveSupport::Duration then value.to_s
+       when ::Numeric, ::Symbol, ActiveSupport::Duration then value.to_s
        when true then "t"
        when false then "f"
        else super
        end
      end

⚓(master)ActionController::Base.log_atを追加

ここからはcloseした6.0.1マイルストーンから見繕いました。

# 同PRより
# リクエストごとにログレベルを変える
class ApplicationController < ActionController::Base
  log_at :debug, if: -> { cookies[:debug] }
end

つっつきボイス:「Base.log_atでリクエストごとにログレベルを変えられるそうです」「ほほぉ〜😋」「うーん、こういうのって気持ちはわかるんだけど、消し忘れて惨事を招きそうな気がしないでもない😇」「同時実行制御あたりとの相性も気になるし」

# actionpack/lib/action_controller/metal/logging.rb
module ActionController
  module Logging
    extend ActiveSupport::Concern

    module ClassMethods
      def log_at(level, **options)
        around_action ->(_, action) { logger.log_at(level, &action) }, **options
      end
    end
  end

「使いみちとしては、うるさいログを黙らせるとかでしょうね☺」「Authenticationコントローラのログはもう出さなくていいよ、とか😆」「惨事というのは、クライアントからcookieを仕込めるからですか?」「その辺ですね、中身を知っていればクライアントが『このリクエストだけログを出さないでくれ』みたいなのをやれる可能性があるといえばある😆

⚓(master、6.0.1)RedisCacheStoreが最大クライアントコネクション数到達時に安全にfaliするよう修正

# activesupport/lib/active_support/cache/redis_cache_store.rb#L477
        def failsafe(method, returning: nil)
          yield
-       rescue ::Redis::BaseConnectionError => e
+       rescue ::Redis::BaseError => e
          handle_exception exception: e, method: method, returning: returning
          returning
        end

キャッシュに使っていたHerokuのRedisに関連して最近サービス中断の憂き目に遭った。Redisサーバーが最大クライアント数に達していた。Redis Cache Storeにはコネクションエラー時にもデータを返せるようにするフェイルセーフがあるが、最大クライアント数に達したときのエラーはRedis::ConnectionErrorではなくRedis::CommandErrorである。データベースから新鮮なデータが返らず、ユーザーに500エラーが表示された。
この修正は単にrescueを変更してRedisのあらゆるエラーでリクエストをsaveする。もちろんエラーハンドラーにはエラーが報告されるので、エラーはユーザーが選択したメソッド経由で引き続き送信される。これは他のキャッシュストアと同様なので↓、実質的にすべてのエラーでsaveできる。

  • MemCacheStoreはすべてのDalliエラーをrescueする(Dalli::DalliError
  • ReadThis(別のRedisキャッシュ)はRedis::BaseErrorをrescueする
  • FileStoreは特定のrescueを宣言しないのですべてrescueする
    同PRより

つっつきボイス:「Redisはコネクション数が最大になったときのエラーだけ種類が違ってたのか😳」「ReadThisっていうRedis互換のキャッシュストアがあるんですね」「dalliとどう違うのやら😆」「そういえばdalliってありましたね(ウォッチ20180413)」

# sorentwo/readthisより
config.cache_store = :readthis_store, {
  expires_in: 2.weeks.to_i,
  namespace: 'cache',
  redis: { url: ENV.fetch('REDIS_URL'), driver: :hiredis }
}

「ReadThisは見た感じ普通のキャッシュgemですね☺」「もしかしてReadThisってRedisのダジャレか😆」「Redisみたいにライセンスであーだこーだ言わないよみたいな😆」「😆」「そういえばちょっと前から議論になってましたね」「Redisが使えなくなると他もいろいろ死にますし😭

参考: Redis Labsの2度のライセンス変更はフリーライドを防げるか - ITmedia エンタープライズ

⚓(master、6.0.1)IE 9互換のためElement.closest()を回避

# actionview/app/assets/javascripts/rails-ujs/utils/form.coffee#L12
  inputs.forEach (input) ->
    return if !input.name || input.disabled
-   return if input.closest('fieldset[disabled]')
+   return if matches(input, 'fieldset[disabled] *')
    if matches(input, 'select')
      toArray(input.options).forEach (option) ->
        params.push(name: input.name, value: option.value) if option.selected
    else if input.checked or ['radio', 'checkbox', 'submit'].indexOf(input.type) == -1
      params.push(name: input.name, value: input.value)

つっつきボイス:「そういえばElement.closest()の互換問題ってありますね」「closest()便利ですし😋」「一番近い要素を取れるんでしょうか?」「ツリーを上に遡っていって一番近いところにあるものを取ってくるヤツですね☺」「なるほど!」「いくつ上に上がればいいかわからないときにホント便利😍

参考: Element.closest() - Web API | MDN

「この修正のファイル名見ると.coffeeってなってますけど、CoffeeScriptってまだ残ってるんですね😳」「入っているといえば入ってるみたいですね〜: 使われているかどうか知りませんが🤣」「🤣

後で調べると、Action Viewのrails-ujs以下は現在もCoffeeScriptでした↓。

参考: rails/actionview/app/assets/javascripts/rails-ujs at master · rails/rails

なお#34177ではAction CableのCoffeeScriptがES2015に書き換えられました↓。

参考: Convert ActionCable javascript to ES2015 modules with a modern build environment by rmacklin · Pull Request #34177 · rails/rails

⚓(番外、master、6.0.1)Arel.sqlをstable APIとしてドキュメントに記載

Arel.sql他のAPIドキュメントdeprecationメッセージから参照されているのに、それ自身にドキュメントがない。
同PRより大意

# activerecord/lib/arel.rb#L30
+ # 既知の安全なSQL文字列をラップしてクエリメソッドに渡せるようにする。例:
+ #
+ #   Post.order(Arel.sql("length(title)")).last
+ #
+ # SQLインジェクションの脆弱性回避には万全の注意を払うこと。<a href="https://techracho.bpsinc.jp/wp-content/uploads/2019/07/ransack-h_captured.png"><img src="https://techracho.bpsinc.jp/wp-content/uploads/2019/07/ransack-h_captured.png" alt="" width="400" class="aligncenter size-full wp-image-78429" /></a>
+ # このメソッドでrequestパラメータやモデル属性といった「安全でない」値を使うべきではない。
+ # 
  def self.sql(raw_sql)
    Arel::Nodes::SqlLiteral.new raw_sql
  end

つっつきボイス:「Arel.sqlに今までAPIドキュメントがなかったので足したそうです」「Arel.sql、こないだRansackあたりでちょっと使ったな〜😆


activerecord-hackery/ransackより

その後社内Slackで、以下の記事↓とともに、回避手段としてのArel.sqlの存在感が再び増してきているのではないかという指摘がありました。ありがとうございます!🙇

参考: Rails 6 以降は order/pluck の引数に SQL 文字列を渡すことはできない (容易に対策可能) - Qiita

⚓Rails

⚓Pumaの新バージョンがリリース(Ruby Weeklyより)


puma.ioより


つっつきボイス:「Pumaの新しいのが出ましたね〜😋」「Pumaといえば以前公開つっつき会で見たあの動画ですね😆ウォッチ20190708)」「ああ😆、バージョン3とさよならするあの動画↓」

「リリースノートには、URLでセミコロンが使えるようになったとかありました」

⚓0番ポートはany

Puma now reports the correct port when binding to port 0, also reports other listeners when binding to localhost (#1786)

「リリースノートにある0番ポートとのバインディング↑ってどういうものでしたっけ…?🤔」「0番ポートはless /etc/servicesにはなかったけど、RFC1700に載ってるらしい↓」

参考: https://www.ietf.org/rfc/rfc1700.txt

Decimal Keyword Protocol References
——- ——- ——– ———-
0 Reserved [JBP]
1 ICMP Internet Control Message [RFC792,JBP]
2 IGMP Internet Group Management [RFC1112,JBP]
3 GGP Gateway-to-Gateway [RFC823,MB]
4 IP IP in IP (encasulation) [JBP]
5 ST Stream [RFC1190,IEN119,JWF]
6 TCP Transmission Control [RFC793,JBP]
7 UCL UCL [PK]
8 EGP Exterior Gateway Protocol [RFC888,DLM1]
9 IGP any private interior gateway [JBP]
(略)

「0番はreservedか」「well-knownポートか、思い出した: 任意のポートでソケットを開くときに0番ポートを使うんだった」「おぉ?」「自分でソケットを開くときに、空いているポートを使いたかったら0番ポートを指定すると開いてるephemeralポートから適当に割り当ててもらえる」「なるほど!」

参考: TCPやUDPにおけるポート番号の一覧 - Wikipedia

0番のポートはエニーポート(any port)と呼ばれ、アプリケーションに対して、動的に別番号の空きポートを割り当てるために用意された特殊なポート番号である。別番号のポートの再割り当てを行わずに0番のポートとして使用することは禁止されているため、利用上では注意が必要である。
Wikipediaより

「たしかephemeralポートの範囲はOSによって違ったはず」「そうなんですか!😳

参考: エフェメラルポート - Wikipedia

  • IANA: 49152〜65535を提言
  • BSD: バージョン5.0以降はIANAの提言に従う
  • Linux: 32768〜61000が多い
  • Windows: XPやServer 2003までは1025〜5000、VistaとServer 2008以降はIANAに従う

「手元のAmazon Linuxの/etc/servicesを見てみると↓、well-knownポートは0〜1023、registeredポートが1024〜49151、ダイナミックなプライベートポートは49152〜65535となってますね」「へぇ〜!」「IANAのサイトにも膨大なリストがあるな↓」「142ページもあるとは😳」「すごい量🐳

参考: Service Name and Transport Protocol Port Number Registry

The latest IANA port assignments can be gotten from
http://www.iana.org/assignments/port-numbers
The Well Known Ports are those from 0 through 1023.
The Registered Ports are those from 1024 through 49151
The Dynamic and/or Private Ports are those from 49152 through 65535
Amazon Linuxの/etc/servicesより

「でPumaの話に戻ると、0番で任意ポートで立ち上げられるようになったということですね」「なるほど〜😋」「空いているポートを自分で調べて使うのってだるいですし😆」「たしかに😆」「空きポートを調べる処理って案外重たいんですよね: netstatとかでやれますけど、仕組みがあるならそっちに任せてしまう方がいいかと」「そういえば最近はssコマンドでやるらしいと昨日知りました😆」「最近のコマンドはわかんないな〜😆」「ssってなんか覚えにくいし😆

参考: netstat - ホストのネットワーク統計や状態を確認する
参考: 【 ss 】コマンド――ネットワークのソケットの情報を出力する:Linux基本コマンドTips(150) - @IT

⚓スケールしたときのAPIエラーを正しく扱う(Ruby Weeklyより)

# 同記事より
def track_exception(exception)
  redis.hincrby("tracked_exceptions", exception.class.to_s, 1)
  raise Monolist::TrackedException.new(exception)
end

def poll_gitplace(user, client)
  time = user.gitplace_last_sync

  client.get_pull_requests({ created_after: time }).each do |pull_request|
    comments = client.get_pull_request_comments(pull_request)

    unless user.action_items.find { |s| s.github_id == pull_request.id }
      create_action_item(user, pull_request, comments)
    end

    time = pull_request.created_at
  end
rescue Gitplace::ConnectionTimeout => e
  track_exception(e)
ensure
  user.update!({ gitplace_last_sync: time })
end

見出し:

  • リトライは早期に、ただし正しく
  • 常にジョブの進捗を確認せよ
  • 失敗をトラッキングせよ

つっつきボイス:「常にmake progressせよはそのとおり: 進捗取れてないとログ見ても何が起こってるかマジでわからないし😅」「なるほど!」「この記事は、APIをいっぱい叩くようなアプリを書くときはこの辺をちゃんとやっときましょうねという定番の注意ですね☺」「ふむふむ」「最近自分もAPI周りでハマって面倒くさいことになってて😆

「APIで何かするときに、やりとりが有限回で済むかどうかが事前にわからないときがあるんですよ」「あ〜😳」「データセットのサイズがめちゃくちゃでかいと、データをぶん投げるまでにものすごく時間がかかったりしますし😭」「たしかに!」「stagingでは一瞬で終わるのに本番だと永遠に返ってこなかったりして、そういうときにプログレス出してないとつらいです😇

⚓RailsのDomain Objectsは善、Service Objectは悪(Ruby Weeklyより)

見出し:

  • Active Recordモデルの長所と短所
  • 「Active Record寄せ集め」アンチパターンの代替に使われるService Object
  • Service Objectがよくない理由
  • Service ObjectよりもDomain Objectがよい
  • もっと知りたい方へ

つっつきボイス:「久しぶりにオピニオン記事を見かけたので」「たしかにService ObjectよりはDomain Objectの方が設計としてはキレイになりますし、おっしゃるとおりという感じ😆」「😆」「Serivce Objectは自分にとって『設計を諦めて追いやる』的な位置づけというか😆」「😆

「記事の見出しに『Active Record grab bagアンチパターン』というのがありますけど、grab bagはいわゆる『福袋』『寄せ集め』のことみたいなので、まあ闇鍋かなと😆

「個人的にはService Objectってそれほどいいパターンには思えないんですけど、ただコードの見通しが悪くなったときに、昔なつかしいSOAP↓の内部API版みたいな感じで使うならワンチャンありかなという気はちょっとしますね🤔」「ふむふむ」「印象としてはそういうのがグローバルな名前空間にふらっと入ってくる感じ: もちろん使うならネームスペースちゃんと切りますけど😆

参考: SOAP (プロトコル) - Wikipedia


Wikipediaより

「Service Objectにしたからといってキレイになったりはしませんし😆」「ただいろいろ条件を整えないといけないもので、かつ繰り返し実行するものならService Objectにしておいてさっと呼べるようにしておくはありかも🤔」「そうかも☺」「Service Objectはそんなにうれしいパターンではないけど、価値がないわけではない💰」「コピペコードになりやすいのが難点かな😅」「Service Objectをキレイにキメるのは割と難しいです☺

Ruby/Railsのプロ開発者としての5年間を振り返る(翻訳)

「このDomain Objectは、この間のForm Object話(ウォッチ20190930)に出てきたApplication ObjectとかApplication Modelとかとはまた違うんでしょうか?」「Domain Objectは、たぶんここではビジネスオブジェクト的な、ビジネスロジックを置く場所という意味でしょうね☺」「なるほど!」

「記事の下の方を見るとMartin Fowlerさんの薄いドメインモデル(Anemic Domain Model)↓のことが書かれてますね」「まさにエンタープライズアプリケーションアーキテクチャパターンで言われているような話かな☺

参考: AnemicDomainModel — Martin Folwer

エンタープライズアプリケーションアーキテクチャパターンを読む: 1.概要

「なになに、『Devise gemはなかなかいい仕事をしてる』ですって🤣」「」「Devise嫌われてるけど🤣」「まあDeviseはでかいのと、どこを触ったら壊れるかがわからないのが怖いんですけどっ👻

[Rails] Devise Wiki日本語もくじ1「ワークフローのカスタマイズ」(概要・用途付き)

⚓Rails 6のZeitwerkを理解する(RubyFlowより)

見出し:

  • Zeitberkの方がよい理由
    • classicモードでのオートロード
    • Zeitwerkのオートロード
  • Rails 6のZeitwerk

つっつきボイス:「Zeitwerkの紹介というかおさらい的な」「内容はRailsガイドでも辿れそうな雰囲気でした」

参考: 定数の自動読み込みと再読み込み (Zeitwerk) - Rails ガイド
参考: 定数の自動読み込みと再読み込み (Classic) - Rails ガイド

「ところでZeitwerkはRubyのautoloadを使っているんですけど、autoloadはずうっと昔にRubyから消そうという話↓があったのをこの間教わって、今のRubyではどうなってるのかが気になりました🤔」「autoloadを消したいのは何となくわかるな〜: うまく動いているときはいいけど、ひとたび暴れ始めると大変そう😅」「下のissueの冒頭でも、autoloadはマルチスレッドで根本的な問題があるみたいなことが書かれてました」「そうでしょうね☺

「autoloadって結局消されないのかな?」「一度入れちゃったら無理でしょうね😆」「マルチスレッドのautoloadはカオスになるかもしれないけど、シングルスレッドだったらそういうことにはなりにくいんじゃないかな?🤔」「あとFiberとかなら😆

「あ、今issueの一番下を見てみたらMatzが8か月前に取り下げてますね↓」「少なくともRuby 3.0では残すと」「8年越しの決定🎉」「autoloadは死なず」


同issueより

⚓chef-sugar gemの作者が米国移民法に抗議のためリポジトリを閉鎖して議論に(Ruby Weeklyより)


つっつきボイス:「ああchef-sugarの問題ね☺」「技術系じゃない英語記事ってあんまり読み慣れてなくて😅」「この件はちょっと前にもはてブで話題になってましたけど😆↓」「う、そうでしたか😅: じゃそっちで見ましょう」

参考: 政治的問題のためRuby GemsとGitHubからChef関連の諸々が消えた件について - tpdn blog

「これは元々、Chefの会社で働いてたメンテナーが、そこを辞めた後に元の会社がICEという政府機関と契約をしたのが気に入らなくて自分が関わったソフトウェアをリポジトリから消しちゃったという話😇」「すごいことするな〜😳」「1つだけかと思ったら他にも消してたとは😳」「あちこちでいっぱいアラートが鳴ってたでしょうね」「何だか壮絶😨」「ある日突然Railsがリポジトリから消えたらと考えたら…💀

参考: アメリカ合衆国移民・関税執行局(ICE) - Wikipedia

「この件に関連して、オープンソースを反社会的なことに利用することについてのライセンス条項を見直すべきなのかどうかみたいな記事をはてブあたりで見た気がするんですが、今日ちょっと体力なくて見つけきれない😅」「そういう議論ってたしかに起きそうですね」「発端はさっきも話に出たRedisとかMongoDBなどのライセンス変更↓ですけど☺

参考: 「Redis」「MongoDB」「Kafka」が相次いで商用サービスを制限するライセンス変更 AWSなどによる「オープンソースのいいとこ取り」に反発 (1/3) - ITmedia エンタープライズ

後で探しましたが、やはりそれらしい記事は見つけられませんでした😇

「Redisとかの件もそうですけど、それ以外にもたとえば人を害するプログラムを使ってはいけないという議論とかもあったりして、でもそれをどうやって実現するの?とか、とにかく最近のオープンソース界隈は議論が紛糾してますね☺」「いろいろ考えさせられます😓」「う〜ん、やっぱり記事見つからないか😢

「こういうのを見ると、gemをrubygems.orgとかから直接取ってくるんじゃなくて社内にリポジトリのミラーサーバーを立てて使うことで、サービス提供が中断しないように手を打つことも考えちゃいますね」「そういえばクックパッドさんとかもミラーやってますね」「そうそう、その方がより確実ですし☺」「gemが汚染されたときにタイミングが悪いとミラーにしばらく残っちゃったりもしそう😅」「それもありますね」

⚓html5_validators: RailsでHTML5バリデーションを自動でやれる

この間amatsudaさんのリポジトリ↓(forkなどは除外)を物色してて知りました。

参考: amatsuda (Akira Matsuda) / Repositories


つっつきボイス:「リポジトリのタイトルにRails 3, Rails 4, Rails 5、Rails 6ってあるのがスゴい💪」「クライアントサイドバリデーション?」「ああなるほど!モデル側にバリデーションを付けるとビュー側でHTML5のこういうrequiredみたいなタグ↓を自動で使ってくれるのね😋」「おぉ〜😍

# 同リポジトリより
class User
  include ActiveModel::Validations
  validates_presence_of :name
end
<input id="user_name" name="user[name]" required="required" type="text" />


同リポジトリより

「そういえばsimple_form↓もそういうタグを付けてくれた覚えが」「やってくれるみたいです」「simple_form、自分は割と好きなんだけど他の人がだいたいキライなので最近はあまり押さなくなってきてます😅」「simple_formはカスタマイズしないのがコツでしたっけ」「そうそう😆


plataformatec/simple_formより

「html5_validators、割とよさそうですね😘」「まあ油断するとブラウザごとのバリデーション実装の違いにやられることもあるかもしれませんけど😆」「フォームで数字を指定したときのちっちゃ〜な上下矢印なんかはブラウザごとに違ってるし、しかもたいてい使いにくい😆」「ブラウザのカレンダーもたいがい使いにくいですし😆」「ブラウザ側の実装の違いはつらいよ〜😭」「ブラウザ組み込みのデフォルトのバリデーションメッセージも、たしか英語レベルですらブラウザごとに文言が違ってて泣いたことあります😭

⚓その他Rails

つっつきボイス:「SQLite3用だそうです」「SQLite3は使ってないな〜😆」「自分はSQLite3割と好きです❤」「よほどじゃないと使いませんし、たまに使おうとするとSQLite3がそもそも入ってなかったりしますし😆


つっつきボイス:「これは?」「以前のRailsウォッチ(ウォッチ20181210)で、Railsチュートリアルの演習問題にupdate_columns(バリデーションやコールバックが発火しない)が使われているという話があったのをこの間やっと思い出して、作者のMichael HeartlさんにDMで知らせたところ一言注意を加えておくよという返信をもらったところです」「update_columnsの方が行数は少なくなりますけど、実際に使うとハマるやつですね☺


前編は以上です。

バックナンバー(2019年度第4四半期)

週刊Railsウォッチ(20191001後編)RedisとRubyをつなぐredis-object gem、Fullstaq Rubyの新バージョン、COUNT(*)とCOUNT(1)の速度ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSなど)です。

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

週刊Railsウォッチ(20191015)スライド「Rails Performance issues and Solutions」を見る、dirtyに*_previously_was が追加、Sidekiq 6.0.1ほか

$
0
0

こんにちは、hachi8833です。台風前のつっつきでしたので、エントリを減らし気味にしてみました🙇


つっつきボイス:「jnchitoさんは関西の方でしたね☺」「mapで風速を秒速から時速に変換してる」

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

⚓Rails: 先週の改修(Rails公式ニュースより)

今回は公式情報を中心に見繕いました。

なお、6.0.1マイルストーンも見てみましたが、「再現できない」「自分のコードが間違ってた」でcloseされているケースがいくつか見当たりました。

⚓新機能: dirtyトラッキング用 *_previously_wasを追加

なおマージされたのは8/2です。

# 同PRより
pirate.update(catchphrase: "Ahoy!")
pirate.previous_changes["catchphrase"] # => ["Thar She Blows!", "Ahoy!"]
pirate.catchphrase_previously_was # => "Thar She Blows!"
# activemodel/lib/active_model/dirty.rb#L125
    included do
      attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
-     attribute_method_suffix "_previously_changed?", "_previous_change"
+     attribute_method_suffix "_previously_changed?", "_previous_change", "_previously_was"
      attribute_method_affix prefix: "restore_", suffix: "!"
    end
...
+   # Dispatch target for <tt>*_previously_was</tt> attribute methods.
+   def attribute_previously_was(attr_name) # :nodoc:
+     mutations_before_last_save.original_value(attr_name.to_s)
+   end

API: ActiveModel::Dirty
参考: 1.4 Dirtyモジュール — Active Model の基礎 - Rails ガイド


つっつきボイス:「またdirtyに新しいメソッドが生えるのね😆」「DHHが自分でツイートしていたような気がします」

探しましたがDHHのツイートはありませんでした😅。代わりに以下を貼っておきます↓。

⚓新機能: MySQLでmatches_regexdoes_not_match_regexpが使えるようになった

# 同PRより
users = User.arel_table;

# 全gmailユーザーを検索
users = User.arel_table; User.where(users[:email].matches_regexp('(.*)\@gmail.com'))

# gmail以外の全ユーザーを検索
users = User.arel_table; User.where(users[:email].does_not_match_regexp('(.*)\@gmai
# activerecord/lib/arel/visitors/mysql.rb#L51
+       def visit_Arel_Nodes_Regexp(o, collector)
+         infix_value o, collector, " REGEXP "
+       end
+
+       def visit_Arel_Nodes_NotRegexp(o, collector)
+         infix_value o, collector, " NOT REGEXP "
+       end

つっつきボイス:「ActiveRecordで#matches_regexpを使った正規表現比較がMySQLでもできるようになった🎉」「今までもwhereの中に書いたりしてたけど、エスケープしてくれるのがありがたい😋「PostgreSQL版の実装は前からあったみたいで、今回はMySQLでも使えるようにしたそうです」

調べてみるとPostgreSQL版は2年前に入っていました↓。

# https://github.com/rails/rails/blob/master/activerecord/lib/arel/visitors/postgresql.rb#L29
        def visit_Arel_Nodes_Regexp(o, collector)
          op = o.case_sensitive ? " ~ " : " ~* "
          infix_value o, collector, op
        end

        def visit_Arel_Nodes_NotRegexp(o, collector)
          op = o.case_sensitive ? " !~ " : " !~* "
          infix_value o, collector, op
        end

「SQL文の中で正規表現が使いたくなることってたまにありますよね?」「今どきのRDBではたいてい使えますけど😋」「あ、SQLite3で正規表現をちょっとだけ使おうとしたときが割と面倒でした😆」「SQLite3にはなさそう😆」「一応あるにはありました↓」

参考: SQLite で正規表現を使う - しょぼんメモリ (´・ω・`)

「まあRDBの正規表現を使いすぎるとちゃんとした速度が出なくなったり、インデックスが効いたり効かなかったりすることもあるので微妙ですけど😆」「そうですよね」「コレーション(照合順序)とかも影響する可能性ありそうですし😅

参考: PostgreSQL 11.5ドキュメント 23.2. 照合順序サポート

⚓joinsのorderをできるだけ維持するよう修正

修正量が多めだったのでソースは貼りませんでした。


つっつきボイス:「最近kamipoさんたちが苦しみ続けていたjoinsのorderの件が『できるだけ維持する』方向でひとまず収束したようです」「ここはたしかに厄介な部分…😅」「Active Recordのオブジェクトとしてどう解釈されるかまで考えたうえでSQL文も見ないといけないのはつらい🥺

⚓RackのMigration::CheckPendingFileUpdateCheckerを使うようにしてマイグレーションのパフォーマンスを大きく改善

ここからはマージされたプルリクから見繕いました。最近は大半がドキュメントの微修正です。

# 改修前
$ be ruby benchmark.rb
Warming up --------------------------------------
   CheckPending#call    11.000  i/100ms
Calculating -------------------------------------
   CheckPending#call    135.510  (±12.5%) i/s -    671.000  in   5.083931s
# 改修後: file_watcher == FileUpdateCheckerの場合
$ be ruby benchmark.rb
Warming up --------------------------------------
   CheckPending#call    34.000  i/100ms
Calculating -------------------------------------
   CheckPending#call    348.063  (±18.1%) i/s -      1.666k in   5.024624s
# 改修後: file_watcher == EventedFileUpdateCheckerの場合
$ be ruby benchmark.rb
Warming up --------------------------------------
   CheckPending#call   128.936k i/100ms
Calculating -------------------------------------
   CheckPending#call      1.884M (± 2.5%) i/s -      9.412M in   5.000561s

従来はlast_migration.mtimeの値で変更を監視していたが、ActiveSupport::FileUpdateCheckerEventedFileUpdateChecker(新しいアプリではこちらがデフォルトになる)を用いてパフォーマンスを大幅に向上させた。
同PRより大意


つっつきボイス:「この間も話に出たファイルウォッチャー(ウォッチ20190930)を使ってマイグレーションのpendingチェックを速くしたそうです」「お〜なるほど、これでファイル更新チェックのために毎回全なめしなくてよくなった😋」「pendingチェックたしかに今まで遅かったですね」「productionも含めてRailsサーバーの起動が速くなりそう👍

「この#37395と同じお題で以前に#29759↓でチャレンジしたときはうまくいかなかったのを再挑戦して今度はうまくいったそうです」「こういうのはどうやってテストするかという問題もありますし、Railsはマルチワーカーで動くからそこでもちゃんと動かないといけないでしょうし☺

⚓(ドキュメント)autosaveの関連付けの振る舞いについて注意点を追加

レコード自身が変更された場合、autosaveは永続化済み関連付けレコードでのみトリガーされる。これは循環した関連付けバリデーションによるSystemStackErrorから保護するのが目的。例外が1つあって、カスタムのバリデーションコンテキストが使われていると、関連付けられたレコードのバリデーションが常に発火する。
同PRより大意


つっつきボイス:「APIドキュメントに注意事項が1つ追加されていました」「circular association validationって😆」「循環参照があるとautosaveの挙動がおかしくなるでしょうね☺」「ただしカスタムのバリデーションコンテキストが使われているとバリデーションが常に発火すると」

参考: 4.3.2.2: has_manyautosave — Active Record の関連付け - Rails ガイド

# 5a3e34eより
  def test_validations_still_fire_on_unchanged_association_with_custom_validation_context
    firm_with_low_credit = Firm.create!(name: "Something", account: Account.new(credit_limit: 50))

    assert firm_with_low_credit.valid?
    assert_not firm_with_low_credit.valid?(:bank_loan)
  end

5a3e34ecreate!でやってるこのあたり↑がそうかな」「おぉ?」「中でAccount.newしているものがsaveされないとidが確定しないからcreate!できなくなりますね」「あ〜なるほど!」「こういうのがカスタムバリデーションコンテキストでのautosaveということだと思います☺: 中のAccountが外のFirmにhas_manyしているときは、firm.account_idをセットするために中のものから先に作らないといけないのでAccount.newが自動でsaveされると」「こういう状態を何て言えばいいんでしたっけ?😅」「循環でもデッドロックでもない、普通の依存関係ですね☺

⚓Rails

⚓スライド: amatsudaさんの「Rails Performance issues and Solutions」


つっつきボイス:「この間の公開つっつきでこちらのスライドを取り上げるのを忘れていました😅」「そうそう、この間の銀座Rails#13↓でamatsudaさんが発表したRailsパフォーマンス話もこれに含まれていると思います😋」「スライドはその後のRubyConf Indonesia 2019でのものだそうです」「つか銀座Railsのスライドはそっち用のドラフトだったのかも😆」「銀座Railsのときにも資料書いてる途中だって言ってましたね☺

銀座Rails#13で「出張Railsウォッチ」発表させていただきました

「このスライドでもRails 6でのrails newでは--skip-spring--skip-bootsnapしてますが↓、銀座Railsのときにも--skip-action-textでAction Textをスキップすることが重要という話がありましたね😆」「え?😅」「今のAction Textが半端なく重いのでとりあえず無効にしておきましょう、だそうです😆

「銀座Railsのときにマジで面白かったのはこの辺の計測の話でした❤」「どんなお話でした?」「まあ実際のアプリでベンチマーク取るときはcurlとかでやればいいんですけど、Railsフレームワークのどこにボトルネックがあるのかを調べるときには、こんなふうに↓イニシャライザで直接モンキーパッチを当ててやる方がいいということでした」「おぉ〜!」「こうすればRackの処理の重さとかに影響されなくなるので純粋にRailsコードの速度を測れるようになるからいいよって😋」「これいいノウハウですね😍」「ここではprependで直接差し込んでます」

「Apache Bench(ab)とかで測定すると、どうしてもRails以外のボトルネックも出てきたりしてしまいますが、この方法ならRailsのコードだけにフォーカスできますね👍」「あとはアタッチする場所を少しずつ変えて試す↓: パッチのコードは他の場所でもほぼそのまま使い回せるので、うまい方法だと思います😋」「いいですね〜😋

参考: Apache Benchでサクッと性能テスト - Qiita

「次がRailsのアロケーションを削減する話」「『先週の改修』でもamatsudaさんがやってるのを見てきましたね」「こうやってProf.memしてプロファイラの結果を眺めて、これはちょっと多くね?と思ったところをひたすら地道に直していくというのをやってるそうです↓」「ふむぅ」

「そうそう、使ってないはずのAction Textエンジンがなぜかいるという話も↓😆」「おおっと😆

「その修正方法はというと、require 'rails/all'をやめてAction Textだけを除いたものを自力で書くと↓🤣」「🤣」「Action Textをrequireするだけで重くなるからRails 6ではこうするとこれだけアロケーションが減って速くなるよと」「何ということでしょう😆


rails newする段階でAction Textを入れないようにする方法ってありますよね?🤔」「あります」「でもたぶんapplication.rbでrequire 'rails/all'するとAction Textも入っちゃうんだと思います: というのもRailsのgemspecにはAction Textも入ってるはずなので、Gemfileでrequire "rails"と書かれている状態でbundle installした段階ではAction Textのgemも入ってくるはずで、その状態でrails newするとapplication.rbでrequire 'rails/all'となって入ってきちゃうと思います」「あ〜」

後で、ごく最近のRails 6で--skip-action-textなどを指定してrails newしたもので見てみるとapplication.rbでは自動的に除外されていますね↓😋rails newで何もスキップしない場合はrequire 'rails/all'になります。rails newした後から機能を外す場合は自分でrequire周りを変える必要がありますね。

require_relative 'boot'

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
# require "action_mailer/railtie"
# require "action_mailbox/engine"
# require "action_text/engine"
require "action_view/railtie"
# require "action_cable/engine"
# require "sprockets/railtie"
require "rails/test_unit/railtie"

参考: Rails 6.0.0 performance regression because of ActionText::Engine hook · Issue #36963 · rails/rails — open

「Action Textはrequireから外さないとアロケーションされてしまうらしい🤔」「amatsudaさんが#36963↑を投げてくれて『Rails 6.0.1で直ることを期待』と」「Action Text以外のオプション機能についてはrequireから外してもなぜかアロケーションは変わらないと」

↓こちらのissueも関連してそうです。

参考: ActionText forces all apps to include an ActionController::Base subclass · Issue #37183 · rails/rails — open


「後は細かいところだと=~よりmatch?の方が速いとか↓」「そういえば今のRubyはmatch?の方が速いんでした」「match?は余分なオブジェクトに触らない分速いとかそんな感じ」

「ハッシュもHash#mergeより添字アクセスのHash#[]=の方が速い↓」

「同じくHash#fetchよりHash#[] || defaultの方が速い↓」「上もそうだけど、前者はメモリがアロケーションされるけど後者はされないのね」「Hash#[]はCRubyのコアのところに直接つながっているはずだけど、Hash#mergeHash#fetchはメソッド呼び出しだから、たぶん仮引数代入とかが発生するんじゃないかしら🤔」「おぉ」

「まさにマイクロオプティマイゼーションを積み上げていく作業😳」「塵も積もれば5〜10%のアロケーション削減が見込まれると🎉」「このスライドはじっくり追いかけていくといろいろ発見がありますね❤

「この辺はテンプレートエンジンの話↓」「hamlは速いぜと😋」「k0kubunさんも記事にしてたヤツですね(ウォッチ20190925)」「stringの式展開にすると速いという話とか」

「amatsudaさんによると実はI18nが重いんですよ↓」「おぉ?」「I18nだけは式展開にできないのでどうしてもメソッド呼び出しが残っちゃう」「なるほど!」

「なのでロケールごとにI18nのキャッシュを作って高速化しようとしてるそうです↓: 銀座Railsで見たときはまだ作業中と言ってたかな」「これもすごく有用な話ですね😋」「こういう積み重ねでメモリアロケーションをじわじわ減らしていると」「すげ〜!」「Railsはメモリをめちゃ食うという印象があったりするので、こういうところが改善されていくのはいいですね〜☺


「上のツイート↑はこの間のウォッチでは裏を取れなくて見送った話でした」「そうそう、Ruby 2.6.4だとMonitor#synchronizeでRailsのベンチマークが遅くなるそうです↓」

参考: class Monitor (Ruby 2.6.0)

⚓Rails 6のAR associationとscopingの変更

ruby-jp Slackで知りました。


つっつきボイス:「上の記事はもともと論理削除が使われていたコードのつらみから始まったとのことで、今日はつっつきに出られないkazzさんにこの記事を見せたら苦笑いしてました😅

しかし、Rails 6では…
仕様が変更された為、先程のコードはそのままでは動きません。具体的には、

Blog.not_deleted.scoping do
  BlogComment.where(blog_id: 1).blog
end

nil を返しません。 BlogComment.blog のAssociation解決時に実行される Blog モデルへのSELECTクエリへのスコープがリセットされ、deleted_at IS NULL の条件が消える為です。
同記事より

参考: Association loading isn’t to be affected by scoping consistently by kamipo · Pull Request #35868 · rails/rails

「Rails 6では上のように変わってたそうです」「へ〜、たしかにassociationのタイミングで違うものを返されるとビビるからマジ止めてほしい😭」「associationの読み込みはdefault_scopeでは有効だけどunscope以外の普通のスコープだと有効じゃなくなったとは😳」「そういえばRailsアップグレードガイド↓でこの変更を翻訳した覚えがありませんでした」

参考: Rails アップグレードガイド - Rails ガイド

「まあscoped chainはやりすぎるとつらくなるし😢」「やっぱこういうのはテストを書いておくべきだな〜: でないとこういうbreaking changesが起きたときに死ぬ😇」「踏んだときに原因がわかりにくそう😅

「記事はやむを得ずdefault_scopeで対応することにしてますね」「default_scope使わないぞキャンペーンもしつつ😆」「default_scopeは止めた方がいいかと😆

Railsのdefault_scopeは使うな、絶対(翻訳)


以下はつっつきの後で見つけたツイートです↓。

⚓Sidekiqが6.0.1で高速化(Ruby Weeklyより)


つっつきボイス:「10〜15%速くなったはず、とありますね」「タグを付けられるようになってる↓」「Pro版だとこれで絞り込めるそうです」

# changelogより
class MyWorker
  include Sidekiq::Worker
  sidekiq_options tags: ['bank-ops', 'alpha']
  ...
end

find_jobもかなり速くなってる↓」

zscan 0.179366 0.047727 0.227093 ( 1.161376)
enum 8.522311 0.419826 8.942137 ( 9.785079)

「結局Sidekiqをナマで使うことになるのかな〜: Active Job越しにやると機能が足りなかったりしますし😅」「そんな雰囲気ですね」「Rails way的には本来Active Jobでジョブワーカーを抽象化するんですけど、ジョブワーカーの種類によって機能が違うから特定のジョブワーカーの機能を使おうと思うとActive Job経由できれいにやろうとするより生のSidekiqを使うことになりそうかな〜🤔」「ラッパー越しだと靴の上から足を掻くみたいになっちゃうんですね😳」「単純な作業ならラッパー越しでもいいんですけど、ジョブの制御までやり始めるとそうなりがち😢」「うーむ」

「ジョブで名前空間を使いたいとかプライオリティ付きのキューとかは、ジョブワーカーによってできるものとできないものがあったと思いますし」「ジョブワーカーというとこのsidekiqの他にdelayed_jobと、あと何だっけ?」「思い出せない😅」「最近sidekiq使うこと多いし😆」「あ、resqueか!」

「sidekiqのリリースノートを見ると、なぜか『ダークモードに対応』ってありますけど😆」「sidekiqのWeb UIには割とお世話になりますし、マウントするだけでWeb UIを使えるのは便利😋」「『もっとイケてるデザイン募集』ともあります🤣」「たしかにあんまりイケてないけど🤣」「まああれで十分ではある☺

⚓ARモデル内でクエリロジックを共有する(Ruby Weeklyより)

# 同記事より
class User < ApplicationRecord
  # our class method from above
  def self.can_receive_alerts
    where(receives_sms_alerts: true).
      or(where(receives_email_alerts: true)).
      joins(:alert_configurations).
      distinct
  end

  # our new instance method which builds on the class method
  def can_receive_alerts?
    self.class.can_receive_alerts.where(id: id).exists?
  end
end

つっつきボイス:「自分も先週見つけていた記事でしたがRuby Weeklyに先越されました😢」「コードの再利用ですね☺: self.can_receive_alertsで作った条件をインスタンスメソッドでちょっと変えてスコープ的に再利用すると」「kazzさんともこの記事で雑談してたんですが今思い出せない😅」「クラスメソッドで使った条件にwhere(id: id).exists?を足してインスタンスメソッドでも使いたいという感じなので、そんなに難しい話ではないですね」「おぉ」「スコープにこういう感じで条件を付けることはよくあるので、インスタンスメソッドでその条件に合っているかを確認するのに同じことをもう一回書かずにやれるよということで☺

⚓sprockets 4.0.0がリリース

これもruby-jp Slackで知りました。キーワード引数周りの修正やアロケーション削減、脆弱性修正などが行われました。Ruby 2.5以降のみがサポート対象になりました。


つっつきボイス:「sprocketや〜😆」「Webpackに押されてるのかと思ったらいろいろやってるんですね」「お、JSのSource Mapが使えるようになってるし😋」「3.xにはなかったのか〜」

参考: WTF is a Source Map

  "version":3,
  "file":"application.js",
  "mappings": "AAAA;AACA;AACA;#...",
    "sources": [
      "jquery.source-56e843a66b2bf7188ac2f4c81df61608843ce144bd5aa66c2df4783fba85e8ef.js",
      "jquery_ujs.source-e87806d0cf4489aeb1bb7288016024e8de67fd18db693fe026fe3907581e53cd.js",
      "local-time.source-b04c907dd31a0e26964f63c82418cbee05740c63015392ea4eb7a071a86866ab.js"
    ],
    "names":[]
}

「manifest.jsも対応してるし!欲しかったものが割と増えてる気がする❤」「デフォルトでES6をサポートですって」

// app/assets/config/manifest.js
//
//= link application.css
//= link marketing.css
//
//= link application.js

「まあES6でやるならWebpackでいいんじゃね?とも思いますが😆」「Sprocketsで十分という人にはありがたそうですね」「RubyのコードでJavaScriptコードを生成したい人はSprocketsを使いたいでしょうね」「というと?」「JavaScriptのコードの中に、パーシャル的にRubyのコードが埋まっているようなコードを書きたいときとか: SprocketsならRubyのコンテキストで処理できるので😋」「おぉ〜」「まあ今となってはあまりやりませんし、manifest.jsがあるならそっちに入れるという手もありますし」

「Rubyエンジニアが多いプロジェクトなら、Rubyが気軽に使えるという意味でSprocketsがいいかも: WebpackにはRubyを処理させられないので(探せばあるかもしれませんが😆)」

「Reactとか使わないんだったらSprocketsでもよさそうですね🤔」「まあそれでもいいんですけど、要はフロントエンドのコードを誰がやるかがポイントかも😆: フロントエンジニアはSprocketsの面倒は見たくないでしょうから彼らが慣れているWebpackの方がいい、とかね」「ふ〜む」「フロントエンジニアにしてみれば、SprocketsのためだけにRailsを勉強するとなるとオーバーヘッドがヤバいですし😆」「たしかに😆」「逆にみんなRailsエンジニアならSprocketsでいいでしょうし、Webpackerがむしろわけわからんかもしれませんし」

⚓Ruby

⚓Ruby 2.7のキーワード引数変更


つっつきボイス:「例の#14183↓がウルトラ長くて私には要約できそうになくて😂」「またしかに😆」「まだ読めてませんが、この記事で端的にまとまってるといいなと願っています🙏: ちなみにこのblog.saeloun.comブログのRuby記事はいいですね😋

⚓ruby/specに新しいexpectationが追加

これもruby-jp Slackで見かけました。

describe "String#start_with?" do
  it "returns true only if beginning match" do
    "hello".start_with?('hel').should == true
  end
end

つっつきボイス:「ruby/specって何だろうと思ったら、Ruby自身のテストのためのものみたいです」「RSpecライクというか☺」「それを動かすのがMSpecというツールだそうです」「RSpecをいっぱい書いている人がRubyのspecも書きたい、とかそういう感じ?😆」「自分なら書かないかな〜😆」「言語のチェックならアサーションとかでやる方がシンプルになりそうですし☺

参考: ruby/spec: The Ruby Spec Suite aka ruby/spec
参考: ruby/mspec: RSpec-like test runner for the Ruby Spec Suite

「なおRSpecも3.9がリリースされたそうです」「お、システムspecのジェネレータができた❤」「今までなかったんですね?」「まあジェネレータがなかったというだけで、ディレクトリは前からありますし」「type: :systemで指定するのか〜」

⚓testrocket: インラインでテストを書きたい(Ruby Weeklyより)

# 同リポジトリより
require 'testrocket'
using TestRocket

# BASIC USAGE
# +-> { block that should succeed }
# --> { block that should fail }

+-> { Die.new(2) }
--> { raise }
+-> { 2 + 2 == 4 }

# These two tests will deliberately fail
+-> { raise }
--> { true }

# A 'pending' test
~-> { "this is a pending test" }

# A description
!-> { "use this for descriptive output and to separate your test parts" }

つっつきボイス:「テストを本編コードにインラインで書きたいそうです」「スーパーシンプルであると😆

「むむ、こういう書き方をする言語って他にもあった気がする!」「見覚えありますね🤔」「オプションつけて実行するとテストが走って、付けないと普通に動くみたいなの、あった」「何だったかな〜?」

「こういうのをうまく設計すれば、APIドキュメントとしても成立しないかなって思ったり」「いや〜邪魔😆」「邪魔😆」「そういう方向よりも、JetBrainsのIDEみたいにコードからテストコードに即ジャンプできるとかマウスオーバーでテストコードが見えるみたいな方がうれしい気がしますけどっ😆」「エンジニアにとってエディタの行数という貴重な有限のスペースを他に使いたくないですし😆

「こういうインラインなアサーションを書く言語…Javaにあった!」「あ〜そうだった!」

参考: - JUnit 実践講座 - シナリオベースのテストケースの書き方

// 同記事より
public class LoginFormTest extends TestCase
{
    public void test() throws Exception
    {
        LoginForm form = new LoginForm();

        form.setUserId("user1");
        form.setPassword("password1");

        form.execute();

        assertEquals("こんにちは,ユーザ1さん!", form.getMessage());
    }
}

⚓その他Ruby

⚓言語・ツール

⚓Bashヒストリーの便利ワザ


つっつきボイス:「!$は知らないな〜」「!:なんちゃらは使うこともあるかな」「!:0はコマンド自身で、!:1以降が引数になる」「知りませんでした😅

$ !:0 !:1 !:3 !:2
tar -cvf afolder.tar afolder

!$:hは知らないな〜」「前回のコマンドの引数の親ディレクトリを取れるらしい」「挙動がよくわからないうちにこの辺を使うのはコワいかも😅

$ tar -cvf system.tar /etc/system
 tar: /etc/system: Cannot stat: No such file or directory
 tar: Error exit delayed from previous errors.

$ cd !$:h
cd /etc

「コマンド履歴を当てにしすぎてると、ある日履歴が吹っ飛んで悲しい思いをしたりしますよね😅」「rsyncとかであるある😅

⚓その他

⚓awesome-for-beginners: 初心者に優しいオープンソースプロジェクトリスト


つっつきボイス:「Gobyの@st0012さんがここにGobyも登録したいと言ってて知りました: 初心者にとって敷居が低いプロジェクトの言語別リストだそうです」「Hanamiも入ってる🌸」「優しいというか比較的開かれたコミュニティという感じでしょうね☺

「Rubyのohai↓もリストにありますね」「ohaiって何でしょう?」「CPUとかメモリみたいな実行環境を詳しくプロファイリングするヤツですね」「へ〜!😳」「ChefとかAnsibleで、たとえばメモリの何割をconfigに使うかとか、Railsのワーカー数とかを動的に指定するときなんかに使えます😎

参考: About Ohai — Chef Docs

{
  "filesystem" => {
    "/dev/disk0s2" => {
      "size" => "10mb"
    },
    "map - autohome" => {
      "size" => "10mb"
    }
  },
  "network" => {
    "interfaces" => {
      "eth0" => {...},
      "eth1" => {...},
    }
  }
}

「Ohaiにとっては、新しい環境が出てきたときにそれを足してくれるだけでありがたいので、コミットしやすいんでしょうね☺」「なるほど!」「だからすご〜くマイナーなディストリとかの環境を追加すればそれだけで喜んでもらえますよきっと😋

「あれ?Gobyもリストに入ってますけど😆」「あホントだ!、いつの間に😳」「早業😆

⚓クンロクモデムが100万円の時代

遠い昔に雑誌で音響カプラの写真を見たとき、何に使うのかわからなかったのを思い出しました。

参考: 音響カプラ - Wikipedia


つっつきボイス:「この辺はおっさん話ですみません😅」「カプラはさすがに知らないわ〜😆」「電話の受話器に無理やりマイクとスピーカーをはめ込んでデータ通信するという原始的なヤツです😆

「そういえばモデムの時代にも、モデムの音を録音されるとパスワード情報を抜き取られるなんて話がありましたね🤣」「あ〜たしかに可能だ🤣」「モデムのピ〜ガガ〜って音にはデータが全部乗ってるから、録音しちゃえばパスワードも含めて復号できちゃいます☺」「デジタルデータを音にエンコード・デコードしてるだけだから、もともとそういうものですし😆

⚓番外

⚓今度はWi-Fiで


つっつきボイス:「この方面の研究は割と前からありますね☺」「ありますね〜☺」「そうでしたか!😳」「データソースは物珍しいけど内容はよくある感☺」「それを実際に実装して動かしたのはスゴい💪」「恣意的な部分も相当ありそうですが😆」「研究ってそんなもんです🤣」「一般性なくても『この人かどうかさえわかればいい』とかかもしれませんね😆


今回は以上です。

バックナンバー(2019年度第4四半期)

週刊Railsウォッチ(20191008後編)Ruby 2.7のInteger#[]でバイナリチェック、rubyzip gemは強力、13KBのJavaScriptゲームほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSなど)です。

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20191021)Rails 6でhas_many関連の修正やSprockets 4.0対応、Shrine 3.0がリリース、Minitestスタイルガイドほか

$
0
0

こんにちは、hachi8833です。スマホで確定申告できるようになるそうです。


つっつきボイス:「スマホで確定申告したい人っているんでしょうか?😆」「スマホとかタブレットでやらないと間に合わないシチュエーションはあるかもですね☺

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

⚓Rails: 先週の改修(Rails公式ニュースより)

今回はコミットリストから見繕いました。has_many関連の修正が目に付きました。

⚓(master)belongs_toからhas_manyへのinverseをサポート

# activerecord/lib/active_record/associations/belongs_to_association.rb#L111
-       # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
-       # has_one associations.
        def invertible_for?(record)
+         inverse = inverse_reflection_for(record)
+         inverse && inverse.has_one?
+         inverse_reflection_for(record)
        end
# activerecord/lib/active_record/associations/collection_association.rb#L288
+     def target=(record)
+       case record
+       when Array
+         super
+       else
+         add_to_target(record)
+       end
+     end

つっつきボイス:「belongs_toからhas_manyへのcollectionのinverseはありそうでなかったんですね😳」「ドキュメントの更新と実際の挙動が合ってなかったので修正したそうです」

「ところでこのr?という「合ってる?」表現↓、ちょっと便利かなと思いました😋」「right?をそこまで略すのはどうかと😆」「SNSのチャット感覚というか😆」「そのぐらいフルで書いてもいいのでは😆」「了解を『り』とか『りょ』と書くみたいな😆


#34533より

⚓(master)inverse_of:を指定したhas_manyのレコード追加で関連付けのコールバックが発火しないように修正

has_manyinverse_of:を追加した場合に関連付けのコールバックが走るのを止める。
上の#34533に関連して、has_manyリレーションでinverse_of:を静かに設定したい、つまり新たに追加したレコード用のロジックをトリガしたくない。
has_manyのinverseを設定すると既存のアプリケーションが壊れる可能性が非常に高いが、これが正しい動作。挙動を選べるようにするプルリクを別途投げて、アプリ側を調整する時間を取れるようにする。
同PRより大意

修正はわずか1箇所です↓。

# activerecord/lib/active_record/associations/collection_association.rb#L293
      def target=(record)
        case record
        when Array
          super
        else
-         add_to_target(record)
+         add_to_target(record, true)
        end
      end

つっつきボイス:「今回はhas_many関連の修正が多くて、特にこれは修正は1箇所だけですがbreaking changeになってますね」「ははぁ😳、こういう変更をバシッと入れるのがすごいな〜って」「さすがに影響大きそうなので、デフォルトではオフにするそうです↓」「使う側はこういう変更追いかけるの大変そう😅

以下がその後に入った、挙動を選べるようにする修正ですね。config.active_record.has_many_inversingという設定(デフォルトはfalse)が追加されています。


なお、以下の記事では「Rails 4.1以降ではinverseの自動検出機能がある」「inverseの自動検出はhas_manyhas_onebelongs_toでのみ効く」「関連付けにそれ以外のオプションを付けると自動検出されなくなる」ともあります。APIのActiveRecord::Associations::ClassMethodsの「Setting Inverses」にも同じ記述がありました。

参考: Rails 4.1+ automatically detects the :inverse_of an association - makandra dev

⚓Sprockets 4.0に合わせてテストスイートを修正


つっつきボイス:「先週取り上げたSprocketsのアップデート(ウォッチ20191015)に合わせてRails側も修正したようです」「ほほぉ〜😋

  • application.cssapplication.css.erbを両方使うのは適切ではなくなった(ナイス変更!)
  • //= link_directory ../javascripts .jsでJavaScriptをデフォルトで再追加することでリンクするのを廃止
    • (これはデフォルトにしたい気がするが、今はWebpackerが望ましいのでJSのテストのためだけに一応足しておいた)
  • アセットのデバッグモードが変わった
    同PRより大意

「ついでにこんな記事↓も見つけたんですが、Rails 6がSprockets 4.0に対応する前にSprocketsを使ってみて、上の修正と同じような箇所でハマったようです😇」「急ぎすぎ😆」「ついでにSprockets 3と4の違いについても追っていますね☺

⚓has_manyのeager loadingのエッジケースを修正

eager loadingの実行結果(のレコード)は重複解除される。
これはhas_manyのeager loadではできているが、できていない組み合わせのケースがあった。
同PRより大意

# activerecord/lib/active_record/relation/finder_methods.rb#L381
      def apply_join_dependency(eager_loading: group_values.empty?)
        join_dependency = construct_join_dependency(
          eager_load_values + includes_values, Arel::Nodes::OuterJoin
        )
        relation = except(:includes, :eager_load, :preload).joins!(join_dependency)

-       if eager_loading && !using_limitable_reflections?(join_dependency.reflections)
+       reflections = join_dependency.reflections + joins_values.map { |joins_value| reflect_on_association(joins_value) }.reject(&:blank?)
+       if eager_loading && !using_limitable_reflections?(reflections)
          if has_limit_or_offset?
            limited_ids = limited_ids_for(relation)
            limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
          end
          relation.limit_value = relation.offset_value = nil
        end
        if block_given?
          yield relation, join_dependency
        else
          relation
        end
      end
# activerecord/test/cases/finder_test.rb#1339
  def test_eager_load_for_no_has_many_with_limit_and_joins_for_has_many
    relation = Post.eager_load(:author).joins(:comments)
    assert_equal 5, relation.to_a.size
    assert_equal relation.limit(5).to_a.size, relation.to_a.size
  end

つっつきボイス:「またhas_many😆」「まただ〜😆」「SQLの結果では複数行になるけどinstantiateするときに1つになるのが本来で、エッジケースでそうならないバグがあったということか: テストコード↑と#37356の再現手順↓を見る方がわかりやすいかも🤔

# #37356より
class Post < ActiveRecord::Base
  has_many :comments
  has_one :author
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

class Author < ActiveRecord::Base
    belongs_to :post
end

class BugTest < Minitest::Test
  def test_in_batches_corner_case
    posts = 5.times.map do
      post = Post.create!
      post.comments << Comment.create!
      post
    end
    multiple_comments = posts[2]
    multiple_comments.comments << Comment.create!

    post_count = 0
    unbatched_query = Post.eager_load(:author).joins(:comments)
    unbatched_query.find_each(batch_size: 2) do |post|
      post_count += 1
    end
    assert_equal unbatched_query.to_a.count, post_count
  end
end

「eager loadingってこれですね↓」「deduplicateって口で言うの大変😅」「デデュプリケート😆

参考: eager loadingって何? - おもしろwebサービス開発日記

参考: ActiveRecordのincludes, preload, eager_load の個人的な使い分け | Money Forward Engineers’ Blog

Rails: JOINすべきかどうか、それが問題だ — #includesの振舞いを理解する(翻訳)

⚓キーが多数の場合のread_multi_entriesのパフォーマンスを改善

ActiveSupport::Cache::Store#read_multi_entriesを以下の3点について少々リファクタリングし、フェッチしたキーが増加したときのパフォーマンスを若干改善した。個人的には読みやすさも向上したと思う。

  • eacheach_with_objectに変更。これによってハッシュを冒頭で宣言して最後に返す必要がなくなった。
  • キャッシュエントリが見当たらない場合にローカル変数versionの算出を回避した。
  • 何もしない場合の条件を削除した。
    同PRより大意
================================== A few keys ==================================

Warming up --------------------------------------
          read_multi    12.831k i/100ms
     fast_read_multi    14.510k i/100ms
Calculating -------------------------------------
          read_multi    146.288k (±26.0%) i/s -    654.381k in   5.010593s
     fast_read_multi    172.428k (±25.9%) i/s -    783.540k in   5.023852s

Comparison:
     fast_read_multi:   172427.5 i/s
          read_multi:   146288.2 i/s - same-ish: difference falls within error


================================== Many keys ===================================

Warming up --------------------------------------
          read_multi   196.000  i/100ms
     fast_read_multi   279.000  i/100ms
Calculating -------------------------------------
          read_multi      1.984k (± 6.7%) i/s -      9.996k in   5.062818s
     fast_read_multi      2.823k (± 7.6%) i/s -     14.229k in   5.072824s

Comparison:
     fast_read_multi:     2823.1 i/s
          read_multi:     1984.3 i/s - 1.42x  slower
# activesupport/lib/active_support/cache.rb#L585
        def read_multi_entries(names, **options)
-         results = {}
-         names.each do |name|
-           key     = normalize_key(name, options)
+         names.each_with_object({}) do |name, results|
+           key   = normalize_key(name, options)
+           entry = read_entry(key, **options)
+
+           next unless entry
+
            version = normalize_version(name, options)
-           entry   = read_entry(key, **options)
-
-           if entry
-             if entry.expired?
-               delete_entry(key, **options)
-             elsif entry.mismatched?(version)
-               # Skip mismatched versions
-             else
-               results[name] = entry.value
-             end
+
+           if entry.expired?
+             delete_entry(key, **options)
+           elsif !entry.mismatched?(version)
+             results[name] = entry.value
            end
          end
-         results
        end

つっつきボイス:「割と読みやすいリファクタリングかなと思いました☺」「names.each do |name|names.each_with_object({}) do |name, results|に変えたことでresults = {}を書かなくてよくなったと、なるほど😋」「余分な条件も削除した」「そして1.42倍速くなって読みやすくなった🎉」「冒頭にresults = {}みたいな空の変数初期化を置くのって何となく悔しいですよね😆」「たしかに😆

⚓Rails

⚓Capistranoに対話処理を取り入れる(Hacklinesより)

# 同記事より
namespace :rails do
  desc "Start a rails console"
  task :console do
    exec_interactive("rails console")
  end

  desc "Start a rails dbconsole"
  task :dbconsole do
    exec_interactive("rails dbconsole")
  end

  def exec_interactive(command)
    host = primary(:web).hostname
    env = "RAILS_ENV=#{fetch(:rails_env)}" # add other ENV variables
    command = "cd #{release_path}; #{env} bundle exec #{command}"

    puts "Running command on #{host}:"
    puts "  #{command}\n\n"

    exec %(ssh #{host} -t "sh -c '#{command}'")
  end
end

つっつきボイス:「CapistranoはRuby製の自動化・デプロイツールでお馴染みですが、それに対話的処理を加えてみたという短い記事です」「Capistranoまだ使ったことなくて😅」「morimorihogeさんはCapistranoいいよって言ってました(ウォッチ20181210)」「お〜見てみます😋

「バッチでえいやする代わりに対話的にやりたいときもあるんでしょうか?」「デプロイってだいたいバッチでえいやが多い気もしますけど😆

「Rubyのは知りませんが、デプロイツールはいろいろ使いました☺」「ちなみにどんなのをお使いでした?」「古いところではmavenとか↓」「おぉ知りませんでした😳」「Apache Antもそうかなと思ったらこっちはビルドツールだった😆」「どちらもJava方面なんですね」「何しろ昔からあるので古いといえば古いかな〜👴」「mavenって辞書見ると『物知り、専門家、達人、玄人、通、目利き、大御所』とか強そうな意味が並んでる😆

参考: Apache Maven - Wikipedia


maven.apache.orgより

参考: Apache Ant - Wikipedia


ant.apache.orgより

⚓Railsのビューでstrftimeを直書きするのはたぶん間違い

# 同記事より: config/initializers/time_formats.rb
Date::DATE_FORMATS[:stamp] = "%Y%m%d" # YYYYMMDD
Time::DATE_FORMATS[:stamp] = "%Y%m%d%H%M%S" # YYYYMMDDHHMMSS

つっつきボイス:「この記事ではstrftimeをビューに直書きする代わりに、書式をグローバル定数に置いてるみたいなんですけど、それもどうなんだろうと思って」「お、最初自分もグローバル定数かと思ったけど、このDATE_FORMATSはどうやらRails組み込みの機能↓のようで、そこに:stampという独自の書式を追加していますね」「あ、そうでしたか😅」「グローバル定数で書くのは止めた方がいいけど、機能としてあるなら使っていいと思います😋

DATE_FORMATS    =   { short: "%d %b", long: "%B %d, %Y", db: "%Y-%m-%d", number: "%Y%m%d", long_ordinal: lambda { |date| day_format = ActiveSupport::Inflector.ordinalize(date.day) date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007" }, rfc822: "%d %b %Y", iso8601: lambda { |date| date.iso8601 } }
DATE_FORMATS    =   { db: "%Y-%m-%d %H:%M:%S", number: "%Y%m%d%H%M%S", nsec: "%Y%m%d%H%M%S%9N", usec: "%Y%m%d%H%M%S%6N", time: "%H:%M", short: "%d %b %H:%M", long: "%B %d, %Y %H:%M", long_ordinal: lambda { |time| day_format = ActiveSupport::Inflector.ordinalize(time.day) time.strftime("%B #{day_format}, %Y %H:%M") }, rfc822: lambda { |time| offset_format = time.formatted_offset(false) time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}") }, iso8601: lambda { |time| time.iso8601 } }

参考: Railsで日付/時刻のフォーマットを設定するTips - Rails Webook
参考: RailsのTime::DATE_FORMATS[:default]は変更しないほうがいい - Qiita

↑2番目の記事ではI18nの機能を使う方法も紹介されています。

⚓スクリーンキャスト: 13分でわかるRails tipsいろいろ(Ruby Weeklyより)


つっつきボイス:「Railsのちょっとした便利ワザ集なんですが、スクリーンキャストって見ます?」「いや〜自分は見ませんけど☺」(少し流し見)

「これが10分あまり続くんですね😅」「まあ10分ならまだ短い方かも: 1時間とかかかるのもざらにありますし😆

「ところでparameterってプラミターとかプウァミターみたいに発音してるんですね😳」「割とそんな感じですね: 「ラ」を強く言うのがポイント」「果物のプラムかと思っちゃいました😅」「あとSQLもあっちの人はだいたいスィークォー(sequel)と言ってます😆」「エスキューエルって言うのは日本人ぐらい?」「たまに言う気もしますけどsequel多いですね☺

なお一般的な意味のsequelは「(ドラマなどの)続編」です。


「スクリーンキャストで思い出したんですけど、昔大学の先輩がライブコーディングをやってみせたときになかなかうまくいかなくて、あれはある意味特殊な才能が必要なんじゃないかって話になりました😆」「あ〜たしかに」「本番でいろいろハプニングが起きうることを見越してライブコーディングするのはホント大変😭」「録画を流す方がまだやりやすいかも: 前にも紹介しましたけど、jnchitoさんは銀座Railsのときに録画を倍速再生して見せてくれました↓」「お〜」

⚓Shrine 3.0がリリース(Awesome Rubyより)


shrinerb.comより

もう3.0.1になっていますね。
コアの再設計以外はほとんどがプラグイン関連の変更のようです。

  • Shrine::Attacherを再設計
  • versionsプラグインを書き直し
  • mirroringプラグインを追加
  • ROMやHanamiとの統合を強化

つっつきボイス:「お、Shrineのアップグレードって前にもつっつきで見たような覚えありますね」「あ、アップグレード予告を取り扱ったような😅

↓こちらでした。

週刊Railsウォッチ(20190902)Ruby 2.6.4セキュリティ修正リリース、スライド「All About Ruby in 2019」、Shrine gem 3.0に入る新機能ほか

「ともあれShrineが3.0でだいぶ強力になったようです🎉」「サイトデザインが新しくなったという見出しがトップ😆」「Attacherというクラスの設計をActive Recordから切り離して他でも使えるようになったり、プラグインを追加更新廃止したり」「ところでなぜShrineという名前なんでしょうね😆」「どの辺が神社仏閣なのかわかりませんし😆

# 同記事より: 単独でも使えるように設計見直し
attacher = ImageUploader::Attacher.new
attacher.attach(file)
attacher.file #=> #<Shrine::UploadedFile>
attacher.url  #=> "https://my-bucket.s3.amazonaws.com/path/to/image.jpg"

shrine: 遺骨・遺物をおさめた箱、櫃(ヒツ)が原義


「Algolia?」「ShrineのサイトがAlgoliaで検索できるようになったと記事にありますね↓: AlgoliaについてはTechRachoにも記事がありますのでどうぞ😋

Rails: 高速リアルタイム検索API「algolia-search-rails」gem README(翻訳)

⚓その他Rails

⚓Ruby

⚓Minitestスタイルガイドが登場(Ruby Weeklyより)


minitest.rubystyle.guideより


つっつきボイス:「rubocopチームが作ってくれたそうですが、シンプルなのがいいなと思って😋」「たしかにあれっと思うぐらい短いですね」「RSpecのスタイルガイドは@willnetさんが日本語にしてくれています↓が、これと比べても短い」

参考: willnet/rspec-style-guide: 可読性の高いテストコードを書くためのお作法集

なお以下は中の人Batsovさんのブログです。rubocop-minitestは進行中のようです。

私はRSpecが大好きですが、Minitestのシンプルな設計にも大いに敬意を払ってます。
私たちRuboCopチームが現在rubocop-minitestに取り組み中であることをお知らせします。
同記事より大意

⚓active_hash: ハッシュをActive Recordモデルっぽくリードオンリーアクセス

ruby-jp Slackで見かけました。


つっつきボイス:「RubyのハッシュをActive Recordみたいにするgemで★700超えてますが、どんな人が使うのかな?🤔

こちらの記事では、都道府県のように更新されないデータをactive_hashで扱ってますね↓。

参考: active_hash[gem]でデータの入ったテーブル作成 - Qiita

ActiveHash::Baseを継承すると、ハッシュのキーがメソッドで生えてくるという」

# 同リポジトリより
class Country < ActiveHash::Base
  self.data = [
    {:id => 1, :name => "US"},
    {:id => 2, :name => "Canada"}
  ]
end

country = Country.new(:name => "Mexico")
country.name  # => "Mexico"
country.name? # => true

「クラスメソッドやインスタンスメソッドもいかにもActive Record風」「おほ、なるほどなるほど☺

# クラスメソッド
Country.all                    # => returns all Country objects
Country.count                  # => returns the length of the .data array
Country.first                  # => returns the first country object
Country.last                   # => returns the last country object
Country.find 1                 # => returns the first country object with that id
Country.find [1,2]             # => returns all Country objects with ids in the array
Country.find :all              # => same as .all
Country.find :all, args        # => the second argument is totally ignored, but allows it to play nicely with AR
Country.find_by_id 1           # => find the first object that matches the id
Country.find_by(name: 'US')    # => returns the first country object with specified argument
Country.find_by!(name: 'US')   # => same as find_by, but raise exception when not found
Country.where(name: 'US')      # => returns all records with name: 'US'
Country.where.not(name: 'US')  # => returns all records without name: 'US'
# インスタンスメソッド
Country#id          # => returns the id or nil
Country#id=         # => sets the id attribute
Country#quoted_id   # => returns the numeric id
Country#to_param    # => returns the id as a string
Country#new_record? # => returns true if is not part of Country.all, false otherwise
Country#readonly?   # => true
Country#hash        # => the hash of the id (or the hash of nil)
Country#eql?        # => compares type and id, returns false if id is nil

# 以下はメタプロで生える
Country#name        # => returns the passed in name
Country#name?       # => returns true if the name is not blank
Country#name=       # => sets the name

「こういう書き方ってしたい方ですか?」「いやぁ〜、今見たときはふむふむと思いましたけど、このgemが入ってきたらまた書き方覚えないといけないのかなって😅」「まあ業務コードに入れるなら確認取ってからでしょうね😆

「Rubyだとハッシュのキーを.で呼べないのが悲しいって言う人をそこそこ見かけたりしますけど、Ruby自身にこういうのは入れないのかな?🤔」「どうでしょう😆、求められているなら入るかもしれませんけど、私は.でハッシュキー呼べなくても別にいいじゃんって思いますし☺

後で探すと、frozen_recordという少し似た感じのgemがありました。

⚓Ruby 2.7のArrayにintersectionuniondifferenceが追加(RubyFlowより)

# 同記事より
[ "a", "b", "z" ].intersection([ "a", "b", "c" ], [ "b" ])  # => [ "b" ]
[ "a", "b", "c" ].union( [ "c", "d", "a" ] ) #=> [ "a", "b", "c", "d" ]
[ 1, 4, 7, 8, "a", :t ].difference([ 4, :t ]) #=> [ 1, 7, 8, "a" ]

つっつきボイス:「Ruby 2.7のArrayに集合論っぽいメソッドが入るそうです」「RubyのSet的なことがArrayでもできるようになったと🤔」「しょっちゅうは使わなくても、たまに欲しくなりそう😆

参考: class Set (Ruby 2.6.0)

⚓parallel: Rubyで並列処理(Ruby Weeklyより)

# 同記事より
# 2 CPUs -> work in 2 processes (a,b + c)
results = Parallel.map(['a','b','c']) do |one_letter|
  expensive_calculation(one_letter)
end

# 3 Processes -> finished after 1 run
results = Parallel.map(['a','b','c'], in_processes: 3) { |one_letter| ... }

# 3 Threads -> finished after 1 run
results = Parallel.map(['a','b','c'], in_threads: 3) { |one_letter| ... }

つっつきボイス:「parallelっていうgemは見たことありそうでなかったので」「このgemは前からありそうな雰囲気ですけど既に標準だったりしません?」「標準ライブラリのリストにはありませんでした」

後で調べると、parallel gemは2009年からありました。

「Rubyで並列的なことをやる方法って他にもいろいろありそうですけど?」「標準ライブラリにThreadとかFiberとかありますね↓😋」「なるほど」

参考: class Thread (Ruby 2.6.0)
参考: class Fiber (Ruby 2.6.0)

「そういえばちょっと前のQuoraで『現状のThreadは直したい』というMatzからの回答↓があったのを思い出しました」「並列系って自分もそもそもほとんどやってこなかったし、ちゃんと勉強してからやらないときっとハマりそう😭」「私もGo言語のgoroutimeまだ自分ではやれてないです😅

参考: Rubyを作り直すとしたら、変更を加える箇所はありますか? - Quora

「Active Recordでもやれるようです↓」「これでテストを速くできるならいいかも😋」「parallel gemのコントリビューターもめちゃ多いですね😳

# reproducibly fixes things (spec/cases/map_with_ar.rb)
Parallel.each(User.all, in_processes: 8) do |user|
  user.update_attribute(:some_attribute, some_value)
end
User.connection.reconnect!

# maybe helps: explicitly use connection pool
Parallel.each(User.all, in_threads: 8) do |user|
  ActiveRecord::Base.connection_pool.with_connection do
    user.update_attribute(:some_attribute, some_value)
  end
end

# maybe helps: reconnect once inside every fork
Parallel.each(User.all, in_processes: 8) do |user|
  @reconnected ||= User.connection.reconnect! || true
  user.update_attribute(:some_attribute, some_value)
end

後で調べると、同じ作者のparallel_testsというgem↓はだいぶ前にウォッチで取り上げたことがありました(ウォッチ20171117)。gemspecでもparallel gemがruntime dependencyになっています。

⚓その他Ruby



つっつきボイス:「川合史朗さんはScheme系言語のGaucheの作者ですね」「ガウシュ?ゴーシュ?😆」「セロ弾きのゴーシュとスペル同じですしゴーシュなのかも🤔」「この方はハワイ在住で俳優もやっていて、いくつか映画にも出演してるそうです🏝」「ソフトウェア開発者で俳優…なんと幸せな人生😊

参考: Gauche - Wikipedia
参考: 17. Gauche Schemeの基本デザインの選択理由、オブジェクトデータベース、浮動小数点数の落とし穴 (川合史朗) — 映画出演の話にも触れられています

⚓DB

⚓Oracleから独自RDBへ


つっつきボイス:「Oracleから自社製RDBMSへの乗り換えニュースを2つ連続で見かけました」「これスゴいですよね〜、自社製のRDBMSがどんなものか触ってみたい気がしますけど」「お、アリババの方はソース公開されてるって記事にありますね😳」「お〜、このoceanbaseというのがそれですか↓: 誰か動かしてみないかな😋」「Amazonの方は名前もわからず😢

ちなみにoceanbaseのドキュメントは中国語かつdocxのようです😭

「oceanbaseについて探してたら、dbdb.io↓というサイトが出てきた」「いろんなRDBMSを一覧できるカタログサイトのようですね☺


dbdb.ioより


以下はつっつき後に見つけました。

参考: 世界1位になったアリババの独自開発DB OceanBaseとは何者か? - ブログなんだよもん

⚓言語・ツール

⚓言語人気の移り変わり


つっつきボイス:「こういうのって単純に楽しいですね😊」「どこ由来のデータかは知りませんけど😆」「大昔はFortranとCobolが人気だったまではわかるんですが、Pascalが1位だった時代ってあったんですか?」「Mac OS 9までのMacだとTurbo Pascalが割と多かった気がします」「そうこうしているうちにC言語トップになって2000年過ぎたあたりからJavaが1位に」「PHP元気だな〜😆」「Rubyもトップテンに」「直近はPythonがトップ」

参考: Turbo Pascal - Wikipedia

⚓その他

⚓キーボードの方がでかい


つっつきボイス:「ぱっと見Wi-Fiアンテナ付きダムハブみたいですけど😆」「あ、これPCですか😳: このスペック(Core i7-8665U 1.9GHz、メモリ16GB)で18万…やばい猛烈に欲しくなってきた❤」「ハートに突き刺さる音が聞こえました😆」「NASつないじゃえば容量問題にならないし、う〜こっち買えばよかったか😭」「もう少しスペック下でもいいから軽くて安いのがあればDocker専用機にして持ち歩きたい😆」(以下延々)

⚓番外

⚓自然言語も移り変わる


つっつきボイス:「最後は言語学ネタで😆: 羽柴秀吉がファシンバ・フィンデヨシだったとか」「何を基準にしてたのかしら😅」「安土桃山時代のポルトガル人宣教師たちが作った日本語-ポルトガル語辞書で当時の発音がキャプチャされてたそうで、は行がɸ音(ph)だったとかかなり違ってますね」「ほぇ〜😳」「ついでに、日本語の濁点や半濁点も、その宣教師たちが必要に迫られてこしらえたのが後に日本語でも定着しちゃったんだそうです」「逆輸入😆

参考: 日葡辞書 - Wikipedia

「そうなるとポルトガル人の発音も今と昔で違ってるでしょうから、そういうフィルタの影響を取り除いて研究するのは大変そう😆」「たしかに😆」「日本人もかつてイギリスをエゲレスとかアメリカンをメリケンとか言ってましたし、外人が日本人の名前を呼ぶときに訛るのと似てるようにも見えますけどね😆」「英語圏の人は『こんにちは』を『コニーチワ』って発音しがちですけど、その辺に通じるかも」「n音が連続するとつながるんでしょうね☺」(以下延々)

江戸時代の長崎出島の通詞(=通訳)が幕末にオランダに渡ったとき、現地の人から「まるでうちのおじいさんが話しているようなオランダ語だ」と妙に感激されたという話を思い出しました。鎖国のせいでタイムカプセル的に保存されてたんでしょうね。

「自然言語もこんなに変わるんだからプログラミング言語も長年の間にがらっと変わったりするかなと」「いやいやプログラミング言語は発音関係ありませんから😆


今回は以上です。

バックナンバー(2019年度第4四半期)

週刊Railsウォッチ(20191015)スライド「Rails Performance issues and Solutions」を見る、dirtyに*_previously_was が追加、Sidekiq 6.0.1ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Publickey

publickey_banner_captured

週刊Railsウォッチ(20191028前編)RailsにSTI用メソッドsti_class_forとpolymorphic_class_forが追加、RuboCopを変更箇所だけにかけるgem、strftime書式生成サイトほか

$
0
0

こんにちは、hachi8833です。Blawn言語が盛り上がってますね。


つっつきボイス:「15歳でプログラミング言語を作ったという話題で、早速Qiitaにもやってみた記事が出てました」「しかもC++で😳」「少し追ってみたところでは、生まれたての言語らしく小さなバグがちょこちょこある様子で、今後を見守っていきたい感じですね☺」「言語を作る人が増えてきっとMatzは内心とても嬉しかったと思います😋

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

⚓お知らせ: 週刊Railsウォッチ「第16回公開つっつき会」(無料)

第16回目公開つっつき会は、11月14日(木)19:30〜にBPS会議スペースにて開催されます。今回は第一木曜日ではありませんのでご注意ください。

週刊Railsウォッチの記事にいち早く触れられるチャンスです!発言も自由です。皆さまのお気軽なご参加をお待ちしております🙇

⚓Rails: 先週の改修(Rails公式ニュースより)

今回はmasterのコミットリストから見繕いました。

⚓ActiveSupport::SafeBufferが6.0で動かない問題を修正

# activesupport/lib/active_support/core_ext/string/output_safety.rb#L298
      def set_block_back_references(block, match_data)
        block.binding.eval("proc { |m| $~ = m }").call(match_data)
+     rescue ArgumentError
+       # Can't create binding from C level Proc
      end

#34405with_indexメソッドと互換性がないらしい。
37422より大意

# #37422より
ActiveSupport::SafeBuffer.new('aaa').gsub!(/a/).with_index {|m,i| i }

# 期待する出力: 012
# 実際の出力: ArgumentError

つっつきボイス:「このwith_indexは、each_with_indexとまた別のメソッドでしたね」「こんなメソッドがあったんですね😳」「indexのオフセットも指定できるけど指定しないとゼロから始まるとかそんな感じで☺」「Enumeratorでインデックスを使いたいときに便利そうですね☺」「修正はrescue ArgumentErrorを追加して終了」

参考: instance method Enumerator#with_index (Ruby 2.6.0)
参考: with_indexが便利だという話とstable_sort_by - Qiita

⚓新機能: STI用メソッドをActiveRecord::Inheritanceのpublic APIに追加

追加したメソッドを使ってSTIやポリモーフィック関連付けをextendできる。
クラスをリネームしてクラス名がデータベース内のデータとマッチしなくなったときに有用。
自分のモデルに以下のメソッドを実装すると、既に存在しなくなったクラスの名前でレコードを読み込めるようになる。以下はシンプルな実装例。
同PRより

# 同PRより
class Animal < ActiveRecord::Base
  @@old_names = {
    "Lion" => "BigCat"
  }
  def self.sti_name
    name = super
    @@old_names[name] || name
  end

  def self.sti_class_for(type_name)
    @@old_names.inverse[type_name]&.constantize || super
  end
end

つっつきボイス:「STI関連の機能追加だそうです」「extendできるとはエグい😆」「STIでクラス名が変わっても読み込めると: テーブルは変わらない前提ですねなるほど ☺

参考: 5 シングルテーブル継承 (STI)– Active Record の関連付け - Rails ガイド

「上の実装例を見るとsti_class_forというAPIが追加されたようなので、例でやっているように継承して使うという意図でしょうね☺」「self.sti_nameもオーバーライドしておく必要があるのか、へ〜」「sti_nameは前からあったみたい」「これが追加されたメソッドのコード↓」

# activerecord/lib/active_record/inheritance.rb#173
+     # 指定されたtype_nameに対応するクラスを返す
+     #
+     # 継承カラムに保存された値に対応するクラスを探索するのに使われる
+     def sti_class_for(type_name)
+       if store_full_sti_class
+         ActiveSupport::Dependencies.constantize(type_name)
+       else
+         compute_type(type_name)
+       end
+     rescue NameError
+       raise SubclassNotFound,
+         "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " \
+         "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " \
+         "Please rename this column if you didn't intend it to be used for storing the inheritance class " \
+         "or overwrite #{name}.inheritance_column to use another column for that information."
+     end

「実装例のLionBigCat、どっちがoldでどっちがnewだろう?🤔」「これだけだとわかりにくいけど、たぶんLionがnewでBigCatがoldなのかな〜」「constantizeは文字列からクラスやモジュールを生成するメソッドでしたね」

参考: constantize — ActiveSupport::Inflector

「なるほど、こう書けばクラス名が変わった場合にデータベースに入っている前のクラス名をアップデートしなくてもよくなると」「さすがに変えてからそのまま放置はしないでしょうね😆」「それやったらカオス確定😆: リネーム前のクラスが存在しなくなってるから後でコード読んだ人がパニクるし😇」「たとえば、いったんこの書き方でしばらく運用して、うまくいくようなら本格的にクラス名を移行するという使い方ができそうですね☺」「なるほど!」

「あるいは、クラスからクラス名を取れるAPIが前からあるなら、その逆にクラス名からクラスを取れるAPIもあった方がいいよね、という発想で作られたのかもしれないと今思いました☺」「そういう見方もあるのか😳」「コードのコメントに『このconstantizeはZeitwerkとコンパチなの?』『大丈夫、decorateしてある』というやり取りもありますね」

「これが2つ目に追加されたAPI↓: polymorphic_class_for

# activerecord/lib/active_record/inheritance.rb#L192
+     # 指定されたnameに対応するクラスを返す
+     #
+     # ポリモーフィックtypeカラムに保存された値に対応するクラスを探索するのに使われる
+     def polymorphic_class_for(name)
+       name.constantize
+     end

「そして既存のfind_sti_classsti_class_forを使う形に変わっている↓」「つかsti_class_forに切り出して委譲した形か」「たしかにfind_sti_classはオーバーライドしたくないし😆」「切り出したことで柔軟になった感じですね😋

# activerecord/lib/active_record/inheritance.rb#L251
        def find_sti_class(type_name)
          type_name = base_class.type_for_attribute(inheritance_column).cast(type_name)
-         subclass = begin
-           if store_full_sti_class
-             ActiveSupport::Dependencies.constantize(type_name)
-           else
-             compute_type(type_name)
-           end
-         rescue NameError
-           raise SubclassNotFound,
-             "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " \
-             "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " \
-             "Please rename this column if you didn't intend it to be used for storing the inheritance class " \
-             "or overwrite #{name}.inheritance_column to use another column for that information."
-         end
+         subclass = sti_class_for(type_name)
+
          unless subclass == self || descendants.include?(subclass)
            raise SubclassNotFound, "Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}"
          end
+
          subclass
        end

⚓Active Storageのコンパイル済みJSをソースと同期

# activestorage/test/javascript_package_test.rb
+# frozen_string_literal: true
+
+require "test_helper"
+
+class JavascriptPackageTest < ActiveSupport::TestCase
+  def test_compiled_code_is_in_sync_with_source_code
+    compiled_file = File.expand_path("../app/assets/javascripts/activestorage.js", __dir__)
+
+    assert_no_changes -> { File.read(compiled_file) } do
+      system "yarn build"
+    end
+  end
+end

Active Storageのコンパイル済みJSがソースコードと同期しない問題を追った。
これを修正するためにActive Storageのコンパイル済みJSバンドルとソースを強制的に同期するようにした。同期していればテストはパスし、していなければfailする。
同PRより大意


つっつきボイス:「これはテストコードの割と小さな修正ですね☺

⚓ActiveModel::Error訳文参照でインデックス付き属性探索をサポート

このPRの意図は、ActiveModel::Errorのインスタンスでattributeがインデックス付きattributeの場合にインデックス付きsuffixなしでメッセージを探索できるようにすること。これはindex_errors: trueを使うActiveRecordモデルによくある。
#37447より大意

# 同PRより
class Manager < ActiveRecord::Base
  has_many :reports, index_errors: true
end

class Report < ActiveRecord::Base
  belongs_to :manager
  validates_presence_of :name
end

manager = Manager.new
invalid_report = Report.new
manager.reports = [invalid_report]
manager.save

error = manager.errors.first
error.attribute
# => :"reports[0].name"
error.type
# => :presence

上のようにattributeがインデックス化されているとする。これはReportレコードの場合はよいが、カスタム訳文メッセージの場合にはインデックスがあるためにうまくいかない。このPRでは探索キーからインデックスを削除する探索エントリを追加することで、インデックスなしで訳文を探索できるようになる。
#37447より大意


つっつきボイス:「エラーのI18n関連の修正だそうで、2つPRがあります」「Reportだとreports[0]みたいなメッセージ参照が生成されるけど、カスタムメッセージのときにインデックスが邪魔だったのね」「1つめのPRの修正は以下のように[インデックス]を削除しただけでした↓」「お〜、attributeからインデックス指定を削るだけでやれちゃうのか😳」「これでインデックスを気にせず書けるようになる😋

attribute = attribute.to_s.remove(/\[\d\]/)

「で2つ目のPRではさらに上の修正の不足部分が修正されていました↓」「おっと+が漏れてた😆」「あやうく1桁インデックスしか処理できないところだった〜😆」「プルリクが2つ並んでたので気づきました☺

# activemodel/lib/active_model/error.rb#L20
-       attribute = attribute.remove(/\[\d\]/)
+       attribute = attribute.remove(/\[\d+\]/)

⚓関連付けリレーションでのインスタンス作成でunscopeが効くように修正

#35868の意図は、関連付けの読み込みを一貫させることにある。関連付けのリレーションでのインスタンス作成は副作用を伴うべきではない。
同PRより大意

上の#35868はウォッチ20191015でも話題にしました。

# activerecord/lib/active_record/association_relation.rb#L18
-   def build(*args, &block)
+   def build(attributes = nil, &block)
      block = _deprecated_scope_block("new", &block)
-     scoping { @association.build(*args, &block) }
+     @association.enable_scoping do
+       scoping { @association.build(attributes, &block) }
+     end
+   end
    alias new build

-   def create(*args, &block)
+   def create(attributes = nil, &block)
      block = _deprecated_scope_block("create", &block)
-     scoping { @association.create(*args, &block) }
+     @association.enable_scoping do
+       scoping { @association.create(attributes, &block) }
+     end
    end

-   def create!(*args, &block)
+   def create!(attributes = nil, &block)
      block = _deprecated_scope_block("create!", &block)
-     scoping { @association.create!(*args, &block) }
+     @association.enable_scoping do
+       scoping { @association.create!(attributes, &block) }
+     end
    end

つっつきボイス:「@kamipoさんによる修正です」「これだけだとわかりにくいので、issue #37138を見てみると、unscopeしたはずの関連付けでunscopeが無視されるという現象が起きてたそうです」「うひゃぁ😱」「そもそもunscope使うなと言いたいけど😆

期待される動作: new_comment = post.comments.unscope(where: :visible).newでデフォルトスコープがunscopeされるので、new_comment.visibleはfalseになる(そのフィールドのデータベースデフォルト値)
実際の動作: new_comment = post.comments.unscope(where: :visible).newのunscopeが効かず、default_scopeによってnew_comment.visibleがtrueになる

「以前訳した記事↓でも『default_scopeは使うな』というのがありました」「まあdefaultのに限らずunscopeしたくなることはまれになくもないんですけど、unscopeするぐらいならもう一回ビルドし直す方がいいんじゃね?って思いますし😆」「😆

Railsのdefault_scopeは使うな、絶対(翻訳)

「再現コードでdefault_scopeは…あるか↓😇」「出たな😆」「visibleをVisibleCommentみたいにラップしちゃうなら、そっちの方でdefault_scopeを使うのは自分は別にいいと思いますし」「ふぅむ🤔

# #37138より
class Comment < ActiveRecord::Base
  belongs_to :post
  default_scope -> { where(visible: true) }
end

「そして修正↓はというと、attributes = nil…?」「従来の*argsは実はattributesで、*は付いてたけどarrayじゃなくてhashを取るという前提だったらしい🤔

# activerecord/lib/active_record/association_relation.rb#L18
-   def build(*args, &block)
+   def build(attributes = nil, &block)
      block = _deprecated_scope_block("new", &block)
-     scoping { @association.build(*args, &block) }
+     @association.enable_scoping do
+       scoping { @association.build(attributes, &block) }
+     end
+   end
    alias new build

「元の*argsという引数名はふわっとしてて少々雑な感じではありますね😆」「引数名をattributesに変えつつ、何も渡されなかった場合に明示的にnilが渡るようにしたのか」「ここは意味は変わらないはずだからリファクタリングですね☺」「修正後はscoping { @association.create!(*args, &block) }enable_scoping↓のブロックで囲んでいて、これがキモかな」「難しいコードや😭」「さすが@kamipoさん」

# activerecord/lib/active_record/associations/association.rb#L46
+       @enable_scoping = false
...
+     def enable_scoping
+       @enable_scoping = true
+       yield
+     ensure
+       @enable_scoping = false
+     end

「以下がテストコード↓」「unscopeしてbuildしたらbulb.nameがnilになるのが正しいと」

# activerecord/test/cases/associations/has_many_associations_test.rb#241
+ def test_build_and_create_from_association_should_respect_unscope_over_default_scope
+   car = Car.create(name: "honda")
+
+   bulb = car.bulbs.unscope(where: :name).build
+   assert_nil bulb.name
+
+   bulb = car.bulbs.unscope(where: :name).create
+   assert_nil bulb.name
+
+   bulb = car.bulbs.unscope(where: :name).create!
+   assert_nil bulb.name
  end

「引数に*argsとか書くとレビューでツッコまれます?」「そうとも限らなくて、昔のRailsでは割と見かける書き方でしたし、引数を触らずに委譲したいコードだと*argsって書いたりしてましたね😋」「おぉ」

⚓Rails

⚓Rails 6のAction Cableテスト機能

# 同記事より
require "test_helper"

class PublishCommentaryJobTest < ActionCable::Channel::TestCase
  include ActiveJob::TestHelper

  # `assert_broadcast_on` asserts exact message sent on a channel stream.
  test "publishes commentary" do
    perform_enqueued_jobs do
      assert_broadcast_on(CommentaryChannel.broadcasting_for('match_1'), comment: "Hello and welcome everyone!!") do
        PublishCommentaryJob.perform_later(1, "Hello and welcome everyone!!")
      end
    end
  end

  # `assert_broadcasts` asserts the number of messages sent to stream
  test "asserts number of messages" do
    perform_enqueued_jobs do
      PublishCommentaryJob.perform_later(1, "Hello and welcome everyone!!")
      assert_broadcasts CommentaryChannel.broadcasting_for('match_1'), 1
    end
  end

  # `assert_no_broadcasts` asserts no messages sent to stream
  test "no comment published if invalid match id" do
    perform_enqueued_jobs do
      PublishCommentaryJob.perform_later(-1, "Hello and welcome everyone!!")
      assert_no_broadcasts CommentaryChannel.broadcasting_for('match_1')
    end
  end
end

つっつきボイス:「上の記事に出てくるaction-cable-testingはたしかEvil Martiansの人が作ったgemでした」「ほほぅ☺」「このgemはRails 5で入りそこなったけどRails 6でマージされたと以下の翻訳記事にあります😋

Rails 6のB面に隠れている地味にうれしい機能たち(翻訳)

「Action Cableのテスト書いたことないんで、そもそも書けるのか?ってちょっと思いましたけど😆」「テストを自力で書こうとすると難しそうですね🤔

  • 接続テスト
  • チャネルのテスト
  • ブロードキャストのテスト

参考: ActionCable::Connection::TestCase

⚓レガシープロジェクトでRuboCopを使う

抜粋:

  • .rubocop_todo.ymlを使わない方針で進める
  • 過去のコードよりも今後のコードチェックを重視
    • 古いコードベースをずっとそのままにするということではない
  • 古いコードベースで当面わずらわされないようにするgemの紹介

つっつきボイス:「レガシーコードになるべく触らずにコードチェックする系の記事で、以下のgemも紹介されています」

⚓Pront

「その中でprontoというgemは変更部分だけをRuboCopとかでチェックできるようにするそうです↓」「GitHubにレビューコメント付けてくれる😋」「つまり修正のプルリク作るところまでやってくれるってことですよね😋」「prontoは自体はランナーを呼ぶようですが、RuboCopやさまざまなlintのランナーがずらりとありますね😍


prontolabs/prontoより

「GitLabでも動くのかな?」「お、GitLabCIもやれるとあります❤: 割と試しやすそうですし社内のGitLabで使ってみません?」

# GitLabCI用config例
lint:
  image: ruby
  variables:
    PRONTO_GITLAB_API_ENDPOINT: "https://gitlab.com/api/v4"
    PRONTO_GITLAB_API_PRIVATE_TOKEN: token
  only:
    - merge_requests
  script:
    - bundle install
    - bundle exec pronto run -f gitlab_mr -c origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME

参考: Prontoでソースレビュー自動化 - アクトインディ開発者ブログ

⚓Overcommit

「overcommit gemはコミット時にローカルでGit Hooksを起動して必須の処理を回すんだったかな」「社内でも誰か使ってた覚えあります」「設定を欲張り過ぎるとコミットのたびにうるさく鳴き出しそう😆

overcommitでmasterへのプッシュを禁止するとかもできるそうです↓。

参考: git hooksをovercommitで管理して作業効率の底上げを狙う - LCL Engineers’ Blog

⚓rubocop_lineup

「rubocop_lineupは新しいせいか日本語記事まだないですね」「ブランチでmasterとの差分行だけをRuboCopでチェックする拡張だそうです」

rubocop_lineupになぜこの写真↓?と思って調べると、lineupには『面通しのために並ばせた容疑者の列』という意味もあるんだそうです(米国のみ)。毎度邪魔になるメッセージを常習犯に見立てた感じですね👮🏼‍♀️


mysterysci/rubocop_lineupより

⚓active_merchant: 支払いサービスの抽象化ライブラリ(Ruby Weeklyより)

# 同サイトより
# ゲートウェイのテストサーバーにリクエスト送信
ActiveMerchant::Billing::Base.mode = :test

# クレジットカードオブジェクトの作成
credit_card = ActiveMerchant::Billing::CreditCard.new(
  :number     => '4111111111111111',
  :month      => '8',
  :year       => '2009',
  :first_name => 'Tobias',
  :last_name  => 'Luetke',
  :verification_value  => '123'
)

if credit_card.valid?
  # TrustCommerceへのゲートウェイオブジェクトを作成
  gateway = ActiveMerchant::Billing::TrustCommerceGateway.new(
    :login    => 'TestMerchant',
    :password => 'password'
  )

  # 10ドル(1000セント)を認証
  response = gateway.authorize(1000, credit_card)

  if response.success?
    # 金額をキャプチャ
    gateway.capture(1000, response.authorization)
  else
    raise StandardError, response.message
  end
end

Active MerchantはeコマースシステムShopifyの抜粋です。このライブラリの主な設計原則は、ShopifyのシンプルAPIや統合APIの要件を用いて、内部APIが大きく異るさまざまな支払いゲートウェイにアクセスすることです。
同リポジトリより

TechRachoにも記事がありました↓。

ActiveMerchant を使ってPayPal Express Checkout の与信取得と回収機能を導入する


つっつきボイス:「支払いを抽象化するという触れ込みのactive_merchantはかなり前からあるみたいで、今も熱心にメンテされているようです」「使ったことなかった😆」「対応している支払いサービスもたくさんあって、当然のようにStripeにも対応してますね😋↓」「StripeにJP含まれてませんけど😆


同リポジトリより

Stripe決済を自社サービスに導入してわかった5つの利点と2つの惜しい点

⚓foragoodstrftime.com: strftimeの書式生成サイト


つっつきボイス:「strftimeの書式をさっと調べられるサイトです」「これは覚えられないヤツ😆」「要素をドロップして並べられるとか、随分凝ったサイトですね😆」「まあそこまでせんでも😆」「作ってみたかったんですよきっと☺

[Ruby/Rails] strftimeのよく使うテンプレート

なお、以下は以前もウォッチで紹介した同様の趣旨のサイトです。


前編は以上です。

バックナンバー(2019年度第4四半期)

週刊Railsウォッチ(20191021)Rails 6でhas_many関連の修正やSprockets 4.0対応、Shrine 3.0がリリース、Minitestスタイルガイドほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20191105前編)Rails 6のデフォルト設定解説、DHHも消したいaccepts_nested_attributes_for、スライド『実践Railsアプリケーション設計』ほか

$
0
0

こんにちは、hachi8833です。今年の3連休は昨日のでおしまいだそうです。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

⚓お知らせ: 週刊Railsウォッチ「第16回公開つっつき会」(無料)

第16回目公開つっつき会は、来週11月14日(木)19:30〜にBPS会議スペースにて開催されます。今回は月初ではありませんのでご注意ください。

週刊Railsウォッチの記事にいち早く触れられるチャンス!発言・質問も自由です。引き続き皆さまのお気軽なご参加をお待ちしております🙇

⚓Rails: 先週の改修(Rails公式ニュースより)

今週は平和に公式情報からです。

⚓Active Storage blobのパーマネントURLを公開可能に

blob向けのパーマネントURL。
configurations.ymlでサービスのキーにpublic: true | falseを設定することで、サービスのblobをpublicまたはprivateにできる。publicなサービスは常にパーマネントURLを返すようになる。
Blob#service_urlは非推奨になり、Blob#urlが推奨される。
changelogより

# activestorage/test/service/s3_public_service_test.rb
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+require "net/http"
+require "database/setup"

+if SERVICE_CONFIGURATIONS[:s3]
+  class ActiveStorage::Service::S3PublicServiceTest < ActiveSupport::TestCase
+    SERVICE = ActiveStorage::Service.configure(:s3_public, +SERVICE_CONFIGURATIONS)
+
+    include ActiveStorage::Service::SharedServiceTests
+
+    test "public URL generation" do
+      url = @service.url(@key, filename: +ActiveStorage::Filename.new("avatar.png"))
+
+      assert_match(/.*\.s3\.amazonaws\.com\/.*\/#{@key}/, url)
+
+      response = Net::HTTP.get_response(URI(url))
+      assert_equal "200", response.code
+    end
+  end
+else
+  puts "Skipping S3 Public Service tests because no S3 configuration was supplied"
+end

つっつきボイス:「issue #31419の、Active StorageのService APIからファイルへのアクセスも許可したいという流れで入ったPRだそうです」「ほほう😋」「パーマネントURLって何を指してるのかな?🤔」「#31419にいいね👍が48個もついてるのでみんな欲しがってるっぽい😆

ActiveStorage::Serviceの現時点のAPIではurlメソッドでしかリンクを取得できず、返されるpublic URLはほとんどのサービスでは同じタイムフレーム内で期限切れになる。サービスからファイルオブジェクトにもアクセスできるfileメソッド的なものがあれば、ファイルの公開方法をより柔軟にできて便利になると思われる。
#31419より大意

「#36729を見ると(ストレージ)プロバイダのpublic URLでは一般にファイル名をカスタマイズできないので、現状はpublic bucketとprivate bucketのディレクトリ構造が違ってしまっている: この修正ではconfigで設定すればS3やAzureやGCSで/キー/ファイル名の固定URLで統一的にアップロードして後でそのURLでダウンロードできるようにする、という感じのようです🤔」「まだActive Storageちゃんと使ってないけど、あるとうれしい機能らしいということはわかった😆

⚓has_manyのinverseが設定可能になった

この間取り上げた#34533の続きだそうです(ウォッチ20191021)が、#37413は取り上げてませんでした。

# activerecord/lib/active_record/railtie.rb#L29
    config.active_record.use_schema_cache_dump = true
    config.active_record.maintain_test_schema = true
+   config.active_record.has_many_inversing = false

つっつきボイス:「先週はhas_many関連が豊作でしたけど😆#34533で入ったhas_manyのinverseを利用できるようにするかどうかを設定で選べるようになったそうです(デフォルトはfalse)」「has_manyでinverseできるのが本来だけどbreaking changeになるから設定を増やしたのね☺

⚓inflectorが:zeitwerkモードでオーバーライド可能になった

# activesupport/lib/active_support/dependencies/zeitwerk_integration.rb#L56
      module Inflector
+       # Concurrent::Map is not needed. This is a private class, and overrides
+       # must be defined while the application boots.
+       @overrides = {}

-       def self.camelize(basename, _abspath)
+         basename.camelize
+         @overrides[basename] || basename.camelize
+       end
+
+       def self.inflect(overrides)
+         @overrides.merge!(overrides)
+       end
      end

つっつきボイス:「Active SupportのinflectorはRailsで名前の単数形複数形みたいな活用形(inflection)を制御するヤツですね☺」「@overridesというハッシュがあればオーバーライドできると」「特殊な活用形をここに入れられる感じですね😋」「basenameがあればよし、なければcamelizeすると」

「Railsガイドの更新↓を見るとautoloaderでこうやって活用形を定義できるとありますね」「html_parserHTMLParserに変換するヤツわかる〜😆」「HtmlParserだと違う感ありますね😆」「SslErrorも違う感😆」「Htmlは現実に使われちゃってるところもあったりするのでワンチャンありな気もしなくもないけど😆」「HTMLは大文字にしたいです〜😭

以下のようにすることでActive Supportの活用形がグローバルに効く。アプリケーションによってはこれでもよいが、Active Supportでデフォルトのinflectorにオーバーライドのコレクションを渡してbasenameを個別にcamerizeすることもできる。
ガイド更新分より大意

# guides/source/autoloading_and_reloading_constants.md#L289
# config/initializers/zeitwerk.rb
-inflector = Object.new
-def inflector.camelize(basename, _abspath)
-  basename == "html_parser" ? "HTMLParser" : basename.camelize
-end
-
Rails.autoloaders.each do |autoloader|
-  autoloader.inflector = inflector
+  autoloader.inflector.inflect(
+   "html_parser" => "HTMLParser",
+   "ssl_error"   => "SSLError"
+ )
end

「inflectionってときどきinfection(感染)と間違えそうになります😆

⚓ルーティングのマッパーでHTTPのOPTIONSをサポート

# actionpack/lib/action_dispatch/routing/mapper.rb#L752
+       # Define a route that only recognizes HTTP OPTIONS.
+       # For supported arguments, see match[rdoc-ref:Base#match]
+       #
+       #   options 'carrots', to: 'food#carrots'
+       def options(*args, &block)
+         map_method(:options, args, &block)
+       end

つっつきボイス:「何とHTTPのOPTIONS verbがルーティングマッパーで初めてサポートされたそうです」「あれ〜今までなかった?」「OPTIONSってそういえばあったわ😆

参考: OPTIONS - HTTP | MDN

curl -X OPTIONS http://example.org -i

HTTP/1.1 200 OK
Allow: OPTIONS, GET, HEAD, POST
Cache-Control: max-age=604800
Date: Thu, 13 Oct 2016 11:45:00 GMT
Expires: Thu, 20 Oct 2016 11:45:00 GMT
Server: EOS (lax004/2813)
x-ec-custom-error: 1
Content-Length: 0
developer.mozilla.orgより

「新機能にしてはテスト無しでズコっと入ってますね😆」「今更ですけどmapper.rbのコード↓めちゃ長い〜😇」「2300行😇

「#37370のプルリク↓見ると、今まではmatchを使わないと書けなかったのか」「シンタックスシュガーというか😳」「OPTIONS何に使うんだろう😆」「要るのかどうかは知らないけど😆」「クローラーとかで使いそうですけど、業務だとあまり使わないかな?」「とも言い切れなさそう😆

# 同PRより
# before
match 'bar', to: 'foo#bar', via: :options

# after
options 'bar', to: 'foo#bar'

「ルーティングといえば、以前つっつきでmorimorihogeさんが『Railsのルーティングは組み合わさったときにどう動くのかがわからなくてつらい』って言ってましたね(ウォッチ20180406)」「ほんにそれ: ひとつひとつのAPIにはドキュメントがあるけど組み合わせたときがマジむずいし、しょうがないからルーティングを切ってはアクセスしてみて動いた〜とか動かない〜とかやってますし😭

Railsのルーティングを極める(前編)

⚓番外: RailsはまだSameSite=Noneパッチがマージされていない


つっつきボイス:「前回のウォッチのレビュー中に教わった情報で、cookieにSameSite=Noneを設定していないサイトは来年2月からChromeでSameSite=Laxとみなすぞということだそうです」「Laxって『ゆるい』ってことか😆」「Railsではそれに対応する2017年の#28297がまだマージされてないそうです😳

参考: Chrome で SameSite=None に関する Cookieについての警告が表示される | ラボラジアン
参考: CookieのSameSite属性 NoneとLaxの違い - Qiita

#28297の最新のコメントを見ると『現在のメンテナンスポリシーによるとこれが入るのは早くてRails 6.1で、バックポートはされないかも』とあります。

⚓Rails

なお、今週のRuby Weeklyの末尾のICYMIがなかなかよさそうなエントリでした。


つっつきボイス:「ICYMIを調べたら『In case you missed it』の略で『もしご存知なければ』という感じですね☺

「その中でこの記事↓はリードオンリーのRailsコンソールを使う方法の解説です」「productionのデータをぶっ壊さずにコンソール使いたいときはあるかも☺」「dry run的な」「saveやめろぉぉみたいなことがなくなるのはよさそう😆」「ローカルでもリードオンリーコンソールをやりたいことはあったりしますね: 頑張って作ったテストデータをうっかり壊したくないときとか😋

参考: How to Setup a Readonly Rails Console - DEV Community 👩‍💻👨‍💻

⚓Rails 6の新しいデフォルト設定の意味と、安全にコメント解除する方法(Ruby Weeklyより)

同記事より(長いのでRails.application.config.は略しました):

  • action_view.default_enforce_utf8 = false
  • action_dispatch.use_cookies_with_metadata = true
  • action_dispatch.return_only_media_type_on_content_type = false
  • active_job.return_false_on_aborted_enqueue = true
  • active_storage.queues.analysis = :active_storage_analysis
  • active_storage.queues.purge = :active_storage_purge
  • active_storage.replace_on_assign_to_many = true
  • action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
  • active_record.collection_cache_versioning = true
  • config.autoloader = :zeitwerk

つっつきボイス:「この記事はRails 6で追加された新しい設定のいくつかを詳しく解説して、移行時に設定のコメントを安全に外す方法も書かれています」「おほ😍

「バージョンアップのたびにconfigの項目って増えますよね😆」「それはしゃーない😆」「RailsのconfigについてはRailsガイド↓にもありますけど最小限しか書かれていないことが多いので😅

「たとえばaction_dispatch.use_cookies_with_metadata = trueを有効にするとpurposeフィールドをcookieに追加してから署名・暗号化する、その代わり一度有効にしたらRails 5.xにダウングレードできなくなる、という具合」「へぇ〜😳」「引き返せない設定😇

action_dispatch.return_only_media_type_on_content_type = falseも長いですけど😆、有効にするとcontent_typeでmedia type以外の値(charset=utf-8など)も含まれるようになる」「最近のcontent_type周りの修正に関連してそう🤔ウォッチ20190902)」

active_job.return_false_on_aborted_enqueue = trueはActive Jobですね」「Active Jobでthrow(:abort)できる↓って知らなかった〜😳」「そこの挙動を変えられるんですね」

# 同記事より
class MyJob < ApplicationJob
  before_enqueue { |job| throw(:abort) if job.arguments.first }
  def perform; end
end

job1 = MyJob.perform_later(false)
job2 = MyJob.perform_later(true) 

active_storage.queues.analysis = :active_storage_analysis」「Active Storageで何か分析してくれるのかな?😆」「あ、画像のheightwidthを取ってmini_magickで使ったりできるのか」

[Rails] MiniMagickでPDFのページ数を取得するときはフォントエラーに注意!

「きりがないのでこの辺で止めますけど、Railsガイドだけだとわからない情報があってよさそうですね😍」「configできれば触りたくないけど😢」「必要になったらこの記事を泣きながら読むことになるんでしょうね😆」「ありそう😆」「この記事翻訳したいです😋

⚓Rails 6でビューヘルパーのimage_altが削除

たしかに最新のapi.rubyonrails.orgから消えています。

# api.rubyonrails.orgより
image_alt('rails.png')
# => Rails

image_alt('hyphenated-file-name.png')
# => Hyphenated file name

image_alt('underscored_file_name.png')
# => Underscored file name

つっつきボイス:「image_altヘルパーがRails 6から消えたそうです」「そういえば最近は自分でaltを設定しないといけないことになってた気がするけどそれかな😎」「image_altがファイル名から適当に推測して生成するaltテキストがスクリーンリーダーなどでいろいろ具合がよくなかったそうです」「やはりaltぐらい自分で書けと」「書きたくないけど😆

後で調べると、Rails 5.2でimage_altが非推奨になっていました↓。image_tagからの呼び出しも削除されたそうです。

⚓DHHも消したがっているaccepts_nested_attributes_for


#26976より

上はDiscourseのclean-railsで知りました↓。


つっつきボイス:「以前から評判のよろしくないaccepts_nested_attributes_forですけど(ウォッチ20180820)、少なくとも2016年の時点ではDHHも殺したいと思っていることを上で知りました😆」「これは殺していいと思う🙋‍♀️」「使ったことあるけど心底つらかった〜😇」「Discourseでjoker1007さんも滅びるべきと書いてますね」「自分もjoker1007さんに全面同意🙋‍♀️

「DHHもコメントで『新しいAPIとして推奨すべきでない』『むしろコントローラで手動でやる方法を示すべき』と書いてますね」「わかる〜😂

「joker1007さんのコメントでも言及されているけど、こういうのはむしろJSON構造から攻略するのがいいんじゃないかって自分も思いますし☺」「なるほど!」「動的にフォームの項目を増やすんなら結局JavaScriptのお世話になりますし、そうやってJavaScript使ったのに結局素のフォームだったら意味ないので、素直にAPI叩けばええやんって思いますし😆

⚓accepts_nested_attributes_forはデモ用なのか?

「ここは自分の推測でしかないんですけど、accepts_nested_attributes_forってもしかすると『Railsなら15分でアプリを書けまっせドヤァ😎』みたいなデモ用なんじゃないかって今思いました」「あ〜何だかわかる気がします😳」「ほとんど何も書かなくてもよしなにやれるあたりとか、そういう用途だと有用なんですよ」「ところがそれを真に受けて業務で使うと途端に破綻するという🤣」「🤣」「モデルもビューも結構癒着しますし😇、これって単純に追加して保存できるだけなんじゃね?って」

「こんな例えが合ってるかどうかわかりませんけど、楽器のキーボードについているデモ演奏ボタン↓にちょっと似ているかもですね😆」「それそれっ🤣」「そのデモ演奏ボタンを本番のライブで無理やり使おうとしているみたいな😆」「デモ演奏だとテンポも変えられませんし😆

「まあそういう感じのデモ機能って15年ぐらい前に流行りましたよね☺」「ボタン一発でブログサイトを作れますとか😆」「そういうデモでhas_many周りを一気にやれるのを見せるのはとってもインパクトあるんですけど、実際には使えないという🤣」「deleteってどうすんの?みたいなレベルで既に悩む」「で建て増しを繰り返すうちに結局詰んだり😇

「idがついてないとcreateだし、idがついていればupdateだし、deleteフラグが立ってればdestroyするし、というのを一見同時にやれそうな気がするんですよ: でも誰もコントロールできない😆」「スレッドセーフとかも大丈夫かどうかよくわからないし😅」「歴史調べてないのであくまで推測&印象😆」「自分一人しか使わない管理画面で、かつ重要じゃないデータを手軽に出したいみたいなユースケースならaccepts_nested_attributes_forはまだワンチャンあるかなって思います☺

「なおaccepts_nested_attributes_forはRails 2.3からあるそうです↓」「割と古くからあった気はします」「さすがに最初期からではなかった😆

⚓Meetup for Rails engineersのスライド3つ


connpass.comより


つっつきボイス:「こちらのイベントを見逃してて、終わってから気づきました😅」「3つは追いきれないので、とりあえず『実践Railsアプリケーション設計』のスライドを見てみましょうか😋

⚓『実践Railsアプリケーション設計』

つっつきではかなり盛り上がりましたが、記事にすると多すぎるので間引いています🙇

「『実装とテストは資料が多いけど、設計の書籍は抽象的な内容が多い』と」「これホントにそう!外部設計と内部設計の本ってたいてい抽象論になっちゃう😭」「なるべく具体的な設計の過程を知りたいですよね」「ところが設計を具体的に書くと、今度は分量が増える割には価値が薄くなっちゃうという😇」「読んだ人にとっては自分の業務に合わないとかが続出しちゃうんですよ😢」「言語が違うだけでも大きく変わりますし」「設計論を具体的に書くと一般性が損なわれちゃうんですね😅

「しかも本当に具体的な設計ってビジネス上の機密に直結しちゃうから、そういうものほど本にできない😆」「そうそう😆

「このスライドでは以下に絞って話を進めていますね」

「要件から重要な名詞と動詞を抽出して、概念を固めたうえで関係をまとめる↓」「つまりエンティティに適切な名前を与える」「そこが超重要👍」「設計って実はものすごく日本語力を要求されるんですよ」「誰が見ても誤解しない名前をつけるのが大事」

「最近自分が設計するときは、いきなり英語名を付けないように注意してますね☺

「これが実際の名前か↓」「ReassociatedRequestだと受動態か完了形か迷っちゃうので、個人的にはReassociationRequestとしたい気がしますけど😆」「実際の業務を見ないとどっちが適切か判断難しいですね😅」「やっぱり名前むずい😭

「このスライドいいですね👍」「ここに書ききれないぐらいいい話がいっぱい出ました😂」「こういうスライドを元に強い人が解説するのがよい気がしました😋

⚓でかい画像を正しく扱う


つっつきボイス:「お馴染みEvil Martiansの記事で、Railsに限定せずにでかい画像を適切に扱う方法を解説しています」「こんな図も↓」「かなり長い記事…😅


同記事より

⚓TimeWithZoneクラス


つっつきボイス:「ベストマサフミさんの短い記事です」「:dbto_timeで変換しないとUTCになっちゃうのか😳

⚓その他Rails

つっつきボイス:「RAILS_ENV=stagingはたしかに悪い文化!」「productionとstagingで環境を分ける意味ってあんまりなくて、stagingはデータが本物ならproductionになれるようにするのが正確ですね☺」「でないとif stagingみたいなのができてだんだんつらくなるし」「挙句の果てにstagingはよくてもproductionでコケたりしますし😇」「そしてproductionはデプロイしないとどうなるかわかりません、になって本末転倒になると😆


つっつきボイス:「まだbetaだそうですが気になりますね😋」「Railsとフロントエンドってそんなに相性悪くないと自分は思うんですけどね☺」「仲良くなれないという思い込みもあるのかも?」「たぶんね」「でもたぶん仲良くはなれない🤣」「🤣


前編は以上です。

バックナンバー(2019年度第4四半期)

週刊Railsウォッチ(20191029後編)Ruby 2.7.0-preview2、tapping_device gemとhumanize gem、平成Ruby会議ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20191106後編)holiday_japan gemで日本の祝日判定、小さい関数が有害になるとき、Gitブランチのファジー検索ほか

$
0
0

こんにちは、hachi8833です。明日から始まるRubyWorld ConferenceにBPSのmorimorihogeも参加いたします。本日現地入りしていますので見かけたらお気軽にお声掛けください🙇(Twitter: @morimorihoge)。

RubyWorld Conference 2019にスポンサー登録しました


  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

⚓お知らせ: 週刊Railsウォッチ「第16回公開つっつき会」(無料)

第16回目公開つっつき会は、来週11月14日(木)19:30〜にBPS会議スペースにて開催されます。今回は月初ではありませんのでご注意ください。

週刊Railsウォッチの記事にいち早く触れられるチャンス!発言・質問も自由です。引き続き皆さまのお気軽なご参加をお待ちしております🙇

⚓Ruby

⚓「小さい関数は有害」か?


つっつきボイス:「Rubyに限らない設計話で、少々長いです」「あえて逆説的な煽りタイトル付けてる感じ☺」「小さい関数有用ですよ何言ってるんですか😆」「普通そう思いますよね😆」「メソッドを切り分けないでやれると思っている人がいるのがいつも不思議でしょうがないですよもう😆

「たぶん関数は小さければいいというものではないということを言ってそう🤔」「記事の雰囲気としては、メソッドの大きい小さいというより設計や抽象化の間違いを問題視してるのかなと☺」「大きい小さいが問題ではないと自分も思いますし」「ネーミングがあかんとかね😆

後でざっと中身を追いかけてみました。classitisという見慣れない造語が出てきたので仮訳にしてみました。

見出しより:

  • 小さい関数のメリットとされている点
    • 1つのことだけをやる
  • DRY絶対視に潜む誤り
  • 名前を付けるのは難しい
  • コードの局所性が失われる
  • クラスの「汚染」
  • 引数を減らすと依存関係が見えにくくなる
  • 小さい関数が増えると読みづらくなる
  • 「薄っぺらなモジュール」と「クラス増殖症(classitis)」の問題
  • 小さい関数に意味がある場合
    • ネットワークI/O
    • プロパティベースのテスト

Sandi Metz氏もAll The Little Thingで「誤った抽象化より、コード重複の方が遥かにコストが低い」「ゆえに誤った抽象化より重複が望ましい」と述べています。
同記事トップハイライトより大意



同記事で引用されている「The Tower of Abstraction」スライドより

同記事で引用されている「The Tower of Abstraction」というスライドは惜しくもSpeakerDeckから失われているので、動画だけ貼っておきます。

⚓holiday_japan: 日本の祝日判定gem


つっつきボイス:「ruby-jp Slackで見かけました」「今年来年あたりの日本の祝日はざわついてますから😆」「ざわついてる😆」「前の天皇誕生日は消えるんですよね?」「名前変えて復活するかもですし」「そうやって祝日が増えていくと」「10万年も経てば365日全部祝日になる😆」「んなこたーない😆

「おお、天皇の即位の日がちゃんと出てる↓❤」「即位礼正殿の儀も😋」「人力でメンテしてるんでしょうね」「そりゃそうですよ🤣」「コンピュータには無理」

# 同リポジトリより
$ ruby -r holiday_japan -e 'HolidayJapan.print_year 2019'
listing year 2019...
2019-01-01 Tue 元日
2019-01-14 Mon 成人の日
2019-02-11 Mon 建国記念の日
2019-03-21 Thu 春分の日
2019-04-29 Mon 昭和の日
2019-04-30 Tue 国民の休日
2019-05-01 Wed 天皇の即位の日
2019-05-02 Thu 国民の休日
2019-05-03 Fri 憲法記念日
2019-05-04 Sat みどりの日
2019-05-05 Sun こどもの日
2019-05-06 Mon 振替休日
2019-07-15 Mon 海の日
2019-08-11 Sun 山の日
2019-08-12 Mon 振替休日
2019-09-16 Mon 敬老の日
2019-09-23 Mon 秋分の日
2019-10-14 Mon 体育の日
2019-10-22 Tue 即位礼正殿の儀
2019-11-03 Sun 文化の日
2019-11-04 Mon 振替休日
2019-11-23 Sat 勤労感謝の日

⚓小賢しいコードは書いてくれるな(Ruby Weeklyより)

# 同記事より: 小賢しくないコード例
class LogController
  def initialize(progname = nil)
    logger_backends = {
      'ELASTICSEARCH' => EsLog.new(progname),
      'POSTGRES'      => PgLog.new(progname),
      'SQLITE'        => SqlLog.new(progname),
    }
    @loggers = Settings.log_backend_priorities.split(',').map { |k| logger_backends[k] }
  end

  def where(query)
    @loggers.each do |logger|
      results = logger.where(query)
      return results if results.present?
    end
  end

  def process(log)
    @loggers.each do |logger|
      return if logger.process(log)
    end
  end
end

つっつきボイス:「短い記事ですが、clever codeは『小賢しい』『猪口才な』コードかなと」「これはいい表現かも😍」「要はトリッキーなコードに走るなということでしょうね☺」「ひねらずに上みたいに素直に書いてくれと」「これはまあ麻疹みたいなもので、一度はこういう道を通るんですっ😆」「悪例みたくrecallとかwhereとか書いてこじらせたくなる時期はある😆」「結構いい年になってもやってる人、いますよ😆

「小賢しくなっちゃったコードって、書いた本人としては愛着が湧いてしまってプッシュしたくてしょうがなくなりがち😆」「そこをうまく分離できるといいかも☺」「翻訳でも同じ現象ありますね: 訳した直後だと愛着が湧いて変えたくない気持ちになるんですけど、翌日頭を冷やしてもう一度見直すとあれ?となったりします😆

⚓DB

⚓クラウドネイティブの地理分散SQLアプリを低レイテンシーで構築するテクニック9つ(DB Weeklyより)


同記事より

見出しより:

  • 地理分散SQLはRDBMSの未来
    • マルチゾーン
    • マルチリージョン
    • マルチクラウド
  • 広域ネットワークのレイテンシを懸念する理由
  • シングルロー(single-row)トランザクションのレイテンシを下げる4つの手法
  • マルチロー(multi-row)トランザクションのレイテンシを下げる5つの手法

つっつきボイス:「地理的に分散しているSQLアプリのレイテンシを下げる手法の解説記事です」「クラウドベンダーを複数使うとか大変そう😅」「leaderとかfollowerとあるのはプライマリとセカンダリに相当するのかな🤔」「普段なかなかお目にかかれない大規模な話がいっぱい😳」「リードオンリーレプリカなら何とか😅」「オリンピックのサイト構築みたいに世界中から大量アクセスされるシステムだとこういうノウハウが必要になるんでしょうね」

「クォーラム(quorum)って何でしたっけ?」「用語としてはWikipediaのこれですね↓」「一種の多数決ですか」「手元の辞書によるとやっぱりラテン語由来でした😳

quorumとは分散システムにおいて、分散トランザクションが処理を実行するために必要な最低限の票の数である。quorumベースの技術は分散システムにおいて、処理の整合性をとるために実装される。
Wikipediaより

参考: Quorum - Wikipedia

quorum: 〔議決に必要な〕定足数◆イギリスで治安判事(justices of the peace)を任命するとき、多くの判事の中から(任命状の「その中で(of whom)」にあたるラテン語がquorumだった)執行に必要不可欠な判事を指名し、これらの判事がjustices of quorumと呼ばれたことから。「定足数」の意味で使われるようになったのは1616年から。


「話は逸れますけど、こういうクォーラム的なアイデアを最初に見たのは小松左京の『果しなき流れの果に』でした: 静止軌道上の複数の衛星にそれぞれコンピュータが搭載されていて、それらの結果の多数決を取って使うというものですが、何しろ当時は大型コンピュータの時代だったのでそれだけでびっくりできました😅

参考: 果しなき流れの果に - Wikipedia

「私はこっち〜↓😆」「エヴァだ😆」「バルタザール、メルキオール、キャスパーで合議するヤツ✝」「昔みんなで力を合わせて作ったLinuxサーバーのホスト名がその3つでした😆」「もろに誰もが通る道ですね〜、4つ目のホスト名が早速足りなくなりますけど😆」「ホスト名から機能がわからないし名前足りなくなるしで、命名体系を考える反省材料になりました😅

参考: 新世紀エヴァンゲリオンの用語一覧 - Wikipedia

⚓クラウド/コンテナ/インフラ/Linux/Serverless

⚓GoogleのBorgとは


つっつきボイス:「はてブに上がってた記事ですがこちらもデカい話😳」「GKE(Google Kubernetes Engine)やGoogle App Engineの実行基盤だから完全にクラウドのバックエンドですね」「絶対ダウンしてはならないとか、もう自分らから直接見えない世界😅」「日本語で読み解いてくれている方に感謝🙏


同記事より

「2015年にBorgの資料が公開されていたんですね↓」「Googleも草創期に比べてだいぶ落ち着いてきたからこの辺の情報を出してもいいかという感じになったのかも☺

参考: GoogleがBorgの詳細を公開

Borgで開発を行っていた技術者の多くは,現在はKubernetesに移行している。
infoq.comより

⚓その他クラウド


つっつきボイス:「Unixの偉い人であるカーニハンがKindle本出したそうです」「2,300円はそこそこ高いか😅」「memoir?」「思い出(memory)をフランス語でオシャレっぽく言ってみるテストでしょうね😆

参考: ブライアン・カーニハン - Wikipedia

Kindle Unlimitedで読めることに後から気づきました😋。200ページ足らずです。



つっつきボイス:「自分もこういうこと言ってみたくなるお年頃〜😅: プログラミングやるならアセンブラからとか基盤から作ろうとか」「WebやるならCGIからとか、いくらでも遡れちゃう😆」「個人差はたしかにありますね☺

⚓JavaScript

⚓Electron 7.0.0リリース(JavaScript Weeklyより)

主な変更は「Arm(64bit)のWindows対応」「TypeScript Definitionsジェネレータに移行」「非推奨APIの削除」などです。


つっつきボイス:「ElectronはChromiumエンジンでクロスプラットフォームなデスクトップアプリを作る基盤で、自分は間接的にですが以下のEpichrome↓とか結構使ってます」

Mac: Chromeブラウザの大量のタブをEpichrome.appですっきりさせよう


electronjs.orgより

「ElectronはChromiumエンジンを丸呑みしているのでお手軽にクロスプラットフォームやれます😋」「JS/HTML/CSSでブラウザアプリっぽくデスクトップアプリを作れると、なるほど😋」「その代わりエンジン丸呑みなのでアプリのサイズが結構でかくて😅

「今どきデスクトップアプリを作りたいとかあります?😆」「自分はChromeのタブがすぐ数百個とかになりがちだったので、タブを減らしたくてEpichromeで別アプリに逃がすことが多いです」「気持ちはわかります☺


「何年か前に、Go言語で書いたHTTPアプリを無理やりElectronに埋めてデスクトップアプリを作ってみたことがあって一応動いたんですが、Appストアに登録する手続きとか証明書の埋め込みとかが面倒でそれきりです😇」「それは審査通らなさそうな雰囲気😆」「当時は前例のない形態のアプリでしたし😅」「キワモノアプリ😆

今はこういうのもあるんですね↓。#194を見ると証明書周りは未実装でした。

参考: [go-astilectron]D&DでGoモジュール側にファイルパスを受け渡す - Qiita

⚓CSS/HTML/フロントエンド/テスト

⚓デザインと目の錯覚


つっつきボイス:「角の丸いレクタングルの縦が揃っているはずなのに揃って見えない、みたいな錯覚について解説している記事ですね」「なるほど、こういうのとかね↓」「並べたアイコンががたぴしして見えるとか」「BPS社内のデザインチームにもSlackで聞いてみたんですが、この記事で扱っている内容はデザイン界隈ではだいたい常識だそうです」


同記事より

optical weightとかジャストロー錯視って初めて見る用語😳」「開発者もこういう記事を一度ざざっと見ておくとよさそう😋」「デザイナーのお気持ちを理解するためにも」

参考: 「ジャストロー錯視」What You Get Is What You See by dotimpact - DMM.make

⚓クロスブラウザテストとデバイスファーム

つっつきボイス:「次の2つはBPS社内Slackで話題になったスライドです」

「クロスブラウザテストは闇しかない、間違いない😇」「BrowserStackという有料サービスはこういうときに有能という話も出ましたね(ウォッチ20190320)」「BrowserStackはエミュレーションとかじゃなくてガチのハードウェア上で動いているヤツ」「スライドにもありますけど海外のサービスなので重いのが難点😅


browserstack.comより

「ついこの間Seleniumをインストールするのに2時間以上かかったんですけど、これは止めた方がいいヤツという心の声に従って引き返しました😆」「きっと止めて正解😆」「掴んだ瞬間にビビッと嫌な予感がしたら当たり牌🀄」「インストールできたところで何が担保できているのか自分的には謎ですし😆

「スライドではBrowserStack以外の海外デバイスファームも紹介されていますね↓😋


causelabs.comより


crossbrowsertesting.comより

⚓スライド: Webデザインドリル


つっつきボイス:「こちらも社内Slackから」「後半はデザイナ向けですが、中盤までは普通にHTMLコーディングしたりシステム開発する人達もおさえておくべきな感じありますね: 穴埋めドリルはともかく資料としては良くまとまってると思います👍」「😍

⚓言語・ツール

⚓Gitブランチのファジー検索


同記事より


つっつきボイス:「Gitブランチをインクリメンタル検索で絞り込むシェル関数が紹介されています」「あ〜これいいかも😍」「シンプルだけど効果大」「後で入れてみよっと😋

「なお上のシェル関数では、以下の記事↓で紹介されていたfzfというpecoに似たツールを使ってますね」

モダンな開発用ターミナル環境のためのツール紹介

「ちょうど今ローカルブランチが死ぬほどあって困ってるんですけど、ブランチ名にissue番号入れろというレギュレーションかけたのも自分なんですよね〜これが😆」「GitHubならサイトのブランチ名コピー機能で少し楽にやれますけど😋」「GitはGUIで使うと決めていたのに、最近何だかCLI操作がじわじわ増えてきてるという😆」「自分は逆にGUIの方が慣れてなくてコワいです😅

⚓その他

⚓昆虫を搭載


つっつきボイス:「昆虫の玉乗りで乗り物を制御させるという試みだそうです」「記事の写真の真ん中辺にある、羽の生えたこのちっちゃいのが虫?!」「オスのカイコガがメスの匂いにめちゃくちゃ敏感なのを利用してるんですって」「玉転がしという進行方向と逆の運動なのに、昆虫があっという間に適応するのがびっくりでした😳」「何だかこえぇ〜😱」「ある意味恐ろしい話😅」「いい悪いは別にして、昆虫ハックはまだまだやれそうな予感😆

⚓手書き数字の地域差


つっつきボイス:「手書き数字の地域差って意外に大きい…」「7にスラッシュ追加するのは何となく前からやってました」「ヨーロッパの手書きの4ってほとんどイナズマっぽく書かれることもあるみたい😳

「欧米のはまだわかるんですけど、アラビア数字の手書き(下の段↓)はさらにスゴいことに」


ameblo.jp/fujisakalughalughaより

参考: 今日のアラビア語。◇文法編◇#12【数字の表記】 | 藤坂託実の語学散策 ~アラビア語とその他諸々~

「オレの知ってるアラビア数字じゃない😇」「9以外ほとんど似てない😆」「2と3は90度回転したようにも見えなくもないけど」「4もちょいイナズマっぽくはあるけど」「アラビア数字とアラビアの数字って別物なんですね…」「どこでどう枝分かれしたのやら😆

参考: アラビア数字 - Wikipedia

⚓番外

⚓PならばQ


つっつきボイス:「以前見に行ったロマンティック数学ナイトというイベントの昔のスライドです」「行けたら行く、は行かないと同義😆」「これ好きっ😆」「身に沁みる😆


romanticmathnight.orgより

参考: 論理包含 - Wikipedia

「推論の論理包含って、ANDやORやXORと違ってベン図や真理値表↓が非対称ですよね」「pが偽だったらqはどっちでもええ😆」「こうすると対偶が取れるんだったかな?🤔

参考: pならばqの真偽


geisya.or.jpより

「『叱られないと勉強しない』の対偶を考える問題も好きです: ついうっかり『勉強すると叱られる』と答えそうになる😆」「😆」「たぶん『勉強しているということは叱られた』みたいに時間の前後関係も考慮しないといけないかも🤔

参考: 【対偶】怒らないと勉強しない?? | 数学美術館


後編は以上です。

バックナンバー(2019年度第4四半期)

週刊Railsウォッチ(20191105前編)Rails 6のデフォルト設定解説、DHHも消したいaccepts_nested_attributes_for、スライド『実践Railsアプリケーション設計』ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。

Ruby Weekly

DB Weekly

db_weekly_banner

JavaScript Weekly

javascriptweekly_logo_captured

Rails: バリデーターの直書きを避けてカスタムバリデーターを作ろう(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Rails: バリデーターの直書きを避けてカスタムバリデーターを作ろう(翻訳)

モデル内のvalidates呼び出しで使われるActive Modelのバリデーションにはさまざまなオプションが用意されており、これを用いて独自のクラスを強化できます。Railsガイドにも6.1 カスタムバリデータというセクションがあります。

以下のように書かないこと

validates呼び出しにコードをごちゃごちゃ書く。

class Invite
  validates :invitee_email, format: {
    with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i,
    message: "is not an email"
  }
end

以下のように書くこと

バリデータークラスを作ってロジックをそこに切り出す。

# app/validators/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
      record.errors[attribute] << (options[:message] || "is not an email")
    end
  end
end
class Invite
  # バリデーション名はRailsのマジックが推測してくれる
  validates :invitee_email, email: true
end

そうする理由

バリデーションロジックを再利用する可能性がある場合や、バリデーションの書式設定ルールが複雑な場合は、その機能を専用のオブジェクトに切り出すのがよい方法です。

これならロジックを切り離してテストできますし、ロジックが使われるときの動作もわかりやすくなります。

そうしない理由があるとすれば?

特にメールのバリデーションの場合、ややトリッキーなバリデーションを書いたことがあります。シンプルだったのでバリデーターも要らないぐらいだったのですが、アプリの認証でdeviseを使う必要が生じたのです。

class Invite
  validates :invitee_email, format: {
    with: Devise.email_regexp,
    allow_blank: false
  }
end

そういう場合は以下の書き方もありです。

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ Devise.email_regexp
      record.errors[attribute] << (options[:message] || "is not an email")
    end
  end
end

関連記事

Rails: メールをActive Recordのコールバックで送信しないこと(翻訳)


Rails 6.0.1がリリース!修正を追ってみました

$
0
0

現地時間11/5のリリースでした。そういえば昨日Dockerでrails newしてたら6.0.1だったような気がしてましたが、見返したら本当でした。

せっかくなので、これまで週刊Railsウォッチの「先週の改修」で追ったプルリクもたどってみました。全変更履歴だと多すぎてつらいので、commits/v6.0.1/activesupportのようにライブラリごとに履歴を追う方がやりやすいことがだんだんわかってきました。

ほとんどがバグ修正でした。セキュリティ修正はchangelogレベルでは見当たりません。

Active Support

#37494
ActiveSupport::SafeBufferEnumeratorのメソッドをサポート(ウォッチ20191028
#37085
Redisのキャッシュストアが「max number of clients reached」時に安全にfailするよう修正(ウォッチ20191007
#37587
メモリキャッシュストアから返された値を改変するとキャッシュされた値まで改変される問題を修正
@8237c4d
デフォルトのinflectorをzeitwerkモードでオーバーライド可能になり、ガイドも更新(ウォッチ20191105
# config/initializers/zeitwerk.rb
Rails.autoloaders.each do |autoloader|
  autoloader.inflector.inflect(
    "html_parser" => "HTMLParser",
    "ssl_error"   => "SSLError"
  )
end
#37178など
Range#===Range#include?Range#cover?を開始値や終了値の省略に対応(ウォッチ20190729
#37114
Process#clock_gettime CLOCK_PROCESS_CPUTIME_IDをSolarisで無効に(ウォッチ20190909

Active Record

#37525
PostgreSQLのリードオンリークエリでCTE(Common Table Expressions)を利用できるようになった
#37511
関連付けリレーションでのインスタンス作成でunscopeが効くように修正(ウォッチ20191028
#37434
find_in_batchesの停止が早すぎるエッジケースを修正(ウォッチ20191021
#37295
カスタムバリデーションのコンテキストでautosaveされた関連付けが常にバリデーションされるよう修正(参考: ウォッチ20191015
#37328
sql.active_record通知のペイロードに:connectionを含めた
@39730bd
after_commitコールバック内でのロールバックで、コミット済みのレコードのステートをロールバックしないよう修正
#37235
eager_load時のjoinsの順序をできるだけ維持するよう修正(参考: ウォッチ20191015
#37240
リードオンリー接続でDESCRIBEクエリを利用できるようになった
#37153
inspectされたレコードがマーシャリングされないことがあったのを修正
36998など
コネクションプールreaperのスレッドがプロセスのfork再度spawnされる問題を修正し、forkしたプロセスのアイドリングコネクションがreaperで処理されないようになった(ウォッチ20190826など)
#36985
ActiveRecord::Relation#takeの結果のメモ化がActiveRecord::Relation#resetActiveRecord::Relation#reloadで正しくクリアされるよう修正(ウォッチ
#37465
MySQL 8.0で導入されたprimary_keysforeign_keysのパフォーマンスリグレッションを修正
#37154
insertinsert_allupsertupsert_allでクエリキャッシュがクリアされるよう修正(ウォッチ20190917
@66bc2ff
connected_toからwhile_preventing_writesを直接呼べるようになった

アプリケーションの作者がデータベース切り替えミドルウェアを用いてconnected_toで明示的に呼び出したい場合がある。アプリの書き込みをオフにしてconnected_to(role: :writing)を呼ぶまでオンにしないようにできる。
今回の変更では、書き込みを許可したいロールが書き込みを行うことを前提とすることでアプリを修正できるようにした(書き込みを明示的にオフにしている場合を除く)。

#36932
mysql2アダプタでファイルソート中にクエリが終了するエッジケースでのActiveRecord::StatementTimeoutによるエラー検出を改善

Action View

@b425af0
IE 9互換のためUJSでElement.closest()を回避(ウォッチ20191007

Action Pack

#36283
ActionDispatch::SystemTestCaseActionDispatch::IntegrationTestではなくActiveSupport::TestCaseを継承するようになり、システムテストでジョブを実行できるようになった
#36996など
登録済みMIMEタイプにフラグを追加できるようになった(ウォッチ20190902
Mime::Type.register "text/html; fragment", :html_fragment

Active Storage

@06f8baf:
ActiveRecord::RecordNotFoundエラーでActiveStorage::AnalyzeJobsをdiscardするようにした
#34827
サービスにアップロードする前にblobがデータベースに常に記録されるようになったことで、生成されたblobのキーが衝突してデータ喪失につながる可能性が修正された

Railties

@3ac9e22
zeitwerk:check rakeタスクがアプリのルートディレクトリの外のファイルもレポートするようになった
#37102
eventedファイルアップロードチェッカーで発生する可能性のあるエラーを修正(ウォッチ20190930
#37053
パラレルテスト機能で生成されたSQLite3データベースファイルが新規アプリの.gitignoreにデフォルトで含まれるようになった
@04cfbc8
tmp/pidsに.keepファイルを生成し、rackupでもサーバーを起動できるよう修正

関連記事

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

Rails 6のB面に隠れている地味にうれしい機能たち(翻訳)

週刊Railsウォッチ(20191107前編)Active Recordモデルをprivateで封じ込める、心折れないRailsスキーマ管理、Railsセッションをクロスドメイン共有ほか

$
0
0

こんにちは、hachi8833です。Rails 6.0.1が先週リリースされましたね🎉

Rails 6.0.1がリリース!修正を追ってみました

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

⚓お知らせ: 週刊Railsウォッチ「第16回公開つっつき会」(無料)

第16回目公開つっつき会は、来週11月14日(木)19:30〜にBPS会議スペースにて開催されます。今回は月初ではありませんのでご注意ください。

週刊Railsウォッチの記事にいち早く触れられるチャンス!発言・質問も自由です。引き続き皆さまのお気軽なご参加をお待ちしております🙇

⚓Rails: 先週の改修(Rails公式ニュースより)

公式情報を中心に見繕いました。

⚓マルチDBのマイグレーション後に同じデータベースに再接続するよう修正

標準的なマルチDBセットアップで、2番目のデータベースがレプリカでなく、独立したテーブルセットを持っている状態で以下の非常にシンプルなタスクがあり、rails db:migrate fooを実行したとする。

task foo: :environment do
  puts User.last # or some model with a table that only exists in the primary db
end

実際の振る舞い: establish_connectionがマイグレーションタスクごとに実行されるため、プライマリではなく直前のマイグレーション対象データベースに対してクエリが実行される。
期待される振る舞い: マイグレーションタスクがクリーンアップされ、実行後プライマリ・データベースに再接続される。
#37578より大意


つっつきボイス:「Rails 6のマルチDBがらみの修正ですね」「お、rakeタスクですか」「コネクションをoriginal_configに保存しておいて、終わったらそれを復元しているんですね↓」「コネクションが1つだったら起きなかった問題っぽい」「本番で知らずに切り替わってたらびっくりして目を疑っちゃいそう😇」「実際に使わないと見つけにくそうなバグですね☺

# activerecord/lib/active_record/railties/databases.rake#L81
  desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
  task migrate: :load_config do
+   original_config = ActiveRecord::Base.connection_config
    ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
      ActiveRecord::Base.establish_connection(db_config)
      ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    db_namespace["_dump"].invoke
+ ensure
+   ActiveRecord::Base.establish_connection(original_config)
  end

⚓ローカルキャッシュを改変するときのバグを修正

このテストケースがバグをある意味正しく示してくれると思う。手短に言うと、戻り値の改変からは既に保護されていたにもかかわらず、元の値の改変から保護されていなかった。
同PRより大意

# 同PRより
my_string = "foo"
cache.write('key', my_string)
my_string << "bar"
cache.read('key') # => "foobar"

つっつきボイス:「上のコード例を見る方が早いと思うんですけど、キャッシュの値に入れた元の値を改変するとキャッシュの値まで変わっちゃってるという😳」「これはアカンやつ〜😆」「dupしないでそのまんまキャッシュに入っちゃってたか😇」「それを防ぐためにdup_value!を追加したんですね」

# activesupport/lib/active_support/cache/strategy/local_cache.rb#L60
-         def write_entry(key, value, **options)
-           @data[key] = value
+         def write_entry(key, entry, **options)
+           entry.dup_value!
+           @data[key] = entry
            true
          end

「これに限らずhashやarrayで割とよくあるバグですね☺」「これはあるある」「単純に@data[key] = valueしたら、valueが変わるとキャッシュの値まで変わっちゃう😇」「誰も書き換えなければ問題ないんですけど、キャッシュだから書き換えあるでしょうし😆」「よけてたはずなのにどして?ってなったり😆」「ぱっと見正しそうなだけに見落としそう😅

「まあ誰しもやりそうなバグですから☺」「でもやったらアカン😆」「キャッシュに入れたmy_stringを後生大事に使い回すのがそもそもよくなかったという考え方もあるかも😆」「修正でdup入ったから微妙に遅くなるでしょうね😆

「#37587の場合は同じKeyに対してread / write処理を書いて、かつ元のオブジェクトが参照できるときにしか発生しないので、『同一リクエスト内で同じオブジェクトをwrite / readした』『クラス変数などのリクエストをまたいでメモリにデータを保持する変数でwrite / readした』とかのケースぐらいで、割とレアな気はしますね☺」「おぉ」


「こういうバグが起きにくい言語仕様ってあるのかなって、ついそっちを考えちゃいます😅」「全部値渡しにしたらデカいオブジェクトのコピーが半端ないコストになりますよ😆」「参照で渡したいときとコピーしたいときとありますからね〜☺」「C言語知ってる人にならRubyは基本的にポインタ渡しだよって説明できますけど」「若い人だとポインタ知らなさそう😆

参考: ポインタ (プログラミング) - Wikipedia

なおdup_value!はActiveSupport::Cache::Entryにありますが、なぜかapi.rubyonrails.orgで出てこなかったのでAPIdockを貼ります。

参考: dup_value! (ActiveSupport::Cache::Entry) - APIdock

⚓インラインジョブを別スレッドで実行できるようになった

# activejob/lib/active_job/queue_adapters/inline_adapter.rb#L13
    class InlineAdapter
      def enqueue(job) #:nodoc:
-       Base.execute(job.serialize)
+       Thread.new { Base.execute(job.serialize) }.join
      end

      def enqueue_at(*) #:nodoc:
        raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at https://guides.rubyonrails.org/active_job_basics.html"
      end
    end

つっつきボイス:「インラインジョブを別スレッドで実行できるようにしたそうです」「今までは別スレッドにできなかったと😳」「Thread.newでジョブを実行してからjoinしてますね」「enqueueだし、もしかするとjob.serializeが重いのかも🤔」「joinするということはジョブの完了を待つってことなのかな?」「issueの方を見るとよさそう↓」

「これはThread.newした中でexecuteさせることで直ちにenqueueした処理を解放させて、次のenqueue 待ちをなるべくゼロにしたい、ということだと思います」「おぉ」「これはOSの割り込みハンドラによるコンテキストスイッチ実装とかでもよくある実装方針で、割り込みハンドラはなるべく限界まで小さくする、というやつですね☺(割り込みハンドラの処理中は他の割り込みハンドラを受けられなくなってしまうので、その時間は最小限にするために、割り込みハンドラでは即queueに処理イベントを積むだけ積んでハンドラを脱出する)」「なるほど!」「多分、micro jobが大量に実行されるようなユースケースになってくると、ジョブのスループットに影響が出てくるんだと思われます」

「あ、issueに例のCurrentAttributes↓が登場してます😆」「憎っくきCurrentAttributes😆」「これってグローバルステートだからスレッドからこちょこちょするときに気を付けないといけないんでしょうね☺」「スレッド周り難しくてよくわかんないけど、上の修正は何となくworkaroundっぽい雰囲気🤔」「この修正で切り抜けられるならまあいいのかなと☺」「スレッド生成してもコストはそんなに変わらなさそうではある🤔

Railsの`CurrentAttributes`は有害である(翻訳)

「上はDHHが入れたCurrentAttributesに反対してた人の記事を翻訳したもので、CurrentAttributesはやりすぎだという主張ですね」「そもそもグローバルステートですし😆」「記事の人はどちらかというと設計として好きになれないみたいです」「わかる😆」「一応CurrentAttributesにはスレッドローカルな変数もあるみたいですけど、それが回り回って今回のissueにつながったんだとしたら何となくわかる気がする☺」「グローバルステートならスレッドセーフであって欲しいですよね」

「こういうCurrentAttributes的な機能って、わかってて使う人にはとっても有用なんですよ😆」「あ〜それはある意味難しい問題😅」「そしてよくわかってない人が飛びつくと詰む、みたいな害の方が大きくなったりしがち😆」「共有情報をスレッドに乗せるみたいなコードをJavaで見たことはあるので、CurrentAttributesがまったくナンセンスということはないんじゃないかとは思いますね☺」「使う人を選ぶ機能😆

⚓GitHub Actionsに対応

# .github/workflows/rubocop.yml
name: RuboCop
on: [push, pull_request]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v1
    - name: Set up Ruby 2.6
      uses: actions/setup-ruby@v1
      with:
        ruby-version: 2.6.x
    - name: Install required package
      run: |
        sudo apt-get install libmysqlclient-dev libpq-dev libsqlite3-dev libncurses5-dev
+   - name: Cache gems
+     uses: actions/cache@preview
+     with:
+       path: vendor/bundle
+       key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
+       restore-keys: |
+         ${{ runner.os }}-gem-
    - name: Build and run RuboCop
      run: |
+       bundle config path vendor/bundle
        bundle install --jobs 4 --retry 3
        bundle exec rubocop --parallel

つっつきボイス:「これはGitHub向けの改修ですね」「gemのキャッシュと最新のRuboCopに対応」「GitHubがRailsでできてるのは有名ですよね😋」「GitHub Actionsはウォッチでも何度か取り上げましたけど(ウォッチ20190925)結構期待できそう😍

rubocop --parallelって知らなかった😳」「お、これは欲しいかも😋」「Rubocopちゃんで大量修正が必要なプロジェクトなんかでも、1ファイルごとにやれればいいからparallelにすれば速くなりますね😍」「どうせRSpecの方が断然遅いからまあ別にって感じですけど🤣」「🤣

参考: rubocop/basic_usage.md at master · rubocop-hq/rubocop
参考: Rubocop を使っているアナタがするべき2つのこと - Qiita

⚓ActiveStorage blobからrequire_dependencyを排除

# activestorage/app/models/active_storage/blob.rb#L17
class ActiveStorage::Blob < ActiveRecord::Base
- require_dependency "active_storage/blob/analyzable"
- require_dependency "active_storage/blob/identifiable"
- require_dependency "active_storage/blob/representable"
+ unless Rails.autoloaders.zeitwerk_enabled?
+   require_dependency "active_storage/blob/analyzable"
+   require_dependency "active_storage/blob/identifiable"
+   require_dependency "active_storage/blob/representable"
  end

つっつきボイス:「ZeitwerkがRails 6で殺したrequire_dependencyがActiveStorage::Blobにちょっぴり残ってたので排除したという小さい修正です」「サーチアンドデストロイ💀」「一応Zeitwerkなしでもやれるようにしないといけませんし☺」「これで殺戮完了したのかな?」「どうでしょう😆

参考: 定数の自動読み込みと再読み込み (Classic) - Rails ガイド

require_dependencyはRailsで独自に作ったメソッドでしたか〜」「以前のウォッチでも名前空間地獄の話題でrequire_dependencyの話になりましたね(ウォッチ20181022)」「Zeitwerkになってrequire_dependencyが不要になったというかむしろ完全排除される方向に」

Rails 6 Beta2時点のZeitwerk情報(要訳)

⚓ActiveRecord::Baseから不要なrequireを削除

# activerecord/lib/active_record/base.rb#L
-require "yaml"
require "active_support/benchmarkable"
require "active_support/dependencies"
require "active_support/descendants_tracker"
require "active_support/time"
-require "active_support/core_ext/module/attribute_accessors"
-require "active_support/core_ext/array/extract_options"
-require "active_support/core_ext/hash/deep_merge"
-require "active_support/core_ext/hash/slice"
-require "active_support/core_ext/string/behavior"
-require "active_support/core_ext/kernel/singleton_class"
-require "active_support/core_ext/module/introspection"
require "active_support/core_ext/class/subclasses"
require "active_record/attribute_decorators"
require "active_record/define_callbacks"
require "active_record/log_subscriber"
require "active_record/explain_subscriber"
require "active_record/relation/delegation"
require "active_record/attributes"
require "active_record/type_caster"
require "active_record/database_configurations"

つっつきボイス:「Active Record::Baseに要らないrequireが結構残ってたのが削除されてました」「Baseから消えるってなかなかスゴい😆」「core extensionあたりのrequireはいつの間にか冗長になってたんだろうな〜」「あ、requireしなくてもautoloadでモジュールが自動読み込みされるようになってたのか☺」「なるほどね!」

# b2c9ce3より
# activerecord/lib/active_record.rb#58
    autoload :Base
    autoload :Callbacks
+   autoload :Core
    autoload :CounterCache
    autoload :DynamicMatchers
    autoload :DynamicFinderMatch

参考: module function Kernel.#autoload (Ruby 2.6.0)

requireって雑に増えていきがちだからコワくてなかなか消せないのが大変😆」「いつかは消さないといけないんでしょうけど」「かぶっててもfalseが返るだけで害はないのでなかなか消されなさそう😆」「このたびfalseになることが確定したんでしょうね☺

⚓Rails

⚓Active Recordモデルをprivateにして大人しくさせてやった(Ruby Weeklyより)


つっつきボイス:「tameは『飼いならす』ですね😆」「モチベの説明が長いので後で追ってみます」

Account.public_methods.sizeでメソッドが685個出てきた😆」「むちゃくちゃや😆」「これは死にたくなる😇

「これがコンセプトみたいです↓」「むむむ…?Accountクラスを複数形のAccountsモジュールにして、Accounts::Modelnewしてからテーブル名だけ指定し、そしてprivate_constant :Modelでモデルをprivateにした…だと..?うはぁ〜!」「ウケた😆、喜びですか驚きですかあきれてますか?」「いや〜、これは面白い!!🎉

# 同記事より
module Accounts
  # Some important stuff up here, which will get to in a bit

  class Model < ApplicationRecord
    self.table_name = 'accounts'
  end
  private_constant :Model

  # Some important stuff down here, which will get to in a bit
end

「これは最近ActiveModel絡みでよく話している、永続化層切り離しですね〜❤」「おぉ」「上のように書くことでActiveRecordのメソッドのほとんどを殺すことができる: そして以下みたいに欲しいメソッドだけself.fetchとかself.createみたいに書いて単にモデルにdelegateして、かつそのモデルを返している」「いわゆる委譲ですね」

ModelモデルはprivateなのでAccountsモジュールの中なら見えるけど外部からはシャットアウトされる」「Accounts::Modelで外からいじるんじゃねーぞ、と😆」「これ確かに面白〜い!😋」「Rails wayじゃありませんけどね😆

# 同記事より
# app/models/accounts.rb
module Accounts
  def self.fetch(id:)
    Model.find(id)
  end

  def self.create(name:)
    Model.create!(name: name)
  end

  class Model < ApplicationRecord
    self.table_name = 'accounts'
  end
  private_constant :Model
end

「オレが委譲で許した以外の方法でモデルにアクセスするなよと😆」「返すものがモデルじゃなくてリレーションになるとまたちょっと微妙な話になるんですけど☺

「その発展型がこれか↓」「お、最後のAccountは普通のPORO(Pure Old Ruby Object)で、fetchcreateが今度はModelじゃなくてカスタムのAccountクラスを返すようにしたと、ほほぉ〜これはたぶんwhereみたいなリレーションを相手にしたくないと言ってそう😋

# 同記事より
# app/models/accounts.rb
module Accounts

  # --- Public APIs
  def self.fetch(id:)
    db_object = Model.find(id)
    Account.new(
      id: db_object.id,
      name: db_object.name,
    )
  end

  def self.create(name:)
    db_object = Model.create!(name: name)
    Account.new(
      id: db_object.id,
      name: db_object.name,
    )
  end

  # --- Private ActiveRecord model
  class Model < ApplicationRecord
    self.table_name = 'accounts'
  end
  private_constant :Model

  # --- Entity for the outside world
  class Account
    attr_reader :id, :name

    def initialize(id:, name:)
      @id = id
      @name = name
    end
  end
end

「さらにトランザクションもこの形↓でやってるし😳」「業務コードらしくなってきた😋

module Accounts
  def add_seat(id:)
    Model.transaction do
      db_object = Model.find(id)
      db_object.number_of_licenses += 1
      db.object.save!

      if db_object.number_of_licenses == 5
        SalesNotification.create!(account_id: id)
      end
    end
  end

  class SalesNotification
    belongs_to :account
  end
  private_constant :SalesNotification

  # Rest of implementation...
end

「まあこのパターンを既存のRailsアプリでいきなりやるのは無理あるのでそれはおいとくとして、今後はこういうふうに作ってみいやという感じかな〜😆」「自分には、ある意味カプセル化の基本に立ち返ったように見えますね☺」「そう!ちゃんとカプセル化してる」「これを実際にやるかどうかは別としても、ちょっと新鮮ですね😍

「一応記事の末尾にもいろいろ書いてますね: これはRailsのデフォルトのパターンじゃないし、どのActive Recordモデルに適用できるとも限らないと」「そりゃそうだ😆」「機が熟すまでこの設計には飛びつかない方がいいということみたいですね☺」「最初からこう書いていれば無駄なメソッドが600個も生えてこなくて済むでしょうけど😆

「元々Active Recordが継承でやるように作られちゃってるからなんでしょうけど😢」「まあそれはあるかも☺」「この記事を書いた人は、たぶんデータベースに直接触らせたくないマン😆

「このパターンでやれそうな例として『AccountUserをいつも同時に変更しているなら、同じモジュールに入れるべきじゃね?』と思えたときが挙げられてますね」「あ〜わかる!離れているモデルをいつも同時に扱ってわけわからなくなるぐらいだったらモジュールに閉じ込めてprivateにしちまえと😆

「いわゆるPoEAA↓のActiveRecordパターンからきちんとビジネスオブジェクトを切り離す前段階としては悪くなさそうですね: テストコードがあればとりあえず不用意にActiveRecordの呼ばれたくないメソッドを隠蔽できるのはまあ悪くないのかも?(つらそうだけど)」

「それにしても面白いパターンだわ〜😋」「カプセル化としてはとてもキレイではある☺」「これが実際にうまく当てはまる場合って何だろう?ん〜とん〜と🤔」(以下延々)


以下は記事冒頭の「モチベーション」より:

システムが大きくなったらカプセル化を強化すべきである。私たちはマイクロサービスや何ちゃらRailsエンジンでやりたいのではなく、Rubyの基本機能を少々用いることで実現する。
(中略)
そういうわけでモデリングにおいて防衛的なアプローチを始めた。つまりサポートできるものだけをpublicにしようということだ。これによってモデルの表面積が小さくなってサポートしやすくなるし、用途が絞られることで内部変更もしやすくなる。
(中略)
最後に、ROMSequelのようにData Accessパターンでこれに近いことをやれるライブラリはいろいろあるものの、それらに完全に乗り換えるのは簡単ではない。おそらく皆さんのアプリは最初からActive Recordを使っているだろう。本記事では、そうした技術への乗り換えが困難なまでに育ったRailsアプリを前提としている。
「データベースはインターフェイスじゃないんだけど!」とつぶやいてる人にはもしかするとこのパターンが向いているかもしれない。
同記事より大意

⚓Railsのセッションをクロスドメインで共有(RubyFlowより)


つっつきボイス:「1本目はcookieとセッションの基本的な解説で、2本目が本題のようです」「クロスドメインでセッションを共有ぅ〜?」「無茶な😆

「どうやらこの人たちはapp.kittens.iodev.kittens.ioという2つのドメインで認証を共有したいらしいです」「なるほどそっちですか😆」「cookieの仕様でこういうのってやれるんだったかな?🤔」「この記事ではセッションストアをRedisにしてるので、それならやれそう☺

「お、このconfig↓でdomain: :allにするのがポイントらしい😳」「これでドメインが変わってもcookieをよしなに扱えるってこと?」「へぇ〜😳

# 同記事2より
Rails.application.config.session_store :redis_session_store,
  key: '_kittens_session',
  serializer: :json,
  domain: :all,
  redis: {
    expire_after: 1.week,
    key_prefix: 'kittens:session:',
    url: ENV['REDIS_SESSIONS_URL']
}

「この記事みたいにサブドメインの違う複数サーバーでセッションを共有したいことって結構あるんでしょうか?」「サブドメインがwwwとかliveとかloginみたいに分かれてて、loginで認証したら他のサブドメインも見られるようにする、なんてのは普通にやりますね☺」「SSO↓でやると大げさになっちゃうんでしょうか?」「まあそのためだけにSSOは使わないでしょう😆」「これでやれるならサブドメインを気軽に作れますし☺」「昔セッション共有やるべきかどうかについて議論になった気がするけど思い出せない😆

参考: シングルサインオン - Wikipedia

追いかけボイス: 「web書くならにcookieのdomain指定は把握しておいてほしいなあ…大昔にこんなのを書いていました↓」「おぉありがとうございます😂」「まあ今はさらに色々ありますが😆

社内勉強会でSOP (Same Origin Policy) の話をしました

⚓Active StorageはRails 6でどう変わったか


つっつきボイス:「記事はActive StorageがRails 6でどう変わったかというまとめで、このSaeloun Blogは最近グロスで翻訳の許可ももらえました😋


「お、mini_magickが置き換わった?」「そういえばウォッチでもimage_processingというgem↓に置き換わったのを扱ってた覚えが(ウォッチ20180511)」「画像の向きも自動で修正してくれるとか、image_processingよさげ😍


「次は画像のvariantのサポート」「variantはサイズ違いの画像ですね」「carrierwaveとかでやってたのがActive Storageでもできるようになった☺

carrerwave↓のgemspecを見るとimage_processingが入ってますね😋。mini_magickもまだありますが。


「最後がhas_many_attached」「これだけで書けるのはアツい❤」「そういえばRails 6で挙動が変更されたんでした(ウォッチ20190729)」「前はattachしてupdateすると追加されてたのが、Rails 6で更新されるようになって他と挙動を合わせたと」「countの結果↓違う〜😨」「breaking changeなのでオプションで選べるようになったんでした」

# 同記事より
# Rails 5まで
blog = ActiveStorage::Blob.create_after_upload!(filename: "updated_pic.jpg")
user.update(images: [blog])

user.images.count
=> 2

# Rails 6
blog = ActiveStorage::Blob.create_after_upload!(filename: "updated_pic.jpg")
user.update(images: [blog])

user.images.count
=> 1

⚓心が折れないRailsスキーマ管理


つっつきボイス:「Railsスキーマ管理で心が折れないための方法😆」「冒頭で早速例の『マイグレーションでActive Recordモデルを参照するな』が出てきてますね」「morimorihogeさんも口を酸っぱくして言ってるヤツ(ウォッチ20190415)」「babaさんも昔記事書いてました↓」

[Rails 3] 失敗しないmigrationを書こう

「次は使い捨てのスクリプトでデータをインポート」「one-offは『使い捨ての』という意味ですね」「捨てスクリプトを全環境で実行したらもうgitにも登録するなと」「それわかる〜😋」「基本的には残す意味ないヤツ😆」「one-off migrationスクリプトを歴史としてコミット履歴に残すというのは別に悪くはない気もしますね: Wikiとかに書いてもいいんだけど『いつのコードで動かすことを想定していたか』を明らかにするという点ではコミットに挟まっててrevertした履歴があるというのも歴史管理としては一つの戦略だとは思う(これがベストだとは思いませんが)」「おぉ」

「次はどの環境のスキーマを『正』にするかみたいな話」「productionが正に決まっとる😆」「病欠でいない人がスクリプトをローカルで走らせてなくて、しかもスクリプトがもう消されたという状況になったら、production->staging->developmentの順で最新にすると」「考えたくない状況😅」「むか〜しスキーマのインデックス周りが環境ごとにちょっぴりずれてて修復したの思い出した😭」「マイグレーションの定番を押さえるのによさそうな記事ですね😋

見出しより:

  • マイグレーションはスキーマだけを変更する
  • seedやデータインポートを使い捨てスクリプトでやる
  • pruneを徹底して環境を同期する
  • おまけ: いらないマイグレーションファイルを消す
  • 上のやり方から離れるべき場合

⚓その他Rails


前編は以上です。

バックナンバー(2019年度第4四半期)

週刊Railsウォッチ(20191106後編)holiday_japan gemで日本の祝日判定、小さい関数が有害になるとき、Gitブランチのファジー検索ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Rails 6の新しいデフォルト設定と安全な移行方法を詳しく解説(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails 6の新しいデフォルト設定と安全な移行方法を詳しく解説(翻訳)

本記事では、rails app:updateで生成されるnew_framework_defaults_6_0.rbの9つのデフォルトフラグについてひととおり解説します。最後までお読みいただければ、自信を持ってこのファイルを削除し、application.rbload_defaults 6.0と堂々と書けるようになります。

本記事ではRails 5.2のデフォルト設定を使っているアプリを前提としています。これについては、自分のアプリのapplication.rbload_defaults 5.2と書かれているかどうかを調べることで確認できます。

訳注: 簡単のため、以下のデフォルト設定ではRails.application.config.を省略しています。

1. action_view.default_enforce_utf8 = false

フラグの機能

formのaccept-charset属性は、IE5やその時代のブラウザで導入されました。この属性は、送信するform要素のデータで用いるエンコーディングをブラウザに指定します。Railsはやがてaccept-charset="UTF-8"を自動的に追加してUTF-8での送信をブラウザに強制するようになりました。

しかしIE 5には困った振る舞いがいくつかありました。送信されたすべての文字がIE 5のデフォルトcharsetで表現できる場合は、accept-charsetを無視するのです。ブラウザのデフォルトcharsetがLatin-1に設定されているユーザーがいるとしましょう(訳注: Latin-1はフランス語やドイツ語などのヨーロッパ系言語で用いられていました)。このユーザーがIE 5からフォームを送信したとき、入力をすべてLatin-1で表現できてしまう場合、accept-charset属性は無視され、Latin-1でエンコードされたフォームが送信されます。つまりUTF-8のデータベースにLatin-1の文字が入ってしまうのです!

Railsではこれを回避するハックとして、2010年の@25215d7[:_snowman_]というHTMLエンティティがhidden formフィールドに追加されました。他のエンコードではサポートされていないUnicodeのsnowman絵文字☃を用いて、IE 5にaccept-charset属性を守らせます。

雑学: このsnowmanはその後、ログに雪だるまの絵文字が出力されるのを嫌がる人たちがいることからチェックマーク「✓」に変更されました。

参考: Rails の `utf8=✓` の歴史と消し方と snowman ☃ - Qiita

このチェックマーク記号は未だにRailsフォームのhiddenフィールドに居座っています。このフラグのコメントを外すと、チェックマーク記号はフォームに含まれなくなります。

このフラグを安全に有効にするには

accept-charsetのすっとこどっこいな振る舞いはIE 5からIE 8まで続きました。これらのブラウザをサポートしなくてもよければ、コメントを外しても安全です。

2. action_dispatch.use_cookies_with_metadata = true

フラグの機能

Railsはcookie値を暗号化および署名し、cookie値の暗号化を解除して署名を照合することで、悪者によって改変されていないことを確認します。しかしこの方法では、一部のcookieで暗号化済みおよび署名済みの値を悪者がコピーして、他のcookieの値として用いることまでは防げません。

次のシナリオで考えてみましょう。

  1. Railsに次の2つのcookieが設定されているとする
    • is_admin: false
    • is_a_doofus: true(doofus: アホ)
  2. 2つのcookieの暗号化/署名済みの値を悪者が入れ替える
  3. Railsはリクエストに含まれる値を読み取って「よろしい、値は改変されていない」と承認する
  4. Railsはユーザーを「adminかつdoofusでない」と認識するが、実際は「非adminかつdoofus」

フラグの話に戻ります。このフラグをコメント解除すると、「purpose」というフィールドをcookieに埋め込んでから、暗号化と署名を行うようになります。するとRailsは、上のステップ3で「ふむ、値はどちらも改変されてはいないが、cookieの名前とpurposeが一致しておらぬな」と判断します。かくして悪人は相変わらずdoofusのままです。

このフラグを安全に有効にするには

このフラグをコメント解除しても既存のcookieが壊れることはありません。cookieはリクエスト時に読み込まれ、レスポンスではpurposeフィールドを追加してリライトされます。以後、新しいcookieにはpurposeフィールドが含まれるようになります。

# This option is not backwards compatible with earlier Rails versions.
# It’s best enabled when your entire app is migrated and stable on 6.0.
大意: このオプションは従来のRailsと互換性がありません。アプリ全体の移行が終わって6.0で安定運用してから有効にするのがベストです。

恐ろしげなwarningが表示されますが怖がることはありません。このフラグをいったんコメント解除すると、アプリがRails 6.xにロックインされますよと言っているのです。Rails 5.xはcookieのpurposeフィールドを解釈できないので、Rails 5.xへのダウングレードは不可能になります。

アプリがRails 6で安定運用できることを確認できれば、このフラグは安全にコメント解除できます。

3. action_dispatch.return_only_media_type_on_content_type = false

フラグの機能

HTTPレスポンスにはcontent-typeが含まれています。RailsがHTMLビューをレンダリングすると、content-type: text/htmlのように設定されます。このヘッダーには”text/html”というメディアタイプだけが含まれていることにご注目ください。

このフラグをコメント解除すると、このヘッダーに他の値も追加されるようになります。デフォルトではcontent-type: text/html; charset=utf-8のようにcharsetが含まれます。

このヘッダーの値は、以前からActionDispatch::Response#content_typeで調べられるようになっていますが、この値にもcharsetが含まれるようになります。

このフラグを安全に有効にするには

このフラグをコメント解除すると、外部と内部の両方に影響が生じます。

外部向けとしては、アプリのユーザーがレスポンスで完全なcontent-typeヘッダーを受け取るようになります。ユーザーがcontent-typeをあれこれ調べるようなことがなければ、これといった問題はないと考えても大丈夫です。

内部では、ActionDispatch::Response#content_typeの値が従来とは異なるものになります。影響が生じるかどうかはテストスイートでわかるでしょう。テストがコケる場合は、以下のような博物館入りのcontroller specが使われているかもしれません。

Failure/Error: expect(response.content_type).to eq("text/html");
expected: "text/html"
            got: "text/html; charset=utf-8"
(compared using ==)
     # ./spec/controllers/users_controller_spec.rb:10:in `block (4 levels) in <top (required)>'

修正の指針としては以下が考えられます。

  • expect(response.media_type).to eq("text/html")にする
  • 従来のcontent_typeと同様にmedia_typeメソッドがメディアタイプだけを返すようにする
  • expect(response.content_type).to include("text/html")にする
  • コントローラのテストをシステムテストにリファクタリングすることを検討する(コントローラのテストは今や非推奨です)

4. active_job.return_false_on_aborted_enqueue = true

フラグの機能

Active Jobにはbefore_enqueueなどのライフサイクルコールバックがあることを、きっと皆さまもご存知でしょう。しかしActive Jobのどの場所であろうと、throw(:abort)を実行した瞬間に処理が止まって終了することや、そうしたコールバックの中で以下のように書けることはご存知ですか?

class MyJob < ApplicationJob
  before_enqueue { |job| throw(:abort) if job.arguments.first }
  def perform; end
end

job1 = MyJob.perform_later(false)
job2 = MyJob.perform_later(true)

job1は、フラグの設定にかかわらず、キューに入ったジョブクラスのインスタンスになります。現時点ではjob2も同様です。そしてこのフラグをコメント解除すると、コールバック内でabortがスローされてjob2falseになります。

このフラグを安全に有効にするには

コードベースでabortをgrepしてください。abortのインスタンスを調べて、ジョブのコールバックにabortが出現しないようにしましょう。

ジョブのコールバックでabortを見つけてしまったら、ジョブがどこでキューに入ったかを調べる必要があります。常にジョブインスタンスにするために、その箇所がperform_laterに依存しないようにします。運悪くperform_laterに依存していたら、falseでもやっていけるようにリファクタリングする必要があります。

5. active_storage.queues.analysis = :active_storage_analysis

フラグの機能

Active Storageでファイルがアタッチされると、ActiveStorage::Attachmentafter_commit_createコールバックが呼び出されます。このコールバックによってActiveStorage::AnalysisJobがキューに入ります。そして実行されると、このジョブでActiveStorage::Blob#analyzeが呼び出されます。このメソッドは、ファイルからメタデータを抽出するのにプラグインシステムを用いています。このメタデータは、blobレコードのmetadataカラムに保存されます。

これは、mini_magickを使って画像からheightwidthを取り出すときによく使われます。

現在のActiveStorage::AnalysisJobdefaultキューに入ります。このフラグをコメント解除すると、専用のactive_storage_analysisキューに送信されるようになります。これによって、ジョブのカスタム優先順位レベルをアプリで設定できるようになります。

このフラグを安全に有効にするには

このフラグをコメント解除すると、新しいActiveStorage::AnalysisJobactive_storage_analysisキューに入るようになります。キューのバックエンドがこのキューのジョブを処理できるように設定されていることを確認する必要があります。

例: Sidekiqを使うアプリの場合、active_storage_analysisキューをconfig/sidekiq.ymlに追加する必要があります。ここに記載した順序によって、他のキューとの相対的な優先順位が決定されます。

既にキューに入った既存のジョブが壊れることはありません。従来どおりdefaultキューで処理されます。

6. active_storage.queues.purge = :active_storage_purge

フラグの機能

以下のコードがあるとします。

class User < ApplicationRecord
  has_one_attached :avatar
end

sam = User.create.avatar.attach(some_image_file)
sam.avatar.attach(different_image_file)

最初のファイルがアタッチされると、S3などのストレージにアップロードされ、Railsはそのストレージの場所を指すレコードを1件作成します。2番目のファイルがアタッチされると同様にS3にアップロードされますが、Railsはこのレコードを更新して、新しいストレージの場所を指すように変更します。

最初にS3にアップロードしたファイルはどうなったのでしょう?Active Storageはファイルをほったらかしにはしません。ActiveStorage::PurgeJobがキューに入り、最終的にファイルをS3から頑張って削除します。

現在のActiveStorage::PurgeJobdefaultキューに入ります。このフラグをコメント解除すると、以後は専用のactive_storage_purgeキューに送信されます。これによって、ジョブのカスタム優先順位レベルをアプリで設定できるようになります。

このフラグを安全に有効にするには

このフラグをコメント解除すると、新しいActiveStorage::PurgeJobactive_storage_purgeキューに入るようになります。キューのバックエンドがこのキューのジョブを処理できるように設定されていることを確認する必要があります。

例: Sidekiqを使うアプリの場合、active_storage_purgeキューをconfig/sidekiq.ymlに追加する必要があります。ここに記載した順序によって、他のキューとの相対的な優先順位が決定されます。

既にキューに入った既存のジョブが壊れることはありません。従来どおりdefaultキューで処理されます。

7. active_storage.replace_on_assign_to_many = true

フラグの機能

以下のコードがあるとします。

class Message < ApplicationRecord
  has_many_attached :uploads
end

files = get_array_of_files
message = Message.create(uploads: files)
files << get_another_file
message.update(uploads: files)

メッセージが作成されると、Active StorageはファイルをS3などのストレージにアップロードします。しかし、ここで最終行の配列uploadsに再代入したらどうなるでしょう?

  1. Railsは既存の配列と新しい配列の差を取る。新しいファイルをアップロードすると、配列に既に存在するファイルは無視される。
  2. Railsは配列の差分を取らない。既存のファイルをすべて破棄して新しい配列内のファイルをすべてアップロードする。

現在のRailsの挙動は1.です。コメント解除するとRailsの挙動は2.になります。

このフラグを安全に有効にするには

コードベースでhas_many_attachedをgrepします。これによって変更の影響を受けるモデルを特定できます。モデルが影響を受けないのであれば、このフラグを安全にコメント解除できます。

モデルが影響を受ける場合は、再代入を行っている箇所をリファクタリングしてattachを使うことをおすすめします。attachは、既存のファイルに触らずに、渡されたファイルを素朴にアタッチします。すなわち、ファイルの重複防止がアプリで重要な場合は、ファイル重複防止ロジックを自分で追加する必要があります。

8. action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"

フラグの機能

ActionMailer::DeliveryJobは、Railsメイラーでdeliver_laterが呼ばれたときにキューに入るジョブです。

RailsチームはActionMailer::DeliveryJobクラスにbreaking changeを入れることを望みました。しかしこれではRailsのアップグレード中に既存のジョブが壊れるかもしれません。そこで新たにActionMailer::MailDeliveryJobを作ることが決まりました。

このフラグをコメント解除すると、以後新しいActionMailer::MailDeliveryJobクラスによるメイラージョブがキューに入るようになります。

狙いは、新しいActionMailer::MailDeliveryJobがキューに入ってもActionMailer::DeliveryJobが引き続き処理されるようにすることです。ActionMailer::DeliveryJobのジョブは今後キューに入らなくなるので、このクラスは最終的に不要になります。実際、RailsチームはActionMailer::DeliveryJobをRails 6.1で削除する予定です。

このフラグを安全に有効にするには

The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions.
If you send mail in the background, job workers need to have a copy of
MailDeliveryJob to ensure all delivery jobs are processed properly.
Make sure your entire app is migrated and stable on 6.0 before using this setting.

大意: デフォルトのデリバリージョブ(ActionMailer::Parameterized::DeliveryJobActionMailer::DeliveryJob)はRails 6.1で削除されます。この設定は以前のRailsとの後方互換性がありません。メールをバックグラウンドで送信すると、ジョブワーカーはMailDeliveryJobですべてのデリバリージョブが正しく送信されるようにする必要があります。アプリの移行が完全に終わって6.0で安定運用するようになってから、この設定を使いましょう。

恐ろしげなwarningが表示されますが怖がることはありません。このフラグをいったんコメント解除すると、アプリがRails 6.xにロックインされますよと言っているのです。Rails 5.xはcookieのpurposeフィールドを解釈できないので、Rails 5.xへのダウングレードは不可能になります。

アプリがRails 6で安定運用できることを確認できれば、このフラグは安全にコメント解除できます。

9. active_record.collection_cache_versioning = true

フラグの機能

Rails 5.2でリサイクル可能なキャッシュキーが導入されました。この機能によって、Active Recordのcache_keyのうち変動の可能性があるupdated_atcache_versionに移動します。

# Rails 5.1の場合
user = User.last
user.cache_key # "users/281-20191007212244313194"
user.touch
user.cache_key # "users/281-20191017003012868191"
# Rails 5.2の場合
user = User.last
user.cache_key # "users/281"
user.cache_version # "20191007212244313194"
user.touch
user.cache_key # "users/281"
user.cache_version # "20191017003012868191"

上のコードで、Rails 5.1の場合とは異なり、Rails 5.2でキャッシュキーが変化しなくなっているのがわかります。キャッシュのミスヒットにつながるキーに頼るのではなく、Rails 5.2ではキーを再利用してキャッシュエントリを更新するようになりました。これによってキャッシュミスが減少し、パフォーマンスが向上します。

このフラグをコメント解除すると、リサイクル可能なキャッシュキーがActiveRecord::Relationなどのコレクションにも導入されます。

Rails.application.config.active_record.collection_cache_versioning = false
User.all.cache_key # "users/query-b03a3611aaa3ed0825f6b93870f69c0e-281-20191007212244313194"
Rails.application.config.active_record.collection_cache_versioning = true
User.all.cache_key # "users/query-b03a3611aaa3ed0825f6b93870f69c0e"

このフラグを安全に有効にするには

このフラグは安全にコメント解除できます。私は以下の2つの理由から、さほどややこしくならずに済むだろうと思うことにしています。

第1に、この変更が懸念材料となるのは、ユーザーのコードではなくキャッシュアダプタ(Redisなど)の側です。この機能をサポートしないモダンなアダプタはちょっと思いつきません。

第2に、既にBig Binaryの良記事(「recyclable cache keys」と「recyclable collection cache keys」)があることです。

10. config.autoloader = :zeitwerk

フラグの機能

これぞまさしくload_defaults 6.0有効にする第10のフラグです。しかしこれはnew_framework_defaultsには含まれていません。このフラグは、RailsのオートローダーをclassicからZeitwerkに切り替えます。

新しいデフォルト設定を読み込む前にZeitwerkを使いたいのであれば、application.rbでconfig.autoloader = :zeitwerkを呼べます。

このフラグを安全に有効にするには

Edgeガイドのマイグレーションガイドは読んでおく価値があります(参考: Railsガイド — オートローディング)。しかし実際にはつらい点は1つしか見当たりません。Zeitwerkはイニシャライザ内でのオートロードをサポートしなくなりました。

テストを実行するときに以下のようなwarningが出力されていないかどうか探してみましょう。

DEPRECATION WARNING: Initialization autoloaded the constant Foo.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload Foo, for example,
the expected changes won’t be reflected in that stale Class object.

These autoloaded constants have been unloaded.

大意: 非推奨警告: 初期化時に定数Fooがオートロードされました。
この挙動を有効にする機能は非推奨であり、初期化時のオートロードは今後のバージョンのRailsでエラーになります。
再読み込みしてもアプリは再起動しないので、コードは初期化中に再実行されません。したがってFooを再読み込みしても、期待する変更は古いClassオブジェクトに反映されなくなります。
オートロードされたこれらの定数はアンロードされました。

何が問題か

以下の2つのファイルがあるとします。

# app/foo.rb
class Foo
  def self.bar?
    true
  end
end
# config/initializers/baz.rb
BAZ = Foo

classicモードのRailsは、イニシャライザで上のコードがある場合にFooを問題なくオートロードします。BAZ.bar?は期待どおりtrueを返します。

foo.rbを編集してfalseを返すようにしたらどうなるでしょうか。BAZ.bar?はtrueを返します。理由は、コードが再読み込みされてもイニシャライザは再実行されないからです。Fooの値はアプリケーションを再起動するまでそのままになります。

Zeitwerkでは、そのような自分の足を撃ち抜く銃を使えません。初期化中にオートロードを検出すると、定数をアンロードしてwarningを表示します。

解決法

そのオートローディングを削除することです。Fooが1つのイニシャライザだけで参照されているのであれば、定義をそのイニシャライザに移動するだけで済みます。

# config/initializers/baz.rb
class Foo
  def self.bar?
    true
  end
end
BAZ = Foo

複数箇所から参照されているのであれば、オートロードされない場所(libなど)にfoo.rbを移動し、明示的にrequireします。

# config/initializers/baz.rb
require Rails.root.join('lib', 'foo')
BAZ = Foo

warningが消えてテストがgreenになれば、Zeitwekを安心して使えるようになります。

関連記事

Rails 6 Beta2時点のZeitwerk情報(要訳)

週刊Railsウォッチ(20191118前編)ActiveJob引数のログ抑制、RailsガイドProプランお試し、ファイルアップロードのレジュームgemほか

$
0
0

こんにちは、hachi8833です。銀座Rails #15やっぱり行けばよかった😢。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • 毎月第一木曜日に「公開つっつき会」を開催しています: お気軽にご応募ください

今回のウォッチは第16回公開つっつき会を元にお送りいたします。ご参加いただいた皆さまありがとうございました!差し入れいただいて感激です😂。

Rails: 先週の改修(Rails公式ニュースより)

今回はコミットリストから見繕いました。


つっつきボイス:「6.0.1リリースから間もないせいか、小さ目のパッチが中心のようです」「明日の銀座Railsのネタ仕込まないといけないの思い出した😅」

新機能: 重要な情報を引数とするジョブでログ出力を抑制するオプションを追加

ジョブがキューに入るときやジョブ実行時に、ジョブの引数のログ出力を無効にするオプションを追加した。
CHANGELOGより大意

class SensitiveJob < ApplicationJob
  self.log_arguments = false

  def perform(my_sensitive_argument)
  end
end

つっつきボイス:「今まではジョブのログに出ちゃってたのか😳」「例のfilter_parameters↓的なことをジョブのログでもやりたいということでしょうね☺️」

参考: 13.1 パラメータをフィルタする — Action Controller の概要 - Rails ガイド

# config/initializers/filter_parameter_logging.rb
config.filter_parameters += :password

「(ゲストの方に)背景を説明しておくと、これはパスワードのような重要な情報をログに出さないための仕組みの話ですね: せっかく他のセキュリティを頑張っても、ログに生のパスワードとかが全部出てたりすると、仮に侵入されたときにログファイルだと楽勝で読めてしまうので、あ…終了😇なんてことがまれによくあったりします😆」「お〜😅」

「フォームに値を入力して送信すると原則としてパラメータ(params)がログ出力されるんですけど、Railsには前述のような問題を防ぐ仕組みとして前からfilter_parametersというものがあって、デフォルトではpasswordという文字をパラメータ名に含む場合は、ログに出すときに値をXXXXみたいに伏せ字にしてくれます」「なるほど!」「今回の改修はそれをジョブの引数についても同じようなことをやれるようになったということですね☺️」

「この改修って割と重要ですよね😆」「一部の人にとっては😆」

スレッドでのdeprecationメッセージ出力を修正

# activesupport/test/deprecation_test.rb#L293
+ def test_silence_threaded
+   barrier = Concurrent::CyclicBarrier.new(2)
+
+   th = Thread.new do
+     ActiveSupport::Deprecation.silence do
+       barrier.wait
+       barrier.wait
+       assert_not_deprecated { ActiveSupport::Deprecation.warn "abc" }
+     end
+     assert_deprecated("abc") { ActiveSupport::Deprecation.warn "abc" }
+   end
+
+   barrier.wait
+
+   assert_deprecated("abc") { ActiveSupport::Deprecation.warn "abc" }
+
+   ActiveSupport::Deprecation.silence do
+     assert_not_deprecated { ActiveSupport::Deprecation.warn "abc" }
+   end
+
+   assert_deprecated("abc") { ActiveSupport::Deprecation.warn "abc" }
+
+   barrier.wait
+   th.join
+ ensure
+   th.kill
+ end

つっつきボイス:「ActiveSupport::Deprecation.silenceがスレッドローカルになってなかったのでConcurrent::ThreadLocalVar↓を使うようにしたのかなと☺️」「そういえばちょっと前にもConcurrentを使った改修がありましたね😳(ウォッチ20190909)」

# activesupport/lib/active_support/deprecation.rb#L38
    def initialize(deprecation_horizon = "6.2", gem_name = "Rails")
      self.gem_name = gem_name
      self.deprecation_horizon = deprecation_horizon
      # By default, warnings are not silenced and debugging is off.
      self.silenced = false
      self.debug = false
+     @silenced_thread = Concurrent::ThreadLocalVar.new(false)
    end

RailsアプリでConcurrent Rubyを使う(翻訳)

「でもdeprecation warningなんだし、そうまでして直さなくてもいいような気もしますけど😆」「この修正がないととても困るという気はあまりしない😆」「マルチスレッドで一部についてだけdeprecation warningを出して、それ以外では抑制したいときとか?」「特定IPのときだけdeprecation warningを出したいとか?😆」「どうしても欲しいシチュエーションがあんまり思いつかない〜🤣」「欲しい人がいるのはわかるけど😆」

「たとえばRailsの移行中とかで、warningを承知のうえで抑制したいけど全部抑制すると後で追えなくなるから欲しくなったとかですかね?☺️」「移行中に自分たちでwarningをある程度制御しておきたいみたいな」「Pumaとかでマルチスレッドで回しているときにこの問題に気づいたとかはあるかも🤔」「せやなと言うしかない😆」

従来はActiveSupport::Deprecation.silence { ... }がすべてのスレッドのdeprecationメッセージを抑制するので、マルチスレッド環境でびっくりする人もいると思う。
ActiveSupport::Deprecation.silenced=はグローバルオプションに設定を残すので少々紛らわしい可能性もあるが、これにはさまざまな用法があるのでこの方法が最もうまく一般的な用法を変えずに済むと思われる。silence {}はコードの小さなセクションのメッセージを抑制し、silenced=はイニシャライザやテストヘルパーでデフォルト設定に用いることが多い。
自分としてはActiveSupport::Deprecation.silenced=を非推奨にして代わりにActiveSupport::Deprecation.behavior = :silenceを使うべきかもしれないとふんわり思っている(絶対そうすべきとまでは思っていないし、このプルリクの趣旨から外れる)。
同PRより大意

新機能: travel_backテストヘルパーがブロックを取れるようになった

# activesupport/lib/active_support/testing/time_helpers.rb#L42
+     def stubbed?
+       !@stubs.empty?
+     end
...
      def travel_back
+       stubbed_time = Time.current if block_given? && simple_stubs.stubbed?
+
        simple_stubs.unstub_all!
+       yield if block_given?
+     ensure
+       travel_to stubbed_time if stubbed_time
+     end

# 同コミットより
Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00

travel_to Time.zone.local(2004, 11, 24, 01, 04, 44)
Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00

travel_back do
  Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
end

Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00

つっつきボイス:「travel_backは時間を巻き戻すヤツでしたね」「ブロックの最終評価値を使ってtravel_backを設定できるようになった😋」「これはわかる〜😋」

参考: travel_back — ActiveSupport::Testing::TimeHelpers

# api.rubyonrails.orgより
Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
travel_to Time.zone.local(2004, 11, 24, 01, 04, 44)
Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
travel_back
Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00

「Railsでテストとかで現在時刻を違う時間に設定するのにtravel_toというメソッドを使うのがRailsらしいですよね☺️」「列車の比喩で『旅』『時間旅行』にかけた感じ😆」

参考: travel_to — ActiveSupport::Testing::TimeHelpers

# api.rubyonrails.orgより
Time.current     # => Sat, 09 Nov 2013 15:34:49 EST -05:00
travel 1.day
Time.current     # => Sun, 10 Nov 2013 15:34:49 EST -05:00
Date.current     # => Sun, 10 Nov 2013
DateTime.current # => Sun, 10 Nov 2013 15:34:49 -0500

Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
travel 1.day do
  User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
end
Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00

「これはテストコードで使うんでしょうか?」「時刻に依存するテストを書きたいときなんかに使いますね☺️」「travel_toで現在時刻を別の時間に変えるんですけど、それを元に戻したいときにtravel_backします⏱」「なるほど〜」「業務アプリを書いていると時刻系はよくハマります🧐」

travel_backは以下の記事にも登場してますね。

Rails: Timecopを使わなくても時間を止められた話

やはりというか原田真二の「タイム・トラベル」という懐メロを思い出しました👨。

「そういえばtimecopってありましたね」「以前はtimecopでやってましたが今はActiveSupportでやれます👍」

時間警察はドラえもんで知りましたが、古くはポール・アンダースン「タイムパトロール」、最近だと仮面ライダーG電王なんですね😳。

参考: タイムパトロール - Wikipedia
参考: 仮面ライダー×仮面ライダー×仮面ライダー THE MOVIE 超・電王トリロジー - Wikipedia

HTTPトークン認証の解説をガイドに追加

# rails/actionpack/lib/action_controller/metal/http_authentication.rb#L405
    module Token
      TOKEN_KEY = "token="
      TOKEN_REGEX = /^(Token|Bearer)\s+/
      AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
      extend self
...

つっつきボイス:「RailsガイドにBASIC認証とダイジェスト認証の説明はあったのにトークン認証の説明がなかったので追加したそうです」「機能は前からあったんですね😳」

トークン認証

HTTPトークン認証は、HTTP AuthorizationヘッダーでBearerトークンの利用を有効にするスキームです。さまざまな形式のトークンを利用可能ですが、詳細は本ドキュメントの範疇を超えます。
たとえば、以下のように事前発行された認証トークンを使って認証やアクセスを実行したいとします。Railsでのトークン認証の実装はauthenticate_or_request_with_http_tokenというメソッド1つでやれるのでとても簡単です。

class PostsController < ApplicationController
  TOKEN = "secret"
  before_action :authenticate
  private
    def authenticate
      authenticate_or_request_with_http_token do |token, options|
        ActiveSupport::SecurityUtils.secure_compare(token, TOKEN)
      end
    end
end

authenticate_or_request_with_http_tokenは上述の例のように2つの引数「トークン」「Hash(HTTP Authorizationヘッダーから取り出したオプションを含む)」を取ります。認証が成功したらこのブロックはtrueを返します。falseまたはnilが返ると認証は失敗です。
同PRより大意

「トークン認証って何だろう?🤔」「Authorizationにトークンを突っ込むだけで使える🧐」「SSOサーバーを自分で実装するときとかに使うのかなと推測🤔」「Authorizationヘッダーでサポートされてるなら大丈夫そう」「Railsガイドに記載されていない機能って結構ありますよね😆」「こういうのが増えるのはありがたい🙏」

参考: RFC 6750 - The OAuth 2.0 Authorization Framework: Bearer Token Usage — トークン認証の仕様
参考: トークンを利用した認証・認可 API を実装するとき Authorization: Bearer ヘッダを使っていいのか調べた - Qiita

トークンはtoken68の仕様↓に沿っていればよいようです。

参考: token68

class_method_defined_withinをリファクタリング

# activerecord/lib/active_record/attribute_methods.rb#L117
      def dangerous_class_method?(method_name)
-       RESTRICTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
-     end
+       return true if RESTRICTED_CLASS_METHODS.include?(method_name.to_s)

-     def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
-       if klass.respond_to?(name, true)
-         if superklass.respond_to?(name, true)
-           klass.method(name).owner != superklass.method(name).owner
+       if Base.respond_to?(method_name, true)
+         if Object.respond_to?(method_name, true)
+           Base.method(method_name).owner != Object.method(method_name).owner
          else
            true
          end
        else
          false
        end
      end

つっつきボイス:「シンプルなリファクタリングのようです」「1箇所でしか呼んでないメソッド要らんだろということでインライン化した感じですね☺️」

match?starts_withends_withに変更


つっつきボイス:「最近も似たような修正あったような🤔」「こういうふうにmatch?start_with?に置き換えたり↓」「地道なマイクロ最適化ですね☺️」

# actionpack/test/dispatch/exception_wrapper_test.rb
    setup do
      @cleaner = ActiveSupport::BacktraceCleaner.new
      @cleaner.remove_filters!
-     @cleaner.add_silencer { |line| !line.match?(/^lib/) }
+     @cleaner.add_silencer { |line| !line.start_with?("lib") }
    end

「正規表現は一般にパフォーマンスがあまり出ないので『〜で始まる文字』みたいなチェックは専用の文字列メソッドの方がわかりやすいし速いですね😋」「特に前方一致は明らかに違い出ますね🧐」

「2つ目はto_jsonstart_with?が効かなかった問題を修正したそうです↓」「こういうのってたまに踏む😆」「ありがたい🙏」「OptionMergerは暗黙で使われるから知らないうちに踏んでたかも😅」

# activesupport/lib/active_support/option_merger.rb#L5
module ActiveSupport
  class OptionMerger #:nodoc:
    instance_methods.each do |method|
-     undef_method(method) unless method.start_with?("__", "instance_eval", "class", "object_id")
+     undef_method(method) unless method.to_s.start_with?("__", "instance_eval", "class", "object_id")
    end

Ruby: 文字列マッチは正規表現より先に専用メソッドを使おう

たぶんこちらも関係してそうです↓。

ポリモーフィック関連付けで:polymorphic:as:foreign_typevalid_optionsに追加した

# activerecord/lib/active_record/associations/builder/belongs_to.rb#L9
    def self.valid_options(options)
-     super + [:polymorphic, :counter_cache, :optional, :default]
+     valid = super + [:counter_cache, :optional, :default]
+     valid += [:polymorphic, :foreign_type] if options[:polymorphic]
+     valid
    end

つっつきボイス:「@kamipoさんの修正です」「Active Recordのこの辺のコードを単体で見てもまるでわからん😆」「😆」「何ができるようになるのやら😆」

y-yagiさんのブログに、これを含めたvalid_options関連のkamipoさんの修正がまとまっていました。

参考: rails commit log流し読み(2019/11/10) - なるようになるブログ

「今Active Record周りを勉強し始めているところなんですけど、上のコードはActive Recordの中身の話なんでしょうか?」「そうですね、Active Recordはこう書かれているというヤツです」「こういうところを読んでくとActive Recordがどう振る舞うかわかるようになるんでしょうか?」「この辺の改修を眺めるというのはどちらかというと、Railsでアプリを開発していてたまたまRailsのバグを踏んだり、うっかりRailsが想定していない書き方をして謎のエラーを出してしまったときなんかに役に立つこと『も』あるという感じです」「あ〜そうでしたか😳」「普通にRailsアプリのコードを書いている分には9割必要ないと思います😆」「😆」

「それでもどういう改修があったかを追っていくことには一定のメリットがありますね: 新しい機能が入ったりバグが修正されたということは、安定版のRailsにはまだ入っていない機能なので、自分たちのところでバグが発現する可能性があるわけです」「ふむふむ」「改修を追っていると、自分がそういうバグを踏んだときに『あ、そういえばこのバージョンのRailsにはこんなバグがあった気がする』という既視感につながっていくので、そういうのがごくまれに役に立つことは、ある🤣」「🤣」「🤣」

「この修正は安定版にはまだ入ってないんですね😳」「改修はmasterブランチにまず入るんですけど、これはまだリリースされていない状態のブランチです: 今だと最新安定版(6-0-stableブランチ)は6.0.1なんですけど、masterはそれより進んだ状態になっていて、今後時期が来たらたとえば6.0.2というタグが打たれて次の最新安定版に入るという具合です」「なるほど😋」「今はrubygems.org↓でRailsを見ると6.0.1になっているので、rails newすると6.0.1が最新として入るようになってます」

参考: RubyGems.org | コミュニティのGemホスティングサービス

新機能: コネクションを管理するRoleManagerを導入

こちらはつっつきの後で気づきました😅。コードが長いので引用はしません。

このプルリクで追加したRoleManagerは複数のRolesを管理できる。各Roledb_configを持つ。
これによりpublic APIの改変を回避し、ハンドラやロールの概念を扱えるようにし、各クラスのハンドラごとに複数のコネクションを内部で確立するのに必要なものを達成する。
同PRより大意

Rails

RSpec 3.9がリリース


つっつきボイス:「いつの間にか3.9になってた😳」「rspec-railsだけ本体とバージョンが合わなくなる?」「rspec-railsは足並み揃えるよりどんどん改修入れる方を優先しようということですね☺️」

なお、私が今転生作業中のオレオレRailsアプリはこっそりminitestにしてます😆。

HerokuがSorbetをやってみてわかったこと(Ruby Weeklyより)


よかった点:

  • 結果を保存せずに同じメソッドを2回呼んでいたのを指摘
  • nullableなカラムを公開してたのを指摘
  • 雑にnilまたはtrueを返していたのを指摘

つっつきボイス:「HerokuがSorbetを使ってみたと」「4人チームで実験的にやってみた段階だそうですが、上のような点を見つけてくれたそうです😆」

「Sorbetはウォッチでも何度か取り上げましたが、RailsというよりRubyやってる人たちの間でアツい静的型チェッカーです😆」「😆」「Railsアプリを普通に分にはSorbetまだ知らなくても大丈夫かなと思いますが😆、とてもミッションクリティカルなアプリを書くときには必要になるかもしれませんね☺️」

「とりあえずアイスクリームアイコン🍨↓がかわいい❤️」


sorbet.orgより

「sorbet-rails試してみたかったんですが、なぜか今日のbundle installがえらく遅くて、しかも今見たらnot publicly available yetとか出てた😇」「😆」


その後うなすけさんの以下のスライドに今頃気づきました。

Basecampが無料個人版をリリース


basecamp.comより


つっつきボイス:「Basecampが個人版を?へぇ〜」「法人版だと価格的に見合わないので出したそうです」「completely freeって書いてますね」「マジで?」「見落としてた😅」「機能制限はあるけど無料!」「ストレージ1GBか〜😅」「個人なら十分かも」

「一応背景を説明すると、BasecampというのはRuby on Railsフレームワークの作者であるDHH(David Heinemeier Hanson)↓がいる会社で、一種のプロジェクト管理ツール的なサービスであるBasecampはもちろんRailsで提供しています」「日本ではあまり知られていませんけど😆」「日本だと(聞き取れず)が使ってるらしいけどネ」

「Basecampのサービスを何に例えたらいいかな🤔」「GitHubプロジェクトからソースコード管理を引いたような感じですかね〜☺️」「タスク管理とかチャットとかスケジュールとか」

「そしてBasecampに新しい機能が入ると、いずれその機能がRailsにも入ってくるということが割とあったりします😆」「へぇ〜」「たとえばRailsのAction CableはRails 4.xで入ったんですけど、それはBasecampにチャット機能が入ったからだろう、なんてことがよく言われます🤣」「😆」「Basecampを見てれば直近の未来がわかるかもしれないと」「BasecampはフルにRailsで書かれているはずなので知っておくといいと思います☺️」

参考: Action Cable の概要 - Rails ガイド

Skunk: 「コードの臭い」を数値化(Ruby Weeklyより)


つっつきボイス:「コードがどのぐらい臭ってるかを数値化するgemだそうです💩」「説明すると、『コードの臭い(code smell)』はプログラミングの世界ではダメなコードの書き方に対して使われます」

# 同リポジトリより
New critique at file:////Users/etagwerker/Projects/fastruby/skunk/tmp/rubycritic/overview.html
+-----------------------------------------------------+----------------------------+----------------------------+----------------------------+----------------------------+----------------------------+
| file                                                | stink_score                | churn_times_cost           | churn                      | cost                       | coverage                   |
+-----------------------------------------------------+----------------------------+----------------------------+----------------------------+----------------------------+----------------------------+
| lib/skunk/cli/commands/default.rb                   | 166.44                     | 1.6643999999999999         | 3                          | 0.5548                     | 0                          |
| lib/skunk/cli/application.rb                        | 139.2                      | 1.392                      | 3                          | 0.46399999999999997        | 0                          |
| lib/skunk/cli/command_factory.rb                    | 97.6                       | 0.976                      | 2                          | 0.488                      | 0                          |
| test/test_helper.rb                                 | 75.2                       | 0.752                      | 2                          | 0.376                      | 0                          |
| lib/skunk/rubycritic/analysed_module.rb             | 48.12                      | 1.7184                     | 2                          | 0.8592                     | 72.72727272727273          |
| test/lib/skunk/cli/commands/status_reporter_test.rb | 45.6                       | 0.456                      | 1                          | 0.456                      | 0                          |
| lib/skunk/cli/commands/base.rb                      | 29.52                      | 0.2952                     | 3                          | 0.0984                     | 0                          |
| lib/skunk/cli/commands/status_reporter.rb           | 8.0                        | 7.9956                     | 3                          | 2.6652                     | 100.0                      |
| test/lib/skunk/rubycritic/analysed_module_test.rb   | 2.63                       | 2.6312                     | 2                          | 1.3156                     | 100.0                      |
| lib/skunk.rb                                        | 0.0                        | 0.0                        | 2                          | 0.0                        | 0                          |
| lib/skunk/cli/options.rb                            | 0.0                        | 0.0                        | 2                          | 0.0                        | 0                          |
| lib/skunk/version.rb                                | 0.0                        | 0.0                        | 2                          | 0.0                        | 0                          |
| lib/skunk/cli/commands/help.rb                      | 0.0                        | 0.0                        | 2                          | 0.0                        | 0                          |
+-----------------------------------------------------+----------------------------+----------------------------+----------------------------+----------------------------+----------------------------+
StinkScore Total: 612.31
Modules Analysed: 13
StinkScore Average: 0.47100769230769230769230769231e2
Worst StinkScore: 166.44 (lib/skunk/cli/commands/default.rb)

「どうやらこのSkunkはRubyCritic(ウォッチ20180223)っていうgemの拡張として作られたみたい☺️↓」「skunkっていう名前が気に入ってエイヤで作ったんじゃないかな〜😆」「😆」「よくskunkっていう名前のgem名取れたなって」「ホントだ😳」「名前の奪い合いとかありますし😆」「自分だったらとりあえずリポジトリ取っちゃうかな😆」「最近だと使ってないリポジトリは譲渡しなさいって迫られたりするみたいですけど😆」


同リポジトリより

参考: RubyCritic + CircleCI + Slack でプロジェクトの Ruby コードを継続的に採点しよう | Engineer’s Base Camp

「gemの名前としてstinkとかskunkとかを使うのはまだわかりますけど、CIでこれが走って『オマエのコードはこれだけ臭いゾ』とか出てきたらバトルになるんじゃないかと😆」「ですよね😆」「もうちょいオブラートにくるむとか😆」

「なおソフトウェアの世界だと、これみたいにコードに何らかの形で点数をつけるツールというのは昔からいっぱいあります」「ふむふむ」「上で言うとchurnとかcostみたいな項目はたぶん昔からあって、そういう値を元にstink_scoreとやらを算出するとかそういう感じかなと☺️」「Code Climate↓も似たようなことをやっていますね」「カバレッジみたいなメジャーな項目については算出方法が確立しているんですけど、項目をずらりと出しただけだとわかりにくくて、知りたいのは『要するに何点なの?』というところなので、このgemとかCode Climateとかはそういうところをやってくれます☺️」


codeclimate.comより

技術的負債を調査する10のポイント(翻訳)

RailsガイドProプランのお試しでクレカが不要に


同記事より


つっつきボイス:「最近始まったRailsガイドProプランは、横断検索がかなりいい感じらしいのでRailsを書き始めて間もない人には結構いいんじゃないかと思います👍」「ほほぉ😋」「本家のRails Guidesにもない機能です」「新しい人が開発中に詰まったときに簡単に調べられるのは便利でしょうね☺️: 自分は脳内にマップできてるのでたぶん使わないけど😆」「Proプランでは例のAlgolia↓でインクリメンタル検索やってるそうです」

Rails: 高速リアルタイム検索API「algolia-search-rails」gem README(翻訳)

activestorage-resumable: レジューム機能をサポート(RubyFlowより)

<!-- 同リポジトリより -->
<%= form.file_field :attachments, multiple: true, resumable_upload: true %>

つっつきボイス:「おぉ?Active Storageでレジューム機能?」「しかもアップロードで?」「レジュームは、アップロード中に回線切れても後で再開する機能ということですね」「まだ★2つ(その後14★に)ですが刺さりました?」

「そもそもHTTPの仕様ではPOSTアップロード中にレジュームできるのか?🤔」「アップロードのレジュームって難しそうですね☺️」「ダウンロードはレジュームできるんでしょうか?」「昔からありますね🧐」「Stackoverflow↓を見た感じではやっぱりアップロードのレジュームはなさそう😇」

参考: Standard method for HTTP partial upload, resume upload - Stack Overflow

「resumable gemのREADMEをざっと見ると、chunk単位でアップロードしてプログレスをLocalStorageに保存することでやってるみたいっすよ😎」「あぁ、そのレベルから頑張ってるのか😆」「これはがんばり屋さんだな〜😆」「LocalStorageって20MBぐらいしか入らないんだっけ?」「プログレス情報だけなら問題なさそう😋」

HTML5のLocal Storageを使ってはいけない(翻訳)

「ちなみにchunkという言葉はコンピューターネットワークの世界でよく使われます」「辞書を見ると『厚切りの一切れ』みたいな感じ」「HTTPの仕様レベルではアップロードのレジューム機能は提供されていないはずなので、このgemはそういうchunk単位で小分けにしたファイルを何回もアップロードすることでレジュームを実現してるということでしょうね☺️」「は〜なるほど!」「何も読んでないけどたぶんそういう実装だと思います😆」「後は使ってみないと😆」

その他Rails



前編は以上です。

バックナンバー(2019年度第4四半期)

週刊Railsウォッチ(20191112後編)invisible gemで可視性を変えずにパッチ当て、スライド:「型なし言語のための型」、自然言語の言語名を推測ほか

今週の主なニュースソース

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Rails 6のDocker開発環境構築をEvil Martians流にやってみた

$
0
0

先々月に公開したこちらの翻訳記事の実践編ということで。試行錯誤しているうちにRailsが6.0.1になりました。

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

Docker for Macについて

これまではピュアな環境を求めてParallels Desktop for MacのUbuntu VM上でDockerを使っていたのですが、久しぶりにDocker for Macを使ってみると速度や使い勝手が随分よくなっていて驚きました。

  • Docker for Macの方がUbuntu VMのDockerよりビルドが速い(体感ですが)
    • ただしEvil Martiansの記事にもあるように、docker-composeでボリュームに:cachedを記述しておかないとDocker for Macで遅くなります
  • 今はdmgファイルをダウンロードしてアプリケーションフォルダにドロップするだけでインストール完了できる

Ubuntuだと基本的にsudoを打たないとDockerが使えないのと(回避方法は一応ありますが)、macOSとの間のレイヤが増えるつらみの方が大きくなってきたので、Docker for Macでやることにしました😅。

環境

  • macOS: 10.15.1(Catalina)
  • Docker for Mac
    • Docker: 19.03.4
    • Docker-compose: 1.24.1
    • Dockerhubにログインしておく
    • (既存のDockerがあれば削除しておく)
  • Rails: 6.0.1
  • Ruby: 2.6.5
  • Git: 2.24.0
  • dipをインストールする

本記事でのDocker操作は全面的にEvil Martians謹製のdipを使っています。dipの説明は割愛しますが、とてもよかったので別記事にしたいと思います。

リポジトリ

今回のサンプルを以下のリポジトリに置きました。

プロジェクトディレクトリ
├── .dockerdev
│   ├── .psqlrc
│   ├── Aptfile
│   └── Dockerfile
├── .env(ダミー)
├── dip.yml
└── docker-compose.yml

変更の方針

  • Evil Martiansの設定からの変更は最小限とし、できるだけ汎用性を高める
    • docker-compose内のimage名は自分のプロジェクトに合わせてリネームし、:1.0.0は残す
  • Evil Martians流でやるとイメージのサイズが1GBになるので軽量化する
    • その代わり基本的なツールは自分で補わないといけない😅
  • SidekiqとRedisは使わないのでdocker-compose.ymlから削除
  • dip.ymlは多少カスタマイズする
  • .psqlrcも入れておく

変更後のDockerfile

ARG RUBY_VERSION
# 後述
FROM ruby:$RUBY_VERSION

ARG PG_MAJOR
ARG NODE_MAJOR
ARG BUNDLER_VERSION
ARG YARN_VERSION

# 依存関係をインストール
# 外部のAptfileでやってる(後ほどお楽しみに!)
COPY .dockerdev/Aptfile /tmp/Aptfile
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade &&\
    DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends curl gnupg2 &&\
    # ソースリストにPostgreSQLを追加
    curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - &&\
    echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list &&\
    # ソースリストにNodeJSを追加
    curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash - &&\
    # ソースリストにYarnを追加
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\
    echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list &&\
    apt-get update -qq &&\
    DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends\
    build-essential\
    postgresql-client-$PG_MAJOR\
    libpq-dev\
    nodejs\
    yarn=$YARN_VERSION-1\
    $(cat /tmp/Aptfile | xargs) &&\
    apt-get clean &&\
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &&\
    truncate -s 0 /var/log/*log

# bundlerとPATHを設定
ENV LANG=C.UTF-8\
  GEM_HOME=/bundle\
  BUNDLE_JOBS=4\
  BUNDLE_RETRY=3
ENV BUNDLE_PATH $GEM_HOME
ENV BUNDLE_APP_CONFIG=$BUNDLE_PATH\
  BUNDLE_BIN=$BUNDLE_PATH/bin
ENV PATH /app/bin:$BUNDLE_BIN:$PATH

# RubyGemsをアップグレードして必要なバージョンのbundlerをインストール
RUN gem update --system &&\
    gem install bundler:$BUNDLER_VERSION &&\
    gem install rails &&\
    mkdir -p /app

WORKDIR /app

Dockerfileの変更のポイント

  • イメージをruby:2.6.5からruby:2.6.5-slim-busterに変更
    • イメージが軽くなる代わりに、そのままだとcurlpingなどの基本的なパッケージも入らないので、apt-getでインストールするかAptfileに書いておきます
  • curlを実行するRUNコマンドをまとめてイメージの肥大化を防ぐ
  • gem install railsを追加

これでイメージは1GBから578MBに半減しました。もっと減らしたいならAlpine Linuxベースのイメージを使う手もあります。

Dockerのマルチステージビルドもやってみたかったのですが、手間の割にあまり報われない気がしたのと、Evil Martians流からあまり離れたくなかったので見送りました。

変更後のdocker-compose.yml

version: '3.4'

services:
  app: &app
    build:
      context: .
      dockerfile: ./.dockerdev/Dockerfile
      args:
        RUBY_VERSION: '2.6.5-slim-buster'
        PG_MAJOR: '11'
        NODE_MAJOR: '11'
        YARN_VERSION: '1.19.1'
        BUNDLER_VERSION: '2.0.2'
    image: yourapp_docker:1.0.0
    tmpfs:
      - /tmp

  backend: &backend
    <<: *app
    stdin_open: true
    tty: true
    volumes:
      - .:/app:cached
      - rails_cache:/app/tmp/cache
      - bundle:/bundle
      - node_modules:/app/node_modules
      - packs:/app/public/packs
      - .dockerdev/.psqlrc:/root/.psqlrc:ro
    environment:
      - NODE_ENV=development
      - RAILS_ENV=${RAILS_ENV:-development}
      - DATABASE_URL=postgres://postgres:postgres@postgres:5432
      - BOOTSNAP_CACHE_DIR=/bundle/bootsnap
      - WEBPACKER_DEV_SERVER_HOST=webpacker
      - WEB_CONCURRENCY=1
      - HISTFILE=/app/log/.bash_history
      - PSQL_HISTFILE=/app/log/.psql_history
      - EDITOR=vi
    depends_on:
      - postgres
    env_file: .env

  runner:
    <<: *backend
    command: /bin/bash
    ports:
      - '3000:3000'
      - '3002:3002'

  rails:
    <<: *backend
    command: bundle exec rails server -b 0.0.0.0
    ports:
      - '3000:3000'

  postgres:
    image: postgres:11.1
    volumes:
      - .psqlrc:/root/.psqlrc:ro
      - postgres:/var/lib/postgresql/data
      - ./log:/root/log:cached
    environment:
      - PSQL_HISTFILE=/root/log/.psql_history
    ports:
      - 5432
    env_file: .env

  webpacker:
    <<: *app
    command: ./bin/webpack-dev-server
    ports:
      - '3035:3035'
    volumes:
      - .:/app:cached
      - bundle:/bundle
      - node_modules:/app/node_modules
      - packs:/app/public/packs
    environment:
      - NODE_ENV=${NODE_ENV:-development}
      - RAILS_ENV=${RAILS_ENV:-development}
      - WEBPACKER_DEV_SERVER_HOST=0.0.0.0

volumes:
  postgres:
  bundle:
  node_modules:
  rails_cache:
  packs:

docker-compose.ymlの変更のポイント

  • redisなど、自分の使わないサービスを削除した
  • 認証情報は.envファイルに含め、.gitignoreに追加した
    • そのためenv_file: .envをyamlに2箇所追加した
    • (リポジトリの.envは空にしてあります)
  • credentialの暗号化機能はまだ使っていません

たとえ空であっても本当は.envをリポジトリに入れたくありませんでしたが、すぐ試せるようにしたかったのでした。

補足

このdocker-compose.ymlはローカル開発環境の構築のみを想定しています。Evil Martiansもそのように作っています。

PostgreSQLの接続文字列はdocker-compose.ymlに以下のように丸ごと埋め込まれています↓ので、db/database.ymlを開発用に設定する必要はありませんし、db/database.ymlにdevelopment環境用の設定を加えても効きません(ハマりました😅)。db/database.ymlを設定するのはCIやproductionぐらいになると思います。

  • DATABASE_URL=postgres://postgres:postgres@postgres:5432

docker-compose.ymlのimage名のyourapp_dockerの部分は適宜自分のプロジェクト名に変えてください。

セットアップ

  • dipをインストールする(homebrewでもできます)
gem install dip
  • git clone git@github.com:hachi8833/rails_docker_like_evilmartians.gitでリポジトリをcloneしてディレクトリに移動する
  • docker-compose.ymlのyourapp_dockerは自分のプロジェクト名に変更する(本記事では変更していません)

  • git checkinしてコミット

  • dip compose buildでビルド

$ dip compose build
# 略
Successfully tagged yourapp_docker:1.0.0
  • dip shでコンテナにログインする
$ dip sh
Creating network "rails_docker_like_evilmartians_default" with the default driver
Creating volume "rails_docker_like_evilmartians_postgres" with default driver
Creating volume "rails_docker_like_evilmartians_bundle" with default driver
Creating volume "rails_docker_like_evilmartians_node_modules" with default driver
Creating volume "rails_docker_like_evilmartians_rails_cache" with default driver
Creating volume "rails_docker_like_evilmartians_packs" with default driver
root@7b09aea79b9f:/app#

以下はDockerコンテナ内部での作業です。

  • yarn -vruby -vbundle -vwhich railsが実行できることを確認
  • rails newに好みのオプションを付けて実行

なおbundle execは不要です。

今回は以下のオプションにしました。アプリ名はappで固定されます。Webpackerもまとめてセットアップされます。

rails new . –database=postgresql –skip-active-storage –skip-action-mailer –skip-active-job –skip-action-cable –skip-action-mailbox –skip-action-text –skip-turbolinks –skip-sprockets –skip-spring –skip-bootsnap –webpacker –webpack=vue

root@7b09aea79b9f:/app# rails new . --database=postgresql --skip-active-storage --skip-action-mailer --skip-active-job --skip-action-cable --skip-action-mailbox --skip-action-text --skip-turbolinks --skip-sprockets --skip-spring --skip-bootsnap --webpacker --webpack=vue
# 略
info All dependencies
├─ @vue/component-compiler-utils@3.0.2
├─ consolidate@0.15.1
├─ de-indent@1.0.2
├─ he@1.2.0
├─ merge-source-map@1.1.0
├─ prettier@1.19.1
├─ vue-hot-reload-api@2.3.4
├─ vue-loader@15.7.2
├─ vue-style-loader@4.1.2
├─ vue-template-compiler@2.6.10
├─ vue-template-es2015-compiler@1.9.1
└─ vue@2.6.10
Done in 5.33s.
Webpacker now supports Vue.js 🎉
  • 環境によっては以下も実行して、bundlerの余分なメッセージを抑制する
bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java
  • 使われていないvendorディレクトリをrm -rf vendorで削除
  • exitでDockerをいったん抜ける
  • git checkinする

起動前の設定

  • config/environments/development.rbに以下を追加する
config.hosts << "localhost"
config.web_console.whitelisted_ips = '0.0.0.0/0'

Rails 6からはconfig.hostを明示的に指定する必要があります(ウォッチ20190401)。

また、Dockerで開発する場合はおそらくwhitelisted_ipsの制約も解除が必要になります。


  • dip provisionを実行してdevelopmentとtestの空データベースを作成
    • (dip.yamlをそれ用にカスタマイズしています)
    • データベース名はapp_developmentapp_testで固定
$ dip provision
# (略)
== Preparing database ==
Created database 'app_development'
== Installing dependencies ==
The Gemfile's dependencies are satisfied

== Preparing database ==

== Removing old logs and tempfiles ==

== Restarting application server ==
== Installing dependencies ==
The Gemfile's dependencies are satisfied

== Preparing database ==
Created database 'app_test'

== Removing old logs and tempfiles ==

== Restarting application server ==
  • 念のためdip minitestでminitestを実行
$ dip minitest
Starting rails_docker_like_evilmartians_postgres_1 ... done
Run options: --seed 59169

# Running:



Finished in 0.000523s, 0.0000 runs/s, 0.0000 assertions/s.
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips
  • git checkinでコミット

いよいよ起動

できた!

$ dip rails s
Starting rails_docker_like_evilmartians_postgres_1 ... done
=> Booting Puma
=> Rails 6.0.1 application starting in development
=> Run `rails server --help` for more startup options
Puma starting in single mode...
* Version 4.3.0 (ruby 2.6.5-p114), codename: Mysterious Traveller
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop

後は好きに進めます。dip lsすればdipのサブコマンド一覧が表示されます。

作業が終わったらdocker-compose downでコンテナを終了します。

参考: 後からプロジェクトに参加する人の作業

  • Git、Docker for Mac、git-flow、dipなどをセットアップ
  • リポジトリをgit cloneする
  • dip compose build
  • dip bundle install
  • dip yarn install --check-files
  • .envに秘密鍵を入れるのであれば、.gitignoreに.envを追加する(必須)
  • データベースにseedデータを入れる(略)

Dockerfileを改定した場合は、docker-compose.ymlのimage名のタグにあるバージョン番号を適宜更新します。

関連記事

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

Viewing all 1381 articles
Browse latest View live