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

Rails: Value Objectを検討してみよう(翻訳)

$
0
0

概要

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

Rails: Value Objectを検討してみよう(翻訳)

ふと気がつくと、アプリケーションで同じようなコンセプトのビューヘルパーをいくつも作ってしまっていることがよくあります(複雑な計算メソッドや、1つ以上の値をいくつものメソッドに渡すなど)。

こんなときは、Active Recordモデルより小規模なシンプルなオブジェクトでその正体を明らかにしてみましょう。

今回の場合は、機能をリファクタリングして、Martin Fowlerが提唱するValue Object」にすることを検討します。

(Value Objectとは)通貨や日付のrangeのようなシンプルで小さなオブジェクトであり、(オブジェクトの)同一性に頼らずに等価性をチェックする。
martinfowler.comより

次のように書くのではなく

普通にヘルパーを書く。

def css_color(r, g, b)
  "##{r.to_s(16)}#{g.to_s(16)}#{b.to_s(16)}"
end

次のように書く

アプリケーションで使われているコンセプトを正しく捉える「Value Object」に切り出す。

class RGBColor
  def initialize(red, green, blue)
    @red = [[0, red].max], 255].min
    @green = [[0, green].max], 255].min
    @blue = [[0, blue].max], 255].min
  end

  def to_hex
    [@red, @green, @blue].each_with_object("") do |part, to_hex|
      to_hex << part.to_s(16)
    end
  end
end

そして以下をヘルパーに書く

def css_color(r, g, b)
  "##{RGBColor.new(r, g, b).to_hex}"
end

そうする理由

アプリケーションで使われているコンセプトがデータベースに保存されないからといって、それをアプリケーションのオブジェクトにすべきということにはなりません。

リファクタリングしてValue Objectにすればコンセプトを表現するコードを分離でき、さまざまなメリットが得られます。「関心の分離(separation of concerns)」という言葉をご存じの方もいることでしょう。このコンセプトの振る舞いのテストも分離されるので、網羅的かつ効率のよいテストを書けるようになります。

コードの整理が進むことで理解もはかどります。このコードとコンセプトは再利用しやすく、オブジェクトの機能を拡張するための格好の場所が手に入ります。

そうしない理由があるとすれば

Value Objectの導入は時期尚早の場合も考えられます。コンセプトをValue Objectに切り出すタイミングを見い出すのは簡単ではありません。Value Objectの導入が早すぎると、コードが無駄に複雑になって混乱を招く可能性もあります。逆に導入が遅れれば、ぐちゃぐちゃのコードを書くはめになります。

上の例で書いたようなクラス定義による方法以外に、Struct、場合によってはOpenStructを使う手もありますが、これらのオブジェクトはミュータブルであり、プロパティを変更できてしまいますので、コードがシンプルになるどころか余計複雑になることもあります。

関連記事

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


Rails: シンプルなForm Object gem「yaaf」(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。

Rails: シンプルなForm Object gem「yaaf」(翻訳)

RailsアプリでForm Objectパターンを使いやすくするyaafというgemをご紹介します。

rootstrap/yaaf - GitHub

Form Objectでつらかった点(yaafがなかった頃)

Form ObjectパターンはRailsアプリで広く用いられており、私たちは今もプロジェクトごとに異なる方法でForm Objectを書く傾向があります。

そうしたForm Objectはほとんどの場合、同一のアプリの中ですらインターフェイスが揃っていません。

また私たちは、Form Objectで発生したエラーが表示されるよう、バリデーションを(それぞれモデルとForm Objectで)2回書いてしまう傾向もあります。

ところで、こうした社内Form Objectのほとんどはデータベーストランザクションをうまく利用できていると思います。しかしそれらが、何か問題が起きたときにエラーをraiseすることが期待される!付きメソッドをコントローラに提供できていることはめったにないことについては賭けてもよいです。

ビジネスロジックは、アプリの「コントローラ」「モデル」「ヘルパー」のあらゆる場所に存在します(中にはRailsのイニシャライザに仕込まれているというつらいシナリオもあります)が、それで正しいのでしょうか?私たちは、(メール送信やレコード更新や外部サービス呼び出しなどで)何かオブジェクトを作成するときにビジネスロジックの追加が必要だと繰り返し気付かされます。コントローラがビジネスロジックの置き場としてふさわしくないことは私たちも既に知っていますし、かといってモデルに置くのも単一責任の原則(SRP)に違反するのでよろしくないことも知っています。しまいには、Service Objectをいくつもこしらえてはビジネスロジックをゴミ箱のようにそこに放り込むのがオチです。

そうせずにやる方法は、ある

yaaf gemを使えば以上の問題を楽に解決できます。これについて説明させてください。

インターフェイスを統一したい: yaafはsaveの利用を促進し、皆さまがご存知のように、フォームのデータをモデルに「保存」するためにsaveを実行します。

コンテキストに沿うバリデーションだけを実行したい: 自分のForm Objectにバリデーションを書いて、データが所属する場所でデータ一貫性バリデーションを維持しましょう。データが所属する場所というとService Objectのことでしょうか?まさか、モデルのことです。yaaf gemは、モデルのあらゆるエラーのコレクションをForm Objectエラーのコレクションに昇格させ、ビューでそれらのエラーを表示できるようにしてくれます。

では、あるForm Objectでsaveが行われなかった場合にエラーをraiseする必要があるときはどうすればよいでしょうか?既にお気づきかと思いますが、!付きのsave!メソッドをお使いください。yaafではsave!が定義済みなので、Active Recordモデルと同様にエラーをraiseします。もちろん、モデルでsaveに失敗した場合はデータベースロールバックもトリガーされます。つまりsavesave!のどちらも使えるのです。

オブジェクト作成に関連するビジネスロジックの置き場に迷う: Form Objectがその置き場所です。yaaf gemを用いることで、モデルでコールバックが動くときと同じ方法でコールバックを利用できるようになります。たとえば、フォームを送信して永続化した後にメールを送信したい場合なら、after_commitコールバックがそれでしょう。詳しくはyaaf gemのREADMEをご覧ください。

Form Objectの標準化

yaaf gemを用いることで、プロジェクトのproduction用コードにわたってForm Objectの定義を標準化できるようになります。

yaaf gemを使わない理由があるとすれば

すべての責務をどうしてもコントローラーで扱いたいのであればご自由にどうぞ。この場合yaaf gemの出る幕はありません。

「全部Service Objectで書きたい」なら、Form Objectを書くよりService Objectを書く方が気分がよいでしょう。

なおJavaで仕事をしている方ならyaaf gemは使いたくならないでしょうね。

yaafのささやかな経緯

私たちはまさにこれと同じアプローチをproductionアプリでほぼ1年間にわたって利用し、この方法で10を超えるForm Objectを書き、そしてうまくいっています。この機能をgemに切り出した理由はこれです(yaaf gemのコード量は現時点でわずか64行であり、私たちもこれ以上の機能は必要としていません)。

このgemをごく初期の段階からシェイプアップしてくれた@santibにひたすら感謝です!

yaaf

yaaf gemの素敵なブランディングはSofi Salazarによるものです。

rootstrap/yaaf - GitHub

関連記事

私はnilを使わない(翻訳)

Rails: ActiveSupport::Concernをextendしたモジュールをprependする機能(翻訳)

$
0
0

概要

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

Rails: ActiveSupport::Concernをextendしたモジュールをprependする機能(翻訳)

Railsにconcernsをprependするためのサポートが追加されました(ba2bea5e)。

⚓ prependとは

あるモジュールをクラスにprependすると、そのモジュールは継承チェインの(そのクラス自身よりも手前の)冒頭に挿入される。

⚓ Rubyのインスタンスメソッドをprependする場合

module Population
  def preferred_transport
    "by walk"
  end
end

class Human
  prepend Population

  def preferred_transport
    "by air"
  end
end

Human.new.preferred_transport #=> by walk

上のコード例では、PopulationモジュールがHumanクラスでprependされています。

これによって、Populationモジュールにあるpreferred_transportは、探索チェイン内のHumanクラスにある(同名の)preferred_transportよりも優先されるようになりました。

以下のようにHumanクラスでancestorsを呼ぶことでこのことを確認できます。

Human.ancestors
#=> [Population, Human, Object, Kernel, BasicObject]

⚓ Rubyのクラスメソッドをprependする場合

module Population
  def self.prepended(base)
    class << base
      prepend ClassMethods
    end
  end

  module ClassMethods
    def count
      5000
    end
  end
end

class Human
  prepend Population

  def self.count
    1000
  end
end

Human.count #=> 5000

上のコード例では、ClassMethodsモジュール内にあるメソッドをprependedフックで拡張し、PopulationモジュールのcountクラスメソッドがHumanクラスの(同名の)countクラスメソッドより優先されるようにしています。

Human.ancestors
#=> [Population, Human, Object, Kernel, BasicObject]

⚓ Railsのconcernsをprependする

Railsのconcernsにprependのサポートが追加されたことで、上述の2つのコード例を以下のようにずっとシンプルにまとめられるようになりました。

module Population
  extend ActiveSupport::Concern

  def preferred_transport
    "by walk"
  end

  class_methods do
    def count
      5000
    end
  end
end

class Human
  prepend Population

  def self.count
    1000
  end

  def preferred_transport
    "by air"
  end
end

Human.new.preferred_transport #=> by walk
Human.count #=> 5000

このコード例では、先ほどのコード例のようにprependedフックを用いてクラスメソッドを手動で追加する必要はありません。

Humanクラスの探索チェインをancestorsで調べると以下のような感じになります。

[Population, Human, ActiveSupport::ForkTracker::CoreExtPrivate, ActiveSupport::ForkTracker::CoreExt, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Tryable, ActiveSupport::Dependencies::Loadable, Kernel, BasicObject]

また、以下のようにモジュールがクラスにprependされるときに、prependedブロックを用いて任意のカスタムコードを実行することもできます。

module Population
  extend ActiveSupport::Concern

  prepended do
    puts "the module is prepended"
  end

  def preferred_transport
    "by walk"
  end
end

このprependedブロックは、そのモジュールがクラスにprependされた後で実行されます。これはincludedブロックの動作と似ています。

注意: ひとつのモジュール内にprependedブロックを複数置くことはできません。

prependedブロックを複数宣言しようとするとActiveSupport::Concern::MultiplePrependBlocks例外がraiseされます。


prependconcerningでも動作します。その場合、以下のようにconcerningブロックが始まる直前にprepend: trueを定義する必要があります。

class Human
  concerning :Preferences, prepend: true do
    def preferred_transport
      "by walk"
    end

    class_methods do
      def count
        5000
      end
    end
  end
end

この場合Humanクラスの探索チェインは以下のような感じになります。

 [Human::Preferences, Human, ActiveSupport::ForkTracker::CoreExtPrivate, ActiveSupport::ForkTracker::CoreExt, ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency, ActiveSupport::ToJsonWithActiveSupportEncoder, Object, PP::ObjectMixin, JSON::Ext::Generator::GeneratorMethods::Object, ActiveSupport::Tryable, ActiveSupport::Dependencies::Loadable, Kernel, BasicObject]

関連記事

Rails: 個別のバリデーションエラーをErrorオブジェクトにカプセル化する(翻訳)

Rails 6.1で`rails new`の生成を最小限にするフラグが追加(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。

Rails 6.1でrails newの生成を最小限にするフラグが追加(翻訳)

追加前

Railsアプリケーションを新たに生成する場合、デフォルトではモダンなフルスタックWebアプリケーションを構築するのに必要なフレームワーク機能を可能な限りすべて取り入れます。

しかし、Railsの機能をすべて使いたい人ばかりではありません。たとえば、アプリケーションでAction CableやAction Mailbox、Action Textを必要としない場合、アプリケーションを生成するときに以下のようにそうしたフレームワーク機能の導入をスキップするフラグを指定します。

rails new my_awesome_app --skip-action-text --skip-action-cable\
--skip-action-mailbox

この機能があるにもかかわらず、フレームワークの追加機能をすべて除外した最小限の「素の」Railsが欲しいという声もありました。実際、ミニマリストが以下のようにフラグを渡すことも珍しくありません。

rails new my_awesome_app --skip-spring --skip-listen --skip-bootsnap\
--skip-action-text --skip-active-storage --skip-action-cable\
--skip-action-mailer --skip-action-mailbox --skip-test\
--skip-system-test --skip-active-job

そのためには、Railsにデフォルトでどんなフレームワーク機能が含まれるかを知ったうえで不要な機能をスキップする必要がありました。

追加後

Railsアプリケーションを生成するときに、以下の方法で最小限のフレームワーク機能だけを持たせられるようになりました(#39282)。

  rails new my_awesome_app --minimal

--minimalを指定することで、以下のフレームワーク機能がすべてスキップされ、ベアボーンのRailsアプリケーションを得られます。

- action_cable
- action_mailbox
- action_mailer
- action_text
- active_job
- active_storage
- bootsnap
- jbuilder
- spring
- system_tests
- turbolinks
- webpack

これでユーザーは自分のユースケースにふさわしい極めて軽量なアプリケーションを手軽に生成できます。

#39444では、アプリケーションを生成するときに--interactiveフラグを指定することで、必要なフレームワーク機能をインタラクティブに選べる機能の導入が計画されています。

関連記事

Rails: ActiveSupport::Concernをextendしたモジュールをprependする機能(翻訳)

Rails: db:migrate:nameコマンドの振る舞いの変更(翻訳)

$
0
0

概要

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

Rails: db:migrate:nameコマンドの振る舞いの変更(翻訳)

Railsアプリに以下のようなマルチデータベースの設定があり、primaryとsecondaryのデータベースがそれぞれあるとします。

default: &default
  adapter: sqlite3

development:
  primary:
    <<: *default
    database: db/development.sqlite3
    pool: 10
    timeout: 6000
  secondary:
    <<: *default
    database: db/secondary_development.sqlite3
    pool: 20
    timeout: 10000

⚓ 変更前

Rails 6でrails db:migrateを実行すると、database.ymlに存在するすべてのスキーマをダンプします。

上の場合、以下の2つのスキーマファイルが生成されます。

db/schema.rb
db/secondary_schema.rb

ここでrails db:migrate:primaryを実行したらprimaryデータベースのスキーマダンプが生成されることが期待されますが、そうならないことに気づきます。

一貫していない点がもうひとつあります。rails db:migrateを実行するとActiveRecord::Baseコネクションがオリジナルの設定に戻りますが、rails db:migrate:primaryではそうなりません。

rails db:migrateを実行した後のActiveRecord::Base.connection_db_config.inspectの結果は以下のようになります。

#<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fdc09a99ee8 @env_name="development", @name="primary", @spec_name="primary", @config={:adapter=>"sqlite3", :database=>"db/development.sqlite3", :pool=>10, :timeout=>6000}, @owner_name=nil

しかしrails db:migrate:secondaryを実行した後のActiveRecord::Base.connection_db_config.inspect の結果は以下のようになります。

#<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007f830df39098 @env_name="development", @name="secondary", @spec_name="secondary", @config={:adapter=>"sqlite3", :database=>"db/secondary_development.sqlite3", :pool=>20, :timeout=>10000}, @owner_name=nil>

⚓ 変更後

Railsのデータベースのダンプスキーマ(構造)が変更され(#38586)、db:migrate:nameを実行するとActiveRecord::Baseをオリジナルの設定にリセットするようになりました(#38587)。

これで、rails db:migrate:nameを実行してデータベースのスキーマファイルが生成されるようになり、これでマイグレーションを実行できるようになります。

例:

rails db:migrate:primaryを実行するとdb/schema.rbが生成されます。

また、rails db:migrate:secondaryの実行前も実行後も、ActiveRecord::Base.connection_db_config.inspectの結果が同じになります。

#<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fa7d9d36ed0 @env_name="development", @name="primary", @spec_name="primary", @config={:adapter=>"sqlite3", :database=>"db/development.sqlite3", :pool=>10, :timeout=>6000}, @owner_name=nil>

週刊Railsウォッチ(20201110前編)Rails 6.1 RC1がリリース、Railsアプリに最適なEC2インスタンスタイプ、n_plus_one_control gemほか

$
0
0

こんにちは、hachi8833です。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

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

以下の公式更新情報より見繕いました。

つっつき直前に、6.1.0rc1タグができていることに気づきました↓。


つっつきボイス:「rc版が出るということはリリースはそう遠くなさそうかな」「6.1.0rc1タグはできていてzipもアップロードされているけど、releaseタブにはまだ6.1.0rc1が置かれてないんですよ」「rcはタグを付けるだけという運用なのかな?」「今まであまり気にしてなかった…」

「以前の6.0.3.rc1を見るとPre-releaseと表示されてReleaseに置かれている↑」「ということはrc版もリリースされたらReleaseタブに出るんでしょうね」「今回の6.1.0.rc1はまだPre-releaseと表示されてないから、まだ動く可能性がありそう」「rc1のアナウンスが出たら表示チェックしてみます」


その後、つっつき翌日の金曜日に6.1 RC1がリリースされました。

ちょっとわかりにくいのですが、Releasesを開いて「Show 2 newer tags」をクリックすると6.1 RC1が表示されます↓。



github.com/rails/rails/releasesより

⚓ ペンディング中のマイグレーションがある場合に画面/コンソール/ログに出力

# activerecord/lib/active_record/migration.rb#L146
    def initialize(message = nil)
-     if !message && defined?(Rails.env)
-       super("Migrations are pending. To resolve this issue, run:\n\n        bin/rails db:migrate RAILS_ENV=#{::Rails.env}")
-     elsif !message
-       super("Migrations are pending. To resolve this issue, run:\n\n        bin/rails db:migrate")
-     else
-       super
-     end
+     super(message || detailed_migration_message)
    end
+
+   private
+     def detailed_migration_message
+       message = "Migrations are pending. To resolve this issue, run:\n\n        bin/rails db:migrate"
+       message += " RAILS_ENV=#{::Rails.env}" if defined?(Rails.env)
+       message += "\n\n"
+
+       pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations
+
+       message += "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}\n\n"
+
+       pending_migrations.each do |pending_migration|
+         message += "#{pending_migration.basename}\n"
+       end
+
+       message
+     end
  end

マイグレーションがペンディングされていたらUIやコンソールやログに表示し、どのマイグレーションがいくつペンディングされているかをすぐ把握できるようにする。
同PRより大意


つっつきボイス:「ペンディングされているマイグレーションを具体的に表示できるようになったんですね👍」「コミットやプルリクのmerge順次第では、未実行のマイグレーション番号が常に最新の番号とは限らないけど、マイグレーション番号単位で具体的に未実行のものが分かるようになるのか」

「タイトルのoutstandingって『秀逸な』という意味かと思ったら『未処理の』という意味もあるそうです」「つまりペンディング中ですね」

⚓ 新機能: paramごとにエンコーディングを設定できるようになった

従来はエンコード(アクションのすべてのパラメータをASCII_8BITとしてエンコードする)をスキップできた。
今回の変更で、アクションの任意のパラメータでparam_encodingを指定できるようになった。
この変更はGitHubでパラメータのエンコーディングを扱うときに#40124の不正なエンコーディング検出をサポートする(この変更は大きい)。
これにより、POSTのbodyパラメータがリクエストのURLパラメータと異なる可能性がある点も配慮する必要がある。
同PRより大意

# actionpack/lib/action_controller/metal.rb#L138
-   def self.binary_params_for?(action) # :nodoc:
+   def self.custom_encoding_for(action, param) # :nodoc:
      false
    end

ウォッチ20200928で取り上げた#40124と関連しているそうです。


つっつきボイス:「デフォルトのエンコーディングはASCII_8BITなのはこれまでどおりだけど、UTF-8なども指定して取り出せるようになったみたい」「Rafaelさんがコメントでハッシュ探索が1段増えるとパフォーマンスに影響するのではと指摘してますね↓」

この変更によるパフォーマンスのインパクトはどうなる?パラメータ値ごとに毎回ハッシュ探索しているが、paramsのハッシュに値が200個以上あったらハッシュ探索も200回行われることになる。この変更で、このメソッドを使わないユーザーも複雑さがO(1)からO(N)に増加したりしないだろうか?
同PRのコメント(rafaelfranca)より

「続きの#40465ではcustom_encoding_forメソッドを使わない場合に呼び出しを回避している↓」「機能を使わない場合でもパフォーマンスが落ちないよう配慮したんですね」

# actionpack/lib/action_controller/metal/parameter_encoding.rb#L18
-     def custom_encoding_for(action, param) # :nodoc:
-       @_parameter_encodings[action.to_s][param.to_s]
+     def action_encoding_template(action) # :nodoc:
+       if @_parameter_encodings.has_key?(action.to_s)
+         @_parameter_encodings[action.to_s]
+       end
      end

custom_encoding_forメソッドを自分で使うところはあまり想像できませんが、プルリクの意図はそういう感じでしょうね」

⚓ crossorigin属性を使うとリソースが2回フェッチされる問題を修正

スクリプトやCSSを(それぞれjavascript_include_tagstylesheet_link_tagで)読み込むときにcrossorigin属性が適用されると、現在のRailsでは一部のブラウザでこれらのリソースを2回読み込みが発生してしまう。理由はlinkヘッダーのpreloadディレクティブやリソース自身のcrossoriginがブラウザでリソースの再利用にマッチする必要があるため。

たとえばビューで以下のタグを使うとする。

<%= javascript_include_tag("[...snip]react.production.min.js", crossorigin: "anonymous") %>

javascript_include_tagはこのリソースをLink HTTPヘッダーにプッシュするが、現在はcrossorigin属性を無視している。
これによって上述のダブルフェッチとなる。
double fetches in network tab chrome

Chromeではwarningも表示される。

chrome warning

A preload for ‘https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js’ is found, but is not used because the request credentials mode does not match. Consider taking a look at crossorigin attribute.

このプルリクではLinkヘッダーのディレクティブに、リソース自身に渡されたものと同じcrossorigin値を含めるように変更し、これによってプリロードされたリソースをブラウザで再利用できるようにする。

double fetches fixed chrome network tab

anonymousを生成するcrossorigin: trueは既存のヘルパーで行われるので、その振る舞いをここにも複製した。
同PRより大意


つっつきボイス:「2回フェッチするってブラウザ側で起きるのか!」「そういえばcrossorigin属性というものがありますね」「javascript_include_tagで作られるHTMLの問題らしい」

参考: HTML crossorigin 属性 - HTML: HyperText Markup Language | MDN

crossorigin 属性は、 <audio>, <img>, <link>, <script>, <video> の各要素で有効であり、 CORS への対応を提供し、したがって要素が読み取るデータのために CORS リクエストの構成を有効にします。要素によっては、属性は CORS 設定属性になります。
developer.mozilla.orgより

⚓ 新機能: connecting_toメソッド

コンソールをreadonlyモードで起動されるようにするコードをGitHubで使おうと思ったが、そのためのpublic APIがなかったのでこのメソッドを追加した。
通常と異なるデフォルトコネクションが必要だが、そのコネクションをブロックで呼んでない場合がたまにある(コンソールを読み出しモードで起動するなど)。このプルリクは、アプリケーションコードで使うconnected_toの振る舞いを維持しつつ、起動時にスクリプトで特定のコネクションを設定する機能を追加する。
同PRより大意


つっつきボイス:「このconnecting_toはコントローラーやモデルロジック内で使うことは想定していなさそうです」「たとえばRailsコンソールのようなインタラクティブ端末内で以後の入力コードのデフォルト接続先を変えたいときなどに、このconnecting_toでできるということらしい」「へ〜」「Railsコンソールでconnecting_toを使って接続先を変えれば、readonlyな状態で操作したいなどのケースではコンソール操作でconnected_toブロックを書き忘れてもconnecting_toで設定したコネクションが使われるので、そのような用途に便利なんでしょうね」「なるほど」

# activerecord/lib/active_record/connection_handling.rb#L175
    # 特定のコネクションを使う。
    #
    # このメソッドは特定のコネクションが使われるようにするときに有用。
    # (コンソールをreadonlyモードで起動するときなど)
    #
    # このメソッドは`connected_to`と違ってブロックでyieldしないので、requestで用いることは推奨されない。
    def connecting_to(role: default_role, shard: default_shard, prevent_writes: false)
      if legacy_connection_handling
        raise NotImplementedError, "`connecting_to` is not available with `legacy_connection_handling`."
      end

      prevent_writes = true if role == reading_role

      self.connected_to_stack << { role: role, shard: shard, prevent_writes: prevent_writes, klass: self }
    end

connected_toはブロックの中でしかコネクションが有効にならないんですけど、Railsコンソールのように自由に操作できる環境だとconnected_toのブロックを律儀にreadonlyを付けて書くとは限らないので、上のコード例のように書くことでコネクションに縛りをかけた状態で操作できるということだと思います」「readonlyモードを使いたくてこのメソッドを作ったのかもしれないと想像してみました」

⚓ データベースタスク作成でyamlを読めない場合に警告を出すようになった


つっつきボイス:「database.ymlの警告を何度も出すのではなく最初に1回だけ出すようにしたらしい」「for_each(databases)に変わったところからするとマルチプルデータベース関連の修正っぽいですね」「たしかにdatabasesが複数形になってる」

# activerecord/lib/active_record/railties/databases.rake#L28
-   ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+   ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
      desc "Create #{spec_name} database for current environment"
      task spec_name => :load_config do
        db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
        ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
      end
    end
  end

「こちらのメッセージもマルチプルデータベース向きになってる↓」「database.ymlが書式レベルでは正しくても内容が正しくないような場合に、従来は最初のステップがパスして次のステップでうまくいかなかったのを、最初のステップで警告が出るようにチェックを加えたんでしょうね」

# activerecord/lib/active_record/tasks/database_tasks.rb#L144
-     def for_each
+     def setup_initial_database_yaml
        return {} unless defined?(Rails)

-       databases = Rails.application.config.load_database_yaml
+       begin
+         Rails.application.config.load_database_yaml
+       rescue
+         $stderr.puts "Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. If you'd like to use this feature, please simplify your ERB."
+
+         {}
+       end
+     end

「テストを見ると、ERBの中身が入っていないような場合にdb_create_with_warningしてる↓」「余分なエラーが減るのは嬉しいですね」

# railties/test/application/rake/dbs_test.rb
      test "db:create and db:drop show warning but doesn't raise errors when loading YAML with alias ERB" do
        app_file "config/database.yml", <<-YAML
          sqlite: &sqlite
            adapter: sqlite3
            database: db/development.sqlite3
          development:
            <<: *<%= ENV["DB"] || "sqlite" %>
        YAML

        app_file "config/environments/development.rb", <<-RUBY
          Rails.application.configure do
            config.database = "db/development.sqlite3"
          end
        RUBY

        db_create_with_warning("db/development.sqlite3")
      end


最初のdatabase.ymlを一旦読み込み、タスクを作成できない場合はwarningを出す。

マルチプルデータベースではRailsアプリケーションの起動前にdatabase.ymlを読み込んでタスク生成を試みる。これはERBをストリップする必要があるが、これはRailsコンフィグを読み出している可能性があるため。

#36540のようないくつかのケースではERBが複雑すぎて#35497で自分たちが使ったDummyCompilierでは上書きできない。複雑さが原因のときは、database.ymlからデータベースタスクを推測できないというwarningを単に出力している。

自分はこの作業をやりながら、同じwarningが何度も出力されるのを避けられるよう、database.ymlを最初に1度だけ読み込むようにコードを更新することに決めた。なお自分のテストではパフォーマンスへの影響はなく、単にエラーをどこかに保存せずに済むようにしたかった。それにこの方がクリーンに思える。

なおこの変更で既存の実行中タスクは壊れない(単にdb:create:other_dbのようなマルチDB向けタスクが生成されなくなる)。database.ymlが実際に読み取り不可能な場合は、通常のrakeタスク呼び出し中に落ちる。
修正対象: #36540
同PRより大意

⚓ TimeWithzoneDateTimeの比較を修正

問題点
#40413TimeWithZoneDateTimeと比較したときに丸めの問題が生じた(失敗するテストを書いたにもかかわらずパスした)。
修正方法
この問題はtime_instance.to_fを用いたときに発生した(特定のケースで精度が不足することがあった)。Timeインスタンスの作成をtime_instance.to_rでRationalにすることでこの問題の解決を試みた。これによってパフォーマンスがわずかに落ちるが引き換えに精度を得られる。
同PRより大意


つっつきボイス:「プルリクで参照している#40413↓の見出しにあるけど、Time.zone.atをTimeWithZoneで呼び出すときとTimeで呼び出すときで振る舞いが異なる場合があったとは」

「TimeWithZoneの場合はto_rすることで修正している↓」「Rationalに変換してるんですね」「DateTimeの場合はこれまでどおりto_fすべきなのか」「この問題よく見つけたな〜」

# #L
    def at_with_coercion(*args)
      return at_without_coercion(*args) if args.size != 1
      # Time.at can be called with a time or numerical value
      time_or_number = args.first

-     if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime)
+     if time_or_number.is_a?(ActiveSupport::TimeWithZone)
+       at_without_coercion(time_or_number.to_r).getlocal
+     elsif time_or_number.is_a?(DateTime)
        at_without_coercion(time_or_number.to_f).getlocal
      else
        at_without_coercion(time_or_number)
      end
    end
    alias_method :at_without_coercion, :at
    alias_method :at, :at_with_coercion

⚓Rails

⚓ 🌟Railsアプリに最適なEC2インスタンスタイプとは(Ruby Weeklyより)🌟


つっつきボイス:「TechRachoの翻訳記事↓でもお世話になっている、ベンチマークに強いNoah Gibbsさんの記事です」「Railsに最適なEC2インスタンス!」「そこそこ長い記事ですね」「駆け足で読んでみますか」

Ruby 2.5.0はどれだけ高速化したか(翻訳)

「burstableなT4インスタンスは短期間のベンチマークだと速いけど長期間だと速度が落ちると書かれてる」「たしかにT系インスタンスはバーストパフォーマンスインスタンスのCPUクレジット問題があるので長期間のベンチマークのようなものに使うべきではないでしょうね: 長期実行だとCPUクレジットを使い切ってしまうという基本的な話」「なるほど」

参考: バーストパフォーマンスインスタンスの CPU クレジットとベースライン使用率 - Amazon Elastic Compute Cloud

「C5などのC系はCPUを重視するコンピューティング最適化インスタンスで、こちらはバーストクレジットがないので動かし続けられます」「RubyにRactorが正式に入ったらRailsでもC系インスタンスを使えるかもしれないと書かれてる」

参考: Amazon EC2 C5 インスタンス | AWS

「記事ではDiscourseのRailsアプリ↓が現実に近いということでベンチマークに使っている」「Railsのプロセスあたりのスレッド数のような具体的な数値が書かれてますね」「Discourseアプリの場合、EC2の2xlargeインスタンスだと1プロセスあたり6スレッドにしたときにプロセス10個でvCPUやメモリをだいたい使い切る感じらしい」「いつだったかRubyKaigiでこのあたりの話を聞いたような気がする🤔

discourse/discourse - GitHub

「記事ではAWSのARMインスタンスやdedicated instance(ハードウェア専有インスタンス)にも触れてますね」「やったことないけどARMでCRubyって動くのかな?」「CRubyのコンパイルぐらいまではできるかもしれないですね」

参考: Amazon EC2 A1 インスタンス | AWS
参考: Amazon EC2 ハードウェア専有インスタンス | AWS

「T系やC系やM6gインスタンスは、特殊なケースならともかく、一般にはRails向けに万人にはおすすめしない、特にスピードテストでは結果が正しくなくなると書かれてる」「なるほど!」

「記事の途中で『M4かM5のどちらか』と書かれてる」「たぶんRailsのメモリ使用量だとそのぐらいが適切で、かつCPUバーストクレジットもないから、特にこだわりがなければM4かM5に絞られるということなんでしょうね」「記事の終わりの方を見ると、コスパも見ると実はオンデマンドインスタンスならM4よりM5の方が安いみたいなことが書かれてる」「なおオンデマンドインスタンスは、スポットインスタンスと違ってコストが固定されます」

参考: Amazon EC2 スポットインスタンス | AWS
参考: オンデマンドインスタンスの料金 - Amazon EC2 (仮想サーバー) | AWS

「インスタンスタイプの一覧↓を見ると『コンピューティング最適化』とか『ストレージ最適化』とかいろんなカテゴリがあるんですね」「M4やM5というとメモリが多めの汎用インスタンスという印象でしたけど、最近だと『メモリ最適化』に含まれるR系がそれ用なのかな」

参考: インスタンスタイプ - Amazon EC2 | AWS

「急いで読んだ範囲では、思った以上に経験に裏打ちされた非常に具体的な記事で、読んでてとても納得がいきます👍」「AWS事情もみっちり解説されてて、親切丁寧さがスゴい」「もっと概念的な話かと思ったら具体的ですね」「Noah Gibbsさんらしい説得力のある記事」

「AWSのEC2でRailsを運用したことがない、または経験の少ないRailsエンジニアはこの記事を読んでおくといいんじゃないかと思います: おそらくそういう人にとって知らない用語がいっぱい出てくると思うので調べながら読むことになるでしょうけど」「この記事翻訳してもらっていいですか?」「はい、この後でオファー出します」「これはいい記事❤


超久しぶりに🌟を進呈します。おめでとうございます。

その後Noah GibbsさんからOKをいただきましたので、近々同記事の翻訳を公開します。ご期待ください。

⚓ Dockerコンテナ内のSSHコンフィグをWSL2で使い回す(Ruby Weeklyより)


つっつきボイス:「Dockerコンテナ内のSSHコンフィグをWSL2で使い回すということは、$HOME/.sshディレクトリをボリュームでマウントしようねという話かなと思ったら本当にそうだった↓」「予想通りでした😆

# 同記事より
    volumes: 
      - type: bind
        source: ${HOME}${USERPROFILE}/.ssh
        target: /home/${DEV_USER:-abapa}/.ssh

⚓ rubocop-rails_config: Rails公式のRuboCop設定と同じスタイルを使える(Ruby Weeklyより)

toshimaru/rubocop-rails_config - GitHub


つっつきボイス:「officialという言葉があったのでRails公式のRuboCopコンフィグかなと思ったら、Rails公式のRuboCopコンフィグを持ってきてそれと同じスタイルにできるというgemだそうです」「inherit_gemで指定できるのね↓」

# 同リポジトリより
inherit_gem:
  rubocop-rails_config:
    - config/rails.yml

「自分で公式のコンフィグをコピペすれば同じことができますけど、gemでインストールする意義って何だろう?🤔」「gemでやれば公式のスタイルが更新されたときに追従できそうですね」「たしかに」「おそらくRails公式のRuboCopコンフィグは今後もRuboCopのバージョンアップにも追従するでしょうから、RuboCopをバージョンアップしやすくなるかもしれない」「Railsのスタイルに常に合わせておきたい人によさそう」

⚓ n_plus_one_control: N+1クエリをマッチャーで調査するgem(Ruby Weeklyより)

palkan/n_plus_one_control - GitHub

RSpecとminitestのどちらでも使えるそうです。


つっつきボイス:「Rails技術記事↓でお馴染みのEvil Martiansがスポンサーになっている、テストのマッチャーでN+1クエリを検出するツールの記事です」

成熟したRailsアプリのフロントエンドを最新にリニューアルする方法(翻訳)

N+1クエリ問題が本当に起きているかどうかを確認するには最終的にコードを動かさないといけないんですけど、このgemはこんなふうに↓『N=2のときは5回』『N=5のときは8回』というふうにテストで実際に回してヒントを出してくれるgemみたいですね」「あ〜なるほど」


同リポジトリより

「↓以下のような感じでスケールファクターを渡して実際にテストを回してみると、N+1クエリ問題が起きていればNの増加に応じて回数が指数関数的に増大するはず: 上のスクショではNと回数の差が常に3なのでN+1は起きていませんが」

# 同リポジトリより
# You can specify the RegExp to filter queries.
# By default, it only considers SELECT queries.
expect { get :index }.to perform_constant_number_of_queries.matching(/INSERT/)

# You can also provide custom scale factors
expect { get :index }.to perform_constant_number_of_queries.with_scale_factors(10, 100)

「N+1クエリ問題の検出によく使われるbullet gem↓はコードをチェックすることで自動検出しますけど、このn_plus_one_controlは実際にいくつかのスケールでクエリを投げていろんな実行回数を出して、それを人間が見てチェックするということでしょうね」「なるほど」

flyerhzm/bullet - GitHub

「n_plus_one_controlは入れただけで自動的にN+1クエリ問題を検出してくれるものではなく、N+1クエリ問題が起きている疑いがある場合に自分で当たりをつけて発生箇所を探すためのツールだと思います👍


Rails: Bulletで検出されないN+1クエリを解消する

⚓ Meilisearch: Rust製の全文検索エンジン

meilisearch/MeiliSearch - GitHub


つっつきボイス:「Rust製のMeilisearchという全文検索エンジンをRailsで使ってみたという記事です」「外部サービスではなくてMeiliSearchサーバーを自分で動かしてやるものみたい」「手元の辞書を見ると、Meiliは人の名前っぽい雰囲気でした」

「タイポや同義語もよしなに解釈してくれるらしい↓」「漢字もサポートしてる!」「漢字はサポートされてても日本語の文法までサポートされているかどうかはわかりませんけど」「あ、たしかに」「軽くググった限りではMeiliSearchの日本語記事が見当たらないので、日本語はこれからなのかもしれませんね」「他のエンジンのtokenizerも使い回したりできたら嬉しいかも」

  • 機能
    • インクリメント検索(50 msec以内の応答)
    • 全文検索
    • タイポ許容(タイポやスペルミスを解釈する)
    • さまざまな検索・フィルタオプション
    • 漢字のサポート
    • 同義語のサポート
    • インストール・デプロイ・メンテが容易
    • 全文を返す
    • 高度なカスタマイズ機能
    • RESTful API

「全文検索機能はあまり使ったことがないけど、Meilisearchは割とホットなプロジェクトみたい」「★が9900個もあってリポジトリも活発みたいです」「こういうものがあることを知っておくとよいでしょうね」


後でMeiliSearchの公式サイトも見つけました↓。公式ツイートはつっつき後に見つけたものです。


追いかけボイス: 「MeiliSearchの同義語サポート↓、one-wayだけじゃなくmulti-wayに同義語検索するようにもできるのがなかなか面白い: 頑張ってDB作るとかなり柔軟&有用な検索が作れそう」

参考: Synonyms | MeiliSearch Documentation v0.16


前編は以上です。

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

週刊Railsウォッチ(20201028後編)RuboCop 1.0.0 stable版がリリース、Ruby DSLのGUIフレームワークGlimmer、Keycloakほか

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

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

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20201111後編)RubyConf 2020が11/17〜19オンライン開催、GitHub Container Registryベータ開始、スマートロックほか

$
0
0

こんにちは、hachi8833です。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

⚓Ruby

⚓ RubyConf 2020が11/17〜11/19にオンライン開催


つっつきボイス:「今年のRubyConf 2020はオンライン開催かつチケット制なのか」「チケットは有料なんですね」「今回のスケジュールは3日間の開催で、スロットは最大で3つになってます」「英語が基本になりますけどオンラインなら多少追いやすいかも」

⚓ RubyConf 2020のセッション

スケジュールの時間帯はCentral Time(CST: 米国中部標準時)だそうです。

スケジュールを見ると興味深そうなセッションがいろいろ並んでいますね。タグで色分けされているのでつっつき後に少し追ってみました。通常のカンファレンスに近づけるためにいろいろ工夫をこらしているように思えました。

  • RubyConf 2020タグ(紫)は全体企画、Keynote(黄)はキーノート。


rubyconf.orgより

  • Talkタグ(赤)は通常のセッション。


rubyconf.orgより

  • Sponsorsタグ(緑)を見ると、詳しくはわかりませんが、Q&AをSlackでやりとりするスポンサーセッションもいくつかあるようです。


rubyconf.orgより

  • スポンサー企業と話ができる「Sponsor Job Fair」という時間も設けられていて、従来のスポンサーブースに相当するようです。


rubyconf.orgより


  1. Does RubyConf have a jobs fair or list?
    Yes, we will be conducting a virtual job fair on the second day of RubyConf 2020 and will also be featuring an official jobs list.
    FAQより
  • Hallway(青)タグのセッションは、おそらく会場の廊下(hallway)に集まってコードを書いたり立ち話や会食をしたりという雰囲気の再現を目指しているように思いました。


rubyconf.orgより

  • Hallwayとは別に1日目と3日目にWorkshop(灰)タグのセッションもあり、その時間帯だけスロットが最大で3つになっています。



rubyconf.orgより


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

⚓ 「RubyにSTM(Software Transaction Memory)を追加する提案」を吟味する(Ruby Weeklyより)


つっつきボイス:「Rubyにソフトウェアトランザクショナルメモリを入れようという話があるとは知りませんでした」「STMがわからない😅」「RubyのSTMはどこかで見かけたような気がする(注: 後述の笹田さん資料を参照)」「詳しい人に教わりたいです」

参考: ソフトウェアトランザクショナルメモリ - Wikipedia

計算機科学において、ソフトウェアトランザクショナルメモリ(英: software transactional memory, STM)は、データベーストランザクションに似た並行性制御機構であり、並列計算を行う際の共有メモリへのアクセス法である。この機構はロックベースの同期を用いた並行性制御の代替手段として機能し、ノンブロッキングな方法で実装される物もある。ここでいうトランザクションとは、共有メモリに対する一連の読み出しと書き込みを実行するコードを意味する。論理的にはこれらの読み出しと書き込みは、時間的なある一点で行われ、他のトランザクションからはその間の状態は見えない。
Wikipediaより

「記事では回路を生成するコードでみっちりデモをやってる↓」「STMとデモプログラムの結びつきが今ひとつよく分からなかったので、時間かけて記事読まないといけなさそう」

chrisseaton/ruby-stm-lee-demo - GitHub

生成にはリーのアルゴリズムを使っているそうです。


同リポジトリより


後で調べると、@_ko1さんによる2018年の「第120回プログラミング研究発表会(SWoPP2018)」発表資料PDFにSTMについて言及がありました。

アクセス時にトランザクション制御を用いる、いわゆる、ソフトウェアトランザクショナルメモリ(STM)を用いるオブジェクトについても検討している。
(中略)
例えば、共有データが複数あるとき、データをまたがって一貫性が求められるような場合、トランザクションを適切に制御することが必要になるが、それを誤らずに制御可能に誘導するインターフェースが重要である。すべての共有コンテナオブジェクトを STM にすることで、複数の共有データにまたがったトランザクションを、ロックの順番によるデッドロックの心配なく実現することができるので、STM は有力な手法である。しかし、一貫しなければならない処理に対して、複数トランザクションを実行してしまうようなプログラムミスを検出できない。
同PDF p4(笹田)より

⚓ LibSassが非推奨に

なおruby-sassは既に非推奨となってアーカイブされています。

sass/ruby-sass - GitHub


つっつきボイス:「C++で書かれたLibSassが非推奨になったそうです」「SassはDart製のDart Sass↓に統合されるという話が前からあって移行パスも用意されていましたけど、ついにLibSassも正式に非推奨になったのか」「あ、前から決まってたんですね」

参考: sass/dart-sass - GitHub

参考: Dart Sass、使ってる?Preprosを使えばコンパイルも楽勝! | Webクリエイターボックス


以下のsass/sassc-rubyはLibSassを使っているので影響を受けることになりますね。

sass/sassc-ruby - GitHub

⚓ その他Ruby

つっつきボイス:「Ruby biz Grand prixといえば、BPSも『入退くん』という入退出管理Webサービスをエントリーしたことありますよ↓」

参考: [2]Ruby biz Grand prixで受賞した企業の声:さまざまなビジネスに拡がるRubyの活用事例から大賞が決定!『Ruby biz Grand prix 2018』|gihyo.jp … 技術評論社


gihyo.jpより

入退くんをはじめとする自社サービス「くんシリーズ」の運用方針を紹介します。


つっつきボイス:「たしかにReactorとThreadはどちらもスレッドの制御を行うので、一方だけを使うならともかく両方を組み合わせて使うのは大変そうですね」

⚓DB

⚓ Percona Toolkitとpt-online-schema-changeの危険なエッジケース


つっつきボイス:「前回取り上げようと思って忘れてた記事です」「CREATE TABLE文とかを書き換えたりするツールだから、危険なエッジケースがあったというのもわかる気がする」「これはバグですね」「たしかにPercona Toolkitの3.0.10以下が該当する問題って書かれてる」

「記事によるとpt-online-schema-changeの基本動作は、内部でtest_not_nullテーブルをtest_nullに変えるときに、まず_test_not_null_newというアンダースコア付きのテーブルを作り、次にINSERT LOW_PRIORITYを使って既存テーブルからデータを入れている」「ふむふむ」「pt_osc_test_test_null_delのようにDELETEとUPDATEとINSERTのトリガーも作るので、INSERT LOW_PRIORITYの実行中に整合性を保てるようになってる、そして最後にトリガーや_test_not_null_newテーブルをDROPする↓」

# 同記事より
$ ./pt-online-schema-change-3.0.10 u=msandbox,p=msandbox,h=localhost,S=/tmp/mysql_sandbox5735.sock,D=test,t=test_not_null --print --alter "engine=InnoDB" --execute
(...)
Altering `test`.`test_not_null`...
Creating new table...
CREATE TABLE `test`.`_test_not_null_new` (
`id` int(11) NOT NULL,
`add_id` int(11) NOT NULL COMMENT 'my generated test case',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
Created new table test._test_not_null_new OK.
Altering new table...
ALTER TABLE `test`.`_test_not_null_new` engine=InnoDB
Altered `test`.`_test_not_null_new` OK.
2020-09-30T21:25:22 Creating triggers...
2020-09-30T21:25:22 Created triggers OK.
2020-09-30T21:25:22 Copying approximately 3 rows...
INSERT LOW_PRIORITY IGNORE INTO `test`.`_test_not_null_new` (`id`) SELECT `id` FROM `test`.`test_not_null` LOCK IN SHARE MODE /*pt-online-schema-change 1438 copy table*/
2020-09-30T21:25:22 Dropping triggers...
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_not_null_del`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_not_null_upd`
DROP TRIGGER IF EXISTS `test`.`pt_osc_test_test_not_null_ins`
2020-09-30T21:25:22 Dropped triggers OK.
2020-09-30T21:25:22 Dropping new table...
DROP TABLE IF EXISTS `test`.`_test_not_null_new`;
2020-09-30T21:25:22 Dropped new table OK.
`test`.`test_not_null` was not altered.
2020-09-30T21:25:22 Error copying rows from `test`.`test_not_null` to `test`.`_test_not_null_new`: 2020-09-30T21:25:22 <strong>Copying rows caused a MySQL error 1364:</strong>
Level: Warning
Code: 1364
Message: Field 'add_id' doesn't have a default value
Query: INSERT LOW_PRIORITY IGNORE INTO `test`.`_test_not_null_new` (`id`) SELECT `id` FROM `test`.`test_not_null` LOCK IN SHARE MODE /*pt-online-schema-change 1438 copy table*/

「でもDEFAULT NULL COMMENTを指定したカラムがあると移行後にそのカラムがNULLになってしまったのか」「このバグを知らずに踏んだらキツい」「実行後の確認は欠かせませんね」「バグが直ってよかった😂

# 同記事より
mysql [localhost:5735] {msandbox} (test) > select * from test_null;
+----+--------+
| id | add_id |
+----+--------+
|  1 |   NULL |
|  2 |   NULL |
|  3 |   NULL |
+----+--------+
3 rows in set (0.00 sec)

⚓リモートワーク

⚓ ZoomがEnd-to-End暗号化機能のtechnical preview版をリリース


つっつきボイス:「ついにZoomがEnd-to-End暗号化(E2EE)をできるようになる」「E2EEだとクラウド録画ができなくなって手元でしか録画できなくなるなどの制約が付きそうですけど」「Zoomサーバー側で生成する暗号化鍵ではなくて、ミーティング参加者の生成した鍵でE2EEするんですね」「Zoomが生成した(Zoomが知り得る)鍵を使いたくないユーザーにとっては嬉しいのかも」

「ミーティング参加者の鍵はどうやって配布するんだろう?」「誰かが作った鍵を公開鍵暗号とかを使って共有するのかな?」

⚓ ビデオ会議でE2EEが要求されそうな用途

「ところでビデオ会議がE2EEかどうかって気にしてますか?」「自分が気にするかどうかというよりは、ビデオ会議でやりとりする顧客側が気にするかどうか次第でしょうね」「そうなんですね」「極端に言えば、クライアントにマルウェアが入ってしまったらE2EEがあっても同じだと思います」「あ、たしかに」「Zoomは現在でも通信路は暗号化されていますし、E2EEにするかどうかは最終的にZoomを信頼するかどうかという話だと思っています」「なるほど」

「たぶん今度の機能追加で、Zoomに全面的な信頼をおくわけにいかない事情のあるユーザーが、E2EEがあることで自分の組織にZoomの利用を認めてもらいやすくなるというメリットはあるでしょうね: たとえば米国の検閲を受ける可能性のある会社とか、国や軍事関連のような特殊な業種のユーザー」「そういう業界はあまりZoomを使わなさそうな気もしますけど、E2EEがあれば急にZoomを使う必要が生じたときでも組織から利用許可が出やすそうですね」「もちろん一般ユーザーにとっても、よりセキュアな方法でビデオ会議ができる選択肢が増えるのはいいことだと思います👍

⚓クラウド/コンテナ/インフラ/Serverless

⚓ joker1007さんの「1000万件オーバーのレコードのデータをカジュアルに扱うための心構え」


つっつきボイス:「はてブで大バズりしていた記事です」「joker1007さんのこの記事はもうおっしゃるとおりだと思います👍」「社内の現場向け教育資料を元にしているというのもよいですね」「現場で得た具体的な知見のエッセンスがまとめられているのが嬉しい」

「『既存のコードを信用するな』『手癖で書くな』は、まさにそのとおり」「ちゃんと理解して把握してから構築を開始しようねという基本的な話ですね」「自分でコードを書く部分が増えるほどそこがバグの温床になる可能性も増えるので、可能ならなるべくクラウドにある機能を活用すべきだと思います」

⚓ Docker Hub関連記事(続報あり)


つっつきボイス:「AWSから50GBまで無料でプルできるのはいい👍」「外からプルできるんですね」「BPSのGitLabもAWSに乗っているんですけど、GitLab CIでDocker Hubからの転送容量制限に引っかかったときにこれでやれたらありがたい」

「どちらかというとAWSのコンテナレジストリよりは、Docker Hubからのプルをミラーしてくれるサービスの方が欲しいかな: コンテナレジストリが増えてあちこちに分散するのは望ましくないので、いわゆるディストリビューションのパッケージリポジトリでやっているような感じでコンテナレジストリのミラーサーバーがある方が嬉しいかも」「たしかに」

「同じAWS記事に、Docker Hubが6か月以上使われなかったコンテナイメージを削除するという方針が保留になったとありますね」「使うものはDocker Hubからプルしてるでしょうから、使わないのは削除でもいいのかとちょっと思いましたけど」「でもCIでキャッシュに乗っていたらプルされませんよ」「あ、そうか」「改めてプルするまでは手元のキャッシュにあるものが使われます」「それである日突然コンテナイメージが消えたら困っちゃいますね」「しかもコンテナイメージが消えたときにはわからなくて、サーバーを新たに再構築したときなどに初めて気づくヤツでしょうね😆」「それは惨事…」


「もうひとつの記事はDocker Hubの無料プラン!」「承認されたオープンソースの名前空間からイメージを取得する場合は制限なしということか」「オープンソースプロジェクトとして認められるための条件が付いてますね↓」

  • パブリックかつ非商用であること
  • Open Source Initiative (OSI)の定義(無償配布、ソースコード、派生物、ソースコードの統合、差別を許容しないことなどの定義を含む)を満たすこと
  • OSIが承認するオープンソースライセンスのもとでイメージを配布すること
  • アプリケーションの実行に使われるDockerイメージを作成すること

「多くのコンテナイメージはUbuntuやAlpineなどのオープンソースOSをベースにしていますけど、ビルドでそれらをプルするときには制限がなくなるということなんでしょうね」「オープンソースの作者はDocker Hubでこれ使っていいよと連絡しないといけないということですか?」「そういう申請は必要そうですね」「もしオープンソースソフトウェアの作者が申請しないままだとリジェクトされるということに…?」

参考: Expanded Support for Open Source Software Projects - Docker Blog

元記事で参照している上の英語記事によると、オープンソースコミュニティ向けの以下のフォームで申請が必要になるそうです。

参考: Open Source Community Application | Docker

⚓ 続報: GitHub Container Registryでやれる

以下のツイートはつっつき後に見つけました。

⚓JavaScript

⚓ yarn whyコマンド


つっつきボイス:「yarn whyなんてコマンドができたのか」「そういえば少し前からGitHubにセキュリティアラートができましたね↓」「そうそう、久々にGitHubにログインしたら使えるようになったのでオンにしてみました」

参考: Security | GitHub

「セキュリティアラートへの対応は基本的にソフトウェアをアップデートすることになりますね」「元記事によると、自分のpackage.jsonに書かれていないけど間接的に依存しているパッケージがアップデート対象の場合にもGitHubセキュリティアラートがチェックしてくれるそうです」「yarn whyでそういう間接的な依存パッケージを調べられるということですね」「Rubyのbundlerにも似たような機能があった↓」

[Ruby] Bundler 1.15の全コマンド

「なおyarn whyは以下の記事で知りました」「ここが定期的に更新されるようになってうれしいです😋

参考: 週刊気になったITニュース(2020/10/25号) - masa寿司の日記

⚓CSS/HTML/フロントエンド/テスト/デザイン

⚓ HTML5のblock-levelとinline-level


つっつきボイス:「はい、HTML5ではブロックレベル要素とインライン要素という概念はなくなっています」「しまった、自分まだHTML4脳だったか😅

「HTMLやCSSの仕様については弊社のbabaさんが詳しいんですけど、以前はブロックレベル要素とインライン要素があったのが、MDNにもあるようにHTML5ではそれらが定義されなくなって↓displayプロパティでいくらでも変更できるようになっています(CSSには引き続き同じような概念はありますが)」「あ〜なるほど」「HTML5の世界からはそういう概念が外されたんですね」

参考: インライン要素 - HTML: HyperText Markup Language | MDN

以下の要素は既定でインラインです (ただし、ブロック要素とインライン要素は HTML5 では定義されなくなり、代わりにコンテンツカテゴリが使用されます)。
developer.mozilla.orgより

「HTML4以前だと、たとえばインライン要素の中にはブロック要素が書けないといった制約がありましたけど、そういうのがなくなったということだと思います」「HTML5脳にならなきゃ😅

「ただ、概念はなくなりましたけど、現場レベルでは今もそういう概念がまだあるかのようにHTMLを書いていることは多いでしょうね」「手癖で今までどおり書いちゃうのはありそう」「自分も意識せずにそう書いているかも」「ツイートにもあるように、HTML5では便宜的にそういうものが残っていて実務上もあまり問題ないと理解しておけばよいと思います」「それに<div>なのにdisplay: inlineとかになってたら気持ち悪いですよね」「あ〜それ無理です😆

「現場レベルのHTML5の書き方は基本的に変わらないと思いますが、今はHTMLを手書きすることが減って、さまざまなフロントエンドフレームワークで自動生成することが増えてきているので、HTML4のような制約があると何かの拍子で自動生成HTMLが仕様に違反してしまう可能性があるかもしれませんね: その意味ではHTML5のように柔軟に定義できる方がフレームワーク側でHTMLを生成しやすいかもしれないと思いました」「そうかも」

「考えてみればブロック要素はインライン要素はデザインやレイアウトに関連するものだから、それもあってHTML5から切り出したのかもしれない」「たしかに文書構造ではありませんね」「より純粋なマークアップ言語を目指したのかも」

⚓ その他フロントエンド


つっつきボイス:「いらすとや以外の選択肢としてよさそうかなと思って拾いました」「たしかに、いらすとやの絵はあまりに普及していますよね」「このサイトのイラスト、色をブラウザ上で変更できますよ!」

「ところで利用規約を見ると『商用・非商用問わず自由にwebサイトや印刷物等に利用頂けます』『利用規約範囲内のイラストに対する編集や加工は可能ですがそれに伴う著作権の譲渡や移動はありません』などとなってるのか🤔」「加工して使うのはOKそう」

「この利用規約については、既に確立しているライセンスが使われていたらなおよかったかなという気持ちがあります」「あ、クリエイティブ・コモンズのような既存のライセンスですね」「たとえば『その他著作権を侵害する行為は禁止です』の『その他著作権』の部分に解釈の余地が考えられますし、利用規約が将来変更されて使えなくなったりする可能性などについても一応使う前に考慮が必要でしょうね」

参考: クリエイティブ・コモンズ・ジャパン

⚓言語/ツール/OS/CPU

⚓ ExcelでVBAを使わずにドラクエを作る


つっつきボイス:「これ見た見た」「これはスゴい」「VBAなしでよくぞここまで」「しかも1か月以上もかけて」「Excelでどえらく頑張ってる」「F9キーだったかな、一箇所どうにもできない部分があったのも面白かった😋」「こういう縛りプレイ的なコーディングを愛する分野ってありますよね」

「元記事に『最後まで見た方が面白いですよ』ってあったので動画の最後を見たらあのイルカが登場してました🐬」「あのイルカ」「あのイルカだ」「あのイルカ、見かける機会が既になくなりましたよね」「若い世代だとほとんど知らないんじゃないかしら」「インターネットミームの一種か何かと思われてたりして😆

後でイルカの名前をこちらで見つけました↓。

参考: 正式名称アーカイブス|ワードやエクセルのイルカの名前

参考: インターネット・ミーム - Wikipedia

⚓その他

⚓ スマートロック


つっつきボイス:「TechRachoにも記事を書いていただいているWingdoorさん↓のメンバーが執筆中の記事でスマートロックというものを知りました」「スマートロックは何年も前から使われてますね」

詳しくは今後公開するWingdoorさんの記事でどうぞ。

「スマートロックは鍵を時間制限付きで渡すこともできたりするので、民泊と相性がいいですよね」「民泊なら何かあったら持ち主が見に行けばいいや、みたいな感じで使えそう」「でも自宅をスマートロックにしたらトラブったときに家に入れなくなったりするのかな?」「たいていのスマートロックは既存の鍵にかぶせる形で取り付けるから、普通の鍵でも開けられますよ」「なら大丈夫そうですね」「つまり物理鍵は結局持ち歩かないといけなくなる?」「そうなりますね」

参考: 民泊 - Wikipedia

「ちなみにスマートロックって、マンションの管理組合によっては認められないこともあるんですよ(エントランスロック式のマンションなどでは、入居者の誰かひとりのスマホが脆弱な管理になるだけで侵入される問題も考えられるので)」「あ、たしかに」

「ちょうど見つけたツイートでもこんな事例がありました↓」「やっぱり物理鍵要るのかな…」「スマートロックデバイスの電池が切れる可能性も考えられますね」

参考: SwitchBot(スイッチボット)カーテン | 太陽の光で朝スッキリ!ワンタッチで自動化&楽々操作

「私はスマート何とかみたいなデバイスは大好きで、カーテンを自動で開閉するデバイス↑とかなら自宅に入れてますし楽しいですけど、自宅の鍵をスマートロックにするのはまだためらいがありますね」「自宅をスマートロックにすると、たとえば鍵を家の中に置いたまま家から閉め出されたときに代替手段がないのが心配」「ホテルや自動車でよくあるインキー(インロック)ですね」「そうなったらもう鍵屋さんを呼ぶしかない🗝」「あれを一度でも経験すると身に沁みます」

⚓ スマートロックはオフィスで便利

「ところで、スマートロックをオフィスで使うのはありかなと思います」「あ、それいいですね!」「たとえば仕事が終わって帰ろうとしたら物理鍵を誰かが持って帰ってしまって戸締まりできないときとか、朝イチで出社したらまだ鍵を持ってる人が出社していなくてオフィスに入れないときでも、管理権限持っている人に連絡して鍵をスマホに送ってもらって開ける、といったことができるのがいいですよね😋」「たしかに!」

「そもそも物理鍵を他人に渡すのって多少なりともリスクがあるじゃないですか」「合鍵を作られる可能性があるからですね😆」「スマートロックなら鍵を時間限定で送信できるので、物理鍵を渡すのがためらわれるような状況、たとえば入社間もない社員やアルバイトの人にも鍵を渡せるのがメリット」「たしかにユースケースとしてとても現実的!」「Webカメラと組み合わせれば解錠施錠する人の顔もチェックできますね」「アルバイトの人に安心して鍵を渡せるという意味では、店舗などでも有用そう」「物理鍵持っている人がシャッターを開けるためだけに出向かなくてもよくなるのはありがたい」


後編は以上です。

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

週刊Railsウォッチ(20201110前編)Rails 6.1 RC1がリリース、Railsアプリに最適なEC2インスタンスタイプ、n_plus_one_control gemほか

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

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

Ruby Weekly

Publickey

publickey_banner_captured

Railsアプリに最適なAWS EC2インスタンスタイプとは(翻訳)

$
0
0

概要

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

Railsアプリに最適なAWS EC2インスタンスタイプとは(翻訳)

皆さんはAmazon EC2 インスタンスタイプのリストをご覧になったことはありますか?このリストにはさまざまなサイズの仮想マシン(VM)が並んでいて、皆さんはこれらをレンタルしてコードを実行できます。グループごとにさまざまなサイズのVMが山ほどあるので、サイズによってグループ分けされています。

では「Railsアプリを動かすならEC2インスタンスのどのタイプで動かすべきでしょうか?」

その答えは見た目よりもシンプルです。百聞は一見にしかず。

皆さんは数字がお好きですか?私は大好きですが、数字を見たくない人は本記事の末尾までスキップしていただければ、いい感じにまとめた結論をご覧いただけます。なお、心の底から数字が好きでたまらない方のために、私が得た直接的な結果をすべて含んだ生データダンプも用意してあります。

⚓ インスタンスタイプと私のテスト方法の解説

既にご存知の方もいらっしゃるかと思いますが、私はある「巨大なRailsアプリ」を頻繁に実行しては多数のリクエストをどのぐらい高速に処理できるかをRails Ruby Bench(RRB)というパラレルベンチマークでチェックしています。

noahgibbs/rails_ruby_bench - GitHub

私は最初にEC2のどのインスタンスタイプで実行するかを検討し、それからベストなインスタンスタイプでスピードテストを実施します。

Railsアプリは驚くほど CPUに束縛されます。このことは、Ruby 2.0から2.6で72%も速度が向上した理由の一部でもあります。つまり、T4のようなバーストパフォーマンスインスタンス(burstable-CPU instance)は短期間のベンチマークでときどき目覚ましい結果を叩き出しますが、実行時間の長いベンチマークでは結果が落ちるということです。自分のアプリが極端にビジーになったり極端なアイドル状態になったりする場合は、T4を検討する価値があるかもしれません(とは言うものの、皆さんのアプリに通用する信頼できる数値を私が代わりに提供するわけにもいきませんが)。結果はそれらの負荷がどのぐらい均等であるかだけに依存します。そして、バーストパフォーマンスインスタンスではそれらの負荷が均等であるほど結果が悪くなる傾向があります。

DiscourseとRailsはメモり食いで、私はこれらのベンチマークをlower-RAM-per-CPUのC5インスタンスで回したことが一度もないほどです。もし皆さんのアプリがメモリ利用量が少ないかメモリ利用量を十分抑えられるのであれば、C5は素晴らしい選択肢になる可能性があります。Discourseやそれに近い多くのRailsアプリでは、メモリをたちまち食い尽くしてしまうでしょう。私は「標準的なRailsアプリ」ではC5をおすすめしません。しかしSinatraEventMachineFalcon/AsyncのようにRubyで別のことをやるのであれば、C5インスタンスは間違いなく試してみる価値があるでしょう。Ruby 3.0以降でRuby Ractorsが十分サポートされれば、C5インスタンスはRailsにとってもよいものになるでしょう。現時点ではほとんどのRailsアプリで必要とするメモリが不足しています。

RRBは古いバージョンのDiscourseを実行しています。Discourseはインターネットフォーラムをホスティングするアプリとして広く人気を得ているRailsアプリであり、入手できる「本物の」オープンソースRailsアプリとしては極めて巨大なので、「現実世界」のベンチマークに向いています。RRBは、疑似乱数でシミュレートした多数のユーザーリクエストのセットをRailsアプリに対して実行し、すべてのリクエストが完了するまでの時間を測定します。つまりスループットテストです。ご興味のある方は 自分でも実行できますが、少々込み入っていて注意を要します。現実世界のソフトウェアを使う場合、現実の複雑さとバグを踏むことが暗黒面になります。

GIL(RubyではGVL)が存在しているので、RRBを実行するときには「マルチプロセス」と「プロセスごとのマルチスレッド」のバランスを取っています。多くのスレッドがI/OやCベースのネイティブ拡張のコード実行を待てるにもかかわらず、1個のRubyプロセスではRubyコードのシングルスレッドしか実行できません。Railsの場合、1プロセスあたり5スレッド程度にバランスさせるのが普通です。なお私はRRBの特定のケースではごく小規模な改善として5よりも6を使います。DiscourseでやっているI/O待ちの量だと、EC2 2xlargeインスタンスのvCPUとRAMは基本的に10個のRailsプロセスで使い切ります。つまり「10プロセス、6スレッド/プロセス」です。

RRBが実行するコードはかなり古いものです。私がRRBを設計したのはRuby 3×3移行をベンチマークするためだったので、古いコードとの互換性を優先しています。2020年のクリスマスの日にRuby 3がリリースされたら、私は片っ端から最新バージョンにアップグレードして回るでしょう。古いバージョンはそのときまで残しておくつもりです。なおM6g ARM インスタンスの出番はまだです。古いRubyをそこでビルドすることならできますが、古代から伝わるNodeJSやlibv8などの他の古いコードもそこで動くようにするのは本当にしんどい作業でした。

もしM6gインスタンスを試してみたいのであれば、エンジニアリングにある程度余分な時間が必要になります。Intelプロセッサーはあらゆるものでデフォルトになっているので、おそらく動かすのも実行を維持するのも手こずるでしょう。M6gインスタンスをうまく活用するのに十分なサーバー予算をお持ちなら、存分に自分で測ってみてください。ただし私のベンチマークで測定が楽になるわけではありません

まとめると、「Tシリーズ」「Cシリーズ」「M6gシリーズ」のインスタンスは、特殊なRailsアプリには向いている可能性もあります。私は一般にこれらのインスタンスを万人にはおすすめしませんし、これらのスピードテストを行ったこともありません(これらのインスタンスはたいていのRailsアプリでは間違った解だからです)。

というわけで、残る選択肢は「主にM4およびM5インスタンス」です。M4のマシンとアーキテクチャは古いのですが、M4とM5の価格は似たようなものです。

⚓ 「スポットインスタンス」vs「オンデマンドインスタンス」

AWS EC2には標準価格のオンデマンドインスタンスがあります。スポットインスタンスについては安く手に入る可能性もあればできない可能性もあり、ちょうど航空チケットを離陸直前に買うときのように価格は運次第です。運がよければ驚くほど低価格な未使用の容量を余分にゲットできますが、ダメなときはダメです。

そこで、この新しいインフラストラクチャ(M5)がどのぐらい高速であるかを知っておくと、M5をお手頃価格に比例して入札できるようになるので便利です。

AmazonはM4ユーザーが他のインスタンスタイプへ移行することを望んでいますが、M4スポットインスタンスはタイミング次第では安く手に入れられる可能性がまだ残されています。M4のコストパフォーマンスはどのぐらいで、どんな場合に向いているのでしょうか。

⚓ M4はシンプルか?

私は何年もかけて、主にm4.2xlargeでベンチマークを回してきました。同じインスタンスタイプを使えば長期間での数値の比較がやりやすくなります。私は本記事でさまざまなバージョンのRubyをM4とM5でチェックしています。スピードが単純に「x倍高速」となることはめったにありませんが、複数のファクターをテストすることで自分の測定結果がどのぐらいシンプルで安定しているかを見極めるのに役立ちます。

私がEC2の4つのインスタンスタイプを用いて最終的に行った測定では、Ruby 2.5.3とRuby 2.6.6をテストしました。また、スピードがどのぐらい変動するかについて当りを付けるために、それ以外の多くのインスタンスでも最初に実行してみました。同一のベンチマークを3年間回し続けていてひとつよかったのは、「通常の」安定性および変動がどのような状態なのかをかなり体感でわかってきたことです。舞台裏の数値を詳しく見てきた分、本記事ではちょっぴり細部をごまかしているところもありますが。さて本記事にある項目のうち、それを5倍〜100倍に増やさなくてもよい項目は何だと思いますか?そう、測定値です。

また、RRBは安定した数値を得られるよう、いい感じに最適化されています。RRBは同じ乱数シードを繰り返し用いて生成した同一のリクエストを実行します。RRBのリクエストは十分小さくかつ高速で、しかもハードウェアのネットワークを一切使いません。リクエストはすべてlocalhostです。EC2のように仮想化された環境ではハードウェアネットワークが不安定性の「大きな」原因となるので、単純にスキップしています。もちろん、どのインスタンスも同じスピードになるはずはありませんが、スピードのばらつきはハードウェアネットワークの場合よりもずっと小さくなります。

EC2では「ハードウェア専有インスタンス(dedicated instance)」の利用も認められており、そこでは同じ物理ハードウェアを他の誰にも共有されないようにできます。私はもう何年もハードウェア専有インスタンス上でテストを繰り返しています。ハードウェア専有インスタンスではある種の時間変動については「確実に」回避できますが、私の経験ではハードウェア専有インスタンスでも未だに速くなったり遅くなったりします。そのため、この特定の測定項目セットでは大きな利点はありません。

⚓ 基本は「M4」

要するにM4インスタンスはどんな具合になるのでしょうか?

Ruby 2.5 ips Ruby 2.6 ips Ruby 2.6の速度差
m4 inst 1 168.9 175.3 +3.8%
m4 inst 2 156.8 164.0 +4.6%
m4 inst 3 169.2 176.8 +4.5%
m4 inst 4 167.4 175.6 +4.9%
m4 overall 167.4 175.3 +4.7%

(「ips」は10000件のHTTPリクエストを30回実行したときの「イテレーション/秒」のメジアン、
「m4 overall」は全リクエストを1回の長時間実行として扱うことを表す)

インスタンス2が目に見えて遅くなっています。最速のインスタンス3は(インスタンス2よりも)およそ7.8%高速です。しかしこれがまったくのランダムではないことがわかってきます。そのインスタンスはRuby 2.6で7.8%高速、Ruby 2.5で7.9%高速です。これが今回のケースで観察される「安定性」です。インスタンスによってわずかに高速なこともあれば、わずかに遅くなることもありますが、相対的な数値は類似しています。それと同様に「このタスクにおけるRuby 2.6はRuby 2.5と比べて3.8%〜4.9%高速」という結果にも若干のばらつきがありますが、このベンチマークにおけるばらつきの総量はかなり正常です。

私はこうした結果を何年も見つめ続けてきました。そしてこれは典型的な結果セットです(統計的に有意なミスでもない限り: もちろんたまにはミスぐらいしましたが)。このトピックに関する私の研究を追いかけている方なら、おそらく何年にも渡って同じ結果が出ていることに気づくでしょう。

ちょっと待った、あるインスタンスが他のより速いことがあるってマジで?」とお思いの方へ: 答えはイエスです。私は、EC2オンデマンドインスタンスの巨大なグループを立ち上げてベンチマークを回し、その中で最も遅い10%をシャットダウンしている人々を知っています。そしてこれはCPUよりもネットワークの方がずっと顕著です。私がEC2ベースのメトリクスでlocalhost以外のネットワークを含めないようにしている理由がこれです。

⚓ M4とM5を比較する

今のはすべてM4インスタンスの話でした。ではM5インスタンスの数値はどうでしょうか?ここは本記事で費用対効果を比較できる有用な部分です。

Ruby 2.5 ips Ruby 2.6 ips Ruby 2.6の速度差
m5 inst 1 206.5 213.3 +3.3%
m5 inst 2 200.7 204.7 +2.0%
m5 inst 3 203.7 213.8 +5.0%
m5 inst 4 214.1 223.5 +4.4%
m5 overall 206.1 214.4 +4.0%

(「ips」は10000件のHTTPリクエストを30回実行したときの「イテレーション/秒」のメジアン、
「m4 overall」は全リクエストを1回の長時間実行として扱うことを表す)

こちらのインスタンスの結果はややばらつきが小さくなっています。最速のインスタンスは最も遅いインスタンスより6.7%高速です。Ruby 2.5とRuby 2.6を比べた数値には依然として多少ばらつきがあるものの、overallはM5インスタンスが4.0%、M4が4.7%と似ています。言い換えると、これはすべて正常なばらつきです。

インスタンス2が+2.0%と差が小さいのはどういうことでしょう?一方はうまくいって他方は不運に見舞われることもあります。そしてRuby 2.5と2.6はたまたまそのインスタンスでそこそこ近い結果になったのです。舞台裏を覗いてみても、これは大きな異常値でもなければ突発性のスローダウンでもありませんでした。そのインスタンスでの実行が通常よりも(高変動ではなく)低変動だったのです。これは、Ruby 2.6がRuby 2.5に対して持つ長所を目減りさせる「遅いEC2インスタンス」の特殊なケースのように見えます。おそらくI/O待ちが(極めて)わずかに長くなったか、CPU時間が(極めて)わずかに長くなったかしたのでしょう。言い換えると、それが何であれ、スローダウンが1分間も続くような一時的なしゃっくりなどではなく、長期的には個別のEC2インスタンスのマイナーなスローダウンと思われます。

⚓ 費用対効果

とにかく、M4インスタンスのメジアンスループットは、Ruby 2.6で175.3リクエスト/秒となり、最新のRubyとほぼ同じスピードです。そしてM5インスタンスのメジアンスループットは、同じ設定で214.4リクエスト/秒となります。これは一体何を意味しているのでしょうか?

手っ取り早く言えば、M5インスタンスの方が36%高速ということです。つまり価格設定をスポットインスタンスにするとしたら、M4インスタンスはM5インスタンスより36%安くなければ割りに合わないということになります。

ではコストはざっくりどのぐらいになるでしょうか?

いいですか、ここが重要です。オンデマンドの場合、実際にはM5インスタンスの方がM4インスタンスよりも時間当りのコストが安いのです。つまり費用をすべてオンデマンドに振り向けるのであれば、細かい比較などしなくても結論は「M5一択」で決まりです。M4インスタンスを一度もアップグレードしたことがないなら、今こそアップグレードすべきタイミングです。us-east-1のオンデマンドm4.2xlargeインスタンスは米ドルで40セント/時間ですが、m5.2xlargeは38.4セント/時間となり、(訳注: スピードの差を加味すると)M4の方が4.1%高くつきます。

つまりM5がM4よりどれだけ高速かを計算して初めて、M5のディスカウントが大きいことが見えてきます。これホントにホント。

また、以下の記事やこれらの数値は次のことも示唆しています。もし小さな速度差を最適化しているのであれば、最近のRuby(少なくとも2.6以上)にアップグレードしましょう。Ruby 2.6は2.5に比べて著しく改善されており、以下の記事にあるようにそれ以前のRubyと比べてかなり速度に差が付きます。

私たちはRuby 3.0の速度がどうなるかをいずれ目にすることになります。以下の記事にあるように、数か月前にリリースされたRuby 3.0プレビュー版は2.7と比べるとそこまで速度差は大きくありませんが。

⚓ まとめ

忙しい人向け: EC2インスタンスはM5シリーズを使いなさい、以上。私はvCPU数やRAM容量の点でm5.2xlargeが好みです。なお、必要に応じてスケールアップやスケールダウンしてもvCPU数とRAM容量の比率は変わりません。かなり特殊なケースであれば、M6g(ARMプロセッサ、移植が困難、CPUは高速)や、C5(CPU1個当りのRAMが少なめ)、T4(CPUをバーストパフォーマンスで動かせる)を検討してもよいかもしれません。

しかし普通のRailsアプリのユースケースであればやはりM5があなたの友です。「でもM4スポットインスタンスは安いし」とお思いのそこのお方、M4が価格性能比でM5と互角に勝負するには、少なくともM5より36%安くなければいけないことをお忘れなく。それでもM4を使いたい方はどうぞ慎重に。

⚓ 「どうも信じられん」

まだ納得いただけませんか?公平のために申し上げると、私のコードはすべてGitHubで公開されていることにお気づきでしょうか?ドキュメントを読んで自分で動かすのも自由ですし、インスタンス数を増やすなどして再実行して、自分の環境でも私と同じ結果になるかどうかを見たい方は、@codefolioまでお声かけいただければ、私の行った正確な方法を喜んでお見せいたします(コードもすべてネット上にあります)。私のデータを使えば皆さんが自分でEC2インスタンスをレンタルするより安上がりです。

⚓ お知らせ

パフォーマンスについて詳しく知りたい方は、FastRuby.ioブログのperformanceタグにある記事をご覧ください。

関連記事

Ruby 2.5.0はどれだけ高速化したか(翻訳)

ベンチマークの詳しい理解と修正のコツ(翻訳)


週刊Railsウォッチ(20201116前編)6.1のActive Storageでimage_processing gemが必須に、Webアプリ設計の変遷とフロントエンド領域の再定義ほか

$
0
0

こんにちは、hachi8833です。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

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

以下のコミットリストのChangelogを中心に見繕いました。ドキュメントの更新が増えているようです。

PredicateBuilderで委譲をサポート

このコミットより前は、where(id: user)where(author: user)オブジェクトを値として指定するクエリがシリアライズに成功するのは、値(この例ではuser)がActiveRecord::Baseの子孫のインスタンスである場合だけだった。
(SimpleDelegatorやdelegate_missing_toやその他の方法で)デコレーターを用いるアプリケーションでは、この変換が必須なのが少々つらくなることがある。
このコミットはPredicateBuilderを変更して、基本となる比較条件を#respond_to?呼び出しに置き換える。
同PRより大意


つっつきボイス:「where(id: user)where(author: user)のようにwhere()にActive Recordオブジェクトを渡すことができるんですが、デコレーターを通す場合は変換しないといけなかったのか」

「Changelogのコード例↓はいわゆるPORO(Plain Old Ruby Object)で、delegate_missing_to :@authorを書いておけばその後のPost.where(author: AdminAuthor.new(author))が動くように変更されたのね」

#idと主キーを委譲するオブジェクトを用いてpredicate条件をビルドする。
Changelogより

# Changelogより
class AdminAuthor
  delegate_missing_to :@author
  def initialize(author)
    @author = author
  end
end

Post.where(author: AdminAuthor.new(author))

参考: ruby - How can I determine if an object is a PORO or not? - Stack Overflow

「変更部分を見ると↓、修正前のif value.is_a?(Base)はActive Recordの場合だけ処理を進めるけど、value.respond_to?(:id)に変えたことで#idメソッドを持っているかどうかでチェックするようになった」「なるほど、Active Record以外のオブジェクトでも#idメソッドがあれば動くんですね」「どのぐらい使われるかはわかりませんが、やりたいことは見えてきました」

# activerecord/lib/active_record/relation/predicate_builder.rb#L61
    def build(attribute, value, operator = nil)
-     value = value.id if value.is_a?(Base)
+     value = value.id if value.respond_to?(:id)
      if operator ||= table.type(attribute.name).force_equality?(value) && :eq
        bind = build_bind_attribute(attribute.name, value)
        attribute.public_send(operator, bind)
      else
        handler_for(value).call(attribute, value)
      end
    end

has_one :throughでコンストラクタを使えるようになった

これに関する参考情報はガイドやドキュメントに見つからなかったが、has_one :through関連付けの関連付けコンストラクタが無効になっているように見える。自分は単にこれをテストで有効にしたところ、何も壊れなかったので、これを無効にすべき理由が何なのかを考えている。そのあたりがわかる情報をもらえると嬉しい。また、無効にすべき十分な理由がある、この変更に重要性がない、または不要なリスクがあるならば、ドキュメントを更新してhas_one :throughではbuild_associationcreate_associationを利用できないことを反映すべき(もしそうならこのPRの代わりに自分がドキュメントを変更してもよい)。
同PRより大意


つっつきボイス:「今まではhas_one :throughのときにbuild_associationcreate_associationなどのコンストラクタが使えなかったのか」「今までできなかった理由が謎」「使ってはいけないという記述がドキュメントに見当たらなかったので実装したそうです」

「テストコードでもコンストラクタのビルドをチェックしている↓」

# activerecord/test/cases/associations/has_one_through_associations_test.rb#L71
+ def test_association_build_constructor_builds_through_record
+   new_member = Member.create(name: "Chris")
+   new_club = new_member.build_club
+   assert new_member.current_membership
+   assert_equal new_club, new_member.club
+   assert_predicate new_club, :new_record?
+   assert_predicate new_member.current_membership, :new_record?
+   assert new_member.save
+   assert_predicate new_club, :persisted?
+   assert_predicate new_member.current_membership, :persisted?
+ end

「なるほど、calculate_constructableメソッドで:throughを止めてたのね↓」「:throughの場合はコンストラクタブルにならないようにわざわざチェックしてたとは」「ホントだ」「このメソッドがあった理由は謎ですが、消したらできるようになったのでプルリクしたんでしょうね」

# activerecord/lib/active_record/reflection.rb#L693
-
-     private
-       def calculate_constructable(macro, options)
-         !options[:through]
-       end

has_one :throughはあまり書かなさそうだけど、使いたいときに使えるようになったのはいいと思います」

開発時のルーティングをリロードフックの後でappendするようになった

appendしたrootルーティングが内部のwelcomeコントローラより優先されるようにする。
自分が解決しようとしているこの特定のユースケースは「rootルーティングがないアプリ」と「rootルーティングをappendするエンジン」の組み合わせである。ルーティングファイルは初期段階ではここで読み込まれるが、welcomeコントローラはここで追加されているらしい。これはつまり、現在はビルトインのルーティングがadd_builtin_routeで追加されるより「前に」実行されるエンジンイニシャライザを作成しないといけないということになる。このパッチによって、ビルトインのルーティングはルーティングファイルが評価された「後で」アプリケーションに追加されるようになる。
同PRより大意


つっつきボイス:「welcomeコントローラは、Railsのdevelopmentモードでデフォルト表示される例のWelcome画面を出すヤツですね」

「なるほど、developmentモードでビルトインのルーティング処理を移動してroutes_reloader.executeを追加している↓」「Railsエンジンとの兼ね合いでデフォルトのrootルーティングの読み込み順がうまく設定できなかったのを、ユーザー定義のルーティングを先に設定してからビルトインのルーティングを設定するように修正したらしい」「appendしたrootがそれによって内部のwelcomeコントローラより優先されるようになったんですね」

# railties/lib/rails/application/finisher.rb#L198 (移動前との差分を表示しています)
      initializer :add_builtin_route do |app|
        if Rails.env.development?
          app.routes.prepend do
            get "/rails/info/properties" => "rails/info#properties", internal: true
            get "/rails/info/routes"     => "rails/info#routes", internal: true
            get "/rails/info"            => "rails/info#index", internal: true
          end

          app.routes.append do
            get "/"                      => "rails/welcome#index", internal: true
          end

+         routes_reloader.execute
        end
      end

「たしかRailsのルーティングは先に読み込まれたものが有効になったと思うので、それも関連していたのかもしれませんね」

参考: Rails のルーティング - Railsガイド

Railsのルーティングは、ルーティングファイルの「上からの記載順に」マッチします。このため、たとえばresources :photosというルーティングがget 'photos/poll'よりも前の行にあれば、resources行のshowアクションがget行の記述よりも優先されますので、get行のルーティングは有効になりません。これを修正するには、get行をresourcesよりも上の行に移動してください。これにより、get行がマッチするようになります。
Railsガイドより

アップグレードガイドにimage_processingセクションを追加

Active Storageでimage_processing gemが必須になった

Active Storageでvariantsを処理するときに、mini_magickを直接用いるのではなくimage_processing gemをバンドルすることが必須になりました。image_processingはデフォルトでmini_magickを背後で用いるよう構成されるので、アップグレードは(訳注: Gemfileの)mini_magick gemを単にimage_processing gemに置き換えて、既に不要になったcombine_optionsが明示的に使われているのを削除しておくのが最も簡単です。
ただし、素のresize呼び出しをimage_processingのマクロに変更することが推奨されます(リサイズ後のサムネイル画像もシャープになるので)。

video.preview(resize: "100x100")
video.preview(resize: "100x100>")
video.preview(resize: "100x100^")

たとえば上をそれぞれ以下のように置き換えます。

video.preview(resize_to_fit: [100, 100])
video.preview(resize_to_limit: [100, 100])
video.preview(resize_to_fill: [100, 100])

同PRより大意

janko/image_processing - GitHub


つっつきボイス:「アップグレードガイドに追加された注意書きです」「以前Railsに導入されたimage_processing gemが6.1からは必須になったのか(ウォッチ20180511)」「これまではmini_magickかimage_processingのどちらかを使えばいいという感じだった気がします」「今後はmini_magickを使いたければimage_processing経由で使ってくれということか」「これまでmini_magickを素で使ってた人は修正が必要になりますね」

minimagick/minimagick - GitHub

Rails

onkさんの「Smart UIパターン」記事


つっつきボイス:「はてブで大バズリしていたonkさんの記事がRailsにも言及していたのでピックアップしました」「そういえば最近のTwitterでテーブルデータゲートウェイの話題を見かけた気がするけど、もしかするとこの記事がきっかけだったのかも🤔」

参考: テーブルデータゲートウェイ

「記事にも登場している『エンタープライズアプリケーションアーキテクチャパターン』(通称PoEAA)↓はやっぱり名著」「記事でもPoEAAに登場したトランザクションスクリプトやアクティブレコードやデータマッパーなどのいろんなパターンをおさらいしていますね」

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

「そしてRailsの密結合設計の話と@_yasaichiさんの名スライド『Ruby on Railsの正体と向き合い方も扱われていますね(ウォッチ20190401)』」「Railsは最短期間で開発できる代わりにその構造のまま大きくなるとすごく大変になる、はそのとおりだと思います」「Railsほどのラピッド開発を他のフレームワークでやるのは大変そう」

「記事にある『本当に複雑なものと、複雑ではあるが工夫で対処できるもの』という見出しはわかりやすい切り口ですね」「おぉ」「本質的に複雑なものもあるけど、複雑なりに対処できるものもあるという話」「おや、TechRacho記事も参照されている」「はい、それでこの記事に気づきました」

本当に複雑なものと、複雑ではあるが工夫で対処できるもの
同記事見出しより

「そして複雑さに対処する方法がいくつかリストアップされていますね: TrailblazerのOperationや、HanamiのInteractorなど」「rectifyは見たことなかった」

andypike/rectify - GitHub

「その次の『同機能の CUI コマンドを作るときに重複が無い』のパラグラフ↓もいいですね: 言い換えるとコントローラがコントローラの役割だけを担うようになっている」「なるほど」

Web アプリケーションとしてのインタフェース (View 以外) が Controller の役目で、「同機能の CUI コマンドを作るときに重複が無い」という基準で考えると良い。
同記事より

「『Controller or Action と 1:1 対応する新しい層』という考え方も流行りましたね: この場合ファイル数がものすごく増えてしまうんですが、そこはもう仕方がない」

「System on Record(SoR)とSystem of Engagement(SoE)という考えも見たことある↓」

「『SoEの戦場がクライアント側に移っている』もわかる: ユーザーとのインタラクション処理をフロントエンド側に移して、そこでGraphQLのインターフェイスが使えればいいという感じでやることが増えているように思います: その分SoRの複雑さについては引き続きサーバーサイドエンジニアが面倒を見る必要はあるでしょうね」


「この記事も含めて、最近の設計関連記事の多くが似たような結論に達しているのが面白いですね: たとえばActive Recordはあまりにもよくできていて、小さい問題を解決するときには非常によい、とか」

「その一方でフロントエンドでGraphQLを使うと割と手軽に同じようなこともできるようになりつつありますね: もちろんActive Recordのようにサーバーサイドでないと解決できないことは依然としてありますが」「従来だとサーバーサイドでAPIを作らないといけなかったのが、GraphQLによってフロントエンジニアがサーバーサイドエンジニアを煩わせずにやれる部分が増えてきたように思います」「それが主戦場がクライアント側に移ったということですね」

「逆にサーバーサイドでないとできないことは何かというと、データの永続化や、複雑なドメインロジックの取り扱いのような、ブラックボックスの中をいじる部分」「たしかに」

「フロントエンド側でやれることが増えてきた分、サーバーサイドエンジニアは今後そうしたブラックボックスの中というか下のレイヤに注力する方が今後生き残りやすいのかもしれないとちょっと思いました」「考えさせらる…」

「onkさんの記事、設計方法の変遷を見渡すことができて素晴らしい👍」「ありがたいです🙏」

スライド『「フロントエンド領域」を再定義する』もチェックしておきたい

「そういえばつい最近mizchiさんもフロントエンドエンジニアの職務範囲についてスライドを発表していましたね」「あ、これですか↓」「そうそう、ここで説明されているフロントエンドの変遷もいろいろ納得がいきました👍」

「従来のモノリシックなフレームワークでは↓、サーバーサイドエンジニアがJSやHTMLまでカバーしていて、マークアップエンジニアがいなければ上から下まで全部やっていた」「とても見やすい図ですね」「Railsの推奨構成は基本的にこれ」

「やがてフロントエンドフレームワークが使われるようになると↓、サーバーは永続化とAPIに集中するようになってくるけど、SSR(Server Side Rendering)をやろうとするとサーバー側かフロントエンド側のどちらかが相手側に越境しないといけなくなってつらくなってきた」「ふむふむ」

参考: 【動画付き】Next.js の Server Side Rendering (SSR) を理解する。create-react-app と比べてみた。 - Qiita

「スライドではそこから更に進んで以下のようにフロントエンドフレームワークがSSRを扱う(つまりサーバー側のレンダリングをフロントエンドで行う)ようになっている↓: そうなるとサーバー側はますますバックエンドAPIに徹するようになってきて、基本的な画面もサーバー側では扱わなくなってきたりする」「最近のフル装備フロントエンドはこんな構成なのか」「フロントエンドの職務範囲が広がってきているんですね」

「先のことはわかりませんが、将来こんなふうにフロントエンドがフルスタックになったらどうなるんだろう↓」「ここまでくると何だか逆方向に振り切って最初のレガシー図に近い形に戻ってませんか?」「結局そういうことですよね😆」「Twitterでも指摘してた人を見ました」「歴史は繰り返しつつあるのかも」

「上のようにフルスタックのフロントエンド構成になったら、APIのインターフェイス部分についてもフロントエンドが書くことになりますが、使うのはフロントエンド側なのでそこは自分たちが使いやすいように書けばいいと思います」「ただ、そのAPIの内部で使うドメインロジックについてはサーバーサイドのブラックボックスの中にしか書けないものもまだまだあるので、フロントエンドと通信するAPIのさらに裏にそのための「内部API」的なものが引き続き必要でしょうね」「APIのAPIという感じですね」「その部分がおそらくonkさんの記事で言う『本当に複雑なもの』に相当するかもしれないと最近思っています」

「少なくともサーバーサイドの仕事がいきなりなくなることはないんじゃないかなと想像してます」「現実にはサーバーサイドエンジニアも多少なりともフロントエンドを触ることになるでしょうね」「ただRailsの位置づけがバックエンドAPI中心になることが増えているのは感じています」「少々極端な想像ですが、たとえば今後Railsサーバーがフロントエンドと従来のような直接のWeb API通信をしなくなって、gRPCのようなものを介して通信する一種のドメインロジック用内部サーバーのようなものになる、という形も一応考えられますね(なお今のRailsはそういうものをまったく目指していないと思います)」

参考: サービス間通信のための新技術「gRPC」入門 | さくらのナレッジ

「Railsフレームワークの立ち位置は、ある意味でCSSフレームワークにおけるBootstrapと少し似ているかもしれませんね: プロダクトが成功してエンジニアリソースを多く割り当てられるようになり、継続開発期間が長くなるに連れて、だんだんBootstrapを必要としなくなったり好みが分かれたりすることもあるけど、プロトタイプを最初に作るときはやっぱりBootstrapが早いし便利なので今も使われ続けている、それと似たような立ち位置」「なるほど」

twbs/bootstrap - GitHub


つっつき後に、「週刊気になったITニュース」↓でも「フロントエンド領域を再定義する」スライドが取り上げられていたことに気づきました。

参考: 週刊気になったITニュース(2020/11/15号) - masa寿司の日記

Railsビューのパーシャルがパフォーマンスに与える影響(Ruby Weeklyより)


つっつきボイス:「ビューのパーシャルがパフォーマンスに与える影響の記事は周期的に見かけますね: パーシャルをループで回したりするよりコレクションを使ったりインライン展開したりする方が速いとか↓」「定番のお題ですね」


同記事より

「Railsのビュー周りは折に触れてパフォーマンスが改善されているので昔と違う部分があるかも」「定番の話ではありますがパフォーマンスへの影響が大きい部分なので、最新のRailsの実装で測定結果がどう変わっているかを知るうえでこういう記事を定期的にチェックしておくとよいと思います」「たとえばERBの速度が大きく向上したら、従来だと10倍の差だったのが20倍になったりするようなことがあるかもしれませんよね」

Railsから切り出されたgem完全ガイド

切り出されたgemがshimと呼ばれているとは知りませんでした。


つっつきボイス:「Railsからいろんな機能がgemに切り出されている」「こういう歴史的な記事はありがたい」「Rails 4.2で切り出されたgemが割と多いのか」「そうしておかないとRailsがいくらでも大きくなってしまいますね」

「shimといえばrbenvの設定にshimsディレクトリがありますね」「関連記事にもrbenvのことが書かれてる↓」「ホントだ」

参考: What is a shim?. A shim is a small library that… | by Ujjawal Dixit | Medium

RailsのトップFAQ 12件(Hacklinesより)


つっつきボイス:「質問の内容はとても基本的なんですが、頻度の高い質問に答えている記事です」「お〜、何て丁寧な回答!」「マイグレーションのロールバックとか、RubyやRailsのnilemptyblankの違いなどにみっちり答えているのがスゴい👍」「Railsにまだ慣れてない人にとって便利そう」

「と思ったら質問は12件なのに回答は5件しかない…?」「あれれ、ホントだ」「今後追加するのかな?」


後で気が付きましたが、元記事の末尾からリンクされていた以下の記事に12件のQ&Aがすべて載っていました。少々わかりにくいですね。


前編は以上です。

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

週刊Railsウォッチ(20201111後編)RubyConf 2020が11/17〜19オンライン開催、GitHub Container Registryベータ開始、スマートロックほか

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines

週刊Railsウォッチ(20201117後編)Rubyのパターンマッチングが3.0で本採用に、AWS Lambdaサイズを縮小する、AppleのM1チップほか

$
0
0

こんにちは、hachi8833です。Googleフォトの変更でちょっとしょんぼりしてます。


つっつきボイス:「いつかは来るかもしれないという気はしていましたが」「さすがにここでハシゴを外しにくるのはキツいかな」「月250円で100GBならリーズナブルかなとも思いますけどね」「やはり容量無限のストレージというものはなかったか」

「一応GoogleのPixelからの利用なら引き続き無料かつ無制限になるようですね」「Googleのスマホを買えば使えるという方針は理解できる」「最初からそういう制度にしておけばよかったかもしれませんね」

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

⚓Ruby

⚓ パターンマッチングがexperimentalから本採用へ(Ruby Weeklyより)

# NEWS.md#L51
-* Find pattern is added. [[Feature #16828]]
+* Pattern matching is no longer experimental. [[Feature #17260]]
+
+* One-line pattern matching now uses `=>` instead of `in`.  [EXPERIMENTAL]
+ [[Feature #17260]]
+
+   ```ruby
+   # version 3.0
+   {a: 0, b: 1} => {a:}
+   p a # => 0
+    
+   # version 2.7
+   {a: 0, b: 1} in {a:}
+   p a # => 0
+   ```
+
+* Find pattern is added.  [EXPERIMENTAL]
+ [[Feature #16828]]

つっつきボイス:「おぉ、ついにRubyのパターンマッチングからExperimentalが外れた🎉」「ワンラインパターンマッチングなどの一部のパターンマッチング機能はまだexperimentalなのか」「これで3.0になったらパターンマッチングを使ってもよくなりますね👍


なお、つっつきの後でパターンマッチングの公式ドキュメントが欲しいというissueを見かけました。

参考: Misc #17329: Doc for pattern match? - Ruby master - Ruby Issue Tracking System

⚓ Rubyのrefinementにいいユースケースがひとつある(Hacklinesより)


つっつきボイス:「one good (rare) use caseと書かれているのがちょっと面白い」「いいユースケースはひとつならあると」「refinementのいい使い方はもっとありそうな気がしますけど」「記事の末尾にも『いいユースケースが他にもあったらぜひ教えて下さい』とありますね」「普段のビジネスアプリケーション開発だとあまり使わないかもしれませんが、ライブラリの挙動を安全に上書きするなど知っておくと有効なユースケースはそれなりにあると思います」

「この記事のconversion wrapperはrefinementでないとできないと書かれてますね」「Kernelをrefineしてる…」

# 同記事より
module TimestampConversionRefinement
  refine Kernel do
    def Timestamp(value)
      return value if value.is_a?(Timestamp)
      Timestamp.new(value)
    end
  end
end

class ElegantBusiness
  using TimestampConversionRefinement

  def self.business; Timestamp(1); end
  def business; Timestamp(1); end
end

# ElegantBusiness.Timestamp(1) # NoMethodError: undefined method `Timestamp' for ElegantBusiness:Class
# ElegantBusiness.send(:Timestamp, 1) # NoMethodError: undefined method `Timestamp' for ElegantBusiness:Class

「Rubyのrefinementは乱用すべきではありませんが、いざというときには便利な機能ですし、有効に使えるところで使う分には良いのではないかと思います」

RubyのRefinement(翻訳: 公式ドキュメントより)

⚓ RubyのQuick tips記事2本

つっつきボイス:「小ネタ集その1は、StringとRegexのどちらを渡しても比較できるようにするにはmatch?ではなくトリプルイコール===にする必要があるそうです」「えぇぇ?言われてみれば😳」「たしかにそのとおりなんですけど、これを意識的に使う機会ってあるんだろうか?」「===で比較しているコードを見て『ああ、これはStringでもRegexでも比較できるのね』とすぐに気付ける気がしない…」「コメントにその旨を書いてあれば使うのはありかなと思いました」

def foo(expected, actual)
  expected === actual # match?だとStringを渡せない
end

「小ネタその2で、fdivというメソッドを初めて知りました」「浮動小数点値を得るために数値をto_fしてから割らなくても、fdivなら一発でできるのね」

1 / 5.to_f  #=> 0.2

1.fdiv(5)  #=> 0.2

fdivは知らなかった」「こういうニッチなメソッドはあまり覚えなさそうですが、こうやって一度でも見かけたことがあれば、後で『Rubyにこんなメソッドがないかな』と探すときに思い出す手がかりになるでしょうね」「たしかに、見たことがないと存在すること自体思い当たらないかも」「自分も明日には忘れそうです😆

「ところで、基本的な演算子をこんな感じでメソッドでも呼び出せるようになっていると、Rubyでよくあるtapを何度もチェインするときやカリー化などで、普通のメソッドと同じように演算子メソッドもチェインできますね」「tapやカリー化といえばkazzさんが詳しいというか好きですね」

Ruby: Object#tap、Object#then を使ってみよう

参考: カリー化 - Wikipedia

⚓クラウド/コンテナ/インフラ/Serverless

⚓ node_modulesを片付けてLambdaのサイズを1/3に(Serverless Statusより)


つっつきボイス:「node_modulesを小さくするのね」「Lambdaのサイズでメモリのフットプリントが変わるから、不要なモジュールを減らせれば相当小さくなりそう」

「なるほど、aws-sdkも削除するのか」「aws-sdkはたしかデフォルトで入るんだったかな」「他のAWSサービスを呼び出すためにLambdaを使う場合なら、aws-sdkがデフォルトで入っていると便利ですね」

aws/aws-sdk-js - GitHub

「その他にTypeScriptのtypeファイルも削除できる」「Lambdaでは不要なので削除できますね」

「そしてこれだけ小さくなった↓」「150MBが46MBになったのは大きい🎉


同記事より

⚓ その他クラウド


つっつきボイス:「Eclipse Theiaを初めて知りました」「Theiaって女性の名前っぽい響きがありますね」「それにしてもEclipseがこんなふうに進化していたとは」

後で調べるとギリシア神話の女神テイアでした↓。

参考: Theiaの意味・使い方・読み方 | Weblio英和辞書

「ブラウザ画面でVSCodeのように編集するのはAWS Cloud9などでもできますけど、これは書いたコードをGoogleのクラウドにそのままデプロイできる、つまり書いたものをちょうどこの図↓のようにGCPのCloud Runで実行できるなどの形で実行環境も統合されているところが強みでしょうね」「それは便利そう!」


cloud.google.comより

参考: AWS Cloud9(Cloud IDE でコードを記述、実行、デバッグ)| AWS
参考: Cloud Run: Container to production in seconds  |  Google Cloud

「Cloud9の編集画面は単にWeb IDEのインスタンスが立てられているようなものなので、そこで書いたものをそのまま実行環境にデプロイできるわけではないんですよ」「なるほど、そういうことでしたか」「おそらくGitHubやGitLabのWeb IDEもCloud9のと実質同じだろうと思います」

「実行環境も統合するのはまったく新しいかというとそうでもなくて、たとえばC4SAというサービス↓はなかなか良かったのですが(現在は終了)、GCPと統合されているのは実用性が高いと思います👍

参考: PaaS「ニフティクラウド C4SA」正式版--Ruby on RailsやMySQLも提供 - CNET Japan

「AWS Lambdaのエディタが現在のものより賢くなればこんな感じになるのかもしれない」「Lambdaにもエディタがあるんですね」「このGCP+Eclipse Theiaほどリッチではありませんが、一応編集環境はあります」

参考: AWS Lambda コンソールエディタを使用した関数の作成 - AWS Lambda

⚓CSS/HTML/フロントエンド/テスト/デザイン

⚓ NAT Slipstreaming攻撃


つっつきボイス:「NAT Slipstreamingか」「図を見るのがわかりやすい」「SIPはSession Initiation Protocolなんですね」

参考: Session Initiation Protocol(SIP) - Wikipedia

「攻撃の形態としては以前からあるかなと思います」「たしかにオンラインゲームなどでよく使われる『NAT越え』↓という技術では、どこかのタイミングでポートマッピングして外部との経路を開通させるので、そこを悪用されると乗っ取られる可能性がありますが、そういう攻撃の亜種のようですね」

参考: NAT越え(NATトラバーサル-NAT traversal)|Web会議・テレビ会議システムならLiveOn(ライブオン)

「SIPの5060/5061ポートへのリクエストがブラウザによってブロックされるように変更されて、whatwgもブロック対象に加えたのか」「通常業務でこのポートをインターネット上に公開することはあまりなさそうなのでそれほど大きな問題にはならないかな」「こういう攻撃手法がまだあったとは」

⚓言語/ツール/OS/CPU

⚓ scpコマンド非推奨化について(StatusCode Weeklyより)

昨年に宣言されていたんですね。

参考: OpenSSH 公式による scp 非推奨宣言を受け, scp, sftp, rsync を比較してみた (2020/5/25 rsync の計測結果について注記追加) - 寒月記


つっつきボイス:「たしかscp非推奨化は去年あたりに話題になりましたね」「この記事見るまで知りませんでした😅」「非互換の問題があるためにscpを抜本的に変えられない話などもあったと思います」「今はだいたいsftpプロトコルを使いますね」「レガシーなコマンドによくある問題」

参考: SSH File Transfer Protocol(sftp) - Wikipedia

⚓その他

⚓ AppleのM1チップ

参考: Apple M1 - Wikipedia


つっつきボイス:「AppleのM1といえば、早くもGeekbenchでベンチマークが出ていたのを見かけましたけど、何かのバグかと思うぐらい飛び抜けたスコアを叩き出していましたね」「おぉ〜!」「シングルコアでデスクトップのiMacよりも速いとか、にわかには信じがたいスコア↓」「これはすごい」

参考: Apple M1、3.2GHz動作でCore i9-10910超のシングルコアスコア - iPhone Mania


iphone-mania.jpより

参考: MacBookAir10,1 - Geekbench Browser
参考: Macmini9,1 - Geekbench Browser
参考: MacBookPro17,1 - Geekbench Browser

「M1のベンチマークスコアはすごくても今の段階ではソフトウェアがついてきていませんが、ソフトウェアはそのうち追いついてくるものなのでそこは大丈夫だと思います」

なお、現時点ではHomebrewは大半のformulaが未対応または未チェック、MacPortsはARM 64(M1のアーキテクチャ)に対応したそうです。

参考: macOS 11.0 Big Sur compatibility on Apple Silicon · Issue #7857 · Homebrew/brew
参考: Release MacPorts 2.6.4 · macports/macports-base

「実機のメモリが16GBというのがちょっと少ないかな、それとも足りるかな?」「ただでさえChromeが8GBぐらい使いますから😆」「Chromeはメモリ喰い👄

「M1上でIntelバイナリは動くんでしょうか?」「基本的には多くのソフトウェアがRosetta 2の上で動くようですが(速度はともかく)、CPUアーキテクチャが変わるので、Intel CPU固有の機能を直接使っているとかカリカリにチューニングしたソフトウェア、あとはプロプライエタリなドライバや周辺機器などが動かなくなる可能性は考えられますね」「M1との互換性リストもたしか出始めていたと思います」「そういえば知人が自分で書いたかな漢字変換がM1 Macで動くかどうかを気にしてました」

参考: Rosetta - Wikipedia
参考: Product Compatibility for Apple M1 Silicon Macs | Green Screen Blog

「M1はいろんな意味で興味がありますね」「知り合いにもM1 Macを買う宣言してる人がいるので、人柱報告があがったのを見てから買うかどうか決めようかな」「でも今のPC環境で満ち足りているから急いで買うほどでもないか…」「M1は面白いので追いかけつつ様子見してみよう」

「M1 MacがWeb開発にすぐ使えるかどうかはまだ何とも言えなさそう」「Docker Desktop for Macが動くかどうかがポイントかも」「たしかAppleは公式にハイパーバイザー上でDockerをサポートすると聞いた覚えがありますけど、Dockerのissueにはまだ動かないという報告が上がってるな」「しばらくは人柱になる覚悟が必要そうですね」

「ちなみにAppleのハイパーバイザーのドキュメント↓にはsupported hardwareにApple Siliconで動くと明記されているので、ドキュメントレベルではAppleのハイパーバイザーでM1がサポートされているということになりますね」「ホントだ」

参考: Hypervisor | Apple Developer Documentation

「ハイパーバイザーだけを使うコードは基本的にM1で動くと思いますが、ドキュメントにも書かれているIntelのVT-xやEPTといった独自機能を直接使うコードは当然ながらM1では動きません(M1のCPUに命令がないので)」「なるほど」

参考: Virtualization Technology (VT-X)を有効にするには - Lenovo Support IT

「ところで、xhyve↓のようなピュアなハイパーバイザーならM1にも移植できるかなと思ったけど、EPTをサポートするCPUが必須とREADMEに書かれているのでダメか」「xhyveは最近更新されてないみたいですね」「xhyveはシンプルなので比較的移植しやすいかもしれないと思ったけど、I/Oもあるからドライバ周りも対応しないといけなさそう」

machyve/xhyve - GitHub

追いかけボイス: 「仮想化ソフトウェアのParallelsも対応するぞ宣言はしてるけどソフトはまだなので、しばらくはx86系のOSとかDockerが動くまで時間かかりそうな気配ですね」

参考: Parallels における Mac および Windows の仮想化、Mac の管理、VDI および RDS ソリューション


なお、本日出た以下のDockerブログ記事によると、M1上のDocker DesktopはまだRosetta 2での動作が完全ではなく、Appleの新しいハイパーバイザーフレームワークへの移行や手直しを検討しているようです。


後編は以上です。

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

週刊Railsウォッチ(20201116前編)6.1のActive Storageでimage_processing gemが必須に、Webアプリ設計の変遷とフロントエンド領域の再定義ほか

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

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

Ruby Weekly

Hacklines

Hacklines

StatusCode Weekly

statuscode_weekly_banner

Publickey

publickey_banner_captured

Serverless Status

serverless_status_banner

Railsのルーティングを極める(前編)

$
0
0

更新情報

  • 2014/03/03: 最初の版を公開
  • 2020/11/18: Rails 5と6の情報を反映

Railsのルーティングを極める(後編)もご覧ください。


こんにちは、hachi8833です。今回も弊社CTOの馬場さんによる勉強会のスライドを元に記事を書きました。発表当時はRails3だったので、Rails4情報も追加しました。

⚓ Railsのルーティング(routes)を極めよう

2012/03: baba

Railsのルーティングはきわめて自由度が高い分、気を付けないとすぐカオスになってしまいます。Railsのルーティングのコツについて勉強していきましょう。Railsのルーティングはconfig/routes.rbで設定します。

⚓ まずはrails routes

追記(2020/11/18):以前はrake routesコマンドでしたが、Rails 5や6ではrails routesも使えるようになりましたので原則こちらで表記します(rake routesも引き続き使えます)。

ルーティングを書く際にはrails routesなどでルーティングを確認しながら書く癖をつけましょう。ルーティングを作成するだけでなく、Railsのデバッグ時にも有用で、迷ったらとにかくrails routesを実行してもいいくらいです。

rails routesは実行のたびにRails環境を読み込むので、そのままでは遅いので有名です。以下のような方法で高速化しましょう。

  • dev環境でRailsを起動中ならhttp://localhost:3000/rails/infoを参照する (Rails 4以降)
  • pryとpry-rails gemがインストールされているなら、rails consoleを起動しておいてshow-routesを実行
  • spring gemがインストールされているならspring rails routesとすれば2度目以降から高速になる

参考までに、週刊Railsウォッチ20201012ではRailsのルーティング情報を動的にビジュアル表示する方法が紹介されています(graphvizをインストールする必要があります)。

⚓ 改めて、RESTとは

RESTとは、Railsに敷かれているレールの一つである概念で、REpresentational State Transferの頭字語です。RESTの特徴は以下のとおりです。

  • ステートレスであること
  • すべてを「リソース」で表す
  • リソースは名前を持つ

RESTに従っていることをRESTfulと呼んだりします。

⚓ RESTfulなURLの例

  • http://example.com/prefectures
  • http://example.com/users/1

⚓ RESTの反対はRPC

RESTのちょうど反対の概念がRPC(Remote Procedure Call)です。これはクエリ形式などと呼ばれることもあることからわかるように、?の後ろに問い合わせを&でつないで表現します。

⚓ RPC URLの例

  • http://example.com/index.php?action=prefecture_list&amp;id=1
  • http://example.com/PrefectureList.aspx?id=1

Railsではresourcesでルーティングを記述することで自動的にRESTfulなルーティングが生成されます。
以下はusersコントローラとproductsコントローラへのルーティングです。

resources :users
resources :products

その場合、対応するコントローラにもRESTfulなアクションが揃っている必要があります。rails generate scaffoldで生成した場合は自動的にRESTfulになります。

Railsでは、REST形式もRPC形式も両方扱うことができますが、RailsはRESTを「レール」として定めていますので、基本的には統一のためにRESTfulなルーティングの作成を心がけるようにしましょう。

ただし、RESTはあくまでポリシーであり万能ではないので、時にはRPC形式を一部に導入する方が素直に作れることもあります。

⚓ RESTメソッド

RESTとHTTPメソッドにはそれぞれ以下のような関係があります。なお、Rails 4 からはPUT非推奨となり、PATCHが推奨されています。

⚓ RESTメソッド一覧

メソッド 安全 冪等
GET
POST × ×
PATCH/PUT ×
DELETE ×
  • 安全が×になっているのは、危険という意味ではなく、実行すると元のデータが更新されるという意味です。
  • 同様に、安全が◯になっているのは、実行によって更新される心配がないという意味です。
  • 冪等(べきとう: idempotent)は近年よく使われる用語で、「1回実行しても2回以上実行しても結果が変わらない」ことを指します(例: 1人殺しても3人殺しても死刑、は冪等です。1人殺せば犯罪者、1000人殺せば英雄、1億人殺せば神、だと冪等ではありません)。chefやvagrantなどのサーバーデプロイ用DSLではその目的のため冪等性が重視されます。

⚓ RESTfulなメソッドとURLの例

以下の表では、BPSという会社の所在地をGETメソッドとRESTfulなURLで表現した場合の例を示しています。

概念 RESTfulなメソッドとURL
都道府県 GET /prefectures
東京都 GET /prefectures/tokyo
東京都市区町村一覧 GET /prefectures/tokyo/cities
東京都新宿区 GET /prefectures/tokyo/cities/shinjuku
東京都新宿区会社一覧 GET /prefectures/tokyo/cities/shinjuku/companies
東京都新宿区にあるBPSという会社 GET /prefectures/tokyo/cities/shinjuku/companies/bps
BPSという会社 GET /companies/bps

以下の表は、記事・ユーザ・コメントを表現した場合の例です。特に、IDが複数ある場合の表現方法にご注目ください。

概念 RESTfulなメソッドとURL
記事一覧 GET /articles
記事(ID=1)、コメント一覧 GET /articles/1/comments
記事(ID=1)、コメント(ID=1) GET /articles/1/comments/1
ユーザ(ID=1) GET /users/1
ユーザ(ID=1)、パスワード GET /users/1/password

以下の表は、記事・ユーザ・コメントに対して操作を行なう場合の例です。Rails 4以降ではPUT非推奨になり、PATCHが推奨されます。

概念 RESTfulなメソッドとURL
記事を投稿する POST /articles
記事(ID=1)にコメントを投稿する POST /articles/1/comments
記事(ID=1)を更新する PATCH /articles/1
記事(ID=1)のコメント(ID=1)を更新する PATCH /articles/1/comments/1
記事(ID=1)を削除する DELETE /articles/1
ユーザ(ID=1)のパスワードを更新する PATCH /users/1/password
(エラー) POST /users/1/password

⚓ ルーティングを綺麗に書くコツ

⚓ 1. 原則は「RESTに従う」

原則として、RESTに従うようにしましょう。頑張ればresourceですべて書くことができます。ただし原理主義的に何が何でもresourceで書こうとすると、かえって見通しが悪くなることもありますので、ほどほどにしましょう。

  resources :admin_menus, only:[ :index, :update ]
  resources :menus, only:[ :index ]
  resources :deadlines, except:[ :create, :destroy ]

⚓ 2.以下の用語を理解する

リソース
HTTPメソッド(GET/POST/PATCH/PUT/DELETE)で操作する対象となるURLです。
名前付きルート
「パス_path」(ドメイン名より下のパスのみ)または「パス_url」(httpなどから始まるフルパス)という形式でURL形式を指定できます。たとえばContactページが/contactというパスにある場合、contact_pathまたはcontact_urlという名前付きルートで指定できます。名前付きルートは、パスヘルパーやURLヘルパーとも呼ばれます。これにより、コード上でURL構造を意識せずにパスを指定できます。
なお似ているけど違うのは「名前付きスコープ」と「名前付きパラメータ」です。
ネストしたリソース
ネストしたルーティングを記述することで、あるリソースを他のリソースの子にすることができます。

ネストしたリソースなどについて詳しくは、後編で解説します。

⚓ 3. アルファベット順にソートする

これはメンテナンスのしやすさに通じます。

なお、ルーティングはファイルの上から順に有効になりますので、同じものを下に書いても効きません。

⚓ 4. ルーティングファイルの分割を検討する

Railsアプリケーションが成長してルーティングが膨大になったら、config/routes.rbの分割を検討しましょう。

たとえば以下のようにconfig/application.rbに記載することで、config/routes.rbを分割してconfig/routes/以下のroutes_1.rbとroutes_2.rbに置くことができます。

config.paths["config/routes"] << "config/routes/routes_1.rb" 
config.paths["config/routes"] << "config/routes/routes_2.rb" 

関連記事

Railsのルーティングを極める (後編)

Railsのルーティングを極める (後編)

$
0
0

更新情報

  • 2014/03/03: 初版公開
  • 2020/11/20: Rails 6で確認および更新

こんにちは、hachi8833です。「Railsのルーティングを極める」の後編です。今回はRails 4.0.3 + Ruby 2.1.1の環境で動作確認しています。

⚓ Railsのルーティング(routes)を極める

2012/03(baba

⚓ resourcesとネスト

Railsのルーティング記法の基本は、複数形のresourcesメソッドと単数形のresourceメソッドです。また、Railsのルーティングにはネストを含む多くのオプションがあり、自由度が飛躍的に高まっています。

以下の2つのルーティングは、ネストしていないシンプルなresourcesルーティングです。prefecturesとarticlesは、いずれもコントローラに合わせて複数形で書く点にご注意ください。

resources :prefectures
resources :articles

rails routesしてみると、prefecturesとarticlesそれぞれについてRESTfulかつ標準的なアクション(indexcreateneweditshowupdatedestroy)を網羅したなルーティングが一気に生成されています。

nonnested

ところで、せっかくなのでRails 4.0以降で使えるルーティング表示機能でも見てみましょう。development環境でRailsを起動して、ブラウザでhttp://localhost:3000/rails/info/routesを開くと以下のように表示されます。

名前付きルートもHelper列にわかりやすく表示され、[Path]と[Url]をクリックすれば名前付きルートの*_path*_urlを切り替えて表示するという細かい芸もやってくれます。

nonnested_4.0

参考までに、最近のRailsでは以下のように表示が洗練されています(6.0.3.4で確認)。

なお名前付きルートのうち、*_pathはドメイン名から下のパス、*_urlはhttp://などから始まるフルパスであることは前編でも説明いたしました。生成された名前付きルートをrails consoleで確認するには、app.に続けて名前付きルートを入力してみます。

namedroutes

「複数形にはidはなく、単数形にはidがある」と覚えておくとよいでしょう。

このようにコードで名前付きルートを使うことで、生のURLをコードに書かずに済みます。

名前付きルートはカスタムで指定することもできますが、なるべくこのように標準的なものをRailsに生成させる方が楽ですし混乱せずに済みます。

では、このうちprefecturesの下でcitiesとcompaniesをネストさせてみましょう。

resources :prefectures do
  resources :cities do
    resources :companies
  end
end

このときのルーティングテーブルは以下のようになります。

nested

見てのとおり、prefecturesのルーティングに加え、prefectures/cities、そしてprefectures/city/companiesという階層が追加されました。いずれも複数形の「resources」を指定しているので、prefectures, city, companyにはidがあります。

このときの名前付きルートは次のようになります。idの部分には適当な数字を入れてあります。

nestedroutes2

⚓ 単数リソース

上では複数形のresourcesを使用してid付きのルーティングを生成しましたが、単純なページへのルーティングのようにidが不要な場合は単数形のresourceを指定することができます。

仮にprefectureでidが不要だとすると、以下のように単数形のresourceを指定し、prefectureも単数で書きます。

resource :prefecture

このときのルーティングは以下のようになります。

resource

見てのとおり、Pathにid:が含まれなくなり、indexアクションもなくなりました。

なお、この記述「resource」も「prefecture」も単数ですが、これによって指定されるコントローラは「prefectures」と複数形になっていることにご注意ください。御存知のとおり、Railsではコントローラ名を複数形で書くことになっています。

名前付きルートを確認します。なお、無効なはずのidをわざと付けてみると、妙なパスが生成されました。

singleresource

⚓ 複数リソースと単数リソースのネスト

今度は複数リソースと単数リソースの組み合わせの例を示します。ユーザーは複数いるのが普通なのでidを指定しますが、ユーザーごとのパスワードは1つしかないのが普通なので、パスワードではidを指定しない、という状況です。

この場合以下のようなルーティングが考えられます。外側のusersは複数リソース、内側のpasswordは単数リソースです

resources :users do
  resource :password
end

この場合は以下のルーティングが生成されます。

combinednest

期待どおり、userにはidがあり、passwordにはidがありません。

なお、user_pathのようにそのリソースがパスの最後尾にある場合のidは「/:id」と表記されていますが、user_password_pathのようにuserがパスの途中にある場合では「/:users_id/」と表記されています。どちらもidです。

nestedsingle

⚓ resourceベースでないルーティングの書き方

resourceベースでない、HTTPメソッドを指定したルーティングも可能です。その方がresourcesで書くよりもルーティングテーブルがシンプルになるのであれば、使う方がよいと思います。

get 'hello1', to: 'pages#hello'
get 'hello2', :controller => 'pages', :action => 'hello2' 
get 'hello3/:id', to: 'pages#hello3'
post 'hello4', to: 'pages#hello4'

⚓ よくない書き方

以前は、GETPOSTPUTUPDATEDELETEのHTTPメソッドすべてにマッチさせたいときはmatchを使ったのだそうですが、ワイルドカードはセキュリティ上の隙になる可能性があるので、Rails 4からvia:オプションなしでのmatch指定は禁止されています。

match :hello5, to: 'pages#hello5' #禁止 (エラーが表示される)
match :hello5, to: 'pages#hello5', via: [:get, :post] #許される

さらに、かつては以下のように書くことができたのだそうです。

match ':controller(/:action(/:id))(.:format)'

こうすると「コントローラ」「アクション」「id」が実在してさえいればupdateやdestory`など何にでもマッチしてしまうという、楽ちんかつ風通しの良すぎるルーティングになります。このヒューヒューの全通しルーティングは当時から危険視されていたらしく、チュートリアルや実験用以外で使うべきでないとされていたようですが、現在は完全に禁止されています。

⚓ namespace

Railsのルーティングでは以下のようにnamespaceを指定してパスをグループ化することができます。これを使用して、たとえば管理用ページ(admin)のパスや置き場所を仕切ることができます。

namespace :page do
  get :privacy_policy
  get :company_information
  get :term_of_use
  get :businessdeal
end

namespaces1

上のように、「名前付きルート」「パス」「コントローラ#アクション」にpageが追加されました。

上は素朴なgetメソッドルーティングでしたが、resourcesルーティングに名前空間を与えることもできます。

namespace :admin do
  resources :users
end

namespaces2

同じく、「名前付きルート」「パス」「コントローラ#アクション」にadminが追加されました。

⚓ :moduleによる名前空間

:moduleオプションを使用してresourcesに名前空間を与えることもできますが、これは上と少し動作が異なります。

resources :users, module: :admin

#以下も同等
scope module: :admin do
  resources :users
end

namespace3

こちらは「コントローラ#アクション」にしかadmin名前空間が追加されていません。ここからわかるように、パスには表したくないが別のディレクトリにまとめたいコントローラがある場合に利用できます。

⚓ collectionとmember

既に見たように、resourcesを使用すれば主要な7つのルーティングが自動的に追加されますが、 それ以外のルーティングをそのリソースに追加したい場合はmemberまたはcollectionを使います。

さっきの「複数形はidなし、単数形はidあり」と同じ考え方で、「collection(集合)はidなし、member(個別)はidあり」と覚えましょう。以下のルーティングを例に取ります。

resources :books do
  collection do
    post :search
    post :remove_multi
  end
  member do
    get :thumbnail
    get :sample_file
  end
end

これをルーティングテーブルにすると以下のようになります。

collectionmember

見てのとおり、いつもの7つのルーティングに加えて4つのルーティングが追加されています。そしてcollectionで指定した2つにはidはなく、memberで指定した2つにはidがあります。

なお、この場合の名前付きルートはbooks_search_pathとかではなくsearch_books_pathのように上位のリソースが後ろに置かれていることにご注意ください。一応名前付きルートも確認してみましょう。

collectmember

以下のような簡略版表記も使用できます。

resources :books do
  post :search, on: :collection
  get :thumbnail, on: :member
end

ここで1つ注意があります。以下のようにcollectionmemberも指定せずに書いた場合はデフォルトで「member扱い」となります。

resources :books do
  post :search
  post :remove_multi
end

上の2つのリソースのルーティングテーブルを見てみると、確かにidが含まれており、member扱いされていることがわかります。

nocollectmember

⚓ root

今更ですが、rootへのルーティングの書き方は以下のとおりです。これだけ他の書き方と比べて少し浮いている感じですね。

root to: 'page#top'

⚓ ルーティングのオプション

最後に、ルーティングでよく使われるオプションを紹介します。

⚓ only:except:

resourcesonly:またはexcept:オプションを使用することで、主要な7つのアクション(index, show, new, create, edit, update, destroy)を限定することができます。

# indexとshowアクションだけ使う場合
resources :prefectures, only: [:index, :show]
# destory アクション以外を使う場合
resources :prefectures, except: :destroy 

updatedestroyのような破壊的なアクションは事前にルーティングレベルで塞いでおきましょう。

⚓ リソース名の変更

asオプションを使用して、リソース名を変更することができます。

get 'home', controller: :users, as: 'user_root'

asoption

⚓ httpsの指定

以下のようにprotocol: httpsを指定することができます。

scope protocol: 'https://', constrains: {protocol: 'https'} do
  root to: 'page#top'
end

なお、アプリの一部だけをHTTPS化するのは手間がかかる上にセキュリティ上の懸念も残るので、アプリ全体をHTTPS化することをおすすめします。

⚓ idを拡張

たとえば、以下のようにidの制約を変更してアルファベットのidを使用することができます。

resources :prefectures, id: '/^[a-z]+$/'

⚓ 最後に

Railsには他にも強力なルーティングのオプションがたくさんありますが、アドホックなカスタムルーティングを避け、なるべくresoucesresourceonlyexceptで素直かつ統一のとれたルーティングを生成するようにします。

コントローラが数百にのぼる巨大なルーティングをすべてresourcesresourceで書いた例もあります。

ただし、そこでRESTfulにしようと頑張り過ぎないのも大事です。

⚓ 追伸

Rails 6.1では、ルーティングの記述を間違えたときにdid you mean?で推測する機能が加わります。

⚓ 参考

関連記事

Railsのルーティングを極める(前編)

Rails: ルーティングを動的にビジュアル表示する方法

$
0
0

以前Railsウォッチ20201012でご紹介した内容を自分用に別記事にしました。

RailsのルーティングをFSM(有限状態機械)の形式でHTMLファイルに出力し、動的にルーティングをシミュレートできる機能です。

参考: 有限オートマトン - Wikipedia

⚓ Railsのルーティングビジュアライザ

⚓ 必要なもの

  • Railsローカル実行環境
  • graphviz

なお、2012年の56fee39でJourneyがAction Dispatchに統合された時点で既にビジュアライザのコードが入っているので、もっと前からこの機能があったようです。少なくともRails 4以上で使えるはずです。

⚓ 利用法

  • 自分のローカル環境にgraphvizをインストールしておきます。MacでHomebrewを使っている場合はbrew install graphvizでインストールできます。
  • Railsプロジェクトのルートディレクトリで以下を実行し、出力されたout.htmlをブラウザで開きます。

$ bin/rails r 'File.binwrite "out.html", <アプリ名>::Application.routes.router.visualizer'

なお<アプリ名>はRailsのアプリ名に置き換えます(アプリ名はconfig/application.rbにある大文字で始まるモジュール名を使います)。

もちろんRailsコンソールでも実行できます。

File.binwrite "out.html", <アプリ名>::Application.routes.router.visualizer

出力されたHTML冒頭のボックスにルーティングを入力して[simulate]を押すと、該当するルーティングがFSM図上でハイライトされます。

⚓ 参考

この機能は、Kaigi on RailsのAaron Pattersonさんによるオープニングキーノートスピーチ『Viewがレンダリングされるまでの技術とその理解』でRailsの秘密機能として紹介されていました(動画は頭出し済み)。

関連記事

Railsのルーティングを極める(前編)

週刊Railsウォッチ(20201124)strict loading violationの振る舞いを変更可能に、Railsモデルのアンチパターン、quine-relayとさまざまなクワインほか

$
0
0

こんにちは、hachi8833です。今回は短縮版でお送りいたします。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

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

公式の更新情報から見繕いました。

参考: 6.1.0マイルストーン 6.1.0 Milestone(84%、残り20件)

⚓ strict loading violationの振る舞いを変更できるようになった

元々strict loading violationは、関連付けでオンになっている場合は常にraiseしていた。今回の変更では、railseの代わりにログ出力するオプションをアプリケーションで選べるようになる。なお、この振る舞いはstrong parametersと似ていて、デフォルトではすべての環境でraiseする。アプリケーションは、lazy loadされたすべての関連付けを探しつつ、production環境ではログ出力することもできるようになる。

raiseしないようにするには、config.active_record.action_on_strict_loading_violation:logに設定する。
同PRより大意

# activerecord/lib/active_record/core.rb#273
+     def self.strict_loading_violation!(owner:, association:) # :nodoc:
+       case action_on_strict_loading_violation
+       when :raise
+         message = "`#{association}` called on `#{owner}` is marked for strict_loading and cannot be lazily loaded."
+         raise ActiveRecord::StrictLoadingViolationError.new(message)
+       when :log
+         name = "strict_loading_violation.active_record"
+         ActiveSupport::Notifications.instrument(name, owner: owner, association: association)
+       end
+     end

つっつきボイス:「strict loading violation?」「これはたぶん割と最近入った、ビューの中でSQLを発行できないようにする機能のことでしょうね」「そんな機能が入ってたんですか」「たしかstrict loadingを有効にしておくと、コントローラからActive Recordオブジェクトをビューに渡した後に、たとえばビューの中でpreloadingされていないhas_many関連付け先のデータを取得しようとするとSQLを発行せずにraiseするというものだったと思います」「あ〜、そういうものなんですね」「lazy loadingをraiseして見つけやすくするときなどに使えますね」

後で調べると、以下のプルリクがそれのようです(ウォッチ20200302)。

「今までは有効にすると常にraiseされていたのが、今回のプルリクでは環境に応じてraiseせずにログ出力できるようになったんですね」「今まではできなかったのか」「従来は既存のコードで後からstrict loading violationを有効にすると常にraiseされるのが不便だったけど、これはなかなかいいですね👍

「調べるときはログでlazy loadedな関連付けをチェックして、修正したらraiseするように変更する感じで作業したりできるんですね」「ちょうどSELinuxでpermissive modeを使うのと似てるかも」「私もそれを思い出しました」「audit logは出すけど動作は継続するところなどが似てますね」「permissive modeを使うとちょっとドキドキする😆

参考: Security-Enhanced Linux - Wikipedia

「Railsでも最終的にはraiseするようにすべきですけど、これで検証中はraiseせずにログを出せるようになった」「当初はraiseだけで実装されてたのがちょっとアグレッシブな印象ですね」

⚓ 新機能: マルチプルデータベース向けconnected_to_many

#40370で高粒度コネクションスワップが実装されたので、マルチプルデータベースに接続できる新しいAPIが必要だ。その理由は、たとえば5つのデータベースのうち3つに読み取り用に接続して残りは書き込み用にしたい場合に、このAPIでネストが深くならないようにするためだ。

このAPIがあれば、以下のように書く代わりに

AnimalsRecord.connected_to(role: :reading) do
  MealsRecord.connected_to(role: :reading) do
    Dog.first    # read from animals replica
    Dinner.first # read from meals replica
    Person.first # read from primary writer
  end
end

以下のように書ける。

ActiveRecord::Base.connected_to_many([AnimalsRecord, MealsRecord], role: :reading) do
  Dog.first    # read from animals replica
  Dinner.first # read from meals replica
  Person.first # read from primary writer
end

これは過去の2つのデータベースのネストが深くなる場合に特に便利になるだろう。
同PRより大意

#40370は以前見たときはWIPでした(ウォッチ20201020)。


つっつきボイス:「connected_to_manyは上のサンプルコードがわかりやすい」「connected_to_manyという名前でいいんだろうかという気がしないでもないですけど」「今までは接続先が同じでもネストしないと書けなかったのが、ネストせずに書けるようになった」「このぐらいならネストしててもいいかなとも思いましたけど、機能が増えたのはいいですね👍

⚓ Host Authorizationでもリクエストを除外する機能を追加

リクエストを強制SSLから除外する必要が生じる可能性があるのと同様に、Host Authorizationチェックでもリクエストを除外する必要が生じる可能性がある。アプリケーションでこの柔軟性を増すことで、適合しない可能性のあるリクエストを除外しつつHost Authorizationを有効にできるようになる。たとえば、AWS Classic Load BalancerはHostヘッダーを提供しておらず、Hostヘッダーを送信するように設定することもできない。つまり、このLoad Balanacerのヘルスチェックを使うためにはHost Authoraizationを無効にしなければならなかった。今回の変更によって、アプリケーションはHost Authorizationで必要とされるヘルスチェックリクエストを除外できるようになる。

自分は(ActionDispatch::SSLと同様の方法で)ActionDispatch::HostAuthorizationミドルウェア引数を受け取れるように変更した。hosts設定はhosts_response_appと同様、引き続き別に存在しているが、自分はssl_optionsと同じような感じでHost Authorizationをグループ化してみた。グローバルなhosts_response_appは、Host Authorization失敗のレスポンスの一部でしか使われないのであれば非推奨化するのがよいかもしれない。既存のテストも更新してメソッドのシグネチャを変更し、この除外機能を検証するテストも追加した。
同PRより大意


つっつきボイス:「Rails 6に入ったHost Authorizationは、DNS Rebindingを悪用した攻撃への対策機能で、ドメインから来るリクエストのHostヘッダーもこの機能でチェックしてたと思います」

参考: Rails 6 adds guard against DNS rebinding attacks | Saeloun Blog
参考: DNS Rebinding ~今日の用語特別版~ | 徳丸浩の日記

「このプルリクでは、そのHost Authorizationで除外リストを指定できるようになった: サンプルコード↓にもあるように、healthcheckリクエストだけ通して欲しいことがよくあります」「Hostヘッダーがついていないリクエストでもこうやって除外リストを指定して通るようにしたいですよね↓」「これはやりたいです」

# actionpack/lib/action_dispatch/middleware/host_authorization.rb#12
 config.host_authorization = { exclude: ->(request) { request.path =~ /healthcheck/ } }

参考: Application Load Balancer とは - Elastic Load Balancing

「通常はX-Forwarded-ForヘッダーとX-Forwarded-Protoヘッダーがリクエストに追加されるので大丈夫なんですが、ALBのhealthcheckリクエストのようにVPCの中(つまりプライベートIP)から来るリクエストや、VPC Lambdaなどから呼び出すインターフェイスは、HTTPで直接アクセスします: このような設計は割とよくあるので、今回のように除外リストを指定することでそうしたリクエストもRailsで受信できるようになるのはいいですね👍」「ありがたい🙏」「これができないと不便なんですよ: 今まではhealthcheckを受信するためにALB healthcheck用のIPアドレス範囲設定が必要でした」

参考: X-Forwarded-For - HTTP | MDN
参考: X-Forwarded-Proto - HTTP | MDN

⚓ 新機能: RailtieのserverブロックでRailsサーバー起動後にコードを実行できるようになった

serverは、consoletaskといった他のRailtieブロックと似ている。目的は、サーバー起動後にアプリケーション(つまりrailtie)でコードを読み込めるようにすること。

ユースケースとしては、WebpackやReactサーバーをdevelopment環境で起動したり、SidekiqやResqueなどの一部のジョブワーカーを起動するなどが考えられる。

現時点ではこれらのタスクはすべて別シェルで実行される必要があるので、Railsサーバーの次に別のプログラムを起動する必要がある場合は、gemメンテナーがgemのライブラリの実行方法をドキュメントに追加する必要がある。
この機能はたとえば以下のように書ける。

  class SuperRailtie < Rails::Railtie
    server do
      WebpackServer.run
    end
  end

同commitより大意


つっつきボイス:「なるほど、サーバー起動後にRailtieでコードを実行できるようにするのか」「ジョブワーカーをここから起動したりすると書いてますね」

なおRailtieは、Railsのコアライブラリのひとつです↓(レールタイ: 線路の枕木(railroad tie)のもじり)。

参考: Rails::Railtie

「このように複数プロセスの起動や管理をつかさどるソフトウェアはいろいろあって、よく『プロセスマネージャー』と呼ばれたりします」「docker-composeとか」「Rubyのinvokerとか」「foremanもありますね」「powもあった」「自分は最近docker-composeでまとめて管理することが多いので、この手のプロセスマネージャーは使わなくなったな〜」「invokerはちょうど今使ってます、重いけど😆

docker/compose - GitHub
code-mancers/invoker - GitHub
ddollar/foreman - GitHub
postageapp/powr - GitHub

「今回追加されたserverは、そういうプロセスマネージャーっぽいこともRailtieでできるようになったということですね: ただこれを見る限りでは、サーバー起動後にフックをかけるだけのようで、これだけでは落ちたプロセスを再起動するなどのプロセス管理まではできなさそうですが」「起動はするけど管理まではしないのか…」「serverブロックにコードを書けるので、もっと複雑なこともやろうと思えば一応できますね」

「この機能は、サンプルコードのWebpackServer.runみたいに、サーバー起動後にこれとこれだけ実行しておきたいというようなシンプルな用途に便利そう: そのためにforemanなどのプロセスマネージャーをわざわざインストールしなくても済むので」「たしかに」

⚓Rails

⚓ Railsモデルのパターンとアンチパターン


  • (Fatと呼ばず)重量オーバーモデル
  • SQLパスタパルメザンチーズ風味
  • Repositoryパターン
  • マイグレーションの心得
    • downメソッドを必ず用意する
    • マイグレーションでActive Recordの呼び出しを避ける
    • データのマイグレーションとスキーマのマイグレーションは分ける

つっつきボイス:「AppSignalの記事です」「なぜかfat modelのfatに取り消し線を付けてoverweightとしてますね」「fatという言葉が使い古されてきたとかそういう意図なのかな?🤔

「SQL Pasta Parmesanも、もしかするとSQLがスパゲッティ状になることをわざとそう呼んでいるのかしら?」「パルメザンチーズ風味🧀

参考: スパゲティプログラム - Wikipedia

# 同記事より
class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = Song.where(status: :published)
                .where(artist_id: artist_id)
                .order(:title)

    ...
  end
end

class SongController < ApplicationController
  def index
    @songs = Song.where(status: :published)
                 .order(:release_date)

    ...
  end
end

class SongRefreshJob < ApplicationJob
  def perform
    songs = Song.where(status: :published)

    ...
  end
end

「↑上のようにwhere(status: :published)のような同じ条件をあちこちにバラまくよりは、下のようにスコープにまとめる方がいいですよね↓」「これはそうですね」

# 同記事より
class Song < ApplicationRecord
  ...

  scope :published, ->            { where(published: true) }
  scope :by_artist, ->(artist_id) { where(artist_id: artist_id) }
  scope :sorted_by_title,         { order(:title) }
  scope :sorted_by_release_date,  { order(:release_date) }

  ...
end

class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = Song.published.by_artist(artist_id).sorted_by_title

    ...
  end
end

class SongController < ApplicationController
  def index
    @songs = Song.published.sorted_by_release_date

    ...
  end
end

class SongRefreshJob < ApplicationJob
  def perform
    songs = Song.published

    ...
  end
end

「この記事では、どうしても必要な場合を除いてRepositoryパターンはおすすめしないと書かれてますね」「どうしても使いたいならたとえば以下のようにSongRepositoryにまとめる方がマシだけど、それもおすすめはできないとも書かれてる」

参考: ドメイン駆動設計 - Wikipedia

# 同記事より
class SongRepository
  class << self
    def find(id)
      Song.find(id)
    rescue ActiveRecord::RecordNotFound => e
      raise RecordNotFoundError, e
    end

    def destroy(id)
      find(id).destroy
    end

    def recently_published_by_artist(artist_id)
      Song.where(published: true)
          .where(artist_id: artist_id)
          .order(:release_date)
    end
  end
end

class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = SongRepository.recently_published_by_artist(artist_id)

    ...
  end
end

class SongController < ApplicationController
  def destroy
    ...

    SongRepository.destroy(params[:id])

    ...
  end
end

「言い換えると、Active Recordを普通に呼び出せば済むことを、わざわざRepositoryパターンでラップするなということなんでしょうね: DDD(ドメイン駆動開発)の文脈ではデータベースロジックをビジネスロジックから分離するためにRepositoryパターンのクラスを間にはさむことがありますけど、たしかに上でやっていることはActive Recordを呼び出せばできるので、ここではRepositoryパターンにしなくてもよさそう」

「なるほど、Active Recordと機能がかぶるSongRepositoryクラスをわざわざ作るのは、Active Recordをむしろ損なっているのではないかという考えかもしれませんね」「Active Recordでできない機能をRepositoryパターンで作る分には構わないと自分は思います」

なお、RepositoryパターンはHanamiで全面的に採用されています↓。

Hanamiフレームワークに寄せる私の想い(翻訳)

「ところでDDD本やデザパタ本を読むと、つい本のとおりにやってみたくなることって一度や二度はあるじゃないですか」「ああ、それ思い当たります😅」「Strategyが1個しかないのにStrategyパターンにしてしまったりとか」「Factoryが一種類しかないのにFactoryを作っちゃったりとか」「そういうノリでRepositoryを無意味に作らないようにしようということなのかもしれませんね」

「記事の後半はマイグレーションの心得についての話です」「マイグレーションにdownメソッドを書きましょうという話はそのとおりですね」

「その次は、マイグレーションの中で既存のActive Record継承モデルを直接参照するのは避けよう、という話」「ああ、これはやってはいけないとよく言われる」「Railsを長くやっている人ならやってはいけないことは知っていると思いますけど、Railsを最近始めた人だとやってしまうかもしれませんね」

「その次の、データのマイグレーションとスキーマのマイグレーションは分けようという話もそのとおり」「時間のかかるデータマイグレーションは切り離して、スキーママイグレーションでデータベースがロックされる時間が長くならないようにするべきですね」

「それほど目新しい内容ではないと思いますが、Railsで定番のアンチパターンや注意事項を押さえられるのがいいと思います👍」「Rails経験の浅い人によさそうな記事ですね」

⚓ listen gemが3.3.0でRuby 3やTruffleRubyに対応(Ruby Weeklyより)


つっつきボイス:「guard/listenはファイルの変更を検出するツール」「そういえば素のRailsにもlisten gemが入りますね↓」

# Rails 6で生成したGemfileより
group :development do
  gem 'web-console',           '4.0.1'
  gem 'listen',                '3.1.5'
  gem 'spring',                '2.1.0'
  gem 'spring-watcher-listen', '2.0.1'
end

「変更通知のインターフェイスはLinuxやWindowsやmacOSでそれぞれ違っているので↓、この種のファイル変更検出では複数のOSの抽象化をサポートすることが大事」「個別のOSのファイル変更通知を直接扱ったら面倒になりますよね」

⚓ Q:「WindowsでRailsを開発しますか?」

ここ3年ほど、Railsアプリ開発はmacOSでしかやっていないのですが、(リモートサービスの利用を考慮しなければ)あらゆる開発者はmacOSかLinuxのどちらかで開発しているように感じます。
そこで気になるのが、Windows上でのRailsアプリ開発がどれだけやりやすいかが気になっています。言語やシェル機能やデータベース管理などはWindows用ツールで十分提供されているのでしょうか?それともWSL 2やDockerでやる方がいいのでしょうか?
同discussionの質問より大意


つっつきボイス:「WSL 2を知らなかったとレスを付けていた開発者がいたのが気になって拾いました」「Sam SaffronさんはWSL 2はいいぞ、素のWindowsはつらいよとも書いてますね(レス)」「WSL 2はWindows上の選択肢としていいと思います、少なくともWindows版RubyバイナリでRailsを動かして開発するよりはずっといい」「同意です」

参考: WSL 2 と WSL 1 の比較 | Microsoft Docs

⚓ 個人のローカル環境構築は成果物にならない

「ちなみに自分がDockerをすすめる理由のひとつは、自分のローカル開発環境の構築ってどんなに一生懸命やっても成果物にならないからなんですよ」「そう、それですよね」「環境構築は楽しいんですけど趣味に近くなりがちなのと、環境構築に費やす時間はアウトプットが出ない時間でもあるので、DockerやVMなどを用いてより短い時間で環境構築を終える方が生産的だと思います」「そうですね」「もちろん、Windowsでの各種環境設定作業を自分でやることを厭わない人は、自分の好きな方法でやってもよいと思います」

⚓Ruby

⚓ スーパークラスのprivateメソッドをオーバーライドする


つっつきボイス:「親クラスにあるprivateなroleメソッドは、子クラスではprivateとして見えるけどpublicとしては見えない↓、これは普通」

# 同記事より
class Parent
  private

  def role
    'parent'
  end
end

class Child < Parent
  def get_role
    role
  end
end

Child.new.get_role # => "parent"

Child.new.private_methods.include?(:role) # => true
Child.new.public_methods.include?(:role)  # => false

「でも子クラスに同名のメソッドがpublicで存在していると、元クラスのprivateなroleメソッドは見えなくなって、子クラスのpublicなroleメソッドが見えるようになる」「へぇ〜」「結果として、親クラスのprivateなroleメソッドが子クラスのpublicなroleメソッドで上書きされて、get_roleの動作が変わるのか」

class Child < Parent
  def role
    'child'
  end
end

Child.new.get_role # => "child"

Child.new.private_methods.include?(:role) # => false
Child.new.public_methods.include?(:role)  # => true

「こういう書き方を実際に使うことってあるだろうか?」「親クラスのprivateメソッドを子クラスのpublicメソッドで上書きするのってちょっとトリッキーですよね😅」「こういうことができるとは…」

⚓ オブジェクト指向をどの言語で学ぶか

「ところで、Rubyのprivateメソッドは、JavaやC++などの他の言語のprivateメソッドと考え方が違っているところがありますよね」「自分はJavaから始めたのでJavaのprivateメソッドの印象が強いかも」

参考: JavaやC#の常識が通用しないRubyのprivateメソッド - give IT a try

「最近だと、オブジェクト指向言語(特にクラスを継承するタイプの言語)の仕様をどんな言語で学んでいるんでしょうね?」「あ〜、どうだろう…?」「この辺の概念って、最初にどの言語でオブジェクト指向を学ぶかで納得の仕方が違ってきそうな気がするんですよ」

「たとえばC++だと明示的にオーバーライドしないと別々の関数になるけど、Javaだと同じ名前で書けばデフォルトでオーバーライドされるといった違いがありますね」「C++はやったことなかったけどそういう仕様なんですか」「そういった書き味の部分は、外見上は似ていても内部実装が違っていたりします」

「あとRubyのprotectedがJavaやC++と考え方が違っているのも有名ですね」「protectedってどこで使ったらいいのかよくわからないかも」「使ったことありませんでした」「protectedを理解して使いこなすのは難しい…」「Gemやライブラリ開発をしないのであれば、privateとpublicで十分かもしれないという気がしています」

参考: Rubyのクラスメソッドは同じクラスのprotectedメソッドやprivateメソッドにアクセスできない - give IT a try

⚓ quine-relay: 128言語の生成チェインでウロボロスの蛇を形成(Ruby Weeklyより)

mame/quine-relay - GitHub


つっつきボイス:「@mameさん作の何だか壮大なクワインプログラムです」「クワインというと真っ先に@mameさんを連想するぐらい、@mameさんが強い分野ですね」


同リポジトリより

「こ、これは何ですか?😆」「Rubyプログラムを生成するRustプログラムを生成するScalaプログラムを生成する…を128言語繰り返して、最終的にRubyに戻るそうです」「トランスパイルに次ぐトランスパイルで元に戻る、まさにウロボロスの蛇🐍」「自分の尻尾は美味い😋

参考: ウロボロス - Wikipedia

「OCamlにBASIC、いろいろある」「zshやtcshまで!」「FORTRANやFORTHもある!」「知らない言語がゴロゴロ…」「言語が多すぎてulimit -s unlimitedしないと動かないっぽい」「最終的に生成されたRubyと元のRubyのdiffを取ると完全一致するんですって」「そこまでやりますか😆」「この完全に元に戻るという動作がクワインの一種ということなんでしょうね」

参考: 【 ulimit 】コマンド――ユーザーが使用できるリソースを制限する:Linux基本コマンドTips(326) - @IT

「この分だとquine-relayのソースコード自体もクワインだったりするかも?」「ソース見てみたら、ウロボロスの蛇があしらわれている…↓」「何だかクラクラしてきた😅

「クワインというものがあるって初めて知りました」「自分もRubyKaigiのアトラクション的なセッションでしか見たことがありませんでした」

参考: クワイン (プログラミング) - Wikipedia

「クワインって随分昔からあるみたいですね」「しかも聞くところによると、Webサーバーか何かで実用的なクワインの例というものがあるらしいですよ」「マジで?!」「あったあった↓、この記事ではクワインを遊びではなく機能として使っているそうです」「へぇ〜!」「これ以外で実用的なクワインの例は見たことがないですね」

参考: 自作Cコンパイラで Ken Thompson のログインハックを再現してみた - 0x19f (Shinya Kato) の日報

Thompson hack ではコンパイラ自身のプログラムが現れたら自分自身と全く同じコードを埋め込むという条件付きのクワインを使うことでソースコードから痕跡を消しつつ、セルフホストしても login を書き換える性質を引き継がせる仕組みになっています。
0x19f.hatenablog.comより

「ググって見つけた『あなたの知らない超絶技巧プログラミングの世界』という本がクワインだらけでした↓」「あ、これも@mameさんの本だ!」「クワインのスライド↓も@mameさんだ…」「なるほど、そういう方なんですね」

「なお@mameさんはIOCCC(国際難読化Cコードコンテスト)で何度も勝っています↓」「おぉ〜!」「IOCCCって今も開催されてるのか」

参考: The International Obfuscated C Code Contest
参考: IOCCC - Wikipedia

「@mameさんのアイデアの豊富さがスゴい」「以前RubyKaigiのコーナーで、どうやってこういうのを作ろうと思いつくんですかと質問されたときに、@mameさんがテクニックよりも面白いアイデアを思いつくかどうかが大事というような回答をされてた覚えがあります」「やはりネタが重要なんですね」

「このissueでのやりとり↓も好きです❤」「質問がWhy? Why?」「そして返信がWhy not? Why not?😆


つっつきの後で、@mameさんがRubyConf 2020のランチで書いたクワインが流れてきました↓。

quine-relayのビルドチェーンを見てて、何となく「これはジャックの建てた家」を思い出しました↓。こちらは循環していませんが。

参考: ジャックのたてた家 The house that Jack built :マザーグースの歌

⚓ その他Ruby

つっつきボイス:「RubyConf 2020、今日(=つっつきの日)が最終日なのか」「タイムゾーンが日本と真逆だから平日の日本だと見るの大変かも」

⚓CSS/HTML/フロントエンド/テスト/デザイン

⚓ POSTを冪等にするIdempotency-Keyヘッダの提案


つっつきボイス:「社内Slackに貼っていただいた記事です」「Idempotency-Keyは、はてブのコメントで『条件付きPUTを使えばできる』とありましたね↓」

POSTリクエストを冪等処理可能にするIdempotency-Keyヘッダの提案仕様 – ASnoKaze blog

冪等が目的なら現状でも条件付きPUTで実装できることが多いので、まずそれを検討したほうが良い。リクエストIDなら標準化しても良いと思う

2020/11/19 09:10

「元記事にもあるように、StripeやPayPalやGoogle Standard PaymentsなどでもIdempotency-Keyと似たようなことは既に行われているんですけど、Idempotency-Keyが標準として定まればそれを使う方がいいでしょうね(もちろんすべてのアプリケーションで使う必要はありませんが)」「今はまだ標準では存在しないんですか?」「request-id的なものを慣例的に冪等性の保証に使うことはありますが、処理の冪等性を保証することを目的とする専用のヘッダーはまだ存在しませんね」「なるほど」

「ドラフトRFCを見ると、Idempotency-KeyにはUUIDを使うことが推奨されている↓」「ただこのキーはクライアント(ブラウザ)側で生成するものなので、そこに関するセキュリティがどうなのか知りたい」「他にも、Idempotency Fingerprintが付けられることでデータだけを変更したreplay attackなどを防ぐようですね」

参考: draft-idempotency-header-00 - The Idempotency HTTP Header Field
参考: UUID - Wikipedia

「リクエストを再生するときに元のリクエストがまだ処理中の場合はHTTP 409 Conflict↓を返すとある: こういう振る舞いが標準で定められていると、APIドキュメントを読んだりAPI仕様を設計するときの手間が省けて助かりますね」「それわかります!」「409 Conflictは前からPUT用にあったけど、提案ではこれをPOSTでも使えるようにするようですね」

参考: 409 Conflict - HTTP | MDN

「このドラフトRFCは比較的短いので実装もそんなに大変ではなさそう: それも含めて自分はこのIdempotency-Keyの提案はなかなかいいと思っています👍」「Idempotency-Keyは必要がなければ使わなくてもよいのもいいですね」

⚓その他

⚓ Steamのインディーズゲーム

つっつきボイス:「pastalogicはプログラミングをお題にしたゲームだそうです」「これはカードを使う対戦型のボードゲームなんですね」「あ、カードに気が付かなかった😅」「そういえばSteam↓でときどきこんな感じのインディーズゲームを見かけます」「プログラミングをお題にしたゲームというものは見たことなかったかも」「Steamだとそれ系のゲームもちょくちょくありますよ」

参考: Steam - Wikipedia
参考: おすすめインディーゲーム27選。担当ライターが推す2019-2020冬の良作【後編】 - ファミ通.com

「インディーズゲームはアイディア勝負な分、ときどき驚くほどいいゲームが登場しますね」「そういえばFactorio↓をやっているとプログラミングしているような気持ちになって、あっという間に15時間ぐらい溶かしちゃいます」「Factorioはマイクラ(Minecraft)でレッドストーン回路を組んでいるようなものですよね」「はい、Factorioはそれに特化したようなゲームだと思います」(以下『天穂のサクナヒメ』の話など延々)

参考: Factorio - Wikipedia
参考: Minecraft - Wikipedia
参考: テクニック/レッドストーン回路 - Minecraft Japan Wiki - アットウィキ

⚓ Baba Is You

「ちなみにSteamのインディーズゲームの中でも、このBaba Is Youというパズルゲームは超名作です↓👍」「不思議な名前!」「何だか倉庫番がパワーアップしたみたいな画面ですね」「このゲームはたしか賞も取っていたと思います」

参考: Steam:Baba Is You
参考: 倉庫番 - Wikipedia

なおBaba Is Youはチューリング完全で、ライフゲームも実装できるそうです↓。


store.steampowered.comより

「基本はBabaというキャラクターを動かして、『Baba』『is』『you』や『Baba』『is』『win』などと並べる↑とクリアになるんですけど、たとえば『Rock』『is』『you』と並べると自分がBabaから岩に変わって岩を動かせるとか、『Lava』『is』『melt』にすると溶岩が溶け流れて通れるようになるなど、面ごとに意表を突くようなクリア条件が設定されているのがめちゃくちゃ面白い」「あ〜、勝利条件を書き換えることもゲームに含まれる感じなんですね」「はい、このゲームを最初にやるときは一切ヒントを見ないのがおすすめです」

「Baba Is You、任天堂SWITCHでも買えるのか、買っちゃおうかな(後で買いました)」「このゲームの面白さはぜひ体験してみることをおすすめします😋」(以下、ゲーム自作話など延々)


今回は以上です。

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

週刊Railsウォッチ(20201117後編)Rubyのパターンマッチングが3.0で本採用に、AWS Lambdaサイズを縮小する、AppleのM1チップほか

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

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

Rails公式ニュース

Ruby Weekly

Rails 6.1: 属性にデフォルト値を設定しても型が失われなくなった(翻訳)

$
0
0

概要

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

Rails 6.1: 属性にデフォルト値を設定しても型が失われなくなった(翻訳)

Railsでは、属性の型を変更することも、デフォルト値を設定することもできます。

以下のように、Messageクラスにdatetime型のsent_at属性があるとしましょう。

class Message
end

> Message.type_for_attribute(:sent_at)
=> <ActiveRecord::Type::DateTime:0x00007fa01c1fd650 @precision=6, @scale=nil, @limit=nil>
> Message.new.sent_at
=> nil

このsent_at属性の型を以下の方法でintegerに変更できます。

class Message
  attribute :sent_at, :integer
end

> Message.type_for_attribute(:sent_at)
=> <ActiveModel::Type::Integer:0x00007fa01bd86ba8 @precision=nil, @scale=nil, @limit=nil, @range=-2147483648...2147483648>

default:オプションを指定すればデフォルト値も設定できます。

class Message
  attribute :sent_at, default: -> { Time.now.utc }
end

> Time.now.utc
=> 2020-10-15 12:10:16 UTC
> Message.new.sent_at
=> 2020-10-15 12:10:16 UTC

しかしここで問題なのは、Railsは属性にデフォルト値を設定するときに属性の型も変更しますが、そのときに正しい型が失われてしまうことです(訳注: 型がType::Valueになってしまいます)。

> Message.type_for_attribute(:sent_at)
=> <ActiveModel::Type::Value:0x00007fd16d255418 @precision=nil, @scale=nil, @limit=nil>

⚓ Rails 6.1での変更

Rails 6.1でこの問題が修正されました(#39830)。

class Message
  attribute :sent_at, default: -> { Time.now.utc }
end

> Time.now.utc
=> 2020-10-15 12:10:16 UTC
> Message.new.sent_at
=> 2020-10-15 12:10:16 UTC
> Message.type_for_attribute(:sent_at)
=> <ActiveRecord::Type::DateTime:0x00007fa01c1fd650 @precision=6, @scale=nil, @limit=nil>

上のように、デフォルト値に基づいてActiveRecord::Type::DateTimeが設定され、型が失われなくなります。

関連記事

Rails: ActiveSupport::Concernをextendしたモジュールをprependする機能(翻訳)


Rails: db:structure:loadとdb:structure:dumpタスクが非推奨化(翻訳)

$
0
0

概要

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

Rails: db:structure:loaddb:structure:dumpタスクが非推奨化(翻訳)

Rails 6.1でdb:structure:loadタスクとdb:structure:dumpタスクが非推奨化されました(#39470)。

⚓ 変更前

config.active_record.schema_formatは、データベーススキーマをファイルにダンプするときのフォーマットを制御します。この設定で有効なオプションは:ruby(デフォルト)と:sqlです。

rails db:schema:{dump,load}を実行するとdb/schema.rbファイルをダンプdb/schema.rbファイルをダンプまたは読み込みます。rails db:structure:{dump.load}を実行すると、config.active_record.schema_formatで指定した値にかかわらずdb/structure.sqlファイルをダンプまたは読み込みます。

⚓ 変更後

rails db:schema:{dump,load}コマンドとrails db:structure:{dump,load}コマンドのどちらを実行した場合にも、config.active_record.schema_formatで指定した値に応じてrails db:schema:{dump,load}が実行されるようになります。

つまり、rails db:schema:dumpまたはrails db:structure:dumpを実行すると、db/schema.rbファイルをダンプします。

config.active_record.schema_format:sqlという値を指定してrails db:schema:dumpまたはrails db:structure:dumpを実行すると、db/stucture.sqlファイルをダンプします。

rails db:structure:{dump,load}を実行すると非推奨の警告が表示されます(これらのコマンドはRails 6.2で削除される予定です)。

DEPRECATION WARNING: Using `bin/rails db:structure:load` is deprecated and will be removed in Rails 6.2.
Configure the format using `config.active_record.schema_format = :sql` to use `structure.sql`
and
run `bin/rails db:schema:load` instead

注意: 上の非推奨メッセージを表示するには、config.active_support.deprecation = :stderrを設定しておく必要があります。

関連記事

Rails: db:migrate:nameコマンドの振る舞いの変更(翻訳)

Rails APIドキュメント: Active Recordのトランザクション(翻訳)

$
0
0

概要

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

Rails APIドキュメント: Active Recordのトランザクション(翻訳)

トランザクションとは、それが1件のアトミックな操作としてすべて成功した場合に限りSQLステートメントが永続化する、保護的なブロックです。古典的な例としては「出金が成功した場合にのみ入金ができる(またはその逆の)2つの口座間での振替」があります。トランザクションはデータベースの一貫性を強制し、プログラムのエラーやデータベースの破損からデータを保護します。つまり、「すべて一括実行される」か「一切実行されない」かのどちらかでなければならないステートメントが複数ある場合は、基本的にトランザクションブロックを使うべきです。

以下の例をご覧ください。

ActiveRecord::Base.transaction do
  david.withdrawal(100)
  mary.deposit(100)
end

このコード例では、withdrawaldepositのどちらも例外をraiseしない場合に、Davidからお金を取り出してMaryに渡します。例外が発生するとROLLBACKを強制的に実行して、データベースをトランザクション開始前の状態に戻します。ただし、このオブジェクトは、トランザクション開始前のステートに戻されたインスタンスデータを「持たない」ことにご注意ください。

⚓ 1つのトランザクション内に異なるActive Recordクラスがある場合

transactionクラスのメソッドは、何らかのActive Recordクラス上で呼び出されますが、そのトランザクションブロック内部にあるこのオブジェクトは、必ずしもそのクラスのインスタンスである必要はありません。その理由は、トランザクションはモデル単位ではなく「データベースコネクション単位」だからです。

以下の例で言うと、balanceレコードは、transactionAccountクラスで呼び出された場合であってもトランザクショナルにsaveされます。

Account.transaction do
  balance.save!
  account.save!
end

transactionメソッドは、モデルのインスタンスメソッドとしても利用できます。たとえば以下のようにも書けます。

balance.transaction do
  balance.save!
  account.save!
end

⚓ Transactionsは複数のデータベースコネクションに分散されない

ひとつのトランザクションの操作は、ひとつのデータベースコネクション上で行われます。クラス固有のデータベースが複数ある場合、トランザクションはそれらのデータベース間でのやりとりを保護しません。これを回避する方法のひとつは、改変するモデルのクラスごとにトランザクションを開始することです。

Student.transaction do
  Course.transaction do
    course.enroll(student)
    student.units += course.units
  end
end

これは解決方法としては今ひとつですが、完全に分散した(複数の)トランザクションがActive Recordのスコープを越えるようになります。

⚓ savedestroyは自動的にトランザクションでラップされる

#save#destroyは、どちらもひとつのトランザクション内にラップされ、バリデーションやコールバックで行うあらゆる操作はこのトランザクションの保護下に置かれます。これによって、トランザクションが依存する値をチェックするためのバリデーションを使うことも、after_*コールバックで例外をraiseしてロールバックすることもできます。

それにより、データベースの変更は「操作が完了するまで」そのデータベースコネクションの外部からは見えなくなります。たとえば、ある検索エンジンのインデックスをafter_saveコールバック内で更新しようとする場合、このインデクサは更新済みレコードを参照しません。唯一の例外はafter_commitコールバックで、更新がひとたびコミットされればトリガーされます。詳しくは以下をご覧ください。

⚓ Exceptionハンドリングとロールバック

もうひとつ忘れてはならないのが、あるトランザクションブロック内で発生した例外は(ROLLBACKがトリガーされた後で)伝搬することです。すなわち、これらの例外はアプリケーションコード内でキャッチできるようにしておくべきです。

ひとつの例外はActiveRecord::Rollbackです。これはraiseの時点でROLLBACKをトリガーしますが、そのトランザクションブロックによって再度raiseされることはありません。

警告: ActiveRecord::StatementInvalid例外をトランザクションブロック内部でキャッチしてはいけません。ActiveRecord::StatementInvalid例外は、エラーがデータベースレベルで発生したことを表します(一意性制約に違反した場合など)。データベースによっては、ひとつのトランザクション内部でのデータベースエラーによってそのトランザクション全体が利用不能になり、最初からやり直すまで利用できなくなるものがあります(PostgreSQLなど)。この問題を説明するためのコード例を以下に示します。

# Numberモデルに`i`というuniqueカラムがあるとする
Number.transaction do
  Number.create(i: 0)
  begin
    # unique制約エラーをraiseする...
    Number.create(i: 0)
  rescue ActiveRecord::StatementInvalid
    # (ここは無視する)
  end

  # PostgreSQLではここでトランザクションが利用不能になる。
  # 以下のステートメントはunique制約に違反しなくなったとしても
  # PostgreSQLエラーになる。
  Number.create(i: 1)
  # => "PG::Error: ERROR:  current transaction is aborted, commands
  #     ignored until end of transaction block"
end

ActiveRecord::StatementInvalidが発生したら、トランザクション全体をやり直すべきです。

⚓ ネステッドトランザクション

transactionの呼び出しはネストできます。デフォルトでは、ネステッドトランザクション(nested transaction)のブロック内にあるデータベースステートメントはすべて「親トランザクションの一部」になります。たとえば、以下の振る舞いに驚くかもしれません。

User.transaction do
  User.create(username: 'Kotori')
  User.transaction do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

上のコードは”Kotori”と”Nemu”を両方とも作成します。その理由は、ネストしたブロック内では ActiveRecord::Rollback例外がROLLBACKを発行しないからです。これらの例外はトランザクションブロック内でキャプチャされるので、親ブロックからは例外が見えず、実際のトランザクションがコミットされます。

ネステッドトランザクションでROLLBACKされるようにするために、実際のサブトランザクションにrequires_new: trueを渡す方法が考えられます。そして何かが失敗すると、データベースはサブトランザクションの冒頭までロールバックし、親トランザクションはロールバックしません。これを上述のコード例に追加すると以下のようになります。

User.transaction do
  User.create(username: 'Kotori')
  User.transaction(requires_new: true) do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback
  end
end

今度は”Kotori”だけが作成されます。この方法はMySQLとPostgreSQLで動作し、SQLite3 3.6.8以上でもサポートされています。

多くのデータベースは、「真の」ネステッドトランザクションをサポートしていません。本ドキュメント執筆時点では、真のネステッドトランザクションをサポートしていることが私たちに認識されているのはMicrosoft SQLだけです。このため、Active RecordではMySQLやPostgreSQLのsavepointを用いてネステッドトランザクションをエミュレートしています。savepointについて詳しくは以下をご覧ください。

参考: MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.3.4 SAVEPOINT、ROLLBACK TO SAVEPOINT、および RELEASE SAVEPOINT 構文

⚓ コールバック

トランザクションのコミットやロールバックに関連するコールバックは、after_commitafter_rollbackの2種類です。

after_commitコールバックは、あるトランザクション内でレコードがsaveまたはdestroyされると、そのトランザクションがコミットされた直後に呼び出されます。after_rollbackコールバックは、あるトランザクション内でレコードがsaveまたはdestroyされると、そのトランザクションまたはsavepointがロールバックされた直後に呼び出されます

これらのコールバックは、データベースが永続的なステートにある場合にのみ実行されることが保証されるので、他のシステムとやりとりするうえで有用です。たとえばafter_commitは、キャッシュをクリアするフックをかけるのに適しています(トランザクション内部でキャッシュをクリアすれば、データベースが更新される前にキャッシュの再生成をトリガーできるようになる)。

⚓ 注意事項

MySQLでは、savepointを用いてエミュレートされるDDL(Data Definition Language: データ定義言語)をネステッドトランザクションブロック内で使いません。したがって、こうしたブロック内部で’CREATE TABLE’のようなステートメントを実行してはいけません。その理由は、MySQLがDDL操作の実行時にすべてのsavepointを自動的に解放してしまうためです。transactionが完了したときに以前作成したsavepointを解放しようとすると、savepointが自動的に解放済みになっているためデータベースエラーが発生します。以下はこの問題を説明するためのコード例です。

Model.connection.transaction do                           # BEGIN
  Model.connection.transaction(requires_new: true) do     # CREATE SAVEPOINT active_record_1
    Model.connection.create_table(...)                    # active_record_1 now automatically released
  end                                                     # RELEASE SAVEPOINT active_record_1
                                                          # ブブー!データベースエラーです!
end

TRUNCATEもMySQLのDDLステートメントのひとつである点にご注意ください。

関連記事

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

アプリケーションコンフィグの設計パターン(銀座Rails#27)

$
0
0

morimorihogeです。いつの間にか12月。師走だ。
先日の銀座Rails#27で話した内容のまとめ直し版記事です。

Webシステムを作っているとあちこちにいろいろな設定が増えていきますが、そういった設定(コンフィグファイルや各種設定値)をどうやって保持する方法があり、それぞれのメリット・デメリットなどをまとめてみた内容になります。
特定のコンフィグ実装について書かれた記事は多いのですが、コンフィグ設計全般に関してまとめられた記事はそこまで見かけることがなかったので、自分なりにまとめてみました。色々と抜け漏れなどあるかと思いますが、その辺りはTwitter、はてブ等でご指摘頂ければブラッシュアップしていきたい所存です。

※本記事はTechRachoアドベントカレンダー2020 1日目の記事になります。

⚓ 本記事で扱う「アプリケーションコンフィグ」の範囲について

単にConfigurationというと人によって想定範囲が大幅にブレてしまうので、まずはその辺りのスコープから整理していきます。

⚓ 12-Factor Appにおける「設定」の定義

DevOpsという言葉が流行った時期を知っている人なら恐らく一度は目にしたことがあると思われる12-Factor Appというものがあります。

12-Factor AppはWebアプリケーションやSaaSといったどこかのインフラにdeployされるソフトウェアのインフラ設計やデプロイ方式、管理方法などについてモダンな設計方針をまとめたもので、Dockerをはじめとするコンテナ型ソリューションとの相性も良く、インフラエンジニアやDevOpsエンジニアといった運用寄りのエンジニアの中では良く参照される資料となっています。

一方で、昨今のRailsアプリケーション開発ではサーバー環境のコンテナ化やフロントエンド周りのフロントエンドエンジニアへの役割分担化に伴い、Railsエンジニアがこれまでのインフラエンジニアの領分にも踏み込んで行かざるを得ないケースが増えているのではないでしょうか。

例えば、ECSやFargate他のDockerバックエンドのアプリケーションサーバーを利用している場合、アプリケーション用のDockerコンテナ設定(Dockerfileそのものや注入する環境変数、起動パラメータなど)はインフラエンジニア任せとはいかないため、アプリケーションサーバー側のエンジニア(=Railsエンジニア)がコンテナ設定を担うことが多いでしょう。

というわけで、12-Factor Appはもし読んだことがなければRailsエンジニアにもぜひ一読しておくことをお勧めします。

そんな12-Factor Appの中で「設定」は1章分を使って取り上げられています。12-Factor Appの中では「設定」の定義は

アプリケーションの 設定 は、デプロイ(ステージング、本番、開発環境など)の間で異なり得る唯一のものである。
(略)
アプリケーションは時に設定を定数としてコード内に格納する。これはTwelve-Factorに違反している。Twelve-Factorは 設定をコードから厳密に分離すること を要求する。設定はデプロイごとに大きく異なるが、コードはそうではない。
The Twelve-Factor App III.設定より

とされています。deploy間で異なるものを対象とするのが特徴ですね。
12-Factor Appでは環境変数が設定の格納方法として推奨されていますが、これは12-Factor Appではアプリケーションはプロセスとして実行することが想定されていることから相性が良いという点もあると思います。

⚓ 本記事で扱う「設定」の範囲

本記事では取り扱う「設定」の範囲を12-Factor Appの範囲よりやや広げてアプリケーション設定にまで拡張します。
Railsアプリケーションでは12-Factor Appの扱うインフラレベルでの設定(接続先外部システムのエンドポイントURLやAPI KEY、動作スレッド数など)以外にも、様々な「アプリケーションの設定」を行うことがあります。
言語別の言語ファイル、休祝日定義設定、デバッグモードのON/OFF、その他ビジネスロジックで利用するパラメータなど、様々な設定があります。

具体的には、本記事では「外部から調整可能な何らかのデータを渡すことにより、アプリケーションの動作を変更し得るもの」として以下のようなものを広義の「アプリケーション設定」として扱って行きます。

  • 環境変数や引数など、プロセス実行時に引き渡されるもの
  • プログラムの外からファイルやURLとして渡されて読み込むもの
  • DBの特定テーブルなどに入っていて都度読み出す設定情報など

⚓ 様々な設定注入方法

Railsアプリケーションを前提として考えた場合に、どのような設定の実装方法があるのか、ざっと一覧してみます。

⚓ ソースコード内定数

いわゆるmagic numberと呼ばれる奴です。これは設定なのかといわれると怪しいところではありますが、ソースコードを書き換えてdeployすることで変更可能な設定である、という見方も出来なくはないため、方式として挙げておきます。
典型的なアンチパターンというかバッドプラクティスなため深く解説することはしませんが、作り捨てのアプリケーション開発だったり、定義した値が変わらないことが保証されている時以外では選択し辛い方式です。

⚓ コマンドライン実行時引数

RakeタスクやRails runner経由でRailsプロセスを実行する場合、実行時に引数として設定を渡すことができます。
CI経由で実行したり、不定期に実行する可能性のあるメンテナンス用タスクなんかは引数で振る舞いを変更できるようにしておくと汎用性が高まって便利になることがあります。

  • RakeやRailsコマンドが実行できるシェルが取れる場合(CI runner経由を含む)に柔軟に利用できる
  • Rails runner経由の場合はRubyコードを記述することもできるので、Time.nowなどの定数値以外のものも引き渡し可能

といった利点があります。

⚓ 環境変数

12-Factor Appでオススメされている設定方法です。Rails及びRubyではENVから取得することができます。
Dockerなどでも多く用いられている設定方法で、汎用的なソフトウェアでは最もメジャーな設定方法の一つです。

  • Rails向けに限らずPaaSやCIツールが設定サポートしているケースが多く、サービス間のつなぎ込みの点では最も柔軟かつ汎用性が高い
  • Rails以外のソフトウェアと設定値を共有したい場合にも、同じ方法で渡すことができる
  • 文字列しか渡せないため、複雑なデータ構造を渡すには不向き

⚓ 定義型設定ファイル(YAML、JSONなど)

rubyconfig/config Gem(旧rails_config)などを代表とする、YAMLファイルやJSON形式の設定ファイルに記述し、アプリケーション側から読み込む方式です。
Railsの設定方式としてはメジャーどころで、database.ymlsecrets.yml、暗号化機能が入りますがcredentials.yml.encなどもこの方式の一つとして考えることができると思います。

  • ArrayやHashなどの文字列以外の構造データを格納することができる
  • 階層構造に対応するので、パラメータ数が増えてもnamespace的な整理が可能
  • リポジトリにコミットせずに、deploy先別にファイルを用意してまるっと差し替えるなどの運用が可能

柔軟性と記述容易性のバランスが良い印象ですね。

⚓ ソースコード形式設定ファイル

config/initializers/*に置かれるような、RubyコードとしてRailsアプリケーションの実行時に初期化・設定のために読み込まれるファイルになります。
初期化用といった用途にも使われるため、厳密に設定のためだけに使われる訳ではありませんが、実質アプリケーション設定として利用されるケースも多いためここで取り上げています。

  • pureなRubyコードが書けるため、値の設定だけでなく読み込みに時間のかかる初期化の重いオブジェクト生成なども可能
  • lambdaやProcなども設定することができるため、定数による設定ファイルでは書けないロジックも記述できる(例:Time.now.yesterdayなど)

定義型設定ファイルの例ではdeploy先別に用意することがありますが、こちらの例ではそういったことはあまりしないように思います。
※どうしても設定を分けたい場合、initializers配下のソースは変更せずに、環境変数を使って設定を差し替え可能にするなどを良く見る印象です

⚓ DBやKVSの設定情報用テーブル

アプリケーション設定情報用のkey-value(設定名:設定値)の組になったデータを格納するテーブルを用意し、必要に応じて参照・更新する方法です。

  • 古くからある方式で、feature toggleなどによく利用される
  • プロセスの再起動なしに値の更新が可能
  • DBアクセスが都度発生するため、DBサーバーが遠かったり設定値が多すぎると速度面が問題になる可能性がある

deploy(プロセスの再起動)なしにオンデマンドで設定値を変更したい場合には良く使われる手法です。
この方法だと運用サイドのチームでも機能や設定の切り替えができるため、うまく使うことで開発チームの手を借りることなく施策を打てるという利点があります。
一方で、あまりにも柔軟にしすぎるとお互いに依存する設定値の組み合わせ数が爆発してしまい、開発時には想定していないバグを引き起こすこともあるため、何を動的設定可能にするかは慎重に決める必要があります。

⚓ その他

フロントエンド系の開発ではサーバーサイドを介さずにGoogle Tag Manager(GTM)などの外部ツールを使って設定を行うこともあると思います。例えばGTMで条件に合わせて表示する内容を切り替えるA-Bテストなどが出来ますが、こういったものは今回非サーバーサイドのものということで、考慮対象外とします。

また、AWSなどのクラウドインフラの機能を使うことで、アプリケーションサーバーには一切手を付けずにcanary deployなどを行うことも昨今ではできそうですが、こうしたインフラサイドだけで可能な設定についても考慮対象外としておきます。
新機能 – 加重ターゲットグループの使用によって Application Load Balancer がデプロイメントをシンプルに

⚓ 各設定実装方法の特徴と使いどころ

ここまでに挙げてきたそれぞれの手法について、ざっくり整理してみたのが下図です。

機微情報を格納すると想定した場合のセキュリティ視点では以下の図の通りになります。

これらを見つつ、各設定方式について比較していきます。

⚓ 設定値の読み込みタイミングによる違い

今回挙げた方式の中ではDB/KVSテーブル方式以外、すべて設定値の読み込み・再読み込みはプロセスのライフサイクルに一致します。すなわち、設定値の変更にdeployが必要になるということです。

昨今ではCI、CD環境の整備により10年以上前に比べれば格段に新コードがdeployしやすくなりましたが、それでもdeployには少なからず時間がかかります。
ダウンタイム許容の小規模なシステムであればそれほど問題にはなりませんが、ゼロダウンタイムを要求される環境の場合、冗長化されたサーバー群に順にdeployしていくなどの手順が必要となるため、deploy実行から完全に完了するまでの間には数分~数十分程度かかるのが一般的でしょう。

裏を返せば「今すぐ(秒~1分以内に)設定変更したい」というニーズにこれらの方式は対応できません。
大規模なリリースの場合にはBlue-Green deploymentのように複数の正環境を用意して切り替え可能にするということも考えられますが、普段の一般的な運用で回すには大げさすぎるのではないでしょうか。

そんなわけで、「deployせずに待ち時間なしに設定変更したい」というニーズがある場合にはDB/KVSに設定値を格納する必要がありそうです。

⚓ 設定変更方法による違い

設定値の変更にリポジトリにコミットされているファイル修正が必要な場合、運用上のニーズによる設定変更が機能開発のソース変更と混じるという問題が発生します。
これはbranchのmerge運用がカオスになりやすい要因の一つで、設定値変更のためのhotfixがちょくちょく発行されるのを許容するのか、という問題になってきます。
※ここはどちらが正解、というよりは開発チームで決めて運用すべきところだと思います。

もし運用で変更する可能性のある設定を導入する必要がある場合、設定値を変更する場合のフローを意識しておくとbranch管理で頭を悩ませる可能性が少しは低くなって良いかもしれません。

⚓ 秘密情報管理の観点

API Keyなどの情報は最低限のメンバーにしか見えないようにしたいため、権限管理が気になります。
ソース内定数方式などは論外ですが、環境変数やファイル方式、クラウドインフラが持つセキュアパラメータ管理サービスを使うなど様々な方式があり、色々なオプションが考えられるところです。
いずれにせよ秘密情報は他のアプリケーション設定とは別に管理する必要があるという点は常に意識しておく必要があります。

・・・とまあ、機微情報の扱いについてはまとめてようとしてみましたが、ちょっと今回の範囲でまとめて話すには範囲が広すぎるということで、本記事では触り程度ということでご了承ください。

※一点大事なのは、どんなセキュアな場所に保存したとしても、shellが取れてrails consoleが触れると見られない情報はないため、もしrails consoleを実行可能な環境を用意する場合は取り扱いに充分注意することが大事です。

⚓ その他のトピック(抜粋)

入りきらなかったテーマをピックアップします。

⚓ 複数の設定方式に同時対応する

OSSやDockerで配布されているアプリケーションなど、様々な人が様々な環境で動作させることを想定とする場合には、これまでに挙げた設定方式の複数に対応することで、より柔軟かつ汎用的なソフトウェアになります

よく見るのは

  • (最優先)環境変数の設定値
  • 設定ファイルの設定値
  • アプリケーションのデフォルト設定値

といった順に優先度が設定されており、より優先度の高い設定で下位の設定を上書きできる、という仕様です。
Nginxを-gオプションでforegroundモードで動かしたり、動作検証の時だけDEBUG=1などを環境変数で渡して起動できるようにする機能などが代表的です。

ただ、すべての設定を環境変数で上書きできるようにするというのも煩雑になりすぎて難しい話なため、どの設定を上書き可能にするのかなどは実際に利用していく中でカスタマイズしていく部分なのかなとも思います。

⚓ デフォルト値問題

可能ならデフォルト値は設定し、デフォルト値でいい感じにアプリケーションが動くようにしておくべきです。
これはRailsのCoC(Convention over Configuration)原則にも則していますし、何より設定が匠の技化しないために大事なことです。

また、デフォルト値がある場合でも、設定ファイルで上書き可能なのであれば、リポジトリに全設定可能パラメータのデフォルト値が記載された「デフォルト設定ファイル(*.default.confなど)」があるととても便利です。
「何が設定変更可能なのか」という情報はまとめておかないとブラックボックス化しやすい情報なので、設定項目が多くなれば多くなるほどデフォルト設定ファイルは用意しておくことをオススメします。

⚓ まとめ

というわけで、(主にRails)アプリケーションコンフィグの設計パターンということで、整理を試みてみました。
元々まとまっている情報を参考にしたわけではないので稚拙な部分もあったかと思いますが、コンフィグの設計について考える一助になれば幸いです。

ではでは。


週刊Railsウォッチ(20201201前編)switch_pointがActive Record 6.0でサポート終了、Rails DBトランザクションの落とし穴ほか

$
0
0

こんにちは、hachi8833です。本日よりTechRachoアドベントカレンダー2020が始まりました。どうぞよろしくお願いします。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

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

今週は公式更新情報から見繕いました。


つっつきボイス:「6.1.0のマイルストーンが先週見た後で急に進捗進んでました」「リグレッションも出ているけど、issueはもう残り2件か」「リリースがだいぶ近づいてきたようですね」

なお、つっつきの翌日はissueが4件に増え、ウォッチ公開日には再び減って1件(99%完了)になっていました。

⚓ partitionedテーブル定義でschema:loadの問題を修正

# activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb#L47
          def add_table_options!(create_sql, o)
            create_sql = super
            create_sql << " DEFAULT CHARSET=#{o.charset}" if o.charset
            create_sql << " COLLATE=#{o.collation}" if o.collation
-           add_sql_comment!(create_sql, o.comment)
+           add_sql_comment!(super, o.comment)
          end

つっつきボイス:「テーブル定義にPARTITION BYを書けるのか↓」「これはいわゆるシャーディングの機能」「この機能を実際に使って起きた問題をissueにあげてくれたんでしょうね」

# activerecord/test/cases/adapters/mysql2/table_options_test.rb#53
  test "charset and partitioned table options" do
    @connection.create_table "mysql_table_options", primary_key: ["id", "account_id"], charset: "utf8mb4", collation: "utf8mb4_bin", options: "ENGINE=InnoDB\n/*!50100 PARTITION BY HASH (`account_id`)\nPARTITIONS 128 */", force: :cascade do |t|
      t.bigint "id", null: false, auto_increment: true
      t.bigint "account_id", null: false, unsigned: true
    end
    output = dump_table_schema("mysql_table_options")
    expected = /create_table "mysql_table_options", primary_key: \["id", "account_id"\], charset: "utf8mb4", collation: "utf8mb4_bin", options: "ENGINE=InnoDB\\n(\/\*\!50100)? PARTITION BY HASH \(`account_id`\)\\nPARTITIONS 128( \*\/)?", force: :cascade/
    assert_match expected, output
  end

参考: Shard (database architecture) - Wikipedia

「PARTITION BYはシャーディングのときにどのキーで分散させるかを指定するものだったと思います」「なるほど」

参考: PostgreSQL 12 ドキュメント: 5.11. テーブルのパーティショニング

⚓ 新機能: Active Storageにstrict loadingを追加

# activestorage/lib/active_storage/attached/model.rb#L63
-       has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy
-       has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
+       has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: :destroy, strict_loading: strict_loading
+       has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob, strict_loading: strict_loading

つっつきボイス:「strict_loading: trueを指定するとlazy loadingを禁止するようになったのか」

# activestorage/lib/active_storage/attached/model.rb#43
class User < ApplicationRecord
  has_one_attached :avatar, strict_loading: true
end

「これまではデフォルトでlazy loadingが効いていて恐らくストリーム処理されていたけど、ユースケースによってはファイルが完全にアップロードを完了してから処理したいというケースがあるので、それに対応したということだと思います」「後処理やバリデーションが重くて、しかも一般ユーザーが頻繁に巨大ファイルをアップロードするサービスでは、こういうオプションがあるといいでしょうね👍

⚓ 同じクラスへの複数のbelongs_toautomatic_inverse_ofの挙動を修正

このプルリクは、foreign_keysが同じautomatic_inverse_ofにバリデーションを追加する。
同じクラスへの複数のbelongs_toがクラスにあると、automatic_inverse_ofで誤ったinverse_nameを検索することがある。

class Room < ActiveRecord::Base
  belongs_to :user
  belongs_to :owner, class_name: "User"
end

class User < ActiveRecord::Base
  has_one :room
  has_one :owned_room, class_name: "Room", foreign_key: "owner_id"
end

user = User.create!
owned_room = Room.create!(owner: user)

p user.room

現在のautomatic_inverse_ofは、関連付けで見つかったリフレクションをバリデーションする。
しかし、このバリデーションは外部キーが同一であることをバリデーションしていない。
外部キーのバリデーションを追加することでこのissueを修正できる。
同PRより大意


つっつきボイス:「上のサンプルコードのように、Roomというクラス内にbelongs_to :userbelongs_to :owner, class_name: "User"という同じクラスを参照するbelongs_toが複数あって、さらにautomatic_inverse_ofを使ったときの挙動に問題があったのね」「選択肢が複数あるときにautomatic_inverse_ofが違う方を返すことがあったらしい」「修正は1行追加だけでした↓」

# activerecord/lib/active_record/reflection.rb#L626
        def valid_inverse_reflection?(reflection)
          reflection &&
+           foreign_key == reflection.foreign_key &&
            klass <= reflection.active_record &&
            can_find_inverse_of_automatically?(reflection)
        end

⚓ 新機能: FFmpegのシーン検出機能による動画プレビューの自動頭出し


つっつきボイス:「RailsであのFFmpegを使うのか!」「フェードインで始まる動画だとプレビューが真っ黒になってしまうので、シーンの始まる位置を検出して変えられるようになったそうです」「Active Storageにデフォルトで動画プレビュー機能があるとは知らなかった」

参考: FFmpeg - Wikipedia

「FFmpegのバージョンも指定されている↓」

# guides/source/active_storage_overview.md#L554
-WARNING: Extracting previews requires third-party applications, FFmpeg for
+WARNING: Extracting previews requires third-party applications, FFmpeg v3.4+ for

⚓ FFmpegよもやま話

「これはFFmpeg職人の出番かな」「FFmpegは今でこそエンジニア以外も使うツールになっていますけど、昔は知る人ぞ知るマニアックなツールでしたね」「懐かしいです」「FFmpegはかなりいろんな画像・音声処理をこなすことができて、画像を集めてアニメーションGIFを作ることも、動画からアニメーションGIFを生成することもできたと思います」「ここで使っているような、指定のキーフレームを画像として取り出したりすることもできます」「FFmpegはスイスアーミーナイフ的に強力なツールなので、その分コマンドラインオプションがすごく長くなったりしましたね」「そうそう」

「Active StorageをインストールするとFFmpegも入るんでしょうか?」「FFmpegはRails外部のソフトウェアなので自動では入らないでしょうね」「コマンドラインで呼び出すと思われるので、あればそれを使うし、入ってなければエラーになるんじゃないかな」「なるほど」「FFmpegが入っているかどうかをチェックするロジックぐらいはありそう(ffmpeg_exists?)」

「FFmpegは普通のWebサーバーにはなかなか入ってないと思います」「それにFFmpegで動画コーデックを使う場合はコーデックのライセンスをチェックしないといけなくなるんですよ」「あ、たしかに」「コーデックなども含めたFFmpeg環境を完全に再現するのは地味に面倒」「WindowsやLinuxだとコーデックがひととおり入ったパッケージがあったと思います」「サイズ大きくなりそう…」

参考: コーデック - Wikipedia
参考: おすすめのコーデックパック - k本的に無料ソフト・フリーソフト — Windows用
参考: Install Multimedia Codecs Ubuntu 20.04 LTS – Linux Hint — Linux用

「動画コーデックの中にはライセンスがかなり複雑なものもあった覚えがあります」「昔は設定をあれこれこねくり回しながら使ってましたけど、最近はこの辺であまり悩まなくなった気がします」「QuickTime動画を再生するためだけにAppleの有料プレーヤーを買ったこともあります」「有料版、そういえばありました」「国によってメジャーなコーデックが違ったりするんですよ」

参考: QuickTime - Wikipedia

⚓Rails

⚓ BasecampのHEY stack


つっつきボイス:「HEY.comはBPS社内Slackに貼っていただいたやつです」「BasecampのHEYというサービスはこれまで今ひとつわからなかったんですが、複数のメールボックスをここに集約するといい感じにメールボックスを横断的に検索したり管理したりできるようですね」

「そのHEY.comのGemfileをDHHが公開しています↓」「ありがたい🙏

⚓ HEY stackのGemfile

「このGemfileで興味深いのは、たとえばsasscやsassc-railsを使っているところ↓: Webpackerは入っているけど、sassはWebpackerではなくsprocketsでコンパイルさせている」「ホントだ」「sassの中にRubyのコードを仕込むのであればsprocketsで処理するしかないでしょうね」

# 同Gistより
# JavaScript and assets
gem 'webpacker', '~> 5.1.1'
gem 'sprockets', github: 'rails/sprockets'
gem 'sprockets-rails', github: 'rails/sprockets-rails'
gem 'jbuilder', '~> 2.9', '>= 2.9.1', github: 'rails/jbuilder'
gem 'sassc-rails', '~> 2.1'
gem 'sassc', '<= 2.1'
gem 'local_time', '~> 2.0'
gem 'turbo', github: 'basecamp/turbo'

sass/sassc - GitHub
sass/sassc-rails - GitHub

「他にも見慣れないgemがいくつか見える」「ジョブの処理にresqueを使ってますね」「同じくジョブ処理でよく使われるsidekiqだと、マルチプロセスなどの高度な機能を使う場合は有料版を買うことになりますけど(機能表)、resqueはオープンソースのみなのでHEY.comで採用したのかもしれないと想像してみました」

# 同Gistより
# Jobs
gem 'resque', '~> 2.0.0'
gem 'resque-multi-job-forks', '~> 0.5'
gem 'resque-pool', github: 'nevans/resque-pool'
gem 'resque-scheduler', github: 'resque/resque-scheduler'
gem 'resque-pause', github: 'basecamp/resque-pause'
gem 'resque-web', require: 'resque_web'
gem 'resque-scheduler-web', github: 'mattgibson/resque-scheduler-web'
gem 'sinatra', github: 'sinatra/sinatra'

resque/resque - GitHub
mperham/sidekiq - GitHub

「resqueのリポジトリを見ると、同じくジョブ処理用のdelayed_jobとの比較は載っているけど、sidekiqとの比較は載ってなかった」「resqueは、delayed_jobから強くインスパイアされて作ったと書かれてる、へ〜」「知らなかった」「世の中的にはsidekiqの方がメジャーな印象があって自分も最近はよく使いますけど、sidekiqは企業がサポートしているという安心感があるからよく使われているのかもしれないとちょっと思いました」

collectiveidea/delayed_job - GitHub

「GistのコメントのFAQを見ると、sidekiqの機能は不要、resqueで十分だからというDHHのツイート↓が引用されてる」「sidekiqは高機能ですけど、それが不要ならresqueで十分というのは理解できる」

別のコメントには、なぜMySQLにしたのかというFAQでもDHHのツイート↓が引用されてますね」「PostgreSQLとMySQLについて特に思うことはない、だからMySQLを使っているという感じかな」

⚓ MySQLの使いどころ

「HEY.comでMySQLを使う理由について今自分が思ったのは、データがものすごく多くて、しかもメールという書式の整ったデータだからという要因もあるんじゃないかということですね: そのような場合はMySQLの方が速くなる見込みがあるんですよ」「おぉ」

「MySQLは、しくみや特性を理解して正しく用いればPostgreSQLより速くなることがよくあります: 全般にPostgreSQLは複雑なクエリを書いたときに速度を出しやすいけど、クエリが比較的単純な場合や複雑なクエリが不要な場合はMySQLの方が速度を出しやすい傾向がありますね(※あくまで経験による主観です)」「なるほど!」

「あと考えられるとすれば、HEY.comを作ったときにAWS Auroraを使いたかったのかもしれませんね」

参考: Amazon Aurora(高性能マネージドリレーショナルデータベース)| AWS

「MySQLもPostgreSQLもRailsでは広く使われているので、データの特性やユースケースなどに応じて適している方を使えばよいと思います」「その意味で、MySQLかPostgreSQLかという二者択一を迫る問いはあまり重要ではないでしょうね」

⚓ Rails DBトランザクションの落とし穴(RubyFlowより)


つっつきボイス:「トランザクションのいい書き方と悪い書き方のコード例が紹介されています」「以下のように↓トランザクションでStandardErrorを書くことも一応できるけど、StandardErrorだとエラーが意味する範囲が広すぎるので、独自のエラークラスを作ってActiveRecord::RecordInvalidで出力する方がずっとよいという話、たしかに」「独自のエラークラスを定義するのはちょっとだけ手間ですが」

# 同記事より
record = MyModel.last
error_for_user = nil

begin
  ActiveRecord::Base.transaction do
    # ...
    record.save!
  end
rescue StandardError => e
  # do something with exception here
  error_for_user = "Sorry your transaction failed. Reason: #{e}"
end

puts error_for_user || "Success"

「ダメな例のひとつが、データベースレベルのエラーであるActiveRecord::StatementInvalidをトランザクションの中でrescueすること↓」「トランザクションの外でbeginendを書いて、トランザクションの外でrescueせよという話、これもそのとおり」「beginendの場所が違うということですね」

# 同記事より: 悪例
record = MyModel.last
error_for_user = nil


ActiveRecord::Base.transaction do
  begin
    # ...
    record.save!
  rescue ActiveRecord::StatementInvalid => e # DON'T DO THIS !
    error_for_user = "Sorry your transaction failed. Reason: #{e}"
  end
end

puts error_for_user || "Success"

「よい例として、トランザクションの中でraise ActiveRecord::Rollbackして明示的にトランザクションをロールバックする↓: これもよく使います」

# 同記事より
def add_bonus(tomas)
  ActiveRecord::Base.transaction do
    raise ActiveRecord::Rollback if john.is_not_cool?
    tomas.update!(money: tomas.money + 100)
  end
end


begin
  add_bonus(tomas)
rescue ActiveRecord::Rollback => e
  puts "Sorry your transaction failed. Reason: #{e}"
end

#transactionMyModel.transactionActiveRecord::Base.transactionは互いにエイリアスなので動作は全部同じという話、おっしゃるとおり」「自分は昔からActiveRecord::Base.transactionで書くのが好きなんですが、SQLのトランザクションを理解していれば、そもそもトランザクションの単位がモデルではないことがわかるので、その意味でもモデルを指定しないこの書き方が好きですね」「なるほど」

理由は、トランザクションの単位はモデルではなく、データベースコネクションを単位とするからだ。
ActiveRecord::Transactions::ClassMethodsより大意

「次はネステッドトランザクションは避けることをおすすめしたいという話」「たしかにネステッドトランザクションはいろいろ難しい: たとえばこの記事↓にもありますけど、ネストの内側でActiveRecord::Rollbackしても握りつぶされる😇

参考: ネストしたトランザクション内で ActiveRecord::Rollback を raise しても握りつぶされるだけだ | TECHSCORE BLOG
参考: 【翻訳】ActiveRecordにおける、ネストしたトランザクションの落とし穴 - Qiita

「そもそもネステッドトランザクションは、RDBMSの実行レベルでサポートされていないことがほとんどです↓」「あ〜」「上の記事にrequires_new: trueを使えば真のネステッドトランザクションを使えると書かれてはいるけど、それでも大変…」(中略)

ほとんどのデータベースは真のネステッドトランザクションをサポートしていない。執筆時点では、真のネステッドトランザクションをサポートしていることがわかっているのはMicrosoft SQL Server(MS-SQL)だけだ。
ActiveRecord::Transactions::ClassMethodsより大意

「記事の最後で、以下のようにトランザクションの中にsaveだけを書く意味はないとあるのはどういうことだろう?🤔

ActiveRecord::Base.transaction do
  user.save!
end

「お、引用されているAPIドキュメント↓によると、savedestroyは自動的に設定されているコールバックごとトランザクションでラップされるのか!」「Active Recordよくできてる〜」「それならたしかに上のように書く必要はありませんね😋

#saveおよび#destroyのどちらも、ひとつのトランザクション内にラップされ、バリデーションやコールバックで行うあらゆる操作はこのトランザクションの保護下に置かれます。これによって、トランザクションが依存する値をチェックするためのバリデーションを使うことも、after_*などのコールバックで例外をraiseしてロールバックすることもできます。
ActiveRecord::Transactions::ClassMethodsより大意

「これはなかなかいい記事だと思います👍


同記事で参照されているAPIドキュメントを翻訳しました↓。

Rails APIドキュメント: Active Recordのトランザクション(翻訳)

⚓ rails-erd: ER図を生成

voormedia/rails-erd - GitHub


つっつきボイス:「rails-erdは前からあったような気がしたんですが、ウォッチで扱ったことがありそうでなかったので取り上げてみました」「こういうER図を生成するのね↓」「使いたい人はどうぞ」


voormedia.github.ioより

「ちなみにJetBrainsのRubyMineでも右クリックでダイアグラムを生成してくれますヨ↓(rails-erdとは違う図ですが)」「なるほど、IDEならgemをインストールせずにできますね」

Rails: RubyMineでテーブル設計する

⚓ switch_pointはActive Record 6.1以降をサポートせず

eagletmt/switch_point - GitHub


つっつきボイス:「BPS社内Slackにebiさんが貼ってくれた記事です」「switch_pointは、Railsでマルチプルデータベースを行うときによく使われるgem」「たしかにRailsが公式にマルチプルデータベースをサポートするようになればswitch_pointでやる必要はなくなりますね」

「記事によると、Rails 6.1でついにswitch_pointが壊れるようになったのか…」「Rails 6.0まではswitch_pointが動いてたというのもスゴい」「少し前にRailsにR/W(リード/ライト)Splittingが入っていたぐらいなので、Railsのマルチプルデータベース機能とswitch_pointの共存はヤバそう」

「シャーディングも含めてswitch_pointの機能がRailsで使えるようになってきたので、今後新しいRailsプロジェクトでマルチプルデータベースが必要なときはRailsの機能を使うことになるでしょうね」「マルチプルデータベース機能の開発リソースがswitch_pointとRailsで分かれてしまうのはもったいないので、switch_pointが6.1以降のサポートを終えたことでRailsでの開発にリソースが集約されることが期待できそう」


前編は以上です。

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

週刊Railsウォッチ(20201124)strict loading violationの振る舞いを変更可能に、Railsモデルのアンチパターン、quine-relayとさまざまなクワインほか

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

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

Rails公式ニュース

RubyFlow

160928_1638_XvIP4h

週刊Railsウォッチ(20201202後編)Rails 6.1 RC2リリース、Ruby STMの詳細な解説記事、RSpecのdiffを見やすくするsuper_diff gemほか

$
0
0

こんにちは、hachi8833です。Rails 6.1 RC2が今朝リリースされたことをつい先ほど知りました。

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇

⚓ 臨時ニュース: Rails 6.1 RC2がリリース

6.1の主な新機能や改善点も上の記事でリストアップされています。

  • マルチプルDBの強化(Horizontal Shardingなど多数)
  • 関連付けのstrict loading(#37400#38541
  • Delegated Typing(#39341
  • 関連付けをバックグラウンドジョブで非同期destory可能に(#40157
  • ActiveModel::Error#32313
  • Active Storageの強化(#34935など)
  • deprecation warning発生時にraiseするオプション(#37940
  • 各種パフォーマンス向上およびバグ修正
  • Zeitwerkより前のクラシックオートローダーが非推奨に

ウォッチ20200907でご紹介した@willnetさんのスライドを再録いたします。

⚓Ruby

⚓ @_ko1さんのSTM解説記事


つっつきボイス:「Software Transaction Memory(STM)を@_ko1さんが詳しく解説してくれてる!」「この間取り上げたSTMの英語記事のときはRactorとの絡みがよくわからなかったけど、@_ko1さんの記事はRactorにも関連して書かれていてありがたい🙏」(一同でしばらく読む)

「Ractorは基本的にスレッド間で共有するものがあってはいけないという考え方で、たとえばクラス変数もRactorでは使えないんですけど、それでも共有したいものがあるときのためにSTMを導入してはどうかという話を何かのイベントで聞いた覚えがあります」「記事でも、Ractorの外では基本的にオブジェクトを共有できないとありますね」

: 本項の以下のイタリック箇条書きはつっつきで注目した部分を元記事から抜粋・要約したものであり、必ずしも元記事の流れに沿っていません。詳しくはぜひ元記事をご覧ください。

  • Ractorではデータ共有のためにオブジェクトをコピーしてメッセージとして送受信する
  • Ractor#sendRactor.receiveRactor.yieldRactor#takeというメソッドが使える

「マルチスレッドプログラムやネットワークプログラミングではよくこのような形でデータを共有しますね」

そこで、Ractor では、メモリを共有するのではなく、オブジェクトをメッセージとしてコピーして送ったり受け取ったりすることで、データを共有します。
同記事より

  • Ractorのオブジェクトの受け渡しは原則として参照ではなくコピーされる
  • ただしコピーなしで受け渡しする場合もあり、それを共有可能オブジェクトと呼ぶ

「この辺の、どれを共有可能オブジェクトにできるか、何が共有できないかを追いかける話なども何かのイベントでしてませんでしたっけ?」「うう、思い出せない😢」「そのときにSTMの話もしてたように覚えてます」

  • たとえばちょっとしたカウンタをRactor間で簡単に共有する方法が現在のRactorにない
  • 単純なカウンタを集約して管理するRactorを作るといったことは可能

「カウンタのためにわざわざRactorを作りたくない気持ち、わかります」

  • ロックによるメモリ共有を検討した
  • しかし値をincrementするだけでもロックが必要になる
  • ロックしないとアクセスできないインターフェイスを試したところ、デッドロックした↓
# 同記事より: デッドロックするコード
c1 = Counter.new(0)
c2 = Counter.new(0)

r1 = Ractor.new do
  c2.lock do
    c1.lock do
      c1.value += 2
      c2.value = c1.value * 2
    end
  end
end

c1.lock do
  c2.lock do
    c1.value += 1
    c2.value = c1.value * 2
  end
end

#...?

「値をincrementするにもロックが必要、たしかに」「やはりデッドロックするのか」

参考: デッドロック - Wikipedia

  • ロックのアプローチに代えて、データベースのトランザクションの概念をメモリに適用するSTMを採用すればそうした問題を解決できるのではないかと考えた

「STMは、とりあえず読み書きして、アクセスが重複したらロールバックするというところがシンプルですね↓」

STM は、DB のトランザクション(楽観的ロック)と同じように、とりあえずなんか読み書きして、あとで、「あ、別の Ractor とアクセスが被った!」となったらロールバックしてしまいます。簡単ですね。
同記事より

  • STMはreadしかしないのであれば並列化して速くなりそう
  • 他にもメリットがある
  • その代わりロールバックが多発すると遅くなる可能性がある

「STMのいろいろな流派の話も興味深い↓」「メモリ操作を全部transactionの対象にするアプローチは聞いたことある」「まさにコンピューターサイエンスの記事ですね」


同記事より

  • Rubyはすべてをロールバックすることはできないので、一部のメモリだけをSTMの対象にすることにする
  • Ractorでは、Ractor.atomicallyでトランザクションを設定すると、Ractor::TVar.newに置いた値だけがトランザクションの中でロールバック対象になる
  • Ractor::TVarは、Concurrent Rubyのインターフェイスを踏襲した

「これらのメソッドが既にconcurrent-rubyのThreadにあったとは知らなかった↓」「TVarはtransaction variableの略なんですね」

ruby-concurrency/concurrent-ruby - GitHub

  • Ractor同士の変更が競合した場合は再実行される(その場合インスタンス変数やI/O処理は元に戻らない)
  • Ractor.atomicallyはネストできる

「『STMの実装』を見ると、STMは時刻をベースにしているらしい」「Ractor.atomicallyが自由にネストできるというのがスゴい」「ネストしても大丈夫だろうかと一瞬思っちゃいました」「読み出しのみの操作でもロールバックするとやり直しになる、だから再実行しても安全になるのか」

  • STMはRuby 3.0ではリジェクトされたが、gemとして公開している↓

ko1/ractor-tvar - GitHub
ko1/ractor - GitHub

「Ractorを使い倒すのであればしばらくはgemでやることになりそう」「gemになった分少し性能は落ちるらしいけど、これはしょうがないですね」


「とても丁寧な解説記事で、しかもめちゃめちゃ面白い!」「実装の話も盛り込まれていて参考になります」「これを日本語で読めるのはありがたい🙏

⚓ RactorやSTMの使われ方を想像する

「この記事で扱われているような、トランザクションをソフトウェアで行ってものすごい数の並列処理を行うといった話題は、Go言語などで作るようなマイクロサービス方面でよく登場しますね」「ふむふむ」「今後Rubyでそうした処理を行うときには、RactorとSTMが大事な考えになってくる気がします」

「RactorやSTMは、RailsのActive Recordのビジネスロジックみたいなアプリケーションコードよりは、もっとプリミティブなAPIサーバーでよく使われそうな気がします」「記事にもあったような、銀行口座の残高移動のように一貫性が一瞬でも崩れてはいけない処理でも使いたいです」

「今後RubyでRactorとSTMが本格的に使えるようになれば、現在はGo言語などで作られているような並列度の高いAPIサービスをRubyで手軽に書けるようになるかもしれませんね」

⚓ lib-ruby-parser: Rustで書かれたRubyパーサー(RubyFlowより)

lib-ruby-parser/lib-ruby-parser - GitHub


つっつきボイス:「まだ新しくて★は少ないですが、RustでRubyパーサーを作ったというのが気になりました」「MRIのparse.y↓をそのままベースとしているので、完全にMRIと同じ順序でトークンを返すとある」「かつRipperプラスjemallocよりも速いそうです」

参考: ruby/parse.y at master · ruby/ruby
参考: class Ripper (Ruby 2.7.0 リファレンスマニュアル)

「レアなエッジケースを与えたらさすがにコケるかな?」「READMEを見るとトップ300 gemでテストされていて、しかもruby/spec↓やruby/rubyでもRipper.lexと同じ結果が出ているとある!」「それはスゴい」「RubyをRustでパースしたいときはこれでしょうね😆

ruby/spec - GitHub

⚓ ShopifyのSorbet利用状況

以下で知りました。

参考: 週刊気になったITニュース(2020/11/22号) - masa寿司の日記


つっつきボイス:「ShopifyでSorbetがどのぐらい使われているかという記事で、masa寿司さんの方で見る方が読みやすそうでした」「ファイルの80%以上が型付けされてるとは」「Shopifyは静的型付け方面もすごく頑張ってるな〜」「Shopifyはサービスの規模も巨大でエンジニアの数も多くて、エンタープライズWebアプリケーションのひとつの大きな流れを形成しつつある感じがしますね」「今後Sorbetの利用を指定する案件が出てくるようになったら面白そう😋

後でShopifyのイベント↓を見てみましたが、今のところイベントの資料は見当たらないようです。

イベント: Shipit! Presents: The State of Ruby Static Typing at Shopify(終了)
類似記事: Adopting Sorbet at Scale — Development

⚓ super_diff: RSpecでデータ構造のdiffを見やすく整形(Ruby Weeklyより)

mcmire/super_diff - GitHub


つっつきボイス:「なるほど、通常だとテストがfailするとデータ構造のdiffがベタに表示されるけど、それをこんなふうに賢く表示するのか↓」「これはスーパー」「賢い〜」「複雑なデータ構造のdiffを見たいときによさそう👍


同リポジトリより

⚓DB

⚓ M1とPostgreSQL

つっつきボイス:「これはデータベース関連のベンチマークかな?」「縦軸は秒あたりのトランザクション数、横軸はクライアント数か」「これが出典のようです↓」

参考: 【x軸とy軸の覚え方】 「横がx軸で縦がy軸です」ととっさに言えますか?! | SMATU.net

「ツイートのグラフを見る限り、M1はクライアント数が増えたときのトランザクション数の伸びが頭打ちになっている感じ」「このグラフがリニアであるほど、並列化がうまくいってることになりますね」「Ryzen 9はやっぱりスゴいという話」「Ryzenつよい💪

⚓クラウド/コンテナ/インフラ/Serverless

⚓ 米バージニア州北部のAWS us-east-1リージョンで障害(現在は回復


つっつきボイス:「我が家のSwitchBotは今の時点でまだ回復してません😇」「主にIoT系デバイスが影響を受けてるようですね」「見た感じでは影響を受けているのはIoTデバイスだけみたい」

参考: SwitchBot(スイッチボット) | スマートホームにらくらくスイッチ| Alexa | Google Home

「お、ステータスを見ると珍しくページトップで障害をレポートしている↓」「Tokyoリージョンの障害もトップに出して欲しい」「今日のお昼はもっと障害件数多かったんですけど、今はほぼ回復してる感じですね」「なるほど、IoT SiteWise以外はグリーンに戻ってる」


同記事より

「AWSでは新しいサービスをまずus-east-1リージョンで立ち上げることが多いんですけど、Amazon Kinesisなどのサービスがまだそのリージョンにしかなかったんでしょうね」「AWSの新しいサービスをいち早く使ってる人はだいたいこのリージョンに乗っかりますけど、TOKYOリージョンに来るまで待ってから使い始めている人たちはたぶん無事だったんじゃないかな」

「記事を見ると家電が動かない報告がいろいろ上がってますね」「エアコンが動かなくて寒いとか」「ご飯が炊けなくなったとか、灯りがつかなくなって寝坊しそうになったというのも見かけました」

「エアコンは物理リモコンで回避できそうですけどね」「私はリモコンを手放さずに全部持ってますけど、リモコン使いたくない人は片付けちゃうのかも」「リモコン使いたくないからIoTを導入することも多いでしょうね」「いずれにしろこの種のデバイスはフォールバックパスを用意しておかないとこういうときに大変」

⚓言語/ツール/OS/CPU

⚓ M1とリーナス・トーバルズ


つっつきボイス:「リーナスはプロプライエタリな製品を好まないことで有名ですね」「記事にはLinuxをM1向けに最適化するつもりはないとあります」「M1 Macの仕様が公開されないとできないでしょうね」

参考: リーナス・トーバルズ - Wikipedia

⚓その他

⚓ スクラムガイド更新版が日本語に


つっつきボイス:「スクラムガイド改訂版が日本語化されたそうです」「改定されてだいぶ短くなったらしいとはてブかどこかで見かけました↓」「スクラムの仕様は定義が厳しくて、資格を取らないとスクラムマスターを名乗れないですね」「BPSでも厳密にスクラムのやり方に則って開発をしているチームはなかったと思います」

参考: スクラムガイドの変更点(2017→2020)から見えるスクラムチームが陥りやすい3つの罠
参考: スクラムマスターの役に立つ認定資格・研修一覧|オッドイー

⚓番外

⚓ 外からわかる


つっつきボイス:「最近こういう外からわかる系の技術をよく見かけるなと思って」「3つ目の記事には、脳が今聞いている音楽や言葉を判定するというのがありました」「2つ目の記事にあるロボット掃除機で音を取る手法なども昔から研究されている技術ですね: 音は物体を揺さぶるので、音の周波数より十分高いサンプリングレートで揺さぶりを観測できれば原理的に可能」「はい、窓ガラスを外からレーザー光線で測定して部屋の中を盗聴するなんてのも20年ぐらい昔にニュースになったのを覚えてます」「それだけ音の周波数は光に比べて低いということですね」

参考: レーザーマイクロフォン - Wikipedia


後編は以上です。

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

週刊Railsウォッチ(20201201前編)switch_pointがActive Record 6.0でサポート終了、Rails DBトランザクションの落とし穴ほか

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

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

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Viewing all 1384 articles
Browse latest View live