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

週刊Railsウォッチ(20200120前編)福岡でも公開つっつき会、Railsのconnection_specification_nameでprimaryという名前が非推奨に、structure.sqlとschema.rbほか

$
0
0

こんにちは、hachi8833です。Chromium入りEdgeが出ましたね。


つっつきボイス:「アイコンがChromiumと色違いというのがまた何とも😆」「大丈夫かしら😆」「Chromium入って〼という表示なのかも😆

「新Edge入れた?」「別に使わないし入れてませんけど😆」「自分もまだWindowsに入れてないので例のbrowserstack.com↓でUser Agent(UA)あたりを見てみるか」(開く)「UA確認に使ってるそのサイトは何でしょう?」「確認くんです」


browserstack.comより

「おし、browserstackでEdge 79 Betaが動くようになってる」「UAにChromeの文字入ってますけど↓😆」「UAの文字って前からいろいろ怪しいじゃないですか😆」「まあ確かに😆

  • browserstackの最新Edge: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.53 Safari/537.36 Edg/80.0.361.33
  • Windows上の少し前のEdge: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763

「この”Edg”↑って何😆」「スペルミスなんじゃ?」


追いかけボイス: 後に”Edg”について以下の情報がありました。

参考: ユーザーエージェント(UA)文字列は時代遅れ? ~「Google Chrome」で凍結・非推奨に - やじうまの杜 - 窓の杜
参考: Microsoft Edge User Agent String - Microsoft Edge Development | Microsoft Docs


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

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

⚓お知らせ: 公開つっつき会2本立て

⚓その1: 週刊Railsウォッチ「第19回公開つっつき会」(無料)

いよいよ第19回を迎えた公開つっつき会は、来月2月6日(木)19:30〜よりBPS株式会社Pubスペースにて開催いたします。

週刊Railsウォッチの記事やここだけの話にいち早く触れられるチャンス!発言・質問も自由です。皆さまのお気軽なご参加をお待ちしております。

⚓その2: 福岡でのリモート公開つっつき会開催のお知らせ(Wingdoor@福岡)

上に先んじて、来週1月30日(木)19:30〜に福岡の株式会社ウイングドアにて、TechRachoでお馴染みのmorimorihogeが現地入りして第2回公開つっつき会を行います。通常の公開つっつき会と基本的に同じ要領で開催いたします。福岡近郊の皆さまのお気軽なご参加をお待ちしております。

  • 会場: 株式会社ウイングドア(〒810-0001 福岡県福岡市中央区天神4丁目1−28 天神リベラ 3F)
  • 日時: 1月30日(木)19:30〜21:00(終了後は21:20頃まで懇親会もございます)
  • 費用: 無料
  • 持ち物: 必要なものは特にありませんが、手元でのドラフト参照・検索用にノートPCなどがあると便利です。

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

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

⚓マルチDBでprimaryという接続種別名を非推奨に

  • ActiveRecord::Baseconnection_specification_name"primary"という名前が非推奨になった。今後は"ActiveRecord::Base"を使う。この変更はActiveRecord::Base.connection_handler.retrieve_connectionActiveRecord::Base.connection_handler.remove_connectionの呼び出しに影響する。これらのメソッドを"primary"で呼んでいるのであれば、"ActiveRecord::Base"に切り替えること。
    changelogより大意

つっつきボイス:「もしかしてポリコレ絡み?😆」「そっちではなさそうです」「デフォルトは名前指定なしと」「デフォルトの接続は1つだよってことにしたいのかな🤔」「さてこれはbreaking changeに…なりますね」「😅

マルチDBが進化するにつれてconnection_specification_nameやデータベースオブジェクトのspec_nameのデフォルトが”primary”である点が混乱気味になってきた(私のせい🙇)。
さらにややこしいのは、接続を確立するすべての非ActiveRecord::Base抽象クラスでこのクラス名を使っていること。たとえばclass MyOtherDatabaseModel < ApplicationRecordとすると、ActiveRecord::Baseでは”primary”が使われるのに、connection_specification_nameで”MyOtherDatabaseModel”が使われる。
このPRでは以下のようになる:

  • handler.establish_connection(:primary)を呼んでも”primary”でdeprecation warningは表示しない。さらに、:primaryという設定名を使ってない接続で実際のActiveRecord::Base接続があやまって上書きされる可能性があるというバグも修正される。
  • handler.establish_connection :primaryhandler.remove_connection "primary"が一度も呼ばれていない状況でhandler.retrieve_connection "primary"を呼ぶと、ActiveRecord::Baseの接続を返してdeprecation warningを表示する。
    同PRより大意

⚓パラレルテストを以前の挙動に戻した


つっつきボイス:「どうやら上の#38190と関連しているようです」

このコミットは#38029#38151で判明したバグに対する、ある意味応急処置的な修正。#38151は、3-tier configを持つアプリでreplicaが使われている場合に問題になる可能性がある(configの順序が変わることで、暗黙で使われるデフォルト接続が変わるため)。
アプリでestablish_connectionを引数なしで呼ぶかApplicationRecordconnects_toを呼び、かつDBのパラレルテストを使っていると、configが正しくなくなる可能性がある。
原因は、#38151のコードがconfigをなめるときにreplica以外のconfigが更新されてリストの末尾に置かれるため。欲しい接続を指定していない場合、Railsはリストの最初の接続をその環境で使う。以下の設定で考える。

test:
  primary:
    database: my_db
  replica:
    database: my_db
    replica: true

このconfigの順序が「replica」「primary」に変わるので、Railsでestablish_connectionを引数なしで呼ぶと、リストの冒頭にあるreplicaが使われる。
(略)
この問題の実際の修正は、引数がenvまたはなしのestablish_connection呼び出しを非推奨にして、(primaryなどの)明示的なconfigを必須にすること。ここは、Rails起動時に:primaryをデフォルト接続として優先することとも関連する。さらに、connection_specification_nameの”primary”を非推奨にして常にクラス名にすることも必要になるだろう(でないとマジで果てしなく混乱するので)。
背後の問題の方も修正するが、従来の振る舞いも復活させたい。
同PRより大意

⚓新機能: GitHub::Deprecation.disallowed_warnings=

このPRで導入されるGitHub::Deprecation.disallowed_warnings=は、そのアプリのdeprecation warningのうち、許されないconfigにマッチするルールを設定できる。ActiveSupport::Deprecation.disallowed_behaviorは、許されないdeprecation warningにマッチしたときの振る舞いを指定する。
同PRより大意


つっつきボイス:「ちょっとわかりにくかったんですが、許されない書き方をdeprecation期間後も阻止するということみたいです」「そういえば最近のRailsでもこれに似た改修が入ってたかも」「たぶんRuby 2.7対応にも欲しいヤツ😋

ユースケース
アプリのdeprecation warningを取り除くとき、非推奨コードが今後も決して導入されないようにしておきたい。そのためにCIやrubocopルールにlintテストを追加することも多いが、理想どおりにいかないことも多い。たとえば、Rails 6ではデフォルトuniquenessバリデータではstring型カラムのdeprecation warningが生成されるが、6.1以降はcase sensitiveな比較を強制しなくなる。rubocopルールにenforce case_sensitive: trueを追加しても、背後がstring型のカラム属性だけをこれでチェックするのは難しい。
この問題に対する私たちのソリューションは、コードベースから削除された後もそうした非推奨コードを許さないよう特定のdeprecation warningを表示できるようにすることだ。許されない非推奨コードがある場合は、devやtest環境でガチで失敗させて例外を表示する。productionではエラーらしくdeprecationをログ出力する。
設定を一時的に緩めることも、条件を指定して一時的に緩めることもできる。
このPRなしでも同じことをやる方法はいろいろあるが、面倒になる。
同PRより大意

同PRのコメントでは、以前のウォッチ(ウォッチ20181001)でも紹介したShopifyのdeprecation_toolkit gem↓にも言及されていますが、仕組みは異なるようです。

「deprecateな機能を使ってるとwarningって出るんですか?」「出ます出ます」「Rubyのレベルというか標準出力で出ますし、Rails標準のconfigだとログにも出ます☺」「自分のプロジェクトでRailsアップグレードする機会があんまりないから目にしないのかな〜😅」「PumaとかUnicornのログを開いたらきっとある😆」「オレオレRailsアプリを2.7にしたら起動しただけでコンソールにいっぱい出ました😰」「2.7はまだ様子見と決めてますっ😆

⚓抽象クラスの数値バリデータを修正

# activerecord/lib/active_record/validations/numericality.rb#L5
    class NumericalityValidator < ActiveModel::Validations::NumericalityValidator # :nodoc:
-     def initialize(options)
-       super
-       @klass = options[:class]
-     end
-
      def validate_each(record, attribute, value, precision: nil)
-       precision = column_precision_for(attribute) || Float::DIG
+       precision = column_precision_for(attribute, record) || Float::DIG
        super
      end

      private
-       def column_precision_for(attribute)
-         if @klass < ActiveRecord::Base
-           @klass.type_for_attribute(attribute.to_s)&.precision
-         end
+       def column_precision_for(attribute, record)
+         record.class.type_for_attribute(attribute.to_s)&.precision
        end
    end

つっつきボイス:「抽象クラスで数値バリデータを定義できなかったのが修正された」「前はできなかったと」「self.abstract_class = trueなのにcreate!してるのかと思って焦ったけどサブクラスでしたか😆」「こう書きたい気持ちはちょっとわかる: 抽象クラスなんだけど共通のバリデータを置きたいんでしょうね☺」「気持ちワカル」「名前が同じでも継承先で振る舞いが違ってたりするとこじれそうなので、あんまり好きじゃないな〜😆

参考: 抽象型 - Wikipedia

# 同PRより
  class AnimalsBase < ApplicationRecord
    self.abstract_class = true

    validates :age, numericality: { min: 18 }
  end

  class Dog < AnimalsBase
  end

  Dog.create!(age: 0) => ActiveRecord::TableNotSpecified: AnimalsBase has no table configured. Set one with AnimalsBase.table_name=

「Rubyで抽象クラスってあんまり使わない印象あるんですけどどうでしょう?🤔」「RailsのSTI(Single Table Inheritance)なんかでは普通に抽象クラス使いますよ🧐」「抽象クラスは継承を強要されることになるので、使わなくていいなら使いたくない😆」「まあSTIのときぐらいでしょうけど☺

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

「なにしろRubyには抽象クラスのための機能がありませんから😆」「抽象クラスっていう概念、どっちかというとJavaから来ている感ありますね」「Javaは抽象クラスを明示的に要求する構文ありますし」「いつだったかjoker1007さんが抽象クラスのgem作ってたの思い出しました↓(ウォッチ20180202)」「自分みたいにオブジェクト指向をJavaで学んでいたら抽象クラスはまあわかるけど、Rubyで学んでたらピンとこないかもですね☺

参考: joker1007/abstriker

⚓最適化3つ

つっつきボイス:「最適化系の読みやすい修正をいくつか」

# activesupport/lib/active_support/core_ext/object/json.rb#L172
-   Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }]
+   result = {}
+   subset.each do |k, v|
+     result[k.to_s] = options ? v.as_json(options.dup) : v.as_json
+   end
+   result
  end

concatの方がオブジェクトを生成しないので速いんだったかな↓」「presenceだとメソッド呼び出しの分遅いし、compact.flattenは対象が大きいと遅くなりそうですし☺

# actionview/lib/action_view/helpers/tag_helper.rb#L335
      private
        def build_tag_values(*args)
          tag_values = []
          args.each do |tag_value|
            case tag_value
            when Hash
              tag_value.each do |key, val|
-               tag_values << key if val
+               tag_values << key.to_s if val && key.present?
              end
            when Array
-             tag_values << build_tag_values(*tag_value).presence
+             tag_values.concat build_tag_values(*tag_value)
            else
              tag_values << tag_value.to_s if tag_value.present?
            end
          end

-         tag_values.compact.flatten
+         tag_values
        end

「これはyieldより&blockの方が速いということか↓」「PRにも余分なスタックフレームのpushを避けるためとあるから高速化ですね」「あはぁ〜そうかも😆」「ブロックを作ってyieldすると1階層余分になっちゃうと」「{ }はハッシュじゃなくてブロックか😳」「{ yield }で受けるのは業務ではあんまり使わないかな☺

# activesupport/lib/active_support/core_ext/benchmark.rb#L13
- def ms
-   1000 * realtime { yield }
+ def ms(&block)
+   1000 * realtime(&block)
  end

⚓番外3つ

つっつきボイス:「#38246はRailsガイドのJavaScriptガイドにWIPが付いたということで、やはり未完成😆」「😆」「へ〜yamlでドキュメントのフラグ立ててるのか↓」

# guides/source/documents.yaml#L134
      name: Working with JavaScript in Rails
+     work_in_progress: true
      url: working_with_javascript_in_rails.html
      description: This guide covers the built-in Ajax/JavaScript functionality of Rails.

「#38240はテストの数値にoctet使うのやめようということだそうです↓」「1時4分をデジタル時計っぽく01, 04と書いたら8進数になってた😆」「危うくバグになりそうだけど7以下だからたまたまセーフ😆」「9時とかだったらアウト😆

# actionmailer/test/message_delivery_test.rb#L78
  test "should enqueue a delivery with a delay" do
-   travel_to Time.new(2004, 11, 24, 01, 04, 44) do
+   travel_to Time.new(2004, 11, 24, 1, 4, 44) do
      assert_performed_with(job: ActionMailer::MailDeliveryJob, at: Time.current + 10.minutes, args: ["DelayedMailer", "test_message", "deliver_now", args: [1, 2, 3]]) do
        @mail.deliver_later wait: 10.minutes
      end
    end
  end

「ところでこういう直さなくても動くトリビアな修正って、コアメンバー以外の人が投げてもすぐにマージされない可能性ありそうですね」「#38240ぐらいの修正ならわかりやすいから通りそうな気もしますけど☺」「Railsに初めてプルリクするならドキュメントのタイポ修正とかの方がむしろマージされやすいかも?🤔


# Gemfile#L44
gem "listen", "~> 3.2", require: false
gem "libxml-ruby", platforms: :ruby
gem "connection_pool", require: false
+gem "rexml", require: false

「rexmlっていうgemがRuby 3.0に標準で入るらしいです」「rexmlって見たことあるな」「XMLパーサー」

⚓Rails

⚓Railsで署名暗号化済みcookieをテストする(Ruby Weeklyより)

# 同記事より
class CookiesControllerTest < ActionDispatch::IntegrationTest
  test "should set cookies when getting the index" do
    get root_url
    assert_response :success
    assert_equal cookies["simple"], "Hello, I am easy to read."
    jar = ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash)
    assert_equal jar.signed["protected"], "Hello, I can be read, but I can't be tampered with."
    assert_equal jar.encrypted["private"], "Hello, I can't be read or tampered with."
  end
end

つっつきボイス:「記事は普通にテストのやり方の紹介でした」「cookieをちゃんと復元してテストしようという感じですね☺」「この辺はたまに追いかけてみるといいと思います😋

⚓structure.sqlのメリットデメリット(Ruby Weeklyより)


つっつきボイス:「structure.sqlって何だったっけ?と思って拾いました」「schema.rbみたいなヤツ?」「たしかschema.rbの代わりに使えるフォーマットが割と昔からRailsにあったと思いますが、それがstructure.sqlだったと思います☺」「そういえば😳

「schema.rbはあくまでRDBMSに依存しないRailsにとっての抽象化形式だけど、structure.sqlは文字どおりSQLで書けるので、使っているRDBMSに特化できるし、たしかrails db:setupなんかはstructure.sqlの方が速かった覚えありますね」「は〜なるほど」

「たしかrails db:setupはマイグレーションは見ないでschema.rbだけ見る: 通常のマイグレーションではマイグレーションファイルを順々に処理しますけど」「そうだった😳」「structure.sqlは要は生SQLなので、たとえばMySQLに特化したSQLとかストアドプロシージャも書こうと思えば書ける」「structure.sqlのメリットもデメリットもその辺にある感じですね」「手書きするとしてもどうせpg_dumpの結果あたりを使うでしょうし😆

「structure.sqlは昔からありますし、特にgemとか入れずにRailsの機能だけでできます☺」「Railsガイドにも載ってますね↓」「使ってるのはあんまり見たことありませんけど」「まあRails wayではないので🛤」「複合キーとか使う場合はstructure.sqlにするってガイドに載ってる👀」「Railsのマイグレーション機能でサポートされていないRDBMSの機能を使うのであればそうなりますね🧐

参考: 6.2 スキーマダンプの種類 — Active Record マイグレーション - Railsガイド

「まあschema.rbって途中参加したプロジェクトだとみっちり読みますけど、structure.sqlが使えるならそっちの方がRailsのデフォルトでもよかったかなという気もしますね😆」「😆

「銀座Rails↓でデータベースビューの話したときに言及したscenicっていうgemはcreate_viewっていうメソッドをマイグレーションに追加するんですよ」「ふむふむ」「rails db:setupで参照するschema.rbは自動生成されたものなので、たとえばマイグレーションのActiveRecord::Base.connection.executeでカスタム実行したSQLはそのままではrails db:setupで実行できないという大いなる問題がある😆: scenicはそれをやるために必要なんですね🧐」「知らなかった!😳」「structure.sqlならやれるんだし、データベースを切り替えることってめったにないので、それならschema.rbじゃなくてもよかったかなって思ったりするわけです😆」「😆

RDBMSのVIEWを使ってRailsのデータアクセスをいい感じにする【銀座Rails#10】

⚓closure_tree: Active Recordモデルで階層を扱う(Ruby Weeklyより)


つっつきボイス:「モデルで階層を扱うgemですが、TechRachoにもこのgemを使った記事がありました↓」「こっちの方が見やすい😋」「ネステッドツリーですね☺

Railsで木構造を扱うには

「この図↓いいですね〜😋

「なおancestryっていうgemの方がclosure_treeより有名かも↓」「こんなのもあるんですね😳」「このへんのドキュメントは1回ちゃんと読みましたよ: 実装するときは本読みながらじゃないとできませんけど、一度読んでおけば原理が何となく頭に残ってやりやすくなりますね☺


同リポジトリより

参考: 多階層カテゴリでancestryを使ったら便利すぎた - Qiita

⚓Webpack関連記事


つっつきボイス:「上は先週出したWebpacker翻訳記事とかぶるところもありますけど」「Webpackを中心に書いてるっぽいですね☺

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳)

「あとSprocketsのアップグレードガイド↓を今更見つけたんですけど、WIPかつ『内容は保証しない』だそうです😅

「そういえばSprocketsってこういう行↓の順序がさりげにすごく重要だったりして大変😅」「そうそうっ😤」「ちょっと順序変わるとなぜかプリコンパイルされなかったり😇」「Sprocketsもう思い出せない😆

// 同ドキュメントより
//= link_tree ../images
//= link application.css
//= link application.js
//
// maybe another non-standard file?
//= link marketing.css

「Webpackerの公式ドキュメントって情報があちこちに散らばってて追いにくくて😢: 最近見つけた以下の記事↓はRails 5.x時代のものなんですけどWebpackerについてよくまとまってて感激しました😂」「ほほぉ😋」「自分で振る舞いを追いかけながらまとめたんでしょうね☺

「記事ではWebpackerを剥がしてWebpackだけでやろうとしてるんですけど、そのためにjavascript_pack_tag的なヘルパーを自作しているのを見て、自分はそこまではしたくないなと思いました😆」「😆」「Webpackerはもともとそのためのものですからね☺」「ヘルパー自作するとほんのり車輪の再発明感が😆」「Webpackも別にRails用に作られたものではないので、まあしゃーない😆

⚓VueのI18nをRailsパイプラインに統合する(Hacklinesより)


同記事より


つっつきボイス:「JSのI18nってどうやったらいいのかいつも迷うヤツだ😭」「JSのI18nだけならそれこそjQueryのSelect2プラグインなんかが前からありますけどね☺」「ありゃそうでしたか😳」「リポジトリのdist/js/i18n/ja.jsあたりを見れば基本的な訳文がありますよ↓」「ほんとだ〜!」「これがJS的にI18nのベストプラクティスなのかどうか知りたいところですけど😆、これは太古の昔からあります」

!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define(“select2/i18n/ja”,[],function(){return{errorLoading:function(){return”結果が読み込まれませんでした”},inputTooLong:function(n){return n.input.length-n.maximum+” 文字を削除してください”},inputTooShort:function(n){return”少なくとも “+(n.minimum-n.input.length)+” 文字を入力してください”},loadingMore:function(){return”読み込み中…”},maximumSelected:function(n){return n.maximum+” 件しか選択できません”},noResults:function(){return”対象が見つかりません”},searching:function(){return”検索しています…”},removeAllItems:function(){return”すべてのアイテムを削除”}}}),n.define,n.require}();

「JSだけならいいんですけど、RailsのI18nをVue.jsに持ってきたいとかをやるのはつらいかなと思って」「あ、I18nファイルをRailsとJSで共有する話か😳」「かなと」「あ〜それは本気でつらいヤツ😭」「APIで問い合わせかけてもツライし😭」「記事真面目に読んでないけど、もしそれならたぶんやめといた方がいい気が😇」「どうやってもこじらせそう😇」「でも文言ドライにしたいしな〜😢

「I18nの共有って両方とも一人でやるならとてもいいんだけど、アプリが育ってフロントとバックエンドを手分けするようになったら速攻で破綻しそう😆」「ワカル😅」「最近はフロントエンジニアとバックエンドエンジニアに分かれて作業することも増えてきているし、そう考えると果たしてこの方法で両方とも幸せになるんだろうかと」「ですね😅

「やっていくうちに双方でキーの名前がだんだん食い違ってきたり😆」「でキーコンバーター作ったり😆」「フロントとバックエンドが別会社だったりするとさらに深刻😇」「その後でAndroidアプリとiOSアプリもやるなんてなったらもう地獄👹」「あ〜きっとそうなる😅」「表示幅もデバイスごとに違ったりもするんだし、そんなに頑張って統合しなくてもよくね?ってちょっと思ったりしますね☺」(以下延々)

⚓その他Rails


前編は以上です。

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

週刊Railsウォッチ(20200115後編)Ruby 2.7関連情報、Bootstrap 5は今年前半リリースか、PostgreSQLでやってはいけないリストほか

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines


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

$
0
0

morimorihogeです。2020とタイプしようとして2019と未だに間違える昨今です。

2020/01/16(木)に開催された銀座Rails#17 @リンクアンドモチベーションでTechRachoの週刊Railsウォッチの出張版ということで参加させていただきました。スライドは以下の通りです。

今回の特集は「リソース管理スコープについて」ということで、Railsアプリケーションコードを書くエンジニアから見たときに扱っているデータ(ファイルやメモリ上に展開したオブジェクト)がどこに保存され、それぞれどのような特徴を持つのかといった話を話題にしてみました。
「リソース管理スコープ」という言葉は僕の造語で一般的な技術用語ではないのですが、アプリケーションプログラマから見たこうしたデータリソース管理の概念を表す一般用語がなさそうだったので今回新たに名付けました。

Webアプリも昔のpureなCGI時代はDBとプロセス内のメモリくらいしかなかったのが、昨今のWebアプリケーションではサーバーホストの冗長化、アプリケーションサーバープロセスのマルチプロセス・マルチスレッド化、DB以外にもRedisなどのKVS的なデータ置き場にAWS S3のようなオブジェクトストレージなど、様々なデータを様々な場所に置いて開発するようになりました。

Rails Wayに従ってgeneratorベースのごくごく一般的なCRUDアプリケーションを書くだけであればDBサーバーなどのアプリケーション・サーバーの外部系のものだけ気にしていればそれほど大きな問題にはならないのですが、アプリケーションが育ってきてサービスオブジェクトやドメインオブジェクトを導入したり、高速化のためにキャッシュを導入していったりすると思わぬところでハマることがあります。

ある程度以上コンピュータサイエンスの知識があったり、そこそこにシステムプログラミングをしている人はこの辺り意識してコードを書いているかと思いますが、Ruby on Railsがはじめてのプログラミング言語という形で使い始めた人などは、今ひとつこの辺りのイメージがしっくり来ていない人もいるのではないでしょうか。

そんなわけで、この辺りの内容を自分でも整理して再構築したのがスライドになります。
この辺りの話はまだ僕の中でもきれいに整理されきった感覚がないのですが、まずはversion 1ということで話させていただきました。
後日きちんと記事に文章でまとめたバージョンも用意する予定ですので、そうした中で更にブラッシュアップしていければと思います。

次回は銀座Rails#18 @リンクアンドモチベーションは2/20(木)19:00~となります。
ピックアップトピックはまた開発・運用系から何か考えようと思っていますが、もし何か取り上げてほしいネタなどありましたらTwitterにて @morimorihoge までご意見お寄せ下さい。

週刊Railsウォッチ 公開つっつき会のお知らせ

1/9(木)19:30より、弊社会議スペースにて毎月恒例の週刊Railsウォッチ 公開つっつき会を開催します。気軽に技術雑談といった感じの会ですので、もしよろしければご参加検討下さい。

Rails 6: each_valueメソッドがActionController::Parametersに追加(翻訳)

$
0
0

概要

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

Rails 6: each_valueメソッドがActionController::Parametersに追加(翻訳)

Rails 6のActionController::Parametersに、新たにeach_valueメソッドが追加されました。

名前でわかるように、このメソッドはActionController::Parametersの各値をイテレートしてyieldします。ちょうどHash#each_valueのように使えます。

Hash#each_value vs ActionController::Parameters#each_value

ActionController::Parameters#each_valueHash#each_valueとは異なる点があります。ActionController::Parameters#each_valueは、値に含まれるすべてのハッシュをActionController::Parametersオブジェクトに変換してから値をブロックにyieldします。値をシンプルに返すわけではありません。

コード例

以下の例で考えてみましょう。ActionController::Parametersにユーザープロファイルの詳細を定義しています。

params = ActionController::Parameters.new({
  name: {
    first: "Narendra",
    last: "Rajput"
  },
  gender: "Male",
})

#=> <ActionController::Parameters {"name"=>{"first"=>"Narendra", "last"=>"Rajput"}, "gender"=>"Male"} permitted: false>

ここで、paramsに普通のハッシュオブジェクトをもうひとつ足してみます。

params[:social_profiles] = {
  twitter: "https://twitter.com/HiSaeloun",
  github: "https://github.com/saeloun"
}

#=> {:twitter=>"https://twitter.com/HiSaeloun", :github=>"https://github.com/saeloun"}

そしてこのparamsActionController::Parameters#each_valueでイテレートします。

params.each_value do |value|
  puts value.inspect
end

<ActionController::Parameters {"first"=>"Narendra", "last"=>"Rajput"} permitted: false>
"Male"
<ActionController::Parameters {"twitter"=>"https://twitter.com/HiSaeloun", "github"=>"https://github.com/saeloun"} permitted: false>

ご覧のとおり、paramsのsocial_profilesは1個のハッシュですが、each_valueブロックでActionController::Parametersに変換されています。

これで、パラメーターオブジェクトに一貫した方法でアクセスしやすくなります。

関連記事

Ruby 2.7: ハッシュからキーワード引数への自動変換が非推奨に(翻訳)

Docker内のbundler-auditが古い脆弱性警告を出す問題

$
0
0

Evil Martians流のRails開発Docker環境を使っていて気づいた点をメモ。

Rails 6のDocker開発環境構築をEvil Martians流にやってみた

Evil Martiansのdipを使う前提です。

docker-composeを便利にするツール「dip」を使ってみた

問題

Dockerにログインした状態でbundler-auditを実行すると、とっくに修正済みのはずの脆弱性警告が表示されます。

root@1528a26e7813:/app# bundler-audit
Name: rails-html-sanitizer
Version: 1.3.0
Advisory: CVE-2015-7579
Criticality: Unknown
URL: https://groups.google.com/forum/#!topic/rubyonrails-security/OU9ugTZcbjc
Title: XSS vulnerability in rails-html-sanitizer
Solution: upgrade to ~> 1.0.3

Name: rails-html-sanitizer
Version: 1.3.0
Advisory: CVE-2015-7578
Criticality: Unknown
URL: https://groups.google.com/forum/#!topic/rubyonrails-security/uh--W4TDwmI
Title: Possible XSS vulnerability in rails-html-sanitizer
Solution: upgrade to ~> 1.0.3

Name: rails-html-sanitizer
Version: 1.3.0
Advisory: CVE-2015-7580
Criticality: Unknown
URL: https://groups.google.com/forum/#!topic/rubyonrails-security/uh--W4TDwmI
Title: Possible XSS vulnerability in rails-html-sanitizer
Solution: upgrade to ~> 1.0.3

Vulnerabilities found!

Dockerの中でbundler-audit updateを実行しても変わりません。

root@0df0fbecba41:/app# bundle audit --update
Updating ruby-advisory-db ...
Skipping update
#(略)

解決方法

bundler-auditのissue↓によると、Dockerにgitをインストールする必要があるとのことでした。そういえば今の設定では入ってませんでした。gitが入ってないと通知してくれればいいのに😢

Evil Martians流であれば以下で修正できます。それ以外の方は頑張ってgitをDockerに追加してください。

  • Aptfilegitを追加する
  • docker rmi <イメージID>で既存のイメージを削除
  • dip compose buildでリビルド
  • dip shでログインし、bundler-audit update

dip + Evil Martians流はイメージのリビルドがやりやすくて助かりました😂rails newyarn install --check-filesをやり直す必要もありません。当初手動でgitを入れて結果を何とかしてDockerイメージにコミットしようとしてもがきましたが、dip + Evil Martians流ならリビルドする方が早かったのでした。

おまけ

特にRailsの開発環境の場合はgemのCネイティブ拡張のビルドのためにビルドツールも必要ですし、今回のようにgitが必要になる場合もあります。Apline Linuxやmulti stage buildでDockerイメージを薄くすることに頑張りすぎない方がいいかもという気がしてきました。

週刊Railsウォッチ(20200127前編)Railsでキーワード引数warning退治始まる、ライブラリとフレームワークの違い、ShopifyのRails高速化記事ほか

$
0
0

こんにちは、hachi8833です。Burikaigiが気になります。


つっつきボイス:「富山のBurikaigiがメシのうまさを大フィーチャーしてるので🍽」「冬の富山といえばブリでしょう🐟」「まだ行ったことなくて😢」「松江もいいけど富山もね😋

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

⚓お知らせ: 公開つっつき会2本立て

⚓その1: 週刊Railsウォッチ「第19回公開つっつき会」(無料)

いよいよ第19回を迎えた公開つっつき会は、来月2月6日(木)19:30〜よりBPS株式会社Pubスペースにて開催いたします。

週刊Railsウォッチの記事やここだけの話にいち早く触れられるチャンス!発言・質問も自由です。皆さまのお気軽なご参加をお待ちしております。

⚓臨時のお知らせ: 福岡でのリモート公開つっつき会は「延期」いたします(Wingdoor@福岡)

今週木曜に予定されていた福岡での公開リモートつっつき会は延期となりました🙇


connpass.comより

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

公式情報とコミットリストから見繕いました。

⚓Ruby 2.7キーワード引数のwarning退治

参考: Ruby 2.7 の変更点 - Module - @tmtms のメモ

# activejob/lib/active_job/arguments.rb#66
-     private_constant :PERMITTED_TYPES, :RESERVED_KEYS, :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
+     private_constant :PERMITTED_TYPES, :RESERVED_KEYS, :GLOBALID_KEY,
+       :SYMBOL_KEYS_KEY, :RUBY2_KEYWORDS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
+
+     unless Hash.respond_to?(:ruby2_keywords_hash?) && Hash.respond_to?(:ruby2_keywords_hash)
+       using Module.new {
+         refine Hash do
+           class << Hash
+             if RUBY_VERSION >= "2.7"
+               def ruby2_keywords_hash?(hash)
+                 !new(*[hash]).default.equal?(hash)
+               end
+             else
+               def ruby2_keywords_hash?(hash)
+                 false
+               end
+             end
+
+             def ruby2_keywords_hash(hash)
+               _ruby2_keywords_hash(**hash)
+             end
+
+             private def _ruby2_keywords_hash(*args)
+               args.last
+             end
+             ruby2_keywords(:_ruby2_keywords_hash) if respond_to?(:ruby2_keywords, true)
+           end
+         end
+       }
+     end

つっつきボイス:「Ruby 2.7のキーワード引数対応は大変な作業😅」「そのあたりのコミットを上にまとめました」「修行感ある😆」「kamipoさんのツイート↓も喜びに満ちてますね」「たしかにruby2_keywords_hash?は欲しい機能でしょうね😋

「あ〜確かにnon UTF-8が混じる可能性ある🥺: 自分の認識ではinspectはもともとヒューマンリーダブルにするためのメソッドなのであんまりバイナリセーフではないかも」「ふ〜む」「バイナリセーフにしたかったらMarshalあたりなのかな?🤔

Object#inspect
オブジェクトを人間が読める形式に変換した文字列を返します。
docs.ruby-lang.orgより

# docs.ruby-lang.orgより
[ 1, 2, 3..4, 'five' ].inspect   # => "[1, 2, 3..4, \"five\"]"
Time.new.inspect                 # => "2008-03-08 19:43:39 +0900"

ふと英語のmarshalとMarshallの違いが気になりました。

marshal: 整列させる; 整理する; 案内する. [軍事] 元帥; 隊長.
Marshall: {人名} : マーシャル◆ファミリーネーム◆【語源】「馬丁」から「位の高い役人」まで広い意味を持つ、マーシャル諸島

「ちなみにちなみに素のRails 6.0.2.1とRuby 2.7では起動しただけでコンソールにこれだけwarningが出ました↓😆」「これを今後つぶしていくのは大変だな…」

/bundle/ruby/2.7.0/gems/actionpack-6.0.2.1/lib/action_dispatch/middleware/stack.rb:37: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/bundle/ruby/2.7.0/gems/actionpack-6.0.2.1/lib/action_dispatch/middleware/static.rb:110: warning: The called method `initialize' is defined here
#(略)
/bundle/ruby/2.7.0/gems/activerecord-6.0.2.1/lib/active_record/type.rb:27: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/bundle/ruby/2.7.0/gems/activerecord-6.0.2.1/lib/active_record/type/adapter_specific_registry.rb:9: warning: The called method `add_modifier' is defined here
Started GET "/" for 172.19.0.1 at 2020-01-23 08:53:58 +0000
/bundle/ruby/2.7.0/gems/activemodel-6.0.2.1/lib/active_model/type/integer.rb:13: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/bundle/ruby/2.7.0/gems/activemodel-6.0.2.1/lib/active_model/type/value.rb:8: warning: The called method `initialize' is defined here
/bundle/ruby/2.7.0/gems/activerecord-6.0.2.1/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb:12: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/bundle/ruby/2.7.0/gems/activemodel-6.0.2.1/lib/active_model/type/value.rb:8: warning: The called method `initialize' is defined here
Processing by Rails::WelcomeController#index as HTML
/bundle/ruby/2.7.0/gems/actionview-6.0.2.1/lib/action_view/view_paths.rb:11: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/bundle/ruby/2.7.0/gems/actionview-6.0.2.1/lib/action_view/lookup_context.rb:140: warning: The called method `template_exists?' is defined here
/bundle/ruby/2.7.0/gems/actionview-6.0.2.1/lib/action_view/unbound_template.rb:24: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/bundle/ruby/2.7.0/gems/actionview-6.0.2.1/lib/action_view/template.rb:130: warning: The called method `initialize' is defined here

⚓MySQL 8.0.19でCIが落ちる問題を修正

# activerecord/test/cases/migration/change_schema_test.rb#L146
        elsif current_adapter?(:Mysql2Adapter)
-         assert_match "int(11)", default.sql_type
-         assert_match "tinyint", one.sql_type
-         assert_match "int", four.sql_type
-         assert_match "bigint", eight.sql_type
+         assert_match %r/\Aint/, default.sql_type
+         assert_match %r/\Atinyint/, one.sql_type
+         assert_match %r/\Aint/, four.sql_type
+         assert_match %r/\Abigint/, eight.sql_type

つっつきボイス:「MySQLの出力する文言が変わったというよくある修正☺」「修正後はアサーションに正規表現が入ってきましたね」

# activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb#L80
-   assert_includes error.message, <<~MSG.squish
-     Column `old_car_id` on table `engines` does not match column `id` on `old_cars`,
-     which has type `int(11)`. To resolve this issue, change the type of the `old_car_id`
-     column on `engines` to be :integer. (For example `t.integer :old_car_id`).
-   MSG
+   assert_match(
+     %r/Column `old_car_id` on table `engines` does not match column `id` on `old_cars`, which has type `int(\(11\))?`\./,
+     error.message
+   )
+   assert_match(
+     %r/To resolve this issue, change the type of the `old_car_id` column on `engines` to be :integer\. \(For example `t.integer :old_car_id`\)\./,
+     error.message
+   )
+   assert_not_nil error.cause
  ensure
    @conn.execute("ALTER TABLE engines DROP COLUMN old_car_id") rescue nil
  end

⚓#remove_connection#remove_connection_poolに変わる

  • #remove_connectionが非推奨になり、ハンドラで呼ばれるときは#remove_connection_poolが推奨になる。
    #remove_connectionが非推奨になった理由は、Hashの代わりにDatabaseConfigを返すことをサポートするため。#remove_connectionは6.2で削除されるので今後は#remove_connection_poolを使うこと。
    Changelogより大意

つっつきボイス:「poolが付いたのは何でなんでしょう?🤔」「おそらくですが、今後はコネクションプール単位でのみ扱うという意図があるのかもしれませんね☺: 今はマルチプルデータベースが基本になっていますし」「ふむふむ」「configuration_hashを参照しているあたり、コネクションプール単位じゃなくてコネクション単位だと不整合が生じそうですし」

「コネクションプール自体は前からあったんでしょうけど」「以前は同じデータベースにつなぐコネクションプールしかありませんでしたね: この修正はたぶんRails 6でマルチプルデータベースになったことと関連しているでしょうし、remove_connectionだとコネクションを削除するように見えて誤解を招くと考えたんじゃないかな?🤔」「なるほど!」「まああんまりアプリ開発者が呼び出すコードではなさそうですけど😆

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L1129
      def remove_connection(owner, pool_key = :default)
+       remove_connection_pool(owner, pool_key)&.configuration_hash
+     end
+     deprecate remove_connection: "Use #remove_connection_pool, which now returns a DatabaseConfig object instead of a Hash"
+
+     def remove_connection_pool(owner, pool_key = :default)
        if pool_manager = get_pool_manager(owner)
          pool_config = pool_manager.remove_pool_config(pool_key)

          if pool_config
            pool_config.disconnect!
-           pool_config.db_config.configuration_hash
+           pool_config.db_config
          end
        end
      end

cc/ @rafaelfranca @matthewd @jhawthorn @tenderlove
AR Baseのremove_connectionの値を返すように変更してよいかどうか今のところ不明だが、ドキュメントに記載がないのでこのやり方にして他を非推奨にする方法を選んだことで、オブジェクトをどこにでも引き回せるようになった。configuration_hashを返すことの問題は、返された設定がestablish_connectionに渡されるとRailsが新しいオブジェクトを生成することだった。このpublicメソッドの戻り値の変更について何か意見は?
同PRより抜粋・大意

⚓新機能: ActionCable::Channelにストリームを個別に終了する機能が追加

ユーザーが特定のストリームのサブスクライブを解除できるよう、ActionCable::Channel#stop_stream_fromActionCable::Channel#stop_stream_forを追加した。
まとめ:
ユーザーがstream_from``stream_forを繰り返し呼ぶと1つのチャンネルで複数のストリームをフォローできるようになる。把握している限りでは、ActionCableには全ストリームのフォローを解除するActionCable::Channel#stop_all_streamsしかなかったので、ユーザーが特定のストリームをサブスクライブ解除できるようメソッドを2つ追加した。
同PRより大意


つっつきボイス:「いわゆるpub/sub」「ぱっと見startとstopかと思ったらstopだけでした😅」「fromとforは対象が違うのか」

# actioncable/lib/action_cable/channel/streams.rb#L105
+     # 名前付きブロードキャストのストリームをサブスクライブ解除
+     def stop_stream_from(broadcasting)
+       callback = streams.delete(broadcasting)
+       if callback
+         pubsub.unsubscribe(broadcasting, callback)
+         logger.info "#{self.class.name} stopped streaming from #{broadcasting}"
+       end
+     end
+
+     # モデルのストリームをサブスクライブ解除
+     def stop_stream_for(model)
+       stop_stream_from(broadcasting_for(model))
+     end

「この||= {}は?↓」「これは単にストリームが空のときは空ハッシュを返したいということです😆」「修正前は配列だったんですね」「そこが今回の修正にも関連していますね: 配列だとeachとかで取り出さないといけないけど、ハッシュにすればbroadcastingを引数にして特定の名前付きストリームを後から指定できるようになりますし」「なるほど!」「delegate :pubsub, to: :connectionとかせやなという感じ☺

# actioncable/lib/action_cable/channel/streams.rb#L127
      private
        delegate :pubsub, to: :connection

        def streams
-         @_streams ||= []
+         @_streams ||= {}
        end

「そんなにやらないかもしれないけど、特定のストリームのstopってたしかに使いたいときありそう、つか今までなかったのね😆

⚓番外: マイグレーションファイルの空行を削除

# activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt#L33
<%- if migration_action -%>
  <%- if attribute.reference? -%>
    remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %><%= foreign_key_type %>
  <%- else -%>
    <%- if attribute.has_index? -%>
    remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
    <%- end -%>
-   <%- if !attribute.virtual? %>
+   <%- if !attribute.virtual? -%>
    remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
    <%- end -%>
  <%- end -%>

つっつきボイス:「公式の更新情報にしてはめちゃトリビアかなと思って😆」「誰もが消すヤツ😆: あ〜-が脱落してたのか」「ERBの-%>って改行しない指定でしたっけ?」「ですです」

参考: class ERB (Ruby 2.7.0 リファレンスマニュアル)

“-“: 行末が-%>のとき改行を出力しない。また、行頭が<%-のとき行頭の空白文字を削除する
docs.ruby-lang.orgより

⚓Rails

⚓PunditとCanCanCanを比較しながら学ぶ


つっつきボイス:「どちらもいわゆる権限管理のgemですね」「どっちに乗るかで常に争いの火種になるヤツ🔫」「まあまあ😆」「使いたい方を使えばいいんじゃないかと😆: なお自分はCanCanCanキライじゃないんだけど、そうではない人もいましてですね🤣」「かといってPunditが好きというわけでもないと聞きました🤣」「マシという程度かも😆

「印象としてはPunditの方が崩壊しずらいけど、CanCanCanはリクエストから原因を比較的追いかけやすい感じですかね、といってもここまでやられると↓つらそう😇」「ヤメテ〜😆


同記事より


目次より、比較のポイント:

  • パーミッションのコード構成
  • レコードのフェッチ方法
  • テストのやりやすさ
  • パフォーマンス

個人的にはこの種のgemはパフォーマンスで選ばない方がいいと思う。既存のアプリでgemを乗り換えるのはたぶん意味ない。
個人的にはPunditがおすすめか: パーミッションをコントローラのアクション単位で定義できるし。
同記事より抜粋・大意

⚓Active Recordメソッドを要件に合わせて選ぶ

関係ありませんが、medium.comのアカウントの試用期間が切れました。期間過ぎると有料だったとは…😇


つっつきボイス:「初心者に優しそうな記事です」「Active Recordはメソッド多すぎですから😆

「なるほどfind()find_by()where()とかか: この辺はぜひRails初心者に知っておいていただきたいですね〜、find()はレコードがないと404をraiseするけどfind_by()はしないとか☺

  • .find()
  • .find_by()
  • .where()

「そうそう、take()はなにげに速いんですよね🚄

  • take()
  • first()last()

「とは言うものの、こういうのは結局自分で動きを追いかけないとわかってこないんですよ😅: あとSQLもある程度わかっておく必要ありますし」「たしかに」「自分の書いたコードがどんなSQLを生成するかに関心を持ってないとなかなかこういう部分を追いかけませんし☺

「こういうのはコードレビューを受けるうちにだんだん身に付いてくると思います: この記事のレベルに達していないコードにはこういう記事のリンクを渡して読んでもらう方が早いかも😆」「身に付けておきたい常識ということですね😊

参考: find、find_by、whereの違い - Qiita

⚓ライブラリとフレームワークの違い


同記事より


つっつきボイス:「ライブラリとフレームワークの違い、これもよく言われているヤツですね☺: ところで最近のイベントでも似たようなテーマで発表しているのを見た気がしますね」「銀座Railsとかでしょうか?」「どれだったかな〜思い出せん😆

「この種の議論にはいろんな視点がありますけど、そのイベントで聞いた見解は割と自分にとってもしっくり来たんですよ😋」「ふむふむ」

「と思ったらこの記事で引用されている図↓(引用元: differencebetween.net)にもそれに通じる見解が書いてあった: 2段目のフレームワークの『自分のコードがフレームワークを呼ぶのではなく、フレームワークが自分のコードを呼ぶ』のあたりが自分にもしっくりくる👍


同記事より

「ライブラリメソッドはユーザーのコードから呼び出すものだし、Railsで言えば、Rails wayに沿ってコードを書けばRailsフレームワークがそのコードを呼び出す: これが自分が持つライブラリとフレームワークのイメージに近いですね☺

⚓中間テーブルを疑え


つっつきボイス:「Andy Crollさんの短い記事です」「HABTM(has_and_belongs_to_many)久しぶりに見た😆」「やめとけってよく言われるヤツですね😆」「とっくにdeprecatedと思ってたけど、いつの記事?」「今年出た記事ですね」「2020年にHABTM見るとは思わなかった😆

# 同記事より
class User < ApplicationRecord
  has_and_belongs_to_many :organisations
end

後で探すと、Rails 6になってもHABTMは一応残ってますね。

現時点のmasterで確認すると「has_and_belongs_to_manyの中間テーブルにカラムを追加することがdeprecated」ということでした。

WARNING: The use of extra attributes on the join table in a has_and_belongs_to_many association is deprecated. If you require this sort of complex behavior on the table that joins two models in a many-to-many relationship, you should use a has_many :through association instead of has_and_belongs_to_many.
github.com/rails/railsより

参考: 仕事のねた: rails3でHABTMが非推奨になってる — 2011年の記事です


「記事の中身はというと、HABTMよりも、名前に意味のあるモデルでやろうという基本的な話かな☺」「ふむふむ」「中間テーブルが持つエンティティに意味があるケースは多いんですけど、Railsのhas_many through:がそのまま使えるとも限らないんですよ」「記事にもhas_many through:はHABTMよりもメソッドが少ないってありますね😳」「HABTM使わなくなって長いんでもう思い出せませんけどっ🤣

⚓GitHub ActionsでRailsとPostgreSQLをセットアップ


つっつきボイス:「チラ見した感じでは結構いい記事っぽい!❤」「やった!」「こういうテンプレ的なものがあるのはいいですね〜😋: GitHub Actionsやりたい人によさそう👍」「たしかに😋」「CIのコンフィグとか、全部自分で考えて構築するよりも誰かが確立したテンプレでやりたいですし☺」「翻訳しようかな😋」「完璧かどうか知りませんけど😆、少なくとも記事のとおりにやれば動くコンフィグ書けそうですし」

目次より:

  • 必要なもの
  • 完全なワークフローの例
  • オプション: secretを環境変数として使う
  • GitHub Actionsを使ってみた所見と現状の問題点:
    • .nvmrcや.ruby-versionのサポートがない
    • ged/ruby-pgなどはlibpq-devを自分でapt installしないといけない(デフォルトにすべき)
    • 新バージョンが出たら通知してくれたらいいのに(ボットがプルリク投げてくれたらさらに嬉しい)
    • ワークフローを手動でトリガできない

⚓Shopify流: Railsで速いコードを書く方法

つっつきボイス:「翻訳記事です」「Shopifyはスゴいですよね〜: エンジニア200人ぐらいいるらしいですけど、Railsエンジニアがそんなにいて崩壊しないところとか💪」「マジで😳

「『クエリキャッシュを信用しすぎない』はたしかに☺

「Large Hadron Migrator?」「Shopifyの独自ツールみたいですね☺」「ハドロンってやっぱり粒子加速器のアレだ: ハドロン、バリオン、メソンとか😆」「へ〜、どうやらLHMは、テーブルをALTERするとロックされるみたいなよくある問題をよしなにうまくやってくれるものみたい」「あ〜以前のウォッチでも話ありましたね」「LHMのThrottlerは、スレーブというかレプリカにそのまま投げると確実に詰まるような巨大データをよしなに絞ってくれるっぽい」「なるほど」


同リポジトリより

参考: 大型ハドロン衝突型加速器 - Wikipedia

「きっと特定のRDBMS用だろうと思ったらやっぱりMySQL用ですね: Shopifyは自分で作って自分で使ってるからいいんですけど、外部の人間としてはこのgemの挙動をきっちり理解しないと怖くて使えない😱」「😆」「データベースのマイグレーションに失敗すると悲惨ですから😇

「それに最近耳にした噂&うろ覚えレベルでは(おそらく銀座Railsあたり?)、MySQL 8あたりからはALTER TABLEしてもロックしなくなったらしいとか、あとAWS Auroraもいけるらしいとか何とかあるみたいですし」「MySQLもいろいろ変わってきてるんですね☺」「MySQLのissueとか注意点はいろいろあがってるので、彼らも修正しようと頑張ってますし☺

「お、このツールはforkを辿るとSoundCloudのリポジトリになってる: こういうgemを公開しているとは知らなかった」「ホントだ😳: gem作ってるということはどっかでRails使ってるんでしょうね」「でしょうね」

「SoundCloudって?」「およ、このアイコン↑見たことありません?」「Instagramの音楽版みたいなSNSでしたっけ」「そんな感じですね: 音楽ずっとやってたならむしろ詳しいかと思ってましたけど😆」「近年は音楽方面を努めて見ないようにしてるので😅

⚓その他Rails

つっつきボイス:「この間BPSの社内Slackに貼った記事なんですけど、手元では既に翻訳したので近々公開します」「そうそう、オブジェクト指向トレースの記事😋」「記事で使っているtapping_deviceにグラフ表示を追加する構想もあるそうです❤

「Rubyってそもそもこういうデバッグ作業がツラいんですよね😭: 特にRailsみたいに動的にクラスやモジュールをロードするような世界だとキレイなグラフを描くのは難しいですし」「う〜む」「自分たちが書いたコードに絞り込めるならやれると思うので、それができるならいいなと思いますね☺

「いわゆるDDDというか、ビジネスロジックをちゃんとオブジェクト指向で開発していれば、こういうツールでのグラフ化はもっとやりやすくなるでしょうけど: Railsフレームワークの動きは見ないようにして、自分たちのロジックだけにフォーカスして呼び出し依存を調べたりとか☺」「なるほど」


「最近流行りのエントリです😆」「ソニックガーデンの伊藤さん編サイコー😆」「😆」「それにしてもよくこんなにたくさん書けるなって思いますね☺」「怒涛の勢い🌊」「自分もこのお題で書いてみようかな?😋

「Rubyのブロックが最初わからなかったとあるのが意外でした」「Rubyのブロックは最初はわからないと思いますよ: あれは結局のところ他の言語で言うlambdaですし」「お〜」「今でこそJavaにもlambdaありますけど昔はありませんでしたし☺

参考: Java 8 のイディオム: 完璧なラムダ式がたった 1 行である理由

「Rails 3.1ぐらいで初めてRuby触ったときもeachで回してdoするとか最初意味わかりませんでしたもん😆」「へぇ〜!」「ブロックが引数として渡されるという概念が当時の自分になかったんですよね: PHPならわかったんで最初はPHPの関数渡しみたいなものかな?って思ったり😆

「ルーティングとRESTも最初わからなかったともありますね」「自分はRESTについては、あのあまりにも有名な論文↓で先に知ってたので😆」「先行する論文があったんですね😳」「ノリとしては『HTTPメソッドを見直そう』という感じで、論文が出た2000年頃ってAjaxが流行りだしたあたり」「なるほど」「そういうのを思い思いにやるんじゃなくて、RESTfulにやるのが大事だよね、という流れでしたね☺

参考: Architectural Styles and the Design of Network-based Software Architectures
参考: REST論文からアーキテクチャを進化させる方法論を学ぶ - Qiita


「Javaのようなきれいなアーキテクチャのある世界からRailsを見たらこういう見解になるかもしれませんね☺: 自分はPHP沼方面から来たんですけど、PHPとかPerlのようなスクリプト言語系で、しかもみんながオレオレフレームワークを作りまくってたような何でもありな世界からRailsに来ると、ジェネレートみたいにスクリプト言語の柔軟性を活かしながら、Java的な固さも取り入れていて、これがRailsなのかな〜って思ったりしましたね😉」「なるほど〜」


前編は以上です。

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

週刊Railsウォッチ(20200121後編)RubyKaigi 2020受付開始、RubyGemsとBundlerの今後、ファイル同期ツールMutagenほか

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

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

Rails公式ニュース

Ruby: 「オブジェクト指向トレース」とtapping_device gemで効率よくデバッグ(翻訳)

$
0
0

概要

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

Ruby: 「オブジェクト指向トレース」とtapping_device gemで効率よくデバッグ(翻訳)

デバッグはプログラマーにとって日常的な仕事のひとつです。プログラミングのスキルのみならず、デバッグのスキルも磨き上げることが重要です。しかしリファクタリングやテストといったコーディングスキルの伸びに比べると、デバッグスキルがイマイチ伸びないと思ったことはありませんか?私も振り返ってみると、数年前のデバッグツールや手順をずっと使い続けていたことに気づきました。

私はここ数週間デバッグを効率よく行う方法を模索し続けてきました。本記事では、デバッグスキルを伸ばすのが難しい理由と、この問題に別の方面から取り組む方法について解説します。

(簡単のため、本記事では「バグ」という言葉を「以前問題なく動いていたプログラムを壊すコード片」という意味に限定して用います)

プログラムのデバッグ手順について

実際のデバッグの方法は、「比較」という極めて素朴な行為です。プログラムの「こう動くべき」動作と「実際の」動作を比較し、プログラムの実行中にそのズレがどこから始まるかを探し当てることでデバッグします。ほとんどの場合、それらがバグを見つける手がかり(バグそのものでなければですが)となります。これは以下の3つの手順で行います。

  1. 期待する実行パスを構築する
  2. 実際の実行パスを構築する
  3. 両者のパスを比較する

以下の図で例を示します。

実行パスを構築する

プログラムの「実行パス」とは、ある意味メソッドや関数の呼び出しのパス、あるいはもっとシンプルに「呼び出しパス」と考えることもできます。しかしプログラムのサイズによってはメソッド呼び出しが膨大になる可能性もありますので、普通は「ビジネスロジックに関連するメソッドの呼び出しパス」に限定します。たとえば、OrdersController#createリクエスト中にはArray#[]とかObject#inspectといった呼び出しが多数発生しますが、実行パスを構築するうえではOrderの作成にのみ注目し、それ以外は無視します。

実行パスを比較する

ほとんどの条件下では、プログラムのバグは振る舞いが最初に異なる場所(戻り値が異なる、違うメソッドが呼ばれるなど)で見つかります。そこで以下のケースでは、バグはおそらくbarメソッドかその依存ファイルの中に潜んでいます。

この方法の困難な点

ほとんどの開発者がこのアプローチでデバッグしていると私は信じていますが、アプローチは完全に同じではありません。理想としては実行パスをできるかぎり詳細に構築すべきであり、それによって正確な比較に必要な情報を十分に得られます。しかし現実には時間も足りませんし勤労意欲が落ちるときもありますので、そのバグに関連すると「思われる」メソッドをエイヤでいくつか選んで、後は選んだメソッドが正しいことを祈るのが関の山です。

つまり、デバッグ手法の効率性は、パスを構築するときにいかに適切なメソッドを選ぶかにかかっています。そしてこれは以下の要素に影響されます。

  1. その言語なりフレームワークなりにおける自分の経験
  2. コードベースをどれだけ理解しているか
  3. 運の強さ?

デバッグスキルの向上をそれほど心がけていなくても、デバッグに要する時間が時間とともに短くなっていく理由はこれです。

私見では、この現象の根本原因はデバッグ戦略とうまく調和する効率的なツールがないからだと思います。その点はputsデバッグであろうとpryデバッガーであろうと同じです。ほとんどの場合、プログラムの振る舞いを精査する作業には膨大な時間を要しますが、それにはいくつもの理由があります。

1. 引数を手動で集める必要がある。

# 超つらい
def foo(var1, var2)
  puts("var1: #{var1}")
  puts("var2: #{var2}")
  ...
end

2. 戻り値を手動で集める必要がある。

# これとか
def new_cart(attributes)
  Cart.new(attributes).tap { |c| puts(c) }
end

# これとか
cart = new_cart(attributes)
puts(cart)

3. さまざまなメソッド間を行ったり来たりしながら、putsやブレークポイントを置いたりする必要がある。

情報を集める作業を手動で行う割合が増えるほど、推測やバクチに頼るようになります。この作業はつらいだけでなく、さまざまな情報をかき集めるうちに以下のような別の問題まで発生します。

  1. 必要な情報(正しいメソッドを正しい場所で呼んでいるかなど)を効率的に集めるには、プログラムを十分理解する必要があります。経験の浅い開発者ほど理解は困難になります。
  2. ブレークポイントやトレース用メソッドを追加するとコードが汚染されます。たとえば、あるメソッドがあるオブジェクトのステートを変更しているとすると、コードのトレース時に(戻り値を取りたいなどの理由で)そのメソッドを呼び出すと、プログラムの振る舞いが変わってしまうかもしれません。
  3. 私の経験では、コードが散らかっていると自分がそもそも何をしたかったのか(=メソッド呼び出しを調べて深掘りする)を忘れてしまいがちです。

「オブジェクト指向トレース」

ここでデバッグ時に私たちが行っていることをよく観察すると、うまく活用できそうなパターンがいくつか見えてきます。たとえば以下がそうです。

  1. Webアプリケーションのある機能をデバッグしているのであれば、特定のエンドポイントのロジック(特定のコントローラのアクションなど)から調査を開始する手が考えられます。これはデバッグを開始するのに最適な場所となります。
  2. Rubyのようなオブジェクト指向プログラミング言語では、ある機能が1つまたは複数のクラスにリンクしているのが普通です。たとえばorderにバグがあるならOrderCreationServiceあたりを調べるべきでしょう。つまりオブジェクトのいくつかのメソッド呼び出しを観察することで実行パスの大半を構築できるわけです。

「オブジェクト指向トレース」という概念は、以上の仮定と、Rubyの強力なTracePoint機能の上に成り立ちます。いくつか例を示して説明します。

OrdersController#createというエンドポイントがあり、ここでorderを作成するとします。

class OrdersController < ApplicationController
  def create
    @cart = Cart.find(order_params[:cart_id])
    promotion = Promotion.find_by(id: order_params[:promotion_id])
    @order = OrderCreationService.new.perform(@cart, promotion)
    ......
  end

このorder作成の実行パスを構築してみたいと思います。これは以下のように書けます。

class OrdersController < ApplicationController
  def create
    @cart = Cart.find(order_params[:cart_id])
    promotion = Promotion.find_by(id: order_params[:promotion_id])

    TracePoint.new(:call) do |tp|
      if tp.self.class.name == "OrderCreationService"
        puts("Called :#{tp.callee_id} from #{tp.path}:#{tp.lineno}")
      end
    end.enable do
      @order = OrderCreationService.new.perform(@cart, promotion)
    end

上のコードから以下のような結果が出力されます。

Called :initialize from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:2
Called :perform from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:6
Called :validate_cart from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:17
Called :apply_discount from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:23
Called :create_order from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:27

これでOrderを1つ作成するためにOrderCreationServiceが何をしているかが判明し、かなり明確な実行パスを直ちに得られました。しかし、ここでRubyのメタプログラミング技を少々用いれば、メソッド呼び出しごとに引数を取るなどの詳細な情報を取れるのです!

class OrdersController < ApplicationController
  def create
    @cart = Cart.find(order_params[:cart_id])
    promotion = Promotion.find_by(id: order_params[:promotion_id])

    TracePoint.new(:call) do |tp|
      if tp.self.class.name == "OrderCreationService"
        puts("Called :#{tp.callee_id} from #{tp.path}:#{tp.lineno}")
        tp.binding.local_variables.each do |name|
          value = tp.binding.local_variable_get(name)
          puts("  Arg #{name}: #{value.inspect}")
        end
      end
    end.enable do
      @order = OrderCreationService.new.perform(@cart, promotion)
    end
Called :initialize from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:2
  Arg options: {}

Called :perform from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:6
  Arg cart: #<Cart id: 1, total: 10, customer_id: 1, promotion_id: nil, reserved_until: nil, created_at: "2020-01-19 08:41:51", updated_at: "2020-01-19 08:41:51">
  Arg promotion: #<Promotion id: 1, amount: 0.5e1, customer_id: nil, created_at: "2020-01-19 08:41:51", updated_at: "2020-01-19 08:41:51">

Called :validate_cart from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:17
  Arg cart: #<Cart id: 1, total: 10, customer_id: 1, promotion_id: nil, reserved_until: nil, created_at: "2020-01-19 08:41:51", updated_at: "2020-01-19 08:41:51">

Called :apply_discount from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:23
  Arg cart: #<Cart id: 1, total: 10, customer_id: 1, promotion_id: nil, reserved_until: nil, created_at: "2020-01-19 08:41:51", updated_at: "2020-01-19 08:41:51">
  Arg promotion: #<Promotion id: 1, amount: 0.5e1, customer_id: nil, created_at: "2020-01-19 08:41:51", updated_at: "2020-01-19 08:41:51">

Called :create_order from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:27
  Arg cart: #<Cart id: 1, total: 5, customer_id: 1, promotion_id: 1, reserved_until: nil, created_at: "2020-01-19 08:41:51", updated_at: "2020-01-19 08:41:51">

めちゃクールですよね?呼び出されたメソッドや、受け取った引数がすっかり明らかになりました。それでは最後の仕上げに戻り値も情報に加えてみましょう。

class OrdersController < ApplicationController
  def create
    @cart = Cart.find(order_params[:cart_id])
    promotion = Promotion.find_by(id: order_params[:promotion_id])

    TracePoint.new(:return) do |tp|
      if tp.self.class.name == "OrderCreationService"
        puts("Called :#{tp.callee_id} from #{tp.path}:#{tp.lineno}")
        tp.binding.local_variables.each do |name|
          value = tp.binding.local_variable_get(name)
          puts("  Arg #{name}: #{value.inspect}")
        end
        puts("  => #{tp.return_value}")
      end
    end.enable do
      @order = OrderCreationService.new.perform(@cart, promotion)
    end
Called :initialize from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:4
  Arg options: {}
  => {}

Called :validate_cart from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:21
  Arg cart: #<Cart id: 1, total: 10, customer_id: 1, promotion_id: nil, reserved_until: nil, created_at: "2020-01-19 08:59:13", updated_at: "2020-01-19 08:59:13">
  =>

Called :apply_discount from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:25
  Arg cart: #<Cart id: 1, total: 5, customer_id: 1, promotion_id: 1, reserved_until: nil, created_at: "2020-01-19 08:59:13", updated_at: "2020-01-19 08:59:13">
  Arg promotion: #<Promotion id: 1, amount: 0.5e1, customer_id: nil, created_at: "2020-01-19 08:59:13", updated_at: "2020-01-19 08:59:13">
  => true

Called :create_order from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:29
  Arg cart: #<Cart id: 1, total: 5, customer_id: 1, promotion_id: 1, reserved_until: nil, created_at: "2020-01-19 08:59:13", updated_at: "2020-01-19 08:59:13">
  => #<Order:0x00007f91455ebd10>

Called :perform from /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:13
  Arg cart: #<Cart id: 1, total: 5, customer_id: 1, promotion_id: 1, reserved_until: nil, created_at: "2020-01-19 08:59:13", updated_at: "2020-01-19 08:59:13">
  Arg promotion: #<Promotion id: 1, amount: 0.5e1, customer_id: nil, created_at: "2020-01-19 08:59:13", updated_at: "2020-01-19 08:59:13">
  => #<Order:0x00007f91455ebd10>

上の例を注意深く読んでいただければ、戻り値が加わった以外にも、コードや出力で変わった点があることに気づくでしょう。

  1. TracePointreturnイベントをトラッキングするようになった(その前の2つの例ではcallイベントだった)。
  2. 出力の順序がその前の結果とどことなく異なっている

理由は、メソッド呼び出しの完了(return)を、戻り値を取り出せるようになるまで待つ必要があるからです。つまり出力は(呼び出された順ではなく)メソッドが戻った順に並びます。

def perform
  # ...
  create_order
end

# call order: perform -> create_order
# return order: create_order -> perform

このわずかな違いを理解しておけば、出力を読んでいてもさほど混乱はしないでしょう。実際、私はこれがいくつかの場合にさらに有用であることに気づきました。たとえば、同じ値をさまざまなメソッドが更新して戻す様子を観察したいのであれば、戻り値順に並ぶ方が自然です。

オブジェクト指向トレースを用いれば、プログラムの呼び出しパス、呼び出しごとの引数、そして戻り値も一発で得られます。オブジェクト指向トレースは実に効率的であるのみならず、前述した問題のいくつかを回避することもできます。

  1. 従来だと、OrderCreationServiceのメソッド内部を把握して必要な情報を集めるために、OrderCreationServiceが何を呼び出すかを理解する必要があったが、OrderCreationService#performが呼び出される場所を把握するだけでできるようになった(これなら楽勝です😋)。
  2. OrderCreationServiceのロジックを詳しく把握したりテストする必要がなければ、コードを一切変更する必要がない。そのおかげで、デバッグ中でもコードを汚さずに済みます。

TappingDevice

欲しい機能はTracePointに既にひととおりありますが、もっと楽にやるための改良点がいくつかあります。

  1. ボイラープレートコード(デバッグ用のテンプレートコード)が10行もあるので覚えるのがダルい。
  2. TracePointに慣れないうちはいくつかエッジケースを踏んでしまうかもしれません(私もそうでした)。それを回避しようとするとボイラープレートコードが長くなりがちです。

そうした理由から、私は皆さんに代わってtapping_deviceを作って楽にやれるようにしました。さっき最後に書いたコード例を思い出せますか?私は無理です😆。では、あのボイラープレートのコード片を1個のメソッド呼び出しにしてみたらどうでしょう?

  def create
    @cart = Cart.find(order_params[:cart_id])
    promotion = Promotion.find_by(id: order_params[:promotion_id])
    service = OrderCreationService.new

    TracePoint.new(:return) do |tp|
      if tp.self.class.name == "OrderCreationService"
        puts("Called :#{tp.callee_id} from #{tp.path}:#{tp.lineno}")
        tp.binding.local_variables.each do |name|
          value = tp.binding.local_variable_get(name)
          puts("  Arg #{name}: #{value.inspect}")
        end
        puts("  => #{tp.return_value}")
      end
    end.enable do
      @order = service.perform(@cart, promotion)
    end
    # ...
  end

上のように書く代わりに、以下のようにたった2行追加するだけでやれます。

  include TappingDevice::Trackable # includeしておく

  def create
    @cart = Cart.find(order_params[:cart_id])
    promotion = Promotion.find_by(id: order_params[:promotion_id])
    service = OrderCreationService.new

    print_calls_in_detail(service) # <--- この場合はこれだけあればよい
    @order = service.perform(@cart, promotion)
    # ...
  end

これで以下のように同じ情報を得られます!

:validate_cart # OrderCreationService
    from: /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:8
    <= {:cart=>#<Cart id: 1, total: 10, customer_id: 1, promotion_id: nil, reserved_until: nil, created_at: "2020-01-20 07:09:22", updated_at: "2020-01-20 07:09:22">}
    => nil
:apply_discount # OrderCreationService
    from: /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:10
    <= {:cart=>#<Cart id: 1, total: 5, customer_id: 1, promotion_id: 1, reserved_until: nil, created_at: "2020-01-20 07:09:22", updated_at: "2020-01-20 07:09:22">, :promotion=>#<Promotion id: 1, amount: 0.5e1, customer_id: nil, created_at: "2020-01-20 07:09:22", updated_at: "2020-01-20 07:09:22">}
    => true
:create_order # OrderCreationService
    from: /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:11
    <= {:cart=>#<Cart id: 1, total: 5, customer_id: 1, promotion_id: 1, reserved_until: nil, created_at: "2020-01-20 07:09:22", updated_at: "2020-01-20 07:09:22">}
    => #<Order id: 1, number: nil, total: 5, customer_id: 1, promotion_id: 1, created_at: "2020-01-20 07:09:22", updated_at: "2020-01-20 07:09:22">
:perform # OrderCreationService
    from: /Users/st0012/projects/tapping_device-demo/app/controllers/orders_controller.rb:10
    <= {:cart=>#<Cart id: 1, total: 5, customer_id: 1, promotion_id: 1, reserved_until: nil, created_at: "2020-01-20 07:09:22", updated_at: "2020-01-20 07:09:22">, :promotion=>#<Promotion id: 1, amount: 0.5e1, customer_id: nil, created_at: "2020-01-20 07:09:22", updated_at: "2020-01-20 07:09:22">}
    => #<Order id: 1, number: nil, total: 5, customer_id: 1, promotion_id: 1, created_at: "2020-01-20 07:09:22", updated_at: "2020-01-20 07:09:22">

tapping_deviceは、print_calls_in_detailの他にも、print_tracesなどのさまざまな情報レベルに応じたAPIを提供しています。

さて、OrderCreationServiceには何も不審な点が見つからなかったとしましょう。次は@cartで何か問題が起きているのではないかと疑ってみることにします。print_tracesを用いれば、@cartがプログラムの他の部分とやりとりする様子を確認できます。

  include TappingDevice::Trackable

  def create
    @cart = Cart.find(order_params[:cart_id])
    promotion = Promotion.find_by(id: order_params[:promotion_id])
    service = OrderCreationService.new

    print_traces(@cart, exclude_by_paths: [/gems/]) # /gems/を除外してActive Recordの内部メソッド呼び出しを出力しないようにする
    @order = service.perform(@cart, promotion)
Passed as 'cart' in 'OrderCreationService#perform' at /Users/st0012/projects/tapping_device-demo/app/controllers/orders_controller.rb:9
Passed as 'cart' in 'OrderCreationService#validate_cart' at /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:8
Called :reserved_until from: /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:18
Called :errors from: /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:9
Passed as 'cart' in 'OrderCreationService#apply_discount' at /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:10
Called :apply_discount from: /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:24
Called :total from: /Users/st0012/projects/tapping_device-demo/app/models/cart.rb:6
Called :update! from: /Users/st0012/projects/tapping_device-demo/app/models/cart.rb:6
Passed as 'cart' in 'OrderCreationService#create_order' at /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:11
Called :total from: /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:28
Called :customer from: /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:28
Called :promotion from: /Users/st0012/projects/tapping_device-demo/app/services/order_creation_service.rb:28

@cartを呼び出したメソッドはもちろん、@cartを引数に取ったメソッドも出力されるようになりました【要チェック】。これでデバッグ時間が少しは減ることでしょう😉

tapping_deviceの利用法について詳しく知りたい方は、readmeでさまざまなヘルパーをご覧いただけます。私がtapping_deviceでRailsのissueを修正したときのやり方については別記事『Debug Rails issues effectively with tapping_device』をどうぞ。

まとめ

デバッグ作業のほとんどは、プログラムの動作(あるいはかつて動作していた)に関する情報収集ですが、これを手動でやっていては相当時間を吸われてしまいます。プログラムの動作について雑に当たりをつけて人間らしく乗り切ろうとすると、問題解決にさらに時間を吸われてしまいます。

しかしオブジェクト指向トレースを援用すれば、オブジェクト指向のパラダイムとRubyの超強力なTracePointの力を借りて、プログラムを効果的に精査できるようになります。私はこの方法で、駆け出し開発者がデバッグするときのつらみも軽減でき、ベテラン開発者のデバッグプロセスも大きく加速できると信じています。

本記事やtapping_deviceについてお気づきの点がありましたら、お気軽に元記事にコメントをどうぞ。皆さんからのご相談を歓迎します!他のデバッグ戦略や、もっとよいデバッグ戦略をご存知でしたら、ぜひお知らせください😉

関連記事

Rails 6: Action Textのファイルアップロードを分解調査する(翻訳)

Railsのフラグメントキャッシュを分解調査する(翻訳)

Rails 6にDNSリバインディング攻撃防止機能が追加された(翻訳)

$
0
0

概要

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

参考: DNS Rebinding ~今日の用語特別版~ | 徳丸浩の日記

Rails 6にDNSリバインディング攻撃防止機能が追加された(翻訳)

Rails 6にはDNSリバインディング攻撃から保護する機能が#33145で追加されました。この機能はdevelopment環境ではデフォルトで有効になっており、他の環境でもオプションで有効にできます。

DNSリバインディング攻撃とは

DNSリバインディング攻撃は、攻撃者が悪意のあるクライアントサイドスクリプトを仕込んだWebページを用いて標的ネットワークに侵入し、そのネットワーク内のブラウザをプロキシとしてネットワーク内の他のデバイスを攻撃することを指す。

Railsアプリで生じる影響

ローカル実行されているRailsアプリケーションに対して、攻撃者がDNSリバインディングを用いてリモートコード実行(RCE)する可能性があります。攻撃者が的確にアプローチすると、ローカルENV(環境変数)情報や、ローカルRailsアプリの全情報にアクセスできてしまうことがあります。

Rails 6の場合

試しにdevelopment環境のRails 6アプリに対して何らかのカスタムドメイン(ngrockやlvh.me、あるいはhostsファイルを編集するだけでも可能)を用いてアクセスしてみると、以下のようなエラーが表示される場合があります。

Blocked host: yourcustomdomain.com

しかしこのときlocalhostを指定すると何の問題もなくアプリにアクセスできます。このエラーの背後では、Rails 6がActionDispatch::HostAuthorizationミドルウェアの助けを借りてDNSリバインディング攻撃からの攻撃を懸命に防ごうとしています。development環境では、以下の設定がデフォルトで含まれています。

Rails.application.config.hosts = [
  IPAddr.new("0.0.0.0/0"), # All IPv4 addresses.
  IPAddr.new("::/0"),      # All IPv6 addresses.
  "localhost"              # The localhost reserved domain.
]

カスタムドメインからのアクセスを許可するには、Rails.application.config.hostsにそのドメインを追加します。たとえばdevelopment.rbに以下のように記述します。

# 'yourcustomdomain.com'からのアクセスを許可する設定
Rails.application.config.hosts << 'yourcustomdomain.com'

サブドメインからのリクエストを許可するには以下のようにします。

# 'www.yourcustomdomain.com'や'blog.yourcustomdomain.com'などの
# サブドメインを許可する設定
Rails.application.config.hosts << '.yourcustomdomain.com'

この保護を完全にバイパスして任意のドメインからのリクエストを許可する必要がある場合は、以下のようにします。

# 任意ドメインからのリクエストを許可する設定
Rails.application.config.hosts = nil

なお、development環境以外のconfig.hostsはデフォルトで空になる点にご注意ください。つまりドメインはチェックされず、保護は無効になります。

保護を有効にするのであれば、該当する環境ファイルでRails.application.config.hostsに指定したいドメイン名を値として設定します。

関連記事

Rails 6のcookieに「purpose」メタデータが追加(翻訳)

週刊Railsウォッチ(20200210前編)Railsのベンチマークジェネレータ、長いバックグラウンドジョブと戦う、Timestamp切り詰めの謎、Open APIツールほか

$
0
0

こんにちは、hachi8833です。そういえば明日は祝日ですね。コロナウイルス流行の様子が早速ビジュアライズされたようです。

「まあこれで何かわかったとしても自分らに打てる手は限られてますし😷」「情報の動き激しくて…😅」(以下延々)

参考: CNN.co.jp : 新型ウイルス、潜伏期間中の感染例は「誤り」 独当局

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

今回は第19回公開つっつき会を元にお送りします。お集まりいただいた皆さまありがとうございます!

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

今週はコミットログより見繕いました。今週は細かめの修正が多い印象です。

⚓default_scopedをpublic APIに

# activerecord/lib/active_record/scoping/named.rb#L25
        def all
          scope = current_scope
          if scope
            if scope._deprecated_scope_source
              ActiveSupport::Deprecation.warn(<<~MSG.squish)
                Class level methods will no longer inherit scoping from `#{scope._deprecated_scope_source}`
                in Rails 6.1. To continue using the scoped relation, pass it into the block directly.
-               To instead access the full set of models, as Rails 6.1 will, use `#{name}.unscoped`,
-               or `#{name}.default_scoped` if a model has default scopes.
+               To instead access the full set of models, as Rails 6.1 will, use `#{name}.default_scoped`.
              MSG
            end

            if self == scope.klass
              scope.clone
            else
              relation.merge!(scope)
            end
          else
            default_scoped
          end
        end
...
-       def default_scoped(scope = relation) # :nodoc:
+       # Returns a scope for the model with default scopes.
+       def default_scoped(scope = relation)
          build_default_scope(scope) || scope
        end

つっつきボイス:「上は実はついさっきkamipoさんがリツイートしたコミットです🐦」「お、unscopedしてしまった後でもデフォルトスコープを取れるのがdefault_scopedっていうことみたい: 自分はあんまり使わなそうだけどpublicメソッドが欲しい気持ちはワカル☺

API: unscope — ActiveRecord::QueryMethods

default_scopedは、スコーピング上デフォルトスコープになっているスコープを強制的に返す唯一の方法であり、マイグレーションでのスコープのリークを回避するのに必要。
同PRより大意

default_scopeを使わないのが一番なんでしょうか?🤔」「そういうわけにもいかないことがありますし、小さいシステムならdefault_scopeがあっても嫌がられないと思いますが、育ってくるとちょっとね…😅

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

⚓delegateのeachを取り除いた

# actionpack/lib/action_dispatch/testing/integration.rb#L80
    class Session
      DEFAULT_HOST = "www.example.com"
      include Minitest::Assertions
      include TestProcess, RequestHelpers, Assertions

-     %w( status status_message headers body redirect? ).each do |method|
-       delegate method, to: :response, allow_nil: true
-     end
-
-     %w( path ).each do |method|
-       delegate method, to: :request, allow_nil: true
-     end
+     delegate :status, :status_message, :headers, :body, :redirect?, to: :response, allow_nil: true
+     delegate :path, to: :request, allow_nil: true

つっつきボイス:「見出しに”delegate allows multiple method names are passed”とあるのは…?」「delegateの内容は変わってないので、eachのループをなくしたということなんでしょうね☺: タイトルは単にこの部分の挙動をメモった感じかな」「走り書きでしたか😅」「Railsの起動がちょっと速くなりそう☺」「以前%w()で書かれてた理由はわかりませんけど😆

⚓キーワード引数対応: define_methodをstringのevalに変更

# actionpack/lib/action_dispatch/testing/integration.rb#L356
      %w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
-       define_method(method) do |*args, **options|
-         # reset the html_document variable, except for cookies/assigns calls
-         unless method == "cookies" || method == "assigns"
-           @html_document = nil
-         end
+       # reset the html_document variable, except for cookies/assigns calls
+       unless method == "cookies" || method == "assigns"
+         reset_html_document = "@html_document = nil"
+       end

-         result = if options.any?
-           integration_session.__send__(method, *args, **options)
-         else
-           integration_session.__send__(method, *args)
+       definition = RUBY_VERSION >= "2.7" ? "..." : "*args"
+
+       module_eval <<~RUBY, __FILE__, __LINE__ + 1
+         def #{method}(#{definition})
+           #{reset_html_document}
+           result = integration_session.#{method}(#{definition})
+           copy_session_variables!
+           result
          end
-         copy_session_variables!
-         result
-       end
+       RUBY
      end

「これもさっきと同じintegration.rbファイルの変更ですね」「今までdefine_methodで定義していたのをmodule_evalに変えたのね😋

「『キーワード引数をif options.any?で扱うのはイケてない』みたいなことが書いてあるのでキーワード引数関連かな🤔: definition = RUBY_VERSION >= "2.7" ? "..." : "*args"とかありますし、stringのevalにすればdefine_method経由ではなくなってstringで書けるようになるので、definitionで2.7以降の場合とそうでない場合に...*argsを使い分けられるようになったということか😋」「なるほど!」「速度が目的ではなさそうなので、たぶんstringじゃないと2.7対応がやりにくいんでしょうね☺

「コミットで引用されてるb7e591aa43de73を見るとamatsudaさんが**options引数周りで試行錯誤してますね」「あぁ、Ruby 2.7のキーワード引数変更問題に引っかからないようにするためのやり方を試行錯誤してる感じ: if options.any?の結果で分岐するか、上みたいにstringでRubyコードそのものにパッチを当てる形で修正するか」「Railsのフレームワークでは複数バージョンのRubyに対応しないといけないので大変😅

⚓スキーマキャッシュ読み込み時のDB configフォールバック

# activerecord/lib/active_record/railtie.rb#L127
    initializer "active_record.check_schema_cache_dump" do
      if config.active_record.delete(:use_schema_cache_dump)
        config.after_initialize do |app|
          ActiveSupport.on_load(:active_record) do
            db_config = ActiveRecord::Base.configurations.configs_for(
              env_name: Rails.env,
              spec_name: "primary",
            )
+           next if db_config.nil?
+
            filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(
              db_config.spec_name,
              schema_cache_path: db_config.schema_cache_path,
            )
            if File.file?(filename)
              current_version = ActiveRecord::Migrator.current_version
              next if current_version.nil?
              cache = YAML.load(File.read(filename))
              if cache.version == current_version
                connection_pool.schema_cache = cache.dup
              else
                warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{current_version}, but the one in the cache is #{cache.version}."
              end
            end
          end
        end
      end

つっつきボイス:「修正は1行追加だけ」「前回のウォッチでもマルチDBがreplicaじゃなくてprimaryから取ってきちゃう問題の修正があったのを少し思い出しますね🤔ウォッチ20200203)」「マルチDBも大変そう…」

概要
スキーマキャッシュはデフォルトでprimaryデータベースconfigを読み出す。しかしspec nameがprimaryのデータベースconfigがアプリにない場合、ファイル名探索に失敗する。
ここでは念のためフォールバックを追加した。
その他
スキーマキャッシュのファイル名はハードコードされていたが、Railsではデフォルトパスをオーバーライドできるようになっている。#38348ではオーバーライドを考慮するようにしたが、spec nameがprimaryのデータベースconfigがないアプリの対応が漏れていた。
GitHubにはprimaryという名前のデータベースがないこともわかったので、このActive Recordイニシャライザに依存していないとしても失敗する。
同PRより大意

⚓GitHub ActionsのRailsビルドを数秒短縮

# .github/workflows/rubocop.yml#L21
-       gem install bundler:2.1.2
+       gem install bundler:2.1.2 --no-document

つっつきボイス:「ドキュメント生成やめたのね😆」「言われてみればなるほど😆」「ドキュメント生成は結構重いし🏋🏻‍♀️」「速くなるのわかります😋

⚓細かな修正系


つっつきボイス:「微修正をまとめてみました」「Railsガイドのパスの間違いとか☺

# activerecord/test/cases/migration_test.rb#L211
      if current_adapter?(:Mysql2Adapter)
        if ActiveRecord::Base.connection.mariadb?
          assert_match(/Can't DROP COLUMN `last_name`; check that it exists/, error.message)
        else
          assert_match(/check that column\/key exists/, error.message)
        end
-     elsif
+     elsif current_adapter?(:PostgreSQLAdapter)
        assert_match(/column \"last_name\" of relation \"people\" does not exist/, error.message)
      end

elsifの条件が抜けてたとは↑😆」「その下のassert_matchが条件扱いされるからエラーなしで通っちゃってた😆」「RuboCopに引っかからなかったのか👮🏼‍♀️

# railties/lib/rails/generators/rails/benchmark/templates/benchmark.rb.tt#L3
-require_relative "../config/environment"
+require_relative "../../config/environment"

「こんなエラー↑が今頃見つかるとは😆」「つかrails benchmarkってコマンドありましたっけ?」「知らない😆」「そんな機能が入ってたことの方がびっくり😳

⚓Railsのベンチマークジェネレーター

概要: パフォーマンス最適化を比較するベンチマークを生成する。
デフォルト: method = ips
:rails generate benchmark opt_compare path_a path_b
上で以下が生成される:
benchmarks/opt_compare.rb
--methodで他のベンチマーク手法を指定できる。有効な手法はips、bm、bmbm
Changelogより

「軽くググると昨年12月13日にベンチマークジェネレーターがmasterに入ってるし↑😆」「Rails 6.1あたりで使えるようになるのかな?」「そんなに複雑なことはやってないと思うけど、ベンチマークって自分でやろうとすると毎回ググるので、ベンチマークのテンプレートをRailsで生成できるならいいかな〜😋」 ​

⚓番外: オプション引数をつぶして回る

# actionpack/lib/abstract_controller/translation.rb#L26
-   def localize(*args)
-     I18n.localize(*args)
+   def localize(object, **options)
+     I18n.localize(object, **options)
    end
# actionpack/lib/abstract_controller/translation.rb#L13
-   def translate(key, options = {})
+   def translate(key, **options)
      if key.to_s.first == "."
-       options = options.dup
        path = controller_path.tr("/", ".")
        defaults = [:"#{path}#{key}"]
        defaults << options[:default] if options[:default]
        options[:default] = defaults.flatten
        key = "#{path}.#{action_name}#{key}"
      end
      I18n.translate(key, **options)
    end
# activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L709
-     def foreign_key_exists?(*args)
-       @base.foreign_key_exists?(name, *args)
+     def foreign_key_exists?(*args, **options)
+       @base.foreign_key_exists?(name, *args, **options)
      end

つっつきボイス:「ああ*argsoptions = {}を置き換えるヤツ☺」「options = {}みたいなレガシーな書き方がまだまだ残ってた😆」「だいぶ前にbabaさんがキライだって言ってた書き方ですね↓」

Railsフレームワークで多用される「options = {} 」引数は軽々しく真似しない方がいいという話

options = {}は、キーワード引数が登場する前のRuby 1.9とも互換性のある書き方だったと思います」「そういえばキーワード引数が登場したのはRuby 2.0でしたね」

「ところでお集まりの皆さんの中でRuby 1.8あたりを経験された方は?: さすがにいませんね😆」「Rails 4からなので😆」「自分の時代は今は亡きRuby Enterprise Edition↓使ってたので1.8経験しました: サイトは生き残ってるけどやはり1.8.7で止まってるか〜😆」「そういえばここってPassengerの会社がやってたんですね😳」「そうそうPhusion

参考: Welcome — Ruby Enterprise Edition


rubyenterpriseedition.comより

「ちなみにRuby Enterprise Editionは一応今でもrbenvにエントリぐらいはあります↓」「おぉっ」「でもさすがにOpenSSLのバージョンが今と違ってるでしょうからビルドは無理じゃないかな〜🤣: 試しに裏でビルドしてみよう」(しばらく経って)「やはりダメか😆

ree-1.8.7-2011.03
ree-1.8.7-2011.12
ree-1.8.7-2012.01
ree-1.8.7-2012.02

参考: OpenSSLが古すぎてbundle updateできない

「今の時代にやんごとない事情でRuby 1.8を動かさないといけなくなったらDockerコンテナでやるとかになるでしょうね🐳」「Docker Hub見ると1.8コンテナ作ってる人がかろうじているけど、どこまで信用できるかわかりませんし😆


つっつきボイス:「上はさっき流れてきたツイートです」「さらっと『全部のコミット読んでる』」「最強すぎ💪」「最後は眼ぢから👁

⚓Rails

今回は大半がRuby Weeklyのエントリになりました。軽く敗北感。

⚓RailsのTimestampが切り詰められた(Ruby Weeklyより)

# 同記事より
{"created_at"=>2020-01-02 13:36:22.459149334 +0000, "updated_at"=>2020-01-02 13:36:22.459149334 +0000}
{"created_at"=>2020-01-02 13:36:22.459149000 +0000, "updated_at"=>2020-01-02 13:36:22.459149000 +0000}

つっつきボイス:「あ〜マイクロ秒以下が落ちちゃうヤツ↑あるある🤣」「🤣」「reloadすると変わる↓とある」

# 同記事より
before perform: 1577985089.0547702
after perform:  1577985089.0547702
after reload:   1577985089.05477

「Active Recordはsaveメソッドを呼ぶとINSERTなりUPDATEなりが行われるんですけど、DBに実際に保存した値を読み込み直すことまではしないので、saveした直後に値を参照すると、DBに保存された値そのものではなくRuby側で設定したときの値を返します」「後でreloadしてみると値が変わってたとは😇」「😳」「めったに踏みませんけど、たまに出くわしますね☺


目次より:

  • 問題: macOS上のdev環境ではテストが通るのにCIでほぼ失敗する(何とかログは取れた)
  • 調査
    • 桁落ちを発見
    • プリントデバッグでは再現せず、calendar.attributes['created_at']でattributeハッシュを直接フェッチすると再現した
  • 修正
    • DateTimeオブジェクトを生成して秒以下を丸め、このオブジェクトでタイムスタンプを明示的に設定した
  • 原因
    • Active RecordのタイムスタンプがTime.nowで設定されているがTimeの精度はOSに依存する
  • 疑問(説明求む)
    • calendar.attributes['created_at']だとリロードされたのにcalendar.created_atだとリロードされないのはなぜ?
    • Linuxと比較してみるとmacOSでの精度は6桁どまりという驚きの結果(7桁目以降は1と2と8と9しか現れない↓)
# 同記事より
10000.times.map { Time.now}.map{|t| t.to_f.to_s.match(/\.(\d+)/)[1] }.select{|s| s.size == 7}.group_by{|e| e[-1]}.map{|k, v| [k, v.size]}.to_h

# MacOS => {"9"=>536, "1"=>555, "2"=>778, "8"=>807}
# Linux => {"5"=>981, "1"=>311, "3"=>1039, "9"=>309, "8"=>989, "6"=>1031, "2"=>979, "7"=>966, "4"=>978}

⚓sequenced: ARモデル向けのシーケンシャルID生成(Ruby Weeklyより)

# 同リポジトリより
class Question < ActiveRecord::Base
  has_many :answers
end

class Answer < ActiveRecord::Base
  belongs_to :question
  acts_as_sequenced scope: :question_id
end

つっつきボイス:「このgemの嬉しみってどのあたりでしょう?」「冒頭に書いてあるそのまんまですが、プライマリキーではない、スコープドのシーケンシャルIDを生成できるということですね☺

「使いみちとしては、たとえばですがUserモデルにhas_many :picturesがあったとすると、どのユーザーについてもユーザー独自の1個目の画像をURLで表現できるようにしたい、なんて場合があればでしょうね」

「Railsで普通にネステッドスコープを作ると、ネステッドの先もサロゲートキーになりますよね: 言い換えればそこは名前空間が同じになるので、ユーザーAにとっての画像1もユーザーBにとっての画像1も同じものを指してしまう」「ふむふむ」「このsequenced gemを使えば、/ユーザーA/1で取れる画像が必ずそのユーザーAにとっての1個目の画像になり、/ユーザーB/1だとそれとは異なるユーザーBにとっての1個目の画像になる、といった具合です」

「このgemを使うかどうかは別として、こういうのをやりたい場合があるのはわかりますね☺」「自分で書くのもありな機能でしょうか?」「スコープドのシーケンシャルID生成ではシーケンス番号がかぶらないように保証するのが面倒くさくなりがちなので、このgemがトランザクション管理までやってくれるのであればワンチャンあるかも🤔

「READMEの下の方にdata integrityとかあるしやってくれるかな?…とよく見ると『これはPostgreSQLでしかコンカレント安全でない』って書いてあるし😆」「😆」「というわけでぽすぐれ以外ではシーケンシャルがIDかぶる可能性あります: 以下のPARTITION BYあたりとかPostgreSQL向けっぽいですし↓」

# 同リポジトリより
# app/db/migrations/20151120190645_add_sequental_id_to_badgers.rb
class AddSequentalIdToBadgers < ActiveRecord::Migration
  add_column :badgers, :sequential_id, :integer

  execute <<~SQL
    UPDATE badgers
    SET sequential_id = old_badgers.next_sequential_id
    FROM (
      SELECT id, ROW_NUMBER()
      OVER(
        PARTITION BY burrow_id
        ORDER BY id
      ) AS next_sequential_id
      FROM badgers
    ) old_badgers
    WHERE badgers.id = old_badgers.id
  SQL

  change_column :badgers, :sequential_id, :integer, null: false
  add_index :badgers, [:sequential_id, :burrow_id], unique: true
end

「こういうことをしたいときが数年に1回ぐらいはありそう😆」「😆」「そのときにこのgemを思い出せるかどうかですけどっ😆

⚓Open API仕様記述ツール


同サイトより


つっつきボイス:「今日のWebチーム内発表がOpen APIの話題だったので先週(ウォッチ20200203)に引き続いて」「そうそう、Open API(Swagger)で仕様を書くことがあるんですけど、上のサイトはOpen APIのツール集ですね: Open APIは夢のツールとまではいきませんが😆

「チーム内発表ではstoplightというツール↓が登場してましたね」「stoplightはちゃんとWYSIWYGエディタになってて、とにかくyamlを手書きしなくていいのが嬉しい❤


stoplight.ioより

「Swaggerエディタ↓って結局yamlを書かないといけないのがつらくありません?😆」「ある程度しょうがないかと思うけど生成されるyamlのファイル長すぎじゃね?って思ったりもするけど、Open APIがなかったらExcelで仕様書かないといけなくなりますし😆


editor.swagger.ioより

参考: Open API仕様記述ツールを比較してみた - Qiita

「Qiitaにある中でAPI Blueprintは書いたことありますね: ちょっとMarkdown風ですが自由に書けすぎる感😆


apiblueprint.orgより

⚓Chrome 80のSameSite属性


つっつきボイス:「SameSiteはもう入ってきたんでしたっけ?」「Chrome 80は昨日見ている目の前でインストールされました」「SameSiteの動作変更は来週からみたい」「デフォルトのSameSiteは本来こうあるべきかなと思いますけど😆」「SameSite=None; Secureを付けると不具合を発生するブラウザって…」

「これで影響を受ける決済ってどのぐらいあるんでしょう?」「まあカード会社のサイトに行って戻るようなECサイトではそもそも普通POSTではやりませんし」「おぉ」

「今どきクロスサイトでPOSTして、しかもまた戻ってくるような実装ってあんまり思いつかない🤔: トラッキング系のシステムとかならもしかするとあるかもしれませんが、jnchitoさんの記事もテスト方法が中心で実装されている例については見当たらないようなので、後で参考記事↓と合わせて読んだ方がよさそう」

参考: Chrome 80が密かに呼び寄せる地獄 ~ SameSite属性のデフォルト変更を調べてみた - Qiita

⚓長期間動き続けるジョブと戦う(Ruby Weeklyより)

割と長い記事です。

  • バックグラウンドジョブでやるべきとき:
    • リクエストがタイム・アウトする
    • メモリスパイクの発生
    • UXに影響する
  • ツール
    • Active Jobにするかどうか
    • Delayed JobかSidekiqか
  • バックグラウンドジョブの基礎
    • グローバルID
    • ジョブの事前条件をチェック(破壊的なアクションを叩かないよう注意)
    • 巨大なジョブを分割
  • ユーザーを上手に待たせる
    • 完了をメールで通知
    • ポーリングしてプログレスバーを表示
    • 合わせ技プラスアルファ
    • 上級技の紹介(取り扱い注意)
  • まとめ

つっつきボイス:「wrangleはざっくり『戦う』ぐらいの感じで😆: Ruby Weeklyのタイトルではtameになってましたが」「長期間のバックグラウンドタスクはいろいろ大変😅

「メモリスパイク、どのツールにするか、Delayed JobかSidekiqか、ジョブはグローバルIDで管理せよ、ジョブは冪等に書け、でかいジョブは分割せよ…と、なかなかいい感じにまとまった記事👍」「Resqueは選択肢に入ってないらしい😆」「やっぱ古いか😆

「ユーザーを待たせる間どうするかの話も押さえてありますね: すぐ終わるダウンロードならいいけど20分ぐらいかかるようなジョブだとユーザーが待ちきれなくてブラウザ閉じちゃったりするので😆、どうハンドリングするかとか」「😆」「対策としてメールで通知するか、ポーリングしてプログレスバー出すかとか実践的なことが書かれてますね😋

「前に勉強会で話していただいたバッチ処理の話↓にも通じるところありそうですね」「たしかに近いかも☺」(ここで当該社内向けスライドを一同で閲覧)

Webのバッチ処理とオンライン処理のポイントとシステムの応答性能を学ぶ#1(社内勉強会)

「ただこの記事が掲載されているboringrails.comは今年春に発売予定のRails本↓に収録するためにサンプル記事以外は非表示にしているようです😅ウォッチ20190930)」「ありゃ😆」「もしかすると後で非表示になるかも」


boringrails.comより

⚓その他Rails

つっつきボイス:「前にもウォッチ↓で取り上げた内容ですが一応リマインダーとして」「そうそう、Active SupportのdowncaseupcaseswapcaseとかがRuby本体でできるようになってましたね☺

週刊Railsウォッチ(20181022)Railsの名前空間地獄とrequire_dependency、PostgreSQL 11がリリース、clean-rails.orgほか


つっつきボイス:「短い記事です」「2.7のdeprecation warningを止める方法が3つ書かれてるけど結局RUBYOPTでやるのは一緒で、それをどう渡すかの違いだけ😆

参考: 環境変数 (Ruby 2.7.0 リファレンスマニュアル)


つっつきボイス:「楽しい😋」「コミッター内でこれだけスタイルの好みが違うという😋」「hashブラケット前後のスペースを入れるかどうかは結構分かれますね〜☺」「自分は入れる派」「linterにお任せ😆


前編は以上です。後編は祝日をはさんで2/12(水)となります。

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

週刊Railsウォッチ(20200204後編)Ruby3.0の他のbreaking change、Rubyのシリアライザ、GitHubのcode ownersほか

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines


週刊Railsウォッチ(20200217前編)Railsのオプション引数退治、HSTSのデフォルトmax-ageが1年から2年に変更、semantic_logger gemほか

$
0
0

こんにちは、hachi8833です。皆さま息災でいらっしゃいますか。


つっつきボイス:「たしかに相当影響でかそう😰」「いろんなものが届かなくなったりしそう🏷」「人が大勢集まるイベントも影響受けそうですし🥺

RubyKaigiまでに落ち着いてくれるといいのですが。

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

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

今回は公式の更新情報の他にコミットログからも見繕いました。#38383は先週引き当てたので省略🎯

⚓HSTSのmax-ageをデフォルトで2年に設定

参考: Strict-Transport-Security - HTTP | MDN


つっつきボイス:「前のHSTS max-ageのデフォルト値はいくつだったんだろ?🤔、なるほど1年か↓」

# actionpack/lib/action_dispatch/middleware/ssl.rb#L49
  class SSL
    # :stopdoc:

-   # Default to 1 year, the minimum for browser preload lists.
-   HSTS_EXPIRES_IN = 31536000
+   # Default to 2 years as recommended on hstspreload.org.
+   HSTS_EXPIRES_IN = 63072000

「hstspreload.org↓の推奨値に従ったのね☺」「Mozillaでも2年が推奨値になってるんですね😳


hstspreload.orgより

「HSTSの設定を間違えると場合によってはドメインが使い物にならなくなります🤣: たとえば暗号化されないHTTPも使う必要があるドメインをうっかりHSTS有効で公開してその情報がひととおり行き渡ってしまうと、そのドメインへのHTTPアクセスがブラウザレベルではじかれてしまうのでサービスホスティングに使えなくなるという😇」「ひぇ😅」「まあ今どきHTTP+HTTPSで公開するか?というのはありますけど☺

「コンテンツをHTTPで公開しないけどリダイレクトはしたいということはあるんですが、HSTSを有効にするとリダイレクトできなくなって割と詰みます😇

参考: ネイキッドドメイン+HTTPSで運用するRailsアプリを5.1にアップグレードしたら、サブドメインも強制的にHTTPSになってしまった話 - Qiita
参考: リダイレクトも忘れずに!常時SSL化をする為の13の重要点 | さくらのSSL

HSTSの弱点は1回目のアクセスがhttpになってしまうことですが、予めブラウザに「このサイトはhttpsでアクセスしてください」と登録しておくことができます。これが「HSTS Preload」です。こちらのサイトにURLを登録しておくことで、初回のアクセスからhttpsで接続させることができます。
HSTSを利用する上で注意が必要なのは、何かしらの事情でサイトを「http」に戻した場合です。例えば、HSTSで1年間のキャッシュが指定されている場合、次回以降のアクセスが(1年以内であれば)httpでアクセスされることはありません。このため、サイトがhttpに戻ってしまうと、いつも訪れていた閲覧者がアクセスできない状況に陥ってしまう可能性があります。また、HSTSのキャッシュ時間設定値は最低1年(31536000秒)となるため、これも注意が必要です。
ssl.sakura.ad.jpより

⚓PostgreSQL 11〜のパーティションドインデックスをサポート

PostgreSQL 11以降のupsert_allでpartitioned indexサポートを追加
Changelogより

# activerecord/test/schema/postgresql_specific_schema.rb#L112
+ if supports_partitioned_indexes?
+   create_table(:measurements, id: false, force: true, options: "PARTITION BY LIST (city_id)") do |t|
+     t.string :city_id, null: false
+     t.date :logdate, null: false
+     t.integer :peaktemp
+     t.integer :unitsales
+     t.index [:logdate, :city_id], unique: true
+   end
+   create_table(:measurements_toronto, id: false, force: true,
+                                       options: "PARTITION OF measurements FOR VALUES IN (1)")
+   create_table(:measurements_concepcion, id: false, force: true,
+                                          options: "PARTITION OF measurements FOR VALUES IN (2)")
+ end

つっつきボイス:「PostgreSQLはもともとPARTITION BYという構文がありますけど、パーティションごとのインデックスも作れるのね」「upsert_allで効く、と」

参考: 5.10. テーブルのパーティショニング

サブパーティショニングと呼ばれる方法を使って、パーティションそれ自体をパーティションテーブルとして定義することができます。 パーティションには、他のパーティションとは別に独自のインデックス、制約、デフォルト値を定義できます。 インデックスは各パーティションで別々に作成されなければなりません。
postgresql.jpより

「ぽすぐれのバージョンもチェックしてる↓: バージョン番号の桁数ってこんなふうなんだ😆」「6桁貫きですか😳

# activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb#L160
+     def supports_partitioned_indexes?
+       database_version >= 110_000
+     end

「こんなふうに書くのか↓: パーティショニング使ったことないのでテストで書き方を知るという😆」「😆」「ものすごく長大になる証跡ログなんかではパーティショニングしておく方がよかったりしますね🧐

# activerecord/test/cases/adapters/postgresql/schema_test.rb#L382
      @connection.execute "CREATE INDEX \"#{PARTITIONED_TABLE}_Index\" ON #{SCHEMA_NAME}.#{PARTITIONED_TABLE} (logdate, city_id)"
      assert_nothing_raised { @connection.remove_index PARTITIONED_TABLE, name: "#{SCHEMA_NAME}.#{PARTITIONED_TABLE}_Index" }

「パーティションドテーブルはBigQueryにもあった気がする」

参考: パーティション分割テーブルの概要  |  BigQuery  |  Google Cloud

⚓concerningprepend: trueを指定できるようになった

# activesupport/lib/active_support/core_ext/module/concerning.rb#L108
  module Concerning
    # Define a new concern and mix it in.
+   def concerning(topic, &block)
+     include concern(topic, &block)
+   def concerning(topic, prepend: false, &block)
+     method = prepend ? :prepend : :include
+     __send__(method, concern(topic, &block))
+   end
# activesupport/lib/active_support/concern.rb#L109
+   class MultiplePrependBlocks < StandardError #:nodoc:
+     def initialize
+       super "Cannot define multiple 'prepended' blocks for a Concern"
+     end
+   end

まれなケースとして、あるモジュールを先祖の階層で(単なるincludeではなく)prependする必要が生じることがある。そういう場合で、かつインラインconcernが望ましい場合、concerningでそのconcernをprependすべきと指示できれば有用なことがある。
concerningprepend: trueを指定することでこれが可能になった(デフォルトはfalse)。
@8543974より


つっつきボイス:「concerningでprependできるとうれしいことって何でしょう?」「そもそもレアケースではないかと😆」「kazzさんに話してみたら使いみちで考え込んでました」「prepend: falseが今までの挙動だったのはわかるけど、何が問題だったとかどう使いたいとかが具体的に書いてないし🤣」「このコミットたちをまとめるプルリクってないの?」「それがどうも見当たらなくて、コミットだけみたいです😅

「concernってそもそもあんまりやりたくないし😆: 使うと何となくDRYになったような気がするけど、育ってくると読みにくさが半端なくなる😢」「例の定番記事↓でも言ってるヤツですね」「gemの形にでもなれば違うかもしれませんけど」

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


後で気づきましたが、以下のコミットにドキュメントが少し追加されていました。

ActiveSupport::Concernprependをサポート。
extend ActiveSupport::Concernしたモジュールをprependできるようになる。

module Imposter
  extend ActiveSupport::Concern

  # `included`と同じ(`prepend`されたときしか実行されない点を除く)
  prepended do
  end
end

class Person
  prepend Imposter
end

concerningも更新されてconcerning :Imposter, prepend: true doできるようになった。
Changelogより

その後の#38462から辿って、どうやら以下の#37174が始まりだったようです。この#37174はなぜかマージされず、実際には上の個別のコミットに分けられたものがマージされています🤔。書いてあることは上の個別コミットと大差ありませんでした。

⚓PostgreSQLのOIDを符号なしintegerに修正

# activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb#L4
  module ConnectionAdapters
    module PostgreSQL
      module OID # :nodoc:
-       class Oid < Type::Integer # :nodoc:
+       class Oid < Type::UnsignedInteger # :nodoc:
          def type
            :oid
          end
        end
      end
    end
  end

つっつきボイス:「issue #38425↓によると、OIDがでかい場合に問題が生じることがあったと」「OIDはぽすぐれのオブジェクトIDでしょうね」「OIDがでかすぎるとActive Recordがpg_typeを正しく認識できなくなってカラムをStringと認識しちゃうのか😳」「ホントだ、oid::integerにcastすると死んでる💀: つまりintegerの最上位ビットに到達すると発現する😇」「相当でかいシステムでないと踏まなさそう」

-- Original load_additional_types code
SELECT * FROM foooid WHERE oid::integer IN (2779325372);
 pk | oid | name
----+-----+------
(0 rows)

「なのでunsigned integerで処理することにしたと」「テストも最上位ビットが立つ値に変更されてる↓」

# activerecord/test/cases/adapters/postgresql/datatype_test.rb#L57
  def test_update_oid
-   new_value = 567890
+   new_value = 2147483648

電卓で確認してみました↓。

⚓content_pathを明示的にstringに変換

# activesupport/lib/active_support/configuration_file.rb#L12
    def initialize(content_path)
-     @content_path = content_path
+     @content_path = content_path.to_s
      @content = read content_path
    end

つっつきボイス:「content_pathPathnameが渡されていた場合に対応したそうです」「ああRubyのPathnameオブジェクトを渡すと動かなかったのか: そりゃ渡したいよね😆

require 'pathname'

Pathname.new("foo/bar")       # => #<Pathname:foo/bar>
Pathname.new("foo/bar").to_s  # => "foo/bar"

⚓オプション引数退治は続く


つっつきボイス:「kamipoさんの今週の退治シリーズをまとめてみました」「ああ、@6708f3aとかたしかにこれがあるべき姿↓: 引数を*で受けてextract_options!で展開とかしたくない😆

# activesupport/lib/active_support/message_encryptor.rb#L137
-   def initialize(secret, *signature_key_or_options)
-     options = signature_key_or_options.extract_options!
-     sign_secret = signature_key_or_options.first
+   def initialize(secret, sign_secret = nil, cipher: nil, digest: nil, serializer: nil)
      @secret = secret
      @sign_secret = sign_secret
-     @cipher = options[:cipher] || self.class.default_cipher
-     @digest = options[:digest] || "SHA1" unless aead_mode?
+     @cipher = cipher || self.class.default_cipher
+     @digest = digest || "SHA1" unless aead_mode?
      @verifier = resolve_verifier
-     @serializer = options[:serializer] || Marshal
+     @serializer = serializer || Marshal
    end

「以前からRailsのコードはextract_options!でオプションを展開してたんですけど、extract_options!使われると、Railsのソース読んでても(特にAction Pack周りとか)そこに何が入ってくるのかがマジでわからないんですよ😭」「😭」「こういうふうに修正してもらえるとええわ〜って思います👏㊗🎁」「引数の後ろに**がないところが特にうれしい😋: これをforwardされるとわけわからなくなりますし😆」「😆

# api.rubyonrails.orgより
def options(*args)
  args.extract_options!
end

options(1, 2)        # => {}
options(1, 2, a: :b) # => {:a=>:b}

|(*secrets)|のかっこ、なぜ必要なのかぱっと見わからないけど、ないと展開の順序あたりがうまく動かないんでしょうね😢」「ここでかっこを適切に付ける自信ない😭

# actionpack/lib/action_dispatch/middleware/cookies.rb#L620
-       request.cookies_rotations.encrypted.each do |*secrets, **options|
+       request.cookies_rotations.encrypted.each do |(*secrets)|
          options = secrets.extract_options!
          @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
        end

「@a55620fの2.7対応は__send__で書き直してますね↓」

# activesupport/lib/active_support/option_merger.rb#L15
    private
      def method_missing(method, *arguments, &block)
        options = nil
        if arguments.first.is_a?(Proc)
          proc = arguments.pop
          arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
        elsif arguments.last.respond_to?(:to_hash)
          options = @options.deep_merge(arguments.pop)
        else
          options = @options
        end
-       if options
-         @context.__send__(method, *arguments, **options, &block)
-       else
+       invoke_method(method, arguments, options, &block)
+     end
+
+     if RUBY_VERSION >= "2.7"
+       def invoke_method(method, arguments, options, &block)
+         if options
+           @context.__send__(method, *arguments, **options, &block)
+         else
+           @context.__send__(method, *arguments, &block)
+         end
+       end
+     else
+       def invoke_method(method, arguments, options, &block)
+         arguments << options if options
+         @context.__send__(method, *arguments, &block)
+       end
+     end

上の|(*secrets)|のかっことは関係ありませんが、Rubyのsuperのかっこありなしの違いの話↓をちょっとだけ思い出しました。

Ruby: `super`キーワードの4つの側面(翻訳)

⚓番外: RuboCopお手柄

# activesupport/lib/active_support/configuration_file.rb#L41
      def render(context)
-       erb = ERB.new(@content).tap { |erb| erb.filename = @content_path }
+       erb = ERB.new(@content).tap { |e| e.filename = @content_path }
        context ? erb.result(context) : erb.result
      end

つっつきボイス:「ブロック変数のerbがシャドウイングしてたのをRuboCopが見つけてくれたみたい」「最初RuboCopが誤動作したのかと思っちゃいました😆

後で変更前のコードにRuboCopをかけてみました。

configuration_file.rb:42:38: W: Lint/ShadowingOuterLocalVariable: Shadowing outer local variable - erb.
      erb = ERB.new(@content).tap { |erb| erb.filename = @content_path }
                                     ^^^

⚓Rails

⚓Rails+Amazon Rekognitionで「不適切な画像」を自動修正(RubyFlowより)

# 同記事より
has_one_attached :image

validate :image_moderation

def image_moderation
  # 画像がアップロードされてないor変更なしの場合はバリデーションしない
  return if image.blank? || !image.changed?

  # クライアントの初期化(シングルトンクラスやクラス変数などに移してもいい) -- 単なる概念実証(PoC)
  client = Aws::Rekognition::Client.new

  # Active Storageのattachmentを使うラベルを検出
  moderation_labels = client.detect_moderation_labels({ image: { bytes: attachment_changes['image'].attachable }}).moderation_labels

  # 安全でないコンテンツを検出したらバリデーションエラーを追加
  errors.add(:image, "contains forbidden content - #{moderation_labels[0].name}") if moderation_labels.present?
end

つっつきボイス:「Amazon RekognitionをRailsで使ってみた短い記事です」「今はもう画像解析系の処理を超簡単にやれるものがゴロゴロしてますね☺

参考: Amazon Rekognition(高精度の画像・動画分析サービス)| AWS

⚓semantic_logger: Railsのログをカスタマイズ(Awesome Rubyより)


同サイトより

# 同リポジトリより
logger.measure_info('How long is the sleep', payload: {foo: 'foo', bar: 'bar'}) { sleep 1 }

つっつきボイス:「semantic_loggerは以前のウォッチでURLだけ貼ったことがありました」「前からあったっけかこれ?🤔

参考: semantic_loggerの紹介 - Qiita

「リッチロギングフレームワーク、たしかに欲しいものではある」「お、NewRelicとかいろんなところにログを投げられるのね↓」「fluentdはないけど😆、JSONで投げればいいんだろうし」

  • ファイル
  • 画面
  • ElasticSearch(ダッシュボードとビジュアライズはKibanaで)
  • Graylog
  • BugSnag
  • NewRelic
  • Splunk
  • MongoDB
  • Honeybadger
  • Sentry
  • HTTP
  • TCP
  • UDP
  • Syslog
  • 既存のRuby製何でも
  • 自前の何か

「まあログは永遠の課題というか、簡単なものを書いているときにセマンティックなログフォーマットのことまで考えてロガーを使うのって割と面倒くさいですよね😆」「たしかに😆」「やばそうだと思ったら雑に全部大文字でメッセージ書いたりしますけど、セマンティックに書かないといけなくなるとプロジェクトにふさわしいログフォーマットを考えないといけなくなったり」

「ちなみにJavaにはいにしえの大昔からLog4jというロガーがありますね↓」「ありますね☺、今はバージョン2でしたか」「ガラケー時代に1.4を使おうとしたことはあるけど2は使ったことない😆


logging.apache.orgより

参考: log4j - Wikipedia

// logging.apache.orgより
logger.error((Marker) null, "This is the log message", throwable);

「log4jのサイトのコード例↑見てもわかりますけど、だいたいどのロガーもこういう書式になる😆」「だいたいログレベルとログメッセージでやるという😆」「まあそれ以上のものをロガーに求めませんけど、あそうだ、シングルトンでどこからでも取れて欲しい」「ですね☺

「そういえばIBM Javaなんてのもありましたし😆」「使ったことないです〜😆」「これでないと動かないドライバとか当時ありましてですね😆」(以下延々)

参考: IBMがJava 8を「少なくとも2025年までは確実にサポートする」とアピール - orangeitems’s diary

⚓書籍『ドメイン駆動設計 はじめの一歩』


同記事より


つっつきボイス:「こちらは永和システムマネジメントさんのブログですが、技術書典でこの本出すそうです」「3/1が2日目ということは2/29からかな?」

「人混みと行列苦手なので、技術書典はマジでVR参加したい🤣」「例のOriHime↓にお願いできたらいいのに😆」「私も技術書典は行ってますけど人多くてうんざりです😆」「同じじゃないですか🤣

そういえばOriHimeクリエイターの芳藤さんはこの間NHKの『逆転人生』にも出演されてましたね。

参考: 吉藤健太朗 - Wikipedia

追記(2020/02/19)

残念ながら今回は中止に…

⚓Railsの名前付けカンペ(Hacklinesより)


つっつきボイス:「おそらく自分用のチートシートというか、Railsに慣れてる人には今更かなと思いつつ」「ああRailsの命名コンベンション☺: それ以前にRubyにも命名コンベンションありますし」

後で見つけた以下のGist↓はRubyの名前付けについても載っていますね。

Stack OverflowでもRuboCopのRubyスタイルガイドしか触れられていませんでした。

参考: Ruby naming conventions? - Stack Overflow

【保存版】Rubyスタイルガイド(日本語・解説付き)総もくじ

⚓knock: Rails API向けJWT認証gem(Awesome Rubyより)

# 同リポジトリより
class ApplicationController < ActionController::API
  include Knock::Authenticable
end

class SecuredController < ApplicationController
  before_action :authenticate_user

  def index
    # etc...
  end

  # etc...
end

つっつきボイス:「JWTか〜、課金するようなAPIだと欲しいでしょうけど使われてるのかな?😆」「Awesome Rubyで『Devise vs Knock』って出てきたんですけど、リポジトリにこんなの↓が書いてあってほだされてしまいました😆」「yes、no、yes!🤣


同リポジトリより

「Devise gemとの競合はまあわからなくもないけど、普通ならAPIキー発行系は別にしたいというか、ここまでやるなら別サーバーにするでしょ😆」「JWT、あんまりやりたくない感😅

参考: JSON Web Token - Wikipedia

後で調べると、knockではJWTのruby-jwt gemを使ってますね。

⚓その他Rails


同記事より

「Sumo LogicのダッシュボードをRailsアプリに取り付ける記事だそうです」「賃貸情報のSuumoではなかった🤣」「Sumo Logicは日本法人あるんですね😳

参考: 日本に本格進出したSumo Logicに関する、知らない人は知らない意外な事実 - @IT


前編は以上です。

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

週刊Railsウォッチ(20200212後編)Rubyistが解説するUnicodeとUTF-8、Sorbetが速い理由、CSSの歴史、2019年の脆弱性まとめほか

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

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

Rails公式ニュース

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Publickey

publickey_banner_captured

週刊Railsウォッチ(20200225前編)RubyのShellwordsライブラリは知っておくべき、VCRはやはり有能、copを自作、Hix on Rails記事ほか

$
0
0

こんにちは、hachi8833です。次回の技術書典は残念ながら中止になりました。型システム祭りも延期だそうです。


つっつきボイス:「今日は1名Zoomで臨時リモートつっつき参加です」「はい聞こえま〜す」「five nineでつながってますね」「それは?」「無線用語😆: 了解度5で信号強度9📡」「Zoomやっぱりいいわ〜❤

参考: シグナルレポート(RSレポート)

「今やひととおりのイベントが中止ですし😇」「今度こそ技術書典行けるかと思ったのに😢」「常連ですが残念です😭」「代わりにオンラインでの開催「技術書典 応援祭」なんてのをやるみたい」「今回の出展者は次回優遇措置適用ですって」「出展者も本印刷したりしてますし」「主催者側の負担も半端ないはず」「出展料返却なしとあるけど会場費考えたら無理もない」「いろいろ大変だ…」

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

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

⚓お知らせ: 3月の「公開つっつき」はお休みします

これまで毎月第一木曜に休まず開催してまいりました週刊Railsウォッチ「公開つっつき会」ですが、昨今のコロナウイルス流行を鑑みて3月は初の「開催なし」といたします🙇

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

公式の更新情報がなかったのでコミットリストから見繕いました。

今更ですが@kamipoさんのコントリビューション数がダントツですね。

参考: Rails Contributors - This year

つっつきボイス:「この週は久々に@kamipoさんの修正が少なめでした」「たまには一息入れないと☺

⚓attributes=assign_attributesのエイリアスにした

# activerecord/lib/active_record/attribute_assignment.rb#L13
+   alias attributes= assign_attributes
+

つっつきボイス:「修正はシンプルですね」「2つのメソッドが別々でもよかったんでしょうけど、今後また挙動が変わったときに合わせるのが面倒だからエイリアスにしたのかなと想像☺」「引用されてる#38401の方がメインだったのかも」

#38401まではassign_attributesattributes=もActive Modelで定義されていて引数をコピーしていた。assign_attributesがActive Record内でオーバーライドされるようになってそこではコピーが発生するが、attributes=がオーバーライドされていなかった。
つまりattributes=に代入する引数がネストまたはマルチパラメータだと引数が改変され、引数がコピーされていなかったということになる。
同PRより大意

⚓rails generateの改善

# railties/lib/rails/generators/actions.rb#L250
      def rails_command(command, options = {})
-       execute_command :rails, command, options
+       if options[:inline]
+         log :rails, command
+         command, *args = Shellwords.split(command)
+         in_root do
+           silence_warnings do
+             ::Rails::Command.invoke(command, args, options)
+           end
+         end
+       else
+         execute_command :rails, command, options
+       end
      end

つっつきボイス:「PRの内容がタイトルのshell out(=不意の出費を払う) とどうも意味がつながらなくて悩んだんですが、修正でrequire shellwordsが追加されているので、それにちなんでshell outってもじったのかなと思いました🤔」「ああShellwordsってライブラリあるある😆

⚓Rubyでシェル操作するならShellwordsを使おう

「Shellwordsって、コマンドラインの引数をいい感じに配列に分解したりエスケープしたりしてくれるっぽいですね」「そうそう、Shellwordsはシェルのコマンドライン引数をクォートしたりできます🧐

参考: Rubyから外部コマンドを実行するときはShellwordsモジュールが便利 - ブログのおんがえし

「Shellwordsはいわゆるコマンドインジェクションを防止するライブラリで、コマンドライン引数を扱うなら基本これを使うべきです😎」「そうでしたか!😳」「それをRailsコマンドの処理に使うようにしたと」

OSコマンドインジェクションの仕組みとその対策 | セキュリティ対策 | CyberSecurityTIMES

「Shellwords使わないと引数処理マジつらいですよ〜😭」「おぉ」「コマンド引数が完全に固定されてるとか自分で作った引数しか渡さないならいいんですが、ユーザー入力を含む文字列とかを引数に渡すのであれば、Shellwords使わないときっとインジェクション作り込んじゃいます💀

「一番安全なのはfork()自体に引数を渡すやり方」「おぉ?」「SQLで言うPrepared Statementみたいなもので、コマンドと引数をひとつの文字列にまとめるんじゃなくて引数を分離して渡す: これならどんなinfected stringぶち込まれても大丈夫💪」「あ〜なるほど」

参考: prepareStatementの使用 - データベース接続 - サーブレット入門

「とにかくShellwordsはRubyでシェルを操作するならぜひ知っておくべき🧐」「単に便利というレベルじゃなくてマストなんですね」「Shellwords使わないなら、自分のコードがインジェクションされない理由を全部説明できないといけない🧐」「Shellwordsって今まで知らなかったんですが、重要だったんですね」「まあ忙しいときはString#shellescapeで引数ごとにちまちまやることもありますけど、と思ったらこれも内部でShellwordsを呼んでた😆」「引数をまとめて処理するならShellwordsの方がラク😋

⚓シェルいろいろありすぎ

「ところで引数の扱いってシェルによって全然違ったりしますよね😇: 今はだいたいみんなbash使ってますけど、たまにtcsh使う人がいたりしますし」「私はずっとbash一筋ですけど、AIX(IBMのUnix)を使うはめになったときは強制的にkshでした😆」「ksh😆」「名前しか見たことない😆」「kshってKorn Shellの略でしたっけ?」「はい、kshマジわからなかった😆」「某大学のデフォルトシェルが何か変だなと思ったらtcshで絶句しました🤣

参考: tcsh - コマンド (プログラム) の説明 - Linux コマンド集 一覧表
参考: ksh

「macOSもCatalinaからデフォルトがzshになっちゃいましたね😢」「zshでかくてforkが重いからやめた方がいい気がしますけど😆」「🤣🤣」「ぜとしぇをデフォルトにするぐらいならfishあたりの方がよかったのでは😆」「同意です😆

参考: Z Shell - Wikipedia
参考: Friendly interactive shell - Wikipedia

⚓expand_cache_keyのアロケーションを削減

# activesupport/lib/active_support/cache.rb#L80
      def expand_cache_key(key, namespace = nil)
-       expanded_cache_key = (namespace ? "#{namespace}/" : "").dup
+       expanded_cache_key = namespace ? +"#{namespace}/" : +""

        if prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
          expanded_cache_key << "#{prefix}/"
        end
        expanded_cache_key << retrieve_cache_key(key)
        expanded_cache_key
      end

つっつきボイス:「expand_cache_key的なものをこの間も見かけた気がしますね☺ウォッチ20200203)」「dupをやめて+演算子に変えてますね」「frozenの文字列に+するとduplicateするヤツか😆」「まだ慣れない😅」「+ってdup倍以上速いそうです」「解説がないとわかんなさそう😆

参考: String#+@ (Ruby 2.7.0 リファレンスマニュアル)

「そういえば最近Twitterで『速くなるのはいいんだけど他の人が読みづらくなるコードはつらい』みたいなのを見かけましたね😆」「😆😆」「それ拾いました😋↓」「ライブラリコードだしいいのではという考え方もありますが、書くならコメントにも書いて欲しい気持ち😆

⚓NBSPチェックの正規表現を修正

ついでにmatch?include?に修正しています。

# activesupport/lib/active_support/configuration_file.rb#L34
        File.read(content_path).tap do |content|
-         if content.match?(/\U+A0/)
+         if content.include?("\u00A0")
            warn "File contains invisible non-breaking spaces, you may want to remove those"
          end
        end

つっつきボイス:「/\U+A0/マジか😆」「テストなかったのかしら😆」「本来ならテストと一緒に書いておきたいヤツ」

⚓NBSP文字とラテン系言語

「NBSP(ノーブレークスペース)って日本だと普通使わないかなと思うんですけど、どうでしょう?」「HTMLで&nbsp;が出てくることはありますね☺」「Excelとかから自動生成するとちょくちょく混じってくる😆

「ローカライズ時代の経験ですけど、ラテン系ヨーロッパの人がすごくNBSPを使うんですよ: Orquesta De La Luzみたいな一続きの固有名詞を途中で改行したくないときにOrquesta●De●La●Luzみたいに●のところにNBSPを置く慣習がありまして😅」「へぇ〜😳」「スタイルガイドでNBSPを禁止してあっても、フランスやスペインの人が隙を見てNBSPをちょくちょく入れてくるので仕方なく普通のスペース文字に置換したりしてました😭

参考: ノーブレークスペース - Wikipedia
参考: エンジニア・Webデザイナー必読: アプリケーションを国際化・多言語展開する前に知っておくべきこと - Qiita — 昔Qiitaに書いた記事です

⚓rails statsでTypeScriptファイルもカウントするようになった

# railties/lib/rails/code_statistics.rb#L41
-   def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|coffee|rake)$/)
+   def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|ts|coffee|rake)$/)
      stats = CodeStatisticsCalculator.new

      Dir.foreach(directory) do |file_name|
        path = "#{directory}/#{file_name}"
        if File.directory?(path) && !file_name.start_with?(".")
          stats.add(calculate_directory_statistics(path, pattern))
        elsif file_name&.match?(pattern)
          stats.add_by_file_path(path)
        end
      end
      stats
    end

つっつきボイス:「お〜rails statsにJavaScriptの項目があったんだ」「今までtsが入ってなかったという😆

後でオレオレRailsアプリでrails statsやってみました↓。

+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |    344 |    232 |       5 |      30 |   6 |     5 |
| Helpers              |     64 |     44 |       0 |       3 |   0 |    12 |
| Models               |    100 |     39 |       3 |       6 |   2 |     4 |
| JavaScripts          |   2874 |   1740 |       0 |     110 |   0 |    13 |
| Libraries            |     47 |     44 |       0 |       1 |   0 |    42 |
| Model specs          |     91 |     43 |       0 |       0 |   0 |     0 |
| Request specs        |     15 |     11 |       0 |       0 |   0 |     0 |
| Routing specs        |     47 |     40 |       0 |       0 |   0 |     0 |
| Helper specs         |     24 |      2 |       0 |       0 |   0 |     0 |
+----------------------+--------+--------+---------+---------+-----+-------+
| Total                |   3606 |   2195 |       8 |     150 |  18 |    12 |
+----------------------+--------+--------+---------+---------+-----+-------+
  Code LOC: 2099     Test LOC: 96     Code to Test Ratio: 1:0.0

⚓primary db configが見つからない場合に正しくフォールバックするようにした

# activerecord/lib/active_record/railtie.rb#L135
-           next if db_config.nil?

            filename = ActiveRecord::Tasks::DatabaseTasks.cache_dump_filename(
-             db_config.spec_name,
-             schema_cache_path: db_config.schema_cache_path,
+             "primary",
+             schema_cache_path: db_config&.schema_cache_path,
            )

つっつきボイス:「最近この辺の修正をよく見かけるかも🤔」「元々”primary”を省略していたのを後付けでいろいろ直さないといけなくなった感」「マルチDBってやっぱり大変なんですね」「というより最初の設計で”primary”を省略できるようにすることが明示されてなくて後からやらないといけなくなったのかなと😆

⚓番外: ドキュメント修正

アソシエーション名が、joinされたテーブル名と対応していない場合、to_table:キーを持つHashを指定してそのテーブル名を指定すること。
同PRより

つっつきボイス:「add_referenceのAPIドキュメントに追記が入りました」「to_table:でやれるのか😳」「これ使ったことなかったけどいつからあったんだろう?」

後で調べるとRails 5.0.0.1でto_table:が入ったようです。


# activerecord/README.rdoc#L143
    class AddSystemSettings < ActiveRecord::Migration[5.0]
    class AddSystemSettings < ActiveRecord::Migration[6.0]

「トリビアですが、マイグレーションのサンプルコードの5.06.0に書き換えられました」「そのままコピペするとハマるヤツ😆


# activesupport/lib/active_support/inflector/methods.rb#L200
    #   classify('calculus')     # => "Calculus"
    #   classify('calculus')     # => "Calculu"

「意図的なスペルミスがうっかり修正されたのでrevertしたそうです😆」「😆

⚓Rails

⚓Hix on RailsのRailsチュートリ記事


同サイトより


つっつきボイス:「Railsでよくある感じの手順解説が並んでいて、昔だったらScreencastにされがちなノウハウが記事形式になってるのがいいなと思ったので」「こんだけみっちり書くモチベーションが凄い👍」「記事はどれも2019年12月以降なので新しいのも嬉しいです😋」「一覧表示のレイアウトとかTechRachoで参考にしたい❤」「TechRachoでやるなら記事もリニューアルしたいですね☺

⚓Interactorパターン

Factorial社では、早くから動詞(verb)を第一級市民にすることにした。新しいドメインがやってきたらそこにどんな動詞があるかを自問自答する。モデルのあらゆるCRUD/RESTコンベンションへの誘惑と戦い続け、ドメイン固有の動詞を使うようにした。「HelenはJohnからの最新の休暇リクエストをrejected = falseした」という言い方はしないだろう。「HelenはJohnからの最新の休暇リクエストをrejectした」と言うのが普通だ。
私たちはInteractorという新しい種類のオブジェクトを導入することでこれを実装した。新しくも何ともないコンセプトだが、私たちのアプリはRailsアプリとは似ても似つかないものになった。私たちのActive Recordモデルはきわめて小さくなってコールバックをまったく使っていない。ビジネスロジックのほとんどはInteractorに実装されている。コントローラがActive Recordと直接やりとりしなくなったことで、strong parametersの必要性もほぼほぼなくなった。アプリを動詞中心にしたことで、私たちのアプリは再利用が容易になり、もっと重要なことにコンポジション可能になった。
同記事より抜粋・大意


つっつきボイス:「上は『自分たちはこういう方針で設計している』みたいな記事なんですが、引用した法則1でInteractorパターンが登場していたのが気になりました」「お〜Interactorね☺」「この人たちはActive Recordのメソッド名が名詞形で単数複数あるのが好きでないらしくて、動詞の命令形で考えることにしたようです」「まあメンテできるならそういう方針でもいいんじゃないでしょうか☺」「kazzさんも『そこは設計の方針だから好き好きでいいよ☺』と言ってました」

そういえばinteractorというgemがありました(関連記事)。

InteractorパターンはHanamiに取り入れられているんですね。

参考: あーありがち - Clean ArchitectureとHanamiですっきりしてきた

「ふと最近のHanamiを見てみると、コミットの頻度が思ったより緩やかですね↓」「Hanamiは使ってる人は使ってるでしょうし、おそらくですけどHanami自体はあんまり多機能化の方向は目指してない気がしますね: Railsみたいに何でもやれるフレームワークが欲しい人はあまりHanamiは使わないかも🤔」「それもそうですね😅


hanamirb.orgより

⚓RuboCopでコードレビュー支援: Net::HTTPを使わせないcop(Hacklinesより)

# 同記事より
# cop/check_resilient_api_clients.rb

module RuboCop
  module Cop
    module ExternalServices
      class CheckResilientApiClients < Cop
        MSG = 'Use a more resilient API client'.freeze

        def on_send(node)
          add_offense(node, severity: :warning)
        end
      end
    end
  end
end

つっつきボイス:「なるほど自分たちでcopを書く話☺」「先週取り上げたNet::HTTP↓を使わせないためのcop作ったそうです👮‍♂️」「copを書くとDSL的なものやASTを扱ったりするのでいい勉強になりますね👍」「ああ確かに」「copではRubyを解析しないといけないのでRubyに対する理解が深まります😋

参考: Net::HTTP is not your API client - mwallba

# 同記事より
> ruby-parse -e 'Net::HTTP.get_response(uri)'
(send
  (const
    (const nil :Net) :HTTP) :get_response
  (send nil :uri))

参考: RuboCopの新しいルールを追加する方法(Custom Copの作り方) - アジャイルSEの憂鬱

⚓test doubleとVCRでテストを高速化(Hacklinesより)


つっつきボイス:「おぉVCR🥰」「VCRはリポジトリだけ見ててもあまりピンとこないんですけど、確か銀座Railsで実際にVCRを動かしているのを見ておおっなるほど❤という感じになりましたね: 動作をレコーディングして再生&やり直し可能にできるgem」「動いてるのを横から見るとこうやって使うのかと一発でわかる感😋

「VCRは、手続き的に『ここまで進んだらここまではできてる』みたいなものを上から下に書き下していくコードをテストするときにうまくマッチしますね☺」「おぉ」「なのでオブジェクト指向というよりは😆、手続き的な考え方」

「VCRは動作をリプレイできて、しかもそのリプレイをキャッシュしてくれるのがいいですね😋」「カセットというものを入れると1回呼び出したAPIのレスポンスを全部キャッシュしたものを返してくれる」「ふむふむ」「しかもカセットはファイルに保存できるので、カセットも一緒にgitにコミットしておけばテストでそのAPIを叩かなくてもよくなる🔨」「へぇぇ😳」「なのでclosed APIのテストなんかでとっても便利: 自分しかアクセスできないclosed APIに自分の環境でアクセスして取ってきたデータをベースにできる」「おぉ〜😍

「ほら、APIのスタブを書くのってとっても面倒じゃないですか😆」「はいたしかに😆」「APIのスタブやモックを作るということは、そのAPIの内部仕様を把握しないといけないですし、APIが巨大なXMLを返したりするとひたすらつらいですし、APIの仕様が変わってスタブを再編集とかやってられませんし😇」「ですです😇

「VCRはそういうAPIコールのレスポンスをファイルにも出せるので、そのカセットファイルがあればAPIにアクセスする権限のない他の人がテストするときにカセットからロードできる」「いいわ〜❤」「VCRはいろいろ有能💪: スタブとかモックとかAPI内部仕様とかを一切忘れてよくなるので、割り切りの落とし所としてはかなりいいと思いますね☺

参考: VCRを使って開発中にあほみたいにリクエストを飛ばさないようにする - かずおの開発ブログ(主にRuby)

参考: 永久保存版!?伊藤さん式・Railsアプリのアップグレード手順 - Qiita


元記事見出しより:

  • 特定の部分だけをテストしたい
  • VCR gemとカセットでテストを高速化
  • test doubleとメソッドスタブでテストを高速化
  • 重たいfeature testをどうにかする

RSpecで役に立ちそうないくつかのヒント(翻訳)

⚓開発速度と品質の落とし所は


つっつきボイス:「今回は割とThoughtBotのブログから拾いました」「ベロシティとクォリティ😆」「開発速度と品質のバランスをどうするかみたいな話ですね」「そもそもクォリティとは🤣」「🤣

拾い読み:

  • コードの品質とは
    • 「斧を振り回す凶暴なメンテナーがお前の家の場所を知ってると思ってコードを書け」とよく言われますが
    • 品質の高いコードは時間がかかり、顧客からメリットが見えにくい
  • 重要なのはチーム内コミュニケーション
    • 互いの意図を正しく推測できるレベルに持ち込む
    • プロダクトオーナーは設計上の決定の背景や理由を示す
  • 技術的負債を意識すること
  • コードの品質をいかに維持するか
    • 定期的なペアプロとコードレビュー
  • まとめ: 開発者とプロダクトオーナーが最初から忌憚なくやりとりできるようにするのが大事

⚓その他Rails


つっつきボイス:「guardは使いたい人が使うでいいかな〜☺」「チーム全員の環境にguardを入れるのは好きじゃない😆: guardって思わぬときに動き出したりすることがありますし、Docker環境の人もいればそうでない人もいたりすると邪魔になることもありますし😅」「たしかに🤔」「もちろん自分の環境を良くするために使うのは全然OK😋


前編は以上です。

おたより発掘

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

週刊Railsウォッチ(20200218後編)rubyapi.orgで高速検索、RuboCopとJUnitFormatter、AWS Organizationsでの管理ほか

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

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

Rails公式ニュース

Hacklines

Hacklines

週刊Railsウォッチ(20200226後編)dry-rbを使うべき理由、最近のRubyオンライン教材、AWSから乗り換えた話ほか

$
0
0

こんにちは、hachi8833です。リモートワークの追い風が吹いているのかもしれません。自分は特にリモートワーク志向ありませんが。


つっつきボイス:「ビッグウェーブ😆」「リモートワークは人によって向き不向きがどうしてもあるので、そこ次第でしょうね☺」「ですね」「あとメンバー全員がリモートワークについて寛容でないと成り立ちませんし」「リモートワークの難しさってありますね」「自分はリモートで仕事し続けてますし、結果出してれば何も言うことはありませんし😆」「情報の共有が甘い人はたぶんリモートに向いてない🤔

「がっつり自炊はしないけど、蓄えはいろいろあるので折に触れて作ったりしますね🥘」「パスタは超便利🍝: 20キロぐらいまとめて買ってるので余裕」「自分はレトルトのパスタソースと乾麺と調味料とかかな〜」「冷凍パスタも好きです😋」「冷凍はちょっと割高かも」「味のバリエーション的にはとても助かります」

「十割そばはいいですよ👍」「知らない世界😅

「完全食も最近は割とおいしいのが出始めてるみたい」「昔の桂花ラーメンはキャベツ入れて完全食と言ってましたね😆

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

⚓Ruby

⚓RubyGems.orgへの攻撃方法をひょんなことから発見した話


つっつきボイス:「gemを作っていた同僚がたまたまドメイン名をhttps://rubgems.orgとタイポしていたのを見かけて、もしかすると攻撃に使えるのではと思ってやってみたらできちゃった(ので関係者に知らせて塞いでもらった)んだそうです」「原文ではMITM(中間者攻撃)と書いてあるけど、これってそうかなぁ〜?😆」「私もそこがちょっと引っかかってました😆」「中間者攻撃は、文字通りクライアントからはわからないように通信の途中に割り込む手法なので、この記事にあるようなやり方を中間者攻撃って言うんだろうか?🤔」「もやもやする…😅

参考: 中間者攻撃 - Wikipedia

「記事の後半によると、脆弱性を知らせようとしたメアドが無効だったりと多少じたばたしたようです」「以下はrubygemsとbundlerに立ったissueです」

「スクワッティングという言葉を今更知りました😅」「ほら、『ドメイン 女性 振り向き』あたりで検索すると出てきそうな、おそらく世界で一番人目に触れたに違いないあの画像と関連するヤツですよ🤣」「あ〜、ひと頃やんなるほど目にしたアレ🤣

参考: サイバースクワッティング - Wikipedia

typo squatting = 1文字違いのドメイン名を押さえる行為

後でキーワードを日本語英語でいろいろ変えて検索してみましたが、あの振り向き画像を見つけられませんでした。別にいいんですが。

「rubygems.org本体が乗っ取られたならともかく、1文字違いのドメイン名とかまで無限に対処できませんし😆」「そうですね」「連絡された方も対応に困りそうなissue😆」「日本語ドメインみたいなUTF-8ドメイン名まで考えたらきりがない😆」「ドメインスクワッティングはわかるけど中間者攻撃と呼ぶのはやっぱり引っかかるなぁ😆


記事概要:

  • rubygems.orgを使った攻撃を試す
    • gemコマンドでやった場合
    • bundlerでやった場合
    • 結論
  • ダミーを置いたら100 IPほど釣れた
    • この問題を関係者に連絡するのに手間取った

⚓Rubyオンライン学習教材5つ(Hacklinesより)


つっつきボイス:「最近のRubyオンライン教材ってどうなってるかなと思って」「ほほぅ☺

記事の人が以下のサイトに5つの教材リストを置いてあります↓。

⚓dry-rbを使うべき理由(Hacklinesより)


つっつきボイス:「dry-rbはいいですね🥰: 半端なライブラリを再発明しないで済むためにも、dry-rbにどんなものがあるかぐらいは知っておきたい☺」「ですね☺」「こんなライブラリ欲しいな〜って思っているとdry-rbに既にあることが割とありますし😆」「とにかく存在を知っておくことが重要🧐


記事ななめ読み:

  • ビジネスロジックの置き場所にもいろいろあるが、短期プロジェクト(と一部の長期プロジェクト)に向いたやり方というものはある
  • 問題
    • MVCパターンではロジックをモデルに配置し、コントローラを薄くするのが常套手段
    • モデルが育つと分割がつらくなり、SOLIDの原則(特に単一責任の原則)も破られがち
  • dry-rbとは
    • 一種のgemエコシステムであり、必要なものをRubyアプリに取り込んでプラットフォームにできる
    • コントローラで使う例(dry-transactiondry-container
    • Operationクラス(2つのクラスを飲み込んでいる)
      • Containerクラス
      • Transactionクラス
    • 共通ロジックの再利用例
    • dry-valiodationによるスキーマバリデーション
    • 操作のステップ
  • まとめ
    • この方法が万能というわけではなくメンテ問題が魔法のように消えるわけでもない
    • しかしロバストネス、テスタビリティ、スケーラビリティをビジネスロジックに提供してくれる
    • ビジネストランザクションに責任を持つロジックが見つけやすくなる
    • スコープや関連付けといったRailsモデルにありがちなものをスキップできる

3年前に以下の記事を書いたときのdry-rbは16個でしたが今見ると21個に増えてますね。

Ruby: Dry-rb gemシリーズのラインナップと概要

⚓Net::HTTPをAPIクライアントにする必要はない(RubyFlowより)


つっつきボイス:「Ruby標準ライブラリのNet::HTTPはたしかにプリミティブすぎて使いづらい😆」「記事はFaradayとか他にもっといいものがあるんだからという感じ」「Faraday以外にもありますけど😆


記事ななめ読み:

  • 専用gemのない外部APIにアクセスしたいときがある
  • Net::HTTPでやるとタイムアウトエラーまみれになりがち
  • 本番でのいろんなコケ方を考えておく必要がある
    • リクエストがリダイレクトされる
    • ネットワークエラー
    • リクエストが多すぎてサーバーがあふれる
    • 結果が返るのが遅い
    • 転送速度の上限
  • Net::HTTPはベアメタルすぎ
  • Faradayでやれること
    • 認証
    • リトライの頻度を指数関数的に下げる
    • パラレルリクエスト


lostisland.github.io/faradayより

⚓その他Ruby


同リポジトリより


つっつきボイス:「Ruby製のハッキング/ペネトレーションテストツールだそうです」「WinRMってどういう意味かしら?」「わがんね😆

「それにしてもメタル風味溢れるジャケですね🎸」「この手のペネトレーションとかポートスキャナー的なツールはだいたい中二感がそこはかとなく漂います😆」「20年ぐらい前はこういう絵柄をよく見かけた気がするんですけど最近あまり見ないのは自分が知らないだけ?😆」「もしかすると流行が次に進んでたりして😆」「こういうのは万国共通なのかな?🤔

参考: 「お前らは厨二病を患ったことがある?」海外アニメオタクの反応 | 翻訳だだだ! 【海外の反応】
参考: 中二病[zhōng èr bìng] | 中国語を学ぶ~ん
参考: 中二病を意味する英語スラング Edgelord とは|英語ネットスラング辞典

私の中二はこういうの↓でした😇。1969年にあったはずのないカオナシ巨大ロボが目印です🥫

⚓DB

⚓データエンジニアの需要が倍増とのレポート(DB Weeklyより)


同記事より


つっつきボイス:「データベースエンジニアじゃないという 🤣」「ETLとか言ってるからデータアナリストとかそっち系でしょうね☺

参考: Extract/Transform/Load - Wikipedia

「この手の話は微妙で、ニーズがあるということは需要に対して供給が足りないわけですけど、うんとたくさん欲しいかどうかは別😆」「たしかに😆」「仮に1000人欲しいところに200人しか市場にいなければ倍率500%になりますけど、そこに10000人参入してきたら速攻あぶれるでしょうし😆」「パイの大きさ注意😆

「おそらくこの分野で本当に求められているのは、単にその辺に転がっている素材を集めて加工するだけの人よりも、ちゃんと実施計画まで立てて回せる人だと思いますし☺

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

⚓AWSホスティングをやめて売り出し中のUpCloudに乗り換えた話


upcloud.comより

記事より: EC2インスタンスのメール配信、ここがつらい

  • AWSでPTR(またはrDNS)レコードを設定するにはリクエストチケットを送る以外に方法がない
  • AWSのElastic IPアドレスはスパムで汚れがち
  • オープンリレーの疑いで(実際は違ったのに)ポート25番を突然ブロックされることがある

つっつきボイス:「UpCloudってヨーロッパ系のクラウドホスティングサービスみたいですね」

「あ〜たしかにAWSはメール送信というかポート25番にとても厳しいんですよ: それはわかる😢」「ちょっと変な動きするとブロックされる感じですか」「当然なんですが、AWSはスパムメールの温床として使われることについてとてもセンシティブなので、何かあったらカジュアルに即塞ぎにかかる可能性はありますね🧐」「メール送信が中心のサービスだとAWSでは大変そう😅

「記事ではElastic IPにも不満述べてますけど、引越し先のUpCloudがAWSと同じぐらい大きくなれば結局同じことになるのではという気がしますし😆: 記事にあるElastic IPの”bad reputation”というのがそれです」「おぉ?」

「Elastic IPはユーザーが取得して解放することができるので、悪い業者は取得したIPがスパムブラックリストに乗るまでスパムを送りつけて、IPがリストに乗ったら別のIPにさっさと乗り換えたりするんですよ」「あ〜なるほど」「なのでAWSでElastic IPを取得してみると既にブラックリストに乗ってるなんてことがあったりします😇」「取ったばかりでそれだったら悲しすぎ😭」「ユーザーが簡単にElastic IPを取れるようになってたら、そりゃそうやって使われるでしょうし😆

「陳情書を提出しないとPTRレコードを設定できないのはそのとおりで、手続きが面倒😤

⚓さくらのSSL証明書発行時間短縮


つっつきボイス:「社内Slackで喜びの声が上がってたので」「前は2時間だったのが10分に短縮されたと」「まあ2時間は最長の場合で、実際には30分ぐらいかかってましたけど😆

「ちなみに他のホスティングサービスからさくらのレンタルサーバーに移行するときにLet’s Encryptを使おうとすると厄介です: さくらのLet’s Encryptの認証はDNSじゃなくてHTTPで行うので、先にAレコードをさくらに向けておかないと証明書が取れないという😢」「あ〜」「なので移行前から証明書で暗号化してたサイトをさくらに移すときに、暗号化が効かなくなる期間がどうしても発生してしまう😇」「そうでしたか😅」「今度から10分まで短縮はされましたけど、やはり10分間は暗号化が効かないので、夜のうちにさっと済ますとかになりますね😆」「うーむ」

Let’s EncryptがVerisignと棲み分けできる理由: SSL証明書の「DV、OV、EV」とは何か

⚓その他クラウド

↑つっつき時は英語でしたが今日見ると日本語になってました。


参考: Linux カーネル の /dev/random について - myokotaの日記

つっつきボイス:「お/dev/randomの話😋」「昨年の乱数記事↓でも/dev/randomが話題になってましたね☺」「上の記事にもあるけどエントロピーが足りなくなるとブロックするとかあった🎲

乱数について本気出して考えてみる


「うちのラズパイまだ起動してなかった😆」「うちはまだ配線終わってません😆」「まあラズパイでも普通にLinux動きますし: ただし外部出力すると爆熱になる🔥」「どんな出力ですか?」「HDMI🔌: さすがにヒートシンク付けないと放熱がヤバい😇

⚓JavaScript

⚓fishery: ThoughtbotのJavaScript/TypeScript向けファクトリーライブラリ

// 同記事より
// factories/user.ts
import { Factory } from 'fishery';
import { User } from '../my-types';

export default Factory.define<User>(({ sequence, factories }) => ({
  id: sequence,
  name: 'Bob',
  address: { city: 'Grand Rapids', state: 'MI', country: 'USA' },
  posts: factories.post.buildList(2),
}));

つっつきボイス:「factory_botでお馴染みのThoughtbotがJavaScriptのファクトリーライブラリを出したそうです」

fishery: 漁業

「ははぁたしかにfactory_botライク」「この書き方が果たしてわかりやすいかどうかはともかく😆、factory_botを使い続けているRailsエンジニアには敷居が低いのかも?🤔」「JSだとまた違うんでしょうね」「JSのテスティングツールはいっぱいあってようわからん😆」「ファクトリーはデータ生成するんだから、どちらかというとフロントよりサーバーサイドで欲しいんじゃないかしら: クライアント側でビジネスオブジェクトを作るのはなくもないだろうけど、どのぐらい必要なんだろう?🤔

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

⚓Chromeの動画広告をGoogle自ら減らす方向に


つっつきボイス:「Google自らが動き出しました」「や〜YouTube見てると煩わしい動画広告だらけ🤣」「だらけ😆

「Firefoxとかが動画広告を締め出しにかかってきたからChromeも付いてこざるを得なくなったのかしら🤔」「おかげで今度は広告画像をJavaScriptでパラパラ漫画的に配信する輩が登場して、そっちの方が重いという、何だかなあという流れになってきてますし🤣」「🤣

⚓その他フロントエンド

つっつきボイス:「おほ、SPDYからHTTP/2に移行したのか」「すげえ」「LINEはエンジニアどっさり集めてますし」「そういえばLINEは割とJava文化圏なんですよ☺」「知らなかった〜」


「ほほぅ〜WebアプリでNFCタグとな」「NFCは前からありますけど、Webでやれるというのが新しい」

「NFCといえば、最近になってコンビニの端末がNFCクレジットカードにようやく対応し始めましたね😋」「おぉ」「なおNFCクレカは非接触なのでICクレカとは別物です」「このNFC Pay↓とかがそうなんですね」「NFCクレジットカードだとスキャナーとかを通さなくてよくなるので、SUICAの代わりにこれにすれば入退出も気にしなくていいし、こっちの方がいいんじゃね?って😋

参考: NFCとは?スマホやカードをかざせば決済ができる便利な機能とサービス|Have a good Cashless.~ いいキャッシュレスが、いい毎日を作る。~
参考: 日本で「NFC Pay」はいつ普及するのか:モバイル決済最前線 - Engadget 日本版

⚓言語・ツール

⚓git rebase --ontoを思い出せ


つっつきボイス:「今回は割とThoughtbotのブログから拾ったんですが、ブランチを付け替えられるgit rebase --ontoをこれで知りました」「もうコマンドラインでrebaseしてないし😆」「😆」「CLIはコンフリクトの処理がめんどくさすぎて😇

参考: git rebase --onto どこへ どこから どのブランチを - Qiita

⚓形式的な仕様記述

つっつきボイス:「はてブに上がっててみんな『なるほどわからん』という感じでした😆」「こうやって仕様を形式的に記述するのは、その中で全部完結できるといいんですよね😋」「😋」「こういう手法は昔からいろいろあるんですが、今ひとつ普及しませんね🤣」「🤣」「どちらかというとコンピュータサイエンス寄りの手法で、いわゆる『バグのないプログラムを作るには』みたいなヤツ」「宣言的に書く感じですね」

参考: 形式仕様記述 - Wikipedia

「そういえば自動車業界なんかでは、エンジン内部の制御に使うソフトウェアを書くときにそういう形式言語的なものを使ってるという話を小耳に挟んだことがあります👂」「バグがあると人が死ぬようなミッションクリティカルな方面では使っていることありますね: 宇宙関係やプラント、あと金融とか」「放射線医療機器とか」「そういう方面では形式仕様記述のニーズありますね☺

「まあWebではまずやらないと思いますが🤣」「と思います🤣」「基幹系でないWebでそこまで仕様ががっちり固まることはめったにないでしょうし」「まあ最近だと基幹系もビジネスロジックに合わせて変更を強いられたりしますけど😅

⚓その他言語


つっつきボイス:「ブクマが凄いことになってますね」「しかもほとんど無言ブクマ😆」「大学の授業の教材なんですね」「しかもプログラミング演習と銘打たれているということは大学1年とか2年向けの授業🎓

「本編も普通にいい感じですけど、特に2冊目のコラム編は楽しいトピックがいろいろ盛り込まれていて、世間話感サイコー❤」「サイコー😍」「ちょうど教授が授業中に雑談で話すような感じなんですよね: 自分が勉強会で話すときのスライドも話すことの2割ぐらいしか書いてなくて、後はそのとき思いついたことを話してますし😆」「😆」「読み物として面白い😋

「自分で作る教材だと、あんまり盛り込んだら読まれなくなるかなと思ったりすることもありますけど、こうやって広くパブリッシュされる本では面白さも重要な要素だと思います📖」「この間の文字コードの歴史みたいな話(ウォッチ20200212)は、今問題を解決したい人にとってはそんなに役に立たないけど😆、その分野に興味を持って勉強している人にとっては面白く読めたりしますし」「わかります🥰」「ターゲティングが肝心☺

⚓その他

⚓メモリダンプを模様で読む男


つっつきボイス:「これはありますね」「ありますね」「バイナリエディタのマッピングツールで色を眺めて『この辺からプログラムコードが始まる』『この辺からデータ』みたいに目ヂカラでわかる人、います」「私はできませんけど😆、そういう話はしょっちゅう見聞きしてました」「自分もできませんけど😆、壊れたデータをどうにかせざるを得なかったときにやりましたし」「この色とこの色とか、色の散らばり具合とか、バイナリも一応傾向がありますね」「ぐちゃらっとなってたら圧縮ファイルとか😆

「こういうのはゲームのセーブデータの解析してる人たちが強い😆」「あ〜そうそう😆」「ゲームの認証解除したり😆

私は復活の呪文の時代どまりなのでアウトオブスコープです😅

以下はつっつき後のツイートです。

⚓ここから始まった2題

なお、「MacintoshのGUIの父」という認識をされることについては「違います」と否定した上で、「でも、親子鑑定をしたら、祖父母の1人ではあるかも」とコメントしています。
同記事より

参考: ラリー・テスラー - Wikipedia

つっつきボイス:「かつてゼロックスでコピペを生み出した方でした」「当時そういうのをやってたのはゼロックスとかAT&Tぐらいだったのかも」「あとベル研とか」

「当時viのモード切替がめちゃめちゃ大変だったので、それが嫌でモードレスにやれるようにしたらしいと聞きました」「へぇ〜」「当時はviユーザーを仮想敵にしていたんじゃないかという噂もあるみたいです😆」「モード切り替えって人間に負担大きいですよね☺

「ここから始まったといえばこれも↓」「お〜村井先生の最終講義聞きに行きたかった😂」「書き起こしかなり長いです」「村井先生といえばワイルドで楽しい武勇伝が盛りだくさんな方ですけど、公開して大丈夫な話だったのかしら😆」「最終講義だし、さすがにオフレコ話はないか😆」(以下延々ここだけの楽しい話)

⚓番外

⚓吹く楽器は厳しそう


つっつきボイス:「楽器を演奏するための脳のエリアを損傷していないことを確認するために、手術中にバイオリン弾いてもらったんだそうです」「あ〜そういうこと😆」「意識があるまま脳を手術するのって昔の方がやってたかも😆」「脳そのものには痛覚がないって昔知ってびっくりした覚えあります🧠

参考: 脳は痛みを感じない | メディカルクリニック柿の木坂 | 都立大学 内科 神経内科


後編は以上です。

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

週刊Railsウォッチ(20200218後編)rubyapi.orgで高速検索、RuboCopとJUnitFormatter、AWS Organizationsでの管理ほか

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

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

RubyFlow

160928_1638_XvIP4h

Hacklines

DB Weekly

db_weekly_banner

Publickey

publickey_banner_captured

GitHub Trending

160928_1701_Q9dJIU

Rails 6: Docker/docker-compose/dipで`rails new`力を取り戻す

$
0
0

こんにちは、hachi8833です。いつからrails newが面倒になってきたのでしょう。

私の場合rails newする機会が多いので、Evil Martians流のDocker開発環境構築の次の段階として、自分用にDocker環境ビルドとrails newのためのしくみを作りました。

Rails 6のDocker開発環境構築をEvil Martians流にやってみた

概要

やりたいこと

  • Dockerで楽にrails newできるようにする
  • Rails 6 + Ruby 2.7以降のWebpackerを前提とする開発環境をDocker上に構築する
    • Evil Martians流のDocker開発環境構築に倣う
  • 無駄にカスタマイズしない
    • DockerでRailsとWebpackerを動かすために必要な最小限のカスタマイズに留める
  • 今後RailsやRubyやNodeやYarnがアップグレードされてもdocker-compose.ymlのバージョン番号を調整するだけで使えるようにする

前提とする環境

  • macOS(Docker for Mac)
  • Linux(Ubuntuなど)

なおWindows環境では試していません。

必要なもの

  • Docker
  • Docker Compose
  • dip(以下の記事をどうぞ)

docker-composeを便利にするツール「dip」を使ってみた

リポジトリ

手順

1. 準備

  • リポジトリをgit cloneし、ディレクトリ名を適宜変更する
    • ディレクトリ名がDockerコンテナ名に使われる
    • .gitを削除して自分用にgit-flow initなどする
  • 必要に応じてdocker-compose.ymlのアプリ名やバージョンを修正する。
# PostgreSQL版の場合
x-var: &APP_IMAGE_TAG
  "my_app:1.0.0"
x-var: &RUBY_VERSION
  "2.7.0-slim-buster"
x-var: &PG_MAJOR
  12
x-var: &POSTGRES
  "postgres:12"
x-var: &NODE_MAJOR
  12
x-var: &YARN_VERSION
  1.21.1

なお、Ruby 2.7にはbundlerが同梱されているので、別途インストールはしない前提にしました。

  • 必要に応じてdip.ymlのrails new行にオプションを追加しておく。
  - dip rails new . -d postgresql --webpacker --skip-listen --skip-git

2. ビルド

ここまで行えば、後はプロジェクトディレクトリで以下を実行するだけです。

  • SQLite3版の場合
dip provision
  • PostgreSQL版の場合
dip provision
dip rails db:prepare
dip rails db:prepare RAILS_ENV=test # 必要なら

PostgreSQL版だとdip provisionの中でrails db:prepareを呼んだときにPostgreSQLへの接続に失敗する問題が解決できなかったので、db:prepareは手動で実行することにしました。

3. 起動

dip rails sしてhttp://localhost:3000をブラウザで開けばいつものWelcome画面が表示されます。

scaffoldなどで作ったページを開くとWebpackerが動き出します。

[Webpacker] Compiling...
[Webpacker] Compiled all packs in /app/public/packs
[Webpacker] Hash: 32e57f147dbdcbbf0c82
Version: webpack 4.41.6
Time: 3765ms
Built at: 02/27/2020 1:51:09 AM
                                     Asset       Size       Chunks                         Chunk Names
    js/application-bbe9c4a129ab949e0636.js    124 KiB  application  [emitted] [immutable]  application
js/application-bbe9c4a129ab949e0636.js.map    139 KiB  application  [emitted] [dev]        application
                             manifest.json  364 bytes               [emitted]
Entrypoint application = js/application-bbe9c4a129ab949e0636.js js/application-bbe9c4a129ab949e0636.js.map
[./app/javascript/channels sync recursive _channel\.js$] ./app/javascript/channels sync _channel\.js$ 160 bytes {application} [built]
[./app/javascript/channels/index.js] 211 bytes {application} [built]
[./app/javascript/packs/application.js] 749 bytes {application} [built]
[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 552 bytes {application} [built]
    + 3 hidden modules

Completed 200 OK in 7683ms (Views: 7664.0ms | ActiveRecord: 1.8ms | Allocations: 24008)

Docker向けにチューニングした点

基本的にEvil Martians流にしていますが、以下をカスタマイズしてあります。

1. listen gemをインストールしない(Mac向け)

listen gemはRailsでデフォルトでインストールされます。しかし、Docker for Macでこのgemがあると、ソースを更新してブラウザをリロードしても、Docker環境で起動したサーバーで、Dockerボリュームの変更が反映されないという既知の問題がありました。

そのため、--skip-listenを指定してlisten gemをインストールしないようにしています。listenがあっても大丈夫になったら--skip-listenを外すつもりです。

2. node_modules/ディレクトリをgitignoreする

rails newで生成される.gitignoreには、node_modules/やpublic/packs/、public/packs-testといったディレクトリをignoreする設定が含まれていません。

そのため、dipのrails new--skip-gitを追加してgit関連ファイルを生成しないようにしています。

スクリプトで.gitignoreに追加する手もありますが、定番のignoreエントリも含めてキットに.gitignoreを最初から入れておくことにしました。

3. node version manager ‘n’を追加

ビルド時点で最新のNode.jsをインストールし、アップグレードを行いやすくするために、nというバージョンマネージャをインストールする設定をDockerfileに追加しました。dip shでログインし、n ltsなどを実行することでNode.jsを最新にできます。

おまけ

なお、Evil Martians流ではyarnをnpmではインストールせず、curlとaptでインストールしていることに気が付きました。実際、Yarnの公式情報↓にはyarnをnpmでインストールすべきでないと書かれています。

参考: インストール | Yarn

関連記事

Fullstaq Rubyの第一印象とDocker/Kubenetes Rubyアプリとの統合(翻訳)

週刊Railsウォッチ(20200302前編)RubyKaigi 2020は9月に延期、Railsのセキュリティパッチバージョニングが変更、dry-monadsほか

$
0
0

こんにちは、hachi8833です。テックカンファレンスの開催状況をまとめてくれた方がいました。ありがとうございます。


つっつきボイス:「こんなの見つけました」「お〜すげ〜😳、半分以上まっかっかじゃないですか🟥」「RubyKaigiは(つっつき時点では)まだ開催予定ですけど予断を許さない感じ…(臨時ニュース参照↓)」

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

⚓臨時ニュース: RubyKaigi 2020は9月3日〜5日に延期

土曜日に延期のアナウンスがありました。詳しくはアナウンスをどうぞ。宿や交通の取り直しをお忘れなく。


esa-pages.ioより

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

この週はコミットリストから見繕いました。

⚓strict_loadingを追加

関連付けからのlazy loadingを防ぐ#strict_loadingを任意のレコードに追加した。
親レコードがstrict_loadingとマークされている状態で関連付けをlazy loadしようとするとエラーを発生する。余分なクエリを出さないよう関連付けをpreloadする場所を見つけるのに便利。
Changelogより大意

# Changelogより: 使い方
>> Developer.strict_loading.first
>> dev.audit_logs.to_a
#=> ActiveRecord::StrictLoadingViolationError: Developer is marked as strict_loading and AuditLog cannot be lazily loaded.

つっつきボイス:「has_manyの方向にアクセスしたときの振る舞いなのかな🤔」「どうなんでしょう😅」「使い方のコード例だとfirst取った時点でeager loadingされてそうな気がしないでもないけど…ちょい走り書き感😆」「preloadすべき場所を見つけたいときに便利ということみたいです」「このプルリク、久しぶりに👍とか❤がびっしり押されてるのでかなり喜ばれてる感じ😋

⚓SchemaCacheでMarshalオブジェクトからのシリアライズも選べるようになった

# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L139
+     def dump_to(filename)
+       clear!
+       connection.data_sources.each { |table| add(table) }
+       open(filename, "wb") { |f|
+         if filename.end_with?(".dump")
+           f.write(Marshal.dump(self))
+         else
+           f.write(YAML.dump(self))
+         end
+       }
+     end

つっつきボイス:「SchemaCacheってDBのスキーマかな?」「だと思います」「今まではYAMLからdumpしていたのがMarshallからのdumpもできるようになったということか」

このプルリクによって、シリアライズ戦略にMarshalYAMLのどちらかを選べるようになった。スキーマダンプのパスファイル名は、拡張子が.dumpならMarshal、.ymlならYAMLで、データベース接続ごとに定義する。デフォルトはYAMLのまま。
同PRより大意

「なるほどこの辺↓で拡張子を見てファイルを読み込んでる」「どうやらMarshalからのダンプは廃止の流れだったのにまたMarshallが入ってきたので、デフォルトはYAMLにしつつMarshalも選べるようにしたようです」「そのためにload_fromdump_toを追加したということね😋

# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L6
+     def self.load_from(filename)
+       return unless File.file?(filename)
+
+       file = File.read(filename)
+       filename.end_with?(".dump") ? Marshal.load(file) : YAML.load(file)
+     end

参考: 【Rails】SwitchPoint利用時にSchemaCacheを設定しSHOW FULL FIELDSを防ぐ - Qiita

SchemaCacheについて
Railsではrake db:schema:cache:dumpを使うことでdb/schema_cache.ymlにテーブルやカラムの情報が書き出されます。
このキャッシュを使うことでActiveRecordが型情報などを特定する手助けになります。
同記事より

⚓ActiveRecord::Baseサブクラスからのconnected_to呼び出しを禁止

振る舞いは変わっていないが、以前のAPI名だと「そのクラスでだけ接続を切り替えられる」かのようにミスリードする可能性がある。connected_toは現在の接続のコンテキスト(ロールなど)を切り替えるものであって、接続そのものを切り替えるのではない。
同PRより大意

# activerecord/lib/active_record/connection_handling.rb#L138
    def connected_to(database: nil, role: nil, shard: nil, prevent_writes: false, &blk)
+     raise NotImplementedError, "connected_to can only be called on ActiveRecord::Base" unless self == Base
+
      if database
        ActiveSupport::Deprecation.warn("The database key in `connected_to` is deprecated. It will be removed in Rails 6.2.0 without replacement.")
      end

つっつきボイス:「ActiveRecord::Baseのサブクラスでconnected_toを呼べなくするそうです」「connected_toって何するんだっけ?😆」「connects_toとかconnected_to?とかよく似た名前のメソッドがあってややこしいです😅」「親でしか許さないとなると継承とは一体何だったのか🤣」「🤣」「このconnected_toは接続のコンテキストを切り替えるものなのに、接続を切り替えると勘違いされやすかったのね😆」「メソッド名がイマイチなのかも😆

⚓テンプレートレンダリング時のハッシュアロケーションを削減


つっつきボイス:「こちらは引数を修正しつつアロケーションをちょっぴり減らしたそうです」「可読性の向上がメインみたいなのでこれでいいのかも☺: これでカリカリチューニングで可読性落ちたとかだったら残念ですし」

# activerecord/lib/active_record/railties/collection_cache_association_loading.rb#L4
  module Railties # :nodoc:
    module CollectionCacheAssociationLoading #:nodoc:
      def setup(context, options, as, block)
-       @relation = relation_from_options(**options)
+       @relation = nil
+
+       return super unless options[:cached]
+
+       @relation = relation_from_options(options[:partial], options[:collection])

        super
      end

-     def relation_from_options(cached: nil, partial: nil, collection: nil, **_)
-       return unless cached
-
+     def relation_from_options(partial, collection)
        relation = partial if partial.is_a?(ActiveRecord::Relation)
        relation ||= collection if collection.is_a?(ActiveRecord::Relation)

        if relation && !relation.loaded?
          relation.skip_preloading!
        end
      end

「ほほぅ、relation_from_options(**options)をいきなり呼ぶのをやめてるので、**optionsの展開のタイミングを変えてますね」「たしかに」「修正後はrelation_from_options(partial, collection)となってて、前のような雑な**じゃなくてオプションをきちんと展開するようになってるので、やはり可読性向上だと思います😋」「コメコメを追放して読みやすくしてくれたんですね🎉

⚓スキーマキャッシュ関連修正


つっつきボイス:「2つともスキーマキャッシュの話なので冒頭の#38432と関連してるっぽい」「このatomic_write↓っていうファイル関連メソッド初めて知りました」「Active Supportの機能でしたか」

# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L143
      def dump_to(filename)
        clear!
        connection.data_sources.each { |table| add(table) }
-       open(filename, "wb") { |f|
+       File.atomic_write(filename) { |f|
          if filename.end_with?(".dump")
            f.write(Marshal.dump(self))
          else
            f.write(YAML.dump(self))
          end
        }
      end

「ちなみに自分はRubyのopenメソッドって雑なのでキライ😆」「😆」「うろ覚えですけどopen使って外部APIのURLを叩くこともできた気がする」「マジで😅」「openっていろんな意味になったりするのが怖いので、atomic_writeみたいな書き方にするのはいいことだと思います😋

Ruby 2.7で調べると、Kernel#openでURLを開くにはrequire 'open-url'する必要があるようです。昔は違ったのかも?🤔

require 'open-url'
uri = 'https://ドメイン名/'
uri = URI.parse(uri)
open(uri).read 

上でURLにアクセスできましたが、以下のwarningが表示されました。URI.openURI#openだとwarningは出なくなりました。

warning: calling URI.open via Kernel#open is deprecated, call URI.open directly or use URI#open

⚓新しい属性に対応

以下のHTML属性が追加された:
* allowpaymentrequest
* nomodule
* playsinline

# actionview/lib/action_view/helpers/tag_helper.rb#L15
-     BOOLEAN_ATTRIBUTES = %w(allowfullscreen async autofocus autoplay checked
-                             compact controls declare default defaultchecked
-                             defaultmuted defaultselected defer disabled
-                             enabled formnovalidate hidden indeterminate inert
-                             ismap itemscope loop multiple muted nohref
-                             noresize noshade novalidate nowrap open
-                             pauseonexit readonly required reversed scoped
-                             seamless selected sortable truespeed typemustmatch
-                             visible).to_set
+     BOOLEAN_ATTRIBUTES = %w(allowfullscreen allowpaymentrequest async autofocus
+                             autoplay checked compact controls declare default
+                             defaultchecked defaultmuted defaultselected defer
+                             disabled enabled formnovalidate hidden indeterminate
+                             inert ismap itemscope loop multiple muted nohref
+                             nomodule noresize noshade novalidate nowrap open
+                             pauseonexit playsinline readonly required reversed
+                             scoped seamless selected sortable truespeed
+                             typemustmatch visible).to_set

つっつきボイス:「また新しい属性が増えたようです」「改修はビューヘルパーだけど、HTMLの属性が増えたって言ってる?えぐい😆」「allowpaymentrequestってペイメント系のリクエストに関連してる感じ💰」「BOOLEAN_ATTRIBUTESには他にもどっちゃりboolean属性入ってるな〜😅」「フレームワークはこういうのに対応しないといけないから大変そう…」

参考: HTML Standard — whatwg.org

allowpaymentrequest
iframe要素の中でPaymentRequestインターフェイスを用いてpaymentリクエストを許可するかどうかを指定
nomodule
module scriptをサポートするuser agentの実行をscript要素で禁止するかどうかを設定
playsinline
video要素の動画コンテンツをplaybackエリアで再生するようuser agentにすすめる

⚓Railsのセキュリティアップデートポリシーが変更

# guides/source/maintenance_policy.md#L57
-the x-y-stable branch. For example, a theoretical 1.2.3 security release would
+the x-y-stable branch. For example, a theoretical 1.2.2.1 security release would

つっつきボイス:「Railsのセキュリティメンテナンスポリシーが変わってセキュリティパッチのバージョン番号が4桁構成になったそうです」「えっとメジャーバージョンとマイナーバージョンと、3つ目は何だっけ😆」「えっと、パッチバージョン」

参考: セマンティック バージョニング 2.0.0 | Semantic Versioning

「6.2.1を6.2.2とかにするのはちょいコワイけど、6.2.1.1とかにするならセキュリティだから当てなきゃという感じが伝わってきますね❤」「たしかに〜」「Railsのバージョンを上げたいわけじゃないけどセキュリティパッチは当てないといけないということはよくありますので☺

⚓Rails

⚓Dry-rbのモナドでServiceを改善する

# 同記事より
class Reservation::Create
  include Dry::Monads[:result]

  def initialize(user:, room:, start_date:, end_date:, notes: nil)
    @user = user
    @room = room
    @start_date = start_date
    @end_date = end_date
    @notes = notes
  end

  def call
    check_if_room_available
      .bind { create_reservation }
      .bind(method(:send_notification))
  end

  private

  attr_reader :user, :room, :start_date, :end_date, :notes

  def check_if_room_available
    Try(ActiveRecord::ActiveRecordError) { existing_reservations.exists? }.to_result.bind do |result|
      if result
        Failure('The room is not available in requested time range')
      else
        Success(nil)
      end
    end
  end

  def create_reservation
    reservation = Reservation.new(
      user: user, room: room, start_date: start_date, end_date: end_date, notes: notes
    )

    if reservation.save
      Success(reservation: reservation)
    else
      Failure('The reservation could not be created')
    end
  end

  def send_notification(reservation:)
    NotificationMailer
      .notify_room_booked(user: user, reservation: reservation)
      .deliver_later

    Success(reservation)
  end

  def existing_reservations
    Reservation.where(room: room).in_time_range(start_date: start_date, end_date: end_date)
  end
end

つっつきボイス:「kazzさんが最近Railsでパイプラインの実装で悩んでるので、Dry-rbのこの辺の記事が参考になるかと思って今ローカルで雑に翻訳してみました(許可取れたら公開します)」「自分がモナドやるときが来ようとは: 今この記事に沿ってちょっとやってみてるんですけど、Doでやるのとbindでやるところがやっと見えてきたのでモナドはこれから😆

「今ボク関数脳🧠」「この記事で使ってるDry-monadsはDry-transactionの後継ということを翻訳してて知りました」「モナドむずい😭」「大学で数学専攻しててもむずいということは相当手ごわいんですね😳

「そういえばdry-rbのリポジトリにdry-railsというのが作り中なのを見つけました」「dry-rbでRails的なものを作ってみる企画的な感じ☺」「思い切りWIP😆

「この記事では『Railsway Oriented Programming』という関数型由来の概念を援用していて、以下のスライドは割と前のですがその解説です」「どれどれ👀: successとfailという2本の線路で考えるところを絵で説明してるのがとてもいいですね👍」「😋」「エラー処理はraiseしちゃえばいいという考え方もあるんですけど、こういう世界だと単純にraiseするわけにいかなくて、かといって普通にやるとifの嵐になっちゃいますし😢

参考: Railway Oriented Programming | F# for fun and profit
参考: notes/Railway oriented programming.md at master · yukitos/notes — 上の日本語版

⚓コント・モナドロジー

「なははは😆、スライドのくまの会話、今の自分に染みるわ〜」「Maybeモナドと『たぶん』が入り混じったりしてて英語圏らしい笑い😆」「モナド簡単でしょ?とかひどい🤣」「『完全に理解した』出た😆」「endfunctorって何関手だったっけ😅

後で雑に訳を付けてみました。どことなくいしいひさいちを連想するセンスです。

くま: 関数をいくつかチェインしたいんだけど、エラーもキャプチャしたいんだくま。
ブレインズくん: そんなの簡単。モナドでやれます。
くま: モナド何だか難しそうなんだくま。
ブレインズくん: モナドは単に自己関手の圏におけるモノイドですよ。

くま: ⊂( ・∀・)ワケ ( ・∀・)つワカ ⊂( ・∀・)つラン♪
ブレインズくん: 何か問題でも?
くま: 自己関手がわかんないくま。
ブレインズくん: 簡単です。関手というのは圏と圏の準同型のことで、自己関手というのは単に圏自身に写像する関手のことです。
ブレインズくん: ほらこんなにシンプル!
くま: よし完全に理解したくま。

くま: で、ぼくはどうしたらいいくま?
ブレインズくん: モナドを全部理解する必要がないなら、Maybeを使えばいいのでは?
くま: たぶん(maybe)何をくま?
ブレインズくん: Maybeっていうモナド。
くま: だからたぶん(maybe)何て言うモナドくま?
ブレインズくん: だから、”Maybe”っていう、名前の、モナドなの。

くま: 「たぶんそのモナドの名前は…」じゃなかったのかくま?。
ブレインズくん: Maybeのモナドが名前は…ってヨーダみたいなしゃべり方しないでくれる?
くま: ヨーダみたいにしゃべってるのは君の方だくま。
ブレインズくん: とにかく話を戻すと、君に必要なのはきっとMaybe(definitely Maybe)のはず。
くま: つまりDefinitely Maybeなのかくま?

ブレインズくん: ちなみにぼくは『Definitely Maybe』より『<What’s the Story> Morning Glory?』の方が好きだけど。
ブレインズくん: 君にはEitherの方が合ってるかも。
くま: 何と何の『どちらか(either)』なのかくま?
ブレインズくん: Eitherはモナド。
くま: モナドと何の『どちらか(either)』なのかくま?
ブレインズくん: だからEitherだけ(just Either)。
くま: つまりJust Eitherって言うのかくま?

※ 『Definitely Maybe』と『<What’s the Story> Morning Glory?)』はどちらもオアシスのアルバムタイトル。

参考: オアシス (バンド) - Wikipedia

ブレインズくん: ああそうじゃなくって!JustはMaybeの一部なの。
くま: じゃJust Maybeって言うくまか?
ブレインズくん: 違う違う、そこはJustって言わないとだめ。JustかNothingだけ(just Nothing)。
くま: Just Nothingくま?でもさっきDefinitely Maybeって言ってなかったくまか?
ブレインズくん: いや今はEitherの話をしてるんですけど。
くま: Just NothingなのかDefinitely Maybeなのかはっきりして欲しいくま。
ブレインズくん: どっちも違います!いいからEither使えっつーの。
くま: (゜∀。)ワヒャヒャヒャヒャヒャヒャ


「あとRedditに『Dry-rbの問題はドキュメントだ』という書き込みを見つけました」「それほんに: APIの仕様もコード付きのユースケースも見当たらなくて、こんなことできるよってふわっと書いてあるぐらい😢」「さっきのブレインズくんの説明と大差なさそう😆」「今欲しいのは業務に近いユースケース😢」「『これらのpredicateはルールによってカプセル化され、述語論理を用いて互いにコンポジションできる。つまり共通の論理演算子を利用してバリデーションスキーマを構築できる』とか書かれても使い方がわからないって書き込みにありますね😅」「その意味までならわかるんですけど😆

Dry::Structはたまに使う。Dry::StructはVirtusを置き換えるものだと思っていたが、VirtusはミュータブルなのでForm Objectで便利なのにDry::Structはそうではないのが残念。結局Dry::Structはイミュータブルなstructに使っていて、ミュータブルなstructは手作りと(Trailbrazerの)Reformの合わせ技になっている。
Dry::TypesはDry::Structや手作りForm Objectと相性がいいのでしょっちゅう使っている。しかしDry::Typesはバリデーションのライブラリなのか型強制(coercion)ライブラリなのかアサーションのライブラリなのかはっきりしない。型に対する変更の順序が微妙に重要だったりするのもあって、めちゃくちゃ好きというほどではない。Rubyに型システムを追加しようと一人相撲している感もある。
Redditより大意

dry-structのissue #48を見ると「イミュータブルは仕様」とありました。

⚓Railsリンク集


つっつきボイス:「TechRacho記事もいくつか引用されていたQiita記事を見つけました: Railsガイドより先の設計とかの記事リンクだそうです」「ほほぅ〜頑張っていろいろ集めてる😋

「これにjoker1007さんの例の名記事↓も含めたいですね」「Service ObjectはRailsの標準ではないというのはまあ置いとくとして😆」「あとyasaichiさんの『Railsの正体』スライドも冒頭に置きたいです」「うん、あれは本当にいいスライドですよ❤

⚓WebカメラをActive Storageに直結する(Ruby Weeklyより)

# 同記事より
class VideoAttachmentService
  class << self
    def attach(model, video_path)
      model.picture.attach(
        io: File.open(video_path),
        filename: "player_video_#{unique_string}.webm"
      )
    end

    private def unique_string
      SecureRandom.urlsafe_base64(10)
    end
  end
end

つっつきボイス:「直結だそうです」「直結でがんがんストリーム配信しまくる?すげ〜😆」「記事では最初に画像のアタッチ、次に動画のアタッチやってますね」「既存の何かのプロトコルの焼き直しに見えなくもないですけど😆」「どうなんでしょう😆

⚓その他Rails


つっつきボイス:「お馴染みt-wadaさんのプレゼンです」「先週の『開発速度と品質』の話にも通じてそう(ウォッチ20200225)」

「中身見る前に言うと、自分の感覚では品質を下げたからといって開発速度が上がるとは限らないですね: カリッカリのところだと確かにトレードオフになるんですけど、特に開発の初期段階は質と開発速度はむしろ比例すると思ってます🧐」「おぉ」「結局、書くのが早い人は品質もいいし、書くのが遅い人に時間あげてもたいてい品質はよくならない😆」「😆」「言い換えると『開発がスタートする前に設計をどこまでしっかり固めてますか』ということ: このスライドは主に開発が進んでからの話みたいですけど☺

↓後でスライド見つけました。「システムを設計するための判断力をつける一番の方法は、自分で設計したシステムを長い間メンテすることだ(p59)」で考えさせられました。

参考: デブサミ2020、講演関連資料まとめ:CodeZine(コードジン)


今回は以上です。

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

週刊Railsウォッチ(20200226後編)dry-rbを使うべき理由、最近のRubyオンライン教材、AWSから乗り換えた話ほか

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

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

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20200303後編)Ruby 2.7で引数のruby2_keywordsフラグを確認する、fake_apiでAPIプロトタイプ、groupdateで日付をグルーピングほか

$
0
0

こんにちは、hachi8833です。私もしばらくの間リモートワークになりました。

それはそうと確定申告が延長されましたね🎉

参考: 確定申告4月16日まで延長方針 - Yahoo!ニュース

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

⚓Ruby

⚓Ruby 2.7の引数を確認する


つっつきボイス:「詳しくはkamipoさんのブログを見ていただきたいんですが、hashオブジェクトにRuby 2.7のruby2_keywordsフラグが付いているかどうかを確かめる公式の方法がこれまでなかったそうです」「これは渡されたものに対する評価方法がないってことですよね?たしかメソッドに対してはどういう引数があるのかをざっくり調べるメソッドは一応あるはず(名前忘れたけど😆)」

後で調べるとMethod#parametersがそれのようです。

「Ruby 2.7に入ったruby2_keywordsはキーワード的に使ってますね😋」「まあ上書きできるのかもしれませんが☺

後でruby2_keywordsを頼りにMethod#parametersをやってみました。

require 'ruby2_keywords'

module YourModule
  ruby2_keywords def delegating_method(*args)
    other_method(*args)
  end
end

class Foo
  include YourModule
end

Foo.new.method(:delegating_method).parameters
#=> [[:rest, :args]]

これはmameさんのハックを見て知った方法で、ようはフラグが付いてるhashオブジェクトかそうじゃないオブジェクトかで違う振る舞いをする処理を通らせてその結果を観測することでどっちだったかを確かめるという方法です。
同記事より

「観測!Ruby 2.7以降に合わせてすべてを完璧に修正する必要はなくて、観測して振る舞いが違うものに絞り込んで当面修正すればいいというのであれば、なかなかうまい方法かも😋」「観測されないものは存在しない😆

このフラグが付いてるhashオブジェクトかそうじゃないオブジェクトかで違う振る舞いをする処理がRubyの世界からはほとんど存在しないので、普段Rubyでコードを書いてる常人が自力では気づけんやろって方法で、一部のメソッド(initializeとかmethod_missingとか)をRubyのコードから直接呼び出すんじゃなくてCのコードから間接的に呼び出されるときにフラグが付いてるhashオブジェクトをdupするので、dupされずにそのままのオブジェクト(object_id)だったらフラグが付いてなかったってことでdupされて別のオブジェクトが観測されたらフラグが付いてたオブジェクトだったという技を使っています。
同記事より

「ここが振る舞いの違うところですか😳」「一気に書き下した感」「C言語の世界でのこんな微妙な振る舞いの違いを使うのすげ〜」「これでRailsのオプション引数退治をはかどらせてたんですね🍑

「そしてRuby 2.7.1では、フラグを見分けるruby2_keywords_hash?が正式に入るそうです」「お〜バックポートもすると😍」「ライブラリメンテナが泣いて喜ぶ機能😂」「つくづく凄い人たち…」

⚓ferrum: ヘッドレスChrome API


同サイトより

以下を謳っています。

  • Pure Chrome
  • Pure Ruby
  • No addiction

つっつきボイス:「フェラーム?」「あ、元素記号の鉄(Fe)か!ラテン語です」「クロム(Chromium)だから鉄ってシャレね☺」「CapybaraではculpriteというヘッドレスChromeドライバでやってるらしいですが、FerrumはCapybaraなしでもブラウザをRubyからいろいろ制御してクロールとかやれるということみたいです」

# 同リポジトリより
# Trace a 100x100 square
browser = Ferrum::Browser.new
browser.goto("https://google.com")
browser.mouse
  .move(x: 0, y: 0)
  .down
  .move(x: 0, y: 100)
  .move(x: 100, y: 100)
  .move(x: 100, y: 0)
  .move(x: 0, y: 0)
  .up

browser.quit

⚓RuboCop 0.80リリース(Ruby Weeklyより)

今日見るともう0.80.1がリリースされていました。


つっつきボイス:「RuboCop 0.80がリリースされてました」「Style/HashTransformKeysやらStyle/HashTransformValuesやらいろいろ追加されてるな〜」「copますます強くなる👮‍♂️」「この間取り上げたJUnitFormatterも入ってます(ウォッチ20200218)」「JUnitはJavaでは当たり前に使われてるテスティングツールですね☕

「ついでにRubyWeeklyでは以下のRedditも紹介されててて、『RuboCopの取締きびしすぎ』『いやそんなことない』『お前はよくてもチームはそうじゃねえ』『どの辺のルールがつらい?』とかスレがめちゃ伸びてます😆」「自分はありもののルールでやってるから気にしたことないですし😆」「最初からなるべく素のRuboCopでやれたら平和になれるかしら😆」「まあ変にいじるとアップグレードとかでややこしくなったり😆

⚓fake_api: APIのプロトタイプ作成(Ruby Weeklyより)

# 同リポジトリより
# app/fake_api/app_routing.rb
class AppRouting < FakeApi::Routing
  get('/projects').and_return           { create_list(:project, 5) }.with_status(202).with_headers({TOKEN: "SECRET"})
  get(%r{/projects/\d+$}).and_return    { object(:project) }
  post('/projects').and_return          { object(:project).merge({created: 'ok'}) }
  delete(%r{/projects/\d+$}).and_return { { result: :deleted } }.with_status(333)

  post('/auth')
    .and_return { { status: "OK" } }
    .with_cookies({x: "A"})
    .with_session({y: "B"})
    .with_headers({token: "C"})
end

つっつきボイス:「これはもしかして嬉しい人がいるかも?文字どおりAPIのプロトタイプをさくっと作れるみたいです」「(デモ動画を眺めて)API仕様に基づいてJSONを吐けるgemという感じかな😋: まだ本物のAPIサーバーがないけど仕様とデータだけ判明してて、APIないと開発つらいぜみたいなときなんかに結構使えそうな気がします👍」「やった😋」「使い捨てのAPIサーバーとか自分でスクラッチで立てるのイヤですし😆」「JSON手作りするのも😆

fake_apiはRailsエンジン型式になっていて、ファクトリーとルーティングを書けばRailsアプリの中で動かせる感じです。

⚓midas: Rubyでアノマリー検出(Ruby Weeklyより)


つっつきボイス:「用語が何だか難しめなんですが、動的な世界でアノマリー(異常値)を検出するgemみたいです」「元になったC++版のMIDASの挿絵↓を見ると侵入の検出とかを指してるようだ🤔」「下のa1〜a3はいかにも悪い人たちですし🦹🏻‍♀️」「割と難しい世界…」「ankaneさんは最近Rubyで機械学習方面やってるからその絡みかも🤔

グラフの異常値検出は、無数のシステムにおける疑わしい振る舞いを探す(侵入検出、レーティングの捏造、金融詐欺など)うえで重要な問題である。この問題は、静的なグラフに特化したアプローチの多くでは十分な研究がなされているが、現実のグラフは本質的に動的であり、静的接続に基づいた手法ではグラフやアノマリーの一時的な特徴を見落とす可能性がある。
(中略)
私たちの提案するMIDAS(Microcluster-Based Detector of Anomalies in Edge Streams)ではマイクロクラスターの異常値や、互いによく似た疑わしいエッジが突然エッジストリームに入ってくるのを、一定の時間とメモリで検出する。MIDASではさらに従来の手法にはなかった、原則に基づいた仮説のテスティングフレームワークを用いて偽陽性の確率の理論的束縛も提供する。最新のアプローチと比べて644倍高速かつ精度を48%向上させた。
github.com/bhatiasiddharth/MIDASより

直接の関連はありませんが『機械学習を用いた異常検知入門』スライドを貼ります。

「MIDASはギリシャ神話のミダス王なんでしょうね: 『王様の耳はロバの耳』の話と、触ったものが全部黄金になる力を神から授かったら飯も食えなくなった🥘という話の😆」「😆

参考: ミダース - Wikipedia

⚓groupdate: 日付のグルーピングgem(Ruby Weeklyより)

# 同リポジトリより
User.group_by_week(:created_at, time_zone: "Pacific Time (US & Canada)").count
# {
#   Sun, 08 Mar 2020 => 70,
#   Sun, 15 Mar 2020 => 54,
#   Sun, 22 Mar 2020 => 80
# }

つっつきボイス:「こちらは日付時刻のグルーピングgemだそうです」「あああ、こういうgroup_byってたしかにつらい😭」「Active Supportにはこういうのってないんでしたっけ?」「日付のgroup_byってepoch time的な数値でグルーピングしようと思えばできるんですけど、あんまり価値がない😆: 日でグルーピングとか分まで見てグルーピングとかって、やってみると結構エグいんですよこれが」「あ〜なるほど!」「created_atとかは秒以下の時刻も持ってますし、日でグルーピングとかって実はそんなに簡単じゃないんですよ〜😆」「このgemはタイムゾーンも扱えてgroup_by_dayとかgroup_by_weekとかgroup_by_hour_of_dayとか便利そうなのがありますね」「時刻の細かな扱いが多くて、時刻のレンジを切って分析することが多い案件なんかでは有用かも👍」「やった😋」「普通はあまりしませんけど😆、こういうのを知っていれば車輪の再発明しなくて済むかも☺

参考: UNIX時間 - Wikipedia

⚓その他Ruby

つおい😳

⚓DB

⚓iredis: Redisターミナルクライアント(DB Weeklyより)



同リポジトリより


つっつきボイス:「Redis用のインタラクティブなクライアントでiredisということみたいです」「お、redisのターミナルクライアント、よさげ😋」「あると嬉しい感じでしょうか?」「redisをRails経由で使うことはよくやってますけど…そもそもredisのGUIクライアントってありましたっけ?😆」「MySQLのMySQL WorkbenchやPostgreSQLのPgAdminみたいなGUIクライアントなヤツですよね、どうだったかな🤔」「つまりredisを直接触らなくてもやっていけるということで😆

後で探すと、RedisInsightというブラウザベースの公式ツールがあるそうです。今は無料版でも制約なく使えるようです。

サイト: RedisInsight | Redis Labs

参考: Redis向け GUI ツール RedisInsight を使う - tech.guitarrapc.cóm

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

⚓FirefoxのDoH

参考: Firefox、米国では「DNS over HTTPS(DoH)」が初期設定で有効に - ITmedia NEWS


つっつきボイス:「FirefoxがDNS接続をHTTPS化するそうですけど、言われてみれば今までDNSのクエリって暗号化されてませんでしたね」「普通に温かみのある平文でやってますし😆: やりたいのは改ざん防止かな?」「英語記事によると、ISPがその気になればユーザーがDNSにアクセスしたときの情報を取れるのを防ぎたいということみたいです」「ははぁそちらですか」「Mozillaは信頼できるプロバイダとしてCloudflareとNetDNSを使うとありますね」「その2つは信用できるのかしら😆」「Cloudflareは信頼を高めるために何かやってた覚えあります(ウォッチ20180518)」

参考: IPアドレスを保存しない高速パブリックDNSサービス「1.1.1.1」、APNICとCloudflareが無料提供 - INTERNET Watch

記事ななめ読み(抜粋・大意):

プライバシーにどんな脅威があってDoHを導入するのか
ユーザーのインターネット上での活動はDNSリクエストで露わになる。プロバイダ(ISP)やWiFiプロバイダがその気になれば無断でその情報を集められる。
暗号化DNSにする理由と、DNS以外にISPが情報収集に使えるしくみがあるかどうか
ユーザーのプライバシーは多くの脅威にさらされており、1つの技術ではカバーできない。だからこそ個別の問題に取り組むのであり、脅威が多いからといって解決を拒む理由にはならない。Mozillaに限らず、DNS以外の個人識別情報の漏洩を防ぐ適切な方法の定義に取り組んでいる。TLS接続におけるESNI(Encrypted Server Name Indication)もそのひとつだ。
DoHでDNSの中央集約化がさらに進むとネット全体にとってよくないのでは?
中央集約化がネットにとってよくないのは確かだ。現実世界ではISPのDNSサービスにロックインされ、5つの企業が米国のブロードバンドインターネットの80%を牛耳っている。FirefoxをDoH化するとトラフィックが大手ISPから離れるので、中央集約化を弱めることはあっても強めることはなく、企業のDNS設定も尊重しつつユーザーの選択肢を増やせる。
DoHは企業の”split-horizon” DNSなどとうまくやれるか
企業はDoHを簡単に無効にできるし、Firefoxは企業のポリシーを検出して自動で無効にすることもできるようになる。
FirefoxのDoH化は全世界でやるのか
当面米国内のみのリリースに注力する。現時点ではヨーロッパやその他の地域でのリリース計画はないが、DoHはあらゆる人々のプライバシー保護にとってよいものであると私たちは強く信じている。

参考: インターネット用語1分解説~DNS over HTTPSとは~ - JPNIC

⚓JavaScript

⚓プロポーザル: パイプライン演算子をJSに

// 同リポジトリより
let result = exclaim(capitalize(doubleSay("hello")));
result //=> "Hello, hello!"

let result = "hello"
  |> doubleSay
  |> capitalize
  |> exclaim;

result //=> "Hello, hello!"

つっつきボイス:「2年ほど前からあるプロポーザルですが😆」「みんな好きね〜|>パイプライン演算子🚰」「Elixirとか」「Ruby 2.7にもパイプライン演算子が入りかかったけど先送りになってましたね(ウォッチ20191210)」

参考: Elixir (プログラミング言語) - Wikipedia

「やっぱりパイプラインの記号は|>になるのかしら🤔」「|>はJSっぽくはないけど、言葉で書くよりうまい記号があればそっちでやりたいですし☺」「そういえば論理学方面の記号って、画数が少なくて書くのは楽だけど読むのがめちゃ大変なのがちょくちょくあってつらいです😭」「慣れると記号じゃない方がつらくなりますし😆

参考: パイプライン演算子の歴史 - まめめも

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

⚓「牧歌的 Cookie の終焉」


つっつきボイス:「jxckさんによるCookieの今後についてのエッセイなんですが、はてブでものすごいブクマ数になってて、しかもCookieについての説明としてもよくできてて、久しぶりに技術文の名文を読んだ気持ちになりました😂」「考えてみたら自分が知ってるのは牧歌的なCookieの方なのかも😆: Cookieはどうにでも使えちゃってたからトラッキングとかで使われがちだったけど、Cookieでやると何となくイカサマっぽく見えたり😆

「その記事を読んでて、IDFA↓というものを今頃知りました: Webとは別のiOS特有の広告トラッキングシステムだそうです」「まあ使う側はそいつが誰なのかはどうでもよくて、端末が同じかどうかが識別できればそれでいいんですけど、誤解されがち😆」「ですね😆

参考: IDFAについて、理解していますか?いまさら聞けないweb広告用語 | アド論 byGMO

「昔インテルのCPUも固有の番号を刻んだら袋叩きになってたことありましたし😆」「あ〜国民総背番号制とかそっちのイメージで騒ぎになってたような覚えが😆

PSN(プロセッサシリアルナンバー)でした↓。

参考: CPUID - Wikipedia

「あと今月のWeb+DB Pressを買ったらblog.jxck.ioの中の人の新連載が始まっていて↓、これもブログと同じぐらい特濃でおすすめです😋

最新号はjxckさんの『Origin解体新書』の他、mameさんの『コミッター詳解: Ruby 2.7の魅力』、ko1さんの『Rubyのウラガワ』など目白押しです。

⚓その他フロントエンド


つっつきボイス:「BPSのデザインチームがこのNeumorphというデザインで盛り上がってました」「へ〜このエンボスっぽいデザインですか😳: まだWeb界隈では見かけないな〜」「ひと頃のフラットデザインから逆に振れてきた感じなのかしら😆」「ちょっと前までフラットだ何だって言ってたのに😆」「きっとまた逆に振れる😆

「こうやって目立たせたいところが凸になってるのって意外と重要かもしれませんね☺」「色に頼らないのもよさげ😋」「ユニバーサルデザイン的に色覚の弱い人でも使いやすそうな気がしますし」「普段はデザインにあまり目が向かないけど、これはちょっと好き❤

「そう思うとCSSのz-indexって何だったんだろうって😆」「まあz-indexの軸とは違う感じ😆

参考: z-index-スタイルシートリファレンス

⚓言語・ツール

⚓習わぬ経を読む


つっつきボイス:「ちょっとできすぎな話かも😆」「ええ子や、パパの言うとおりよ〜👋

⚓その他言語


つっつきボイス:「これもほっこりしました☺」「三項演算子を使うかどうかの見極めってほんと難しいですね〜😆: うまくハマれば可読性上がることもありますし、メソッド末尾のreturnのところだったら三項演算子でもいいかな思うんですけど」「たいていこじらせる😆」「仕様が変わって三項演算子をifにしないといけなくなったときがめんどい😆

「後置のifは好きです」「ガード文に使う後置のifはいいですね😋

[Ruby/Rails] 例外で深くなったネストをGuard Clauseですっきりさせる

⚓その他

⚓『ちいさい言語学者の冒険-子どもに学ぶことばの秘密』


つっつきボイス:「こないだKindleでポチって読み中なんですが、いきなり面白かったので」「ほほぅ?」「言語学者の著者が主に自分の子をネタに言語学的な発見をする話なんですが、たとえば小さい子どもにこんな質問をすると↓」

  • 「か」に点々を付けると? –> 「が」
  • 「さ」に点々を付けると? –> 「ざ」
  • 「た」に点々を付けると? –> 「だ」
  • 「は」に点々を付けると? –> (ほとんどの子が、喉の奥をならすような文字に書けない音を出す)

「というふうにほとんどの子が『ば』と発音しない: 実は『ば行』の濁点は他の濁点と発音の規則が違っているということを、規則を知らない子どもが図らずも見つけ出したそうです」「だって半濁音の『ぱ』があるくらいですし😆、お子様はそのあたり混じりがちなのでは?」「実際、昔の日本語には『ば行』がなくて『ぱ行』だったという研究もあるそうです」

「ヘリコプターがヘリコとプター🚁」「apoptosisにpが入ってて発音しないって知らなかった」

「pter」は英語では巡り巡ってfeather(羽)に変わり果てたそうです。

⚓その他のその他

つっつきボイス:「なはは😆、ロックないとダメじゃん」「タワー型PCにキャスター要るかしら😆


「タッチタイピングが1位って意外😳」「他は納得: オーダーのOとか習わないと絶対やりませんし😆

参考: ランダウの記号 - Wikipedia

⚓番外

⚓スミソニアンに勝てる気がしない


つっつきボイス:「データ公開に加えて自由に再利用していいって太っ腹」「天才過ぎる」「米国のスミソニアン博物館って一日や二日では絶対見終わらないぐらい規模がでかいらしくて、死ぬまでに一度見に行ってみたいです👴

参考: スミソニアン博物館 - Wikipedia


後編は以上です。

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

週刊Railsウォッチ(20200302前編)RubyKaigi 2020は9月に延期、Railsのセキュリティパッチバージョニングが変更、dry-monadsほか

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

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

Ruby Weekly

DB Weekly

db_weekly_banner

Rails 6のAction Mailboxを使ってみよう(翻訳)

$
0
0

概要

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

  • 英語記事: Rails 6 - Action Mailbox tryout – Saeloun Blog
  • 原文公開日: 2019/11/11
  • 著者: Romil Mehta
  • サイト: Saeloun — Ruby on Railsのコンサルティング会社で、Rails + React開発のほかに、React Nativeによるモバイルアプリ開発も手がけています。

Rails 6のAction Mailboxを使ってみよう(翻訳)

アプリケーションで大量のメールを受信するはめになることがままあります。そういうメールを扱わなければならなくなった場合、操作のためにメールをひとつひとつ処理しなければなりません。

人事部門のユースケースで考えてみましょう。ある組織から、オープニングの参加候補者たちに「レジュメを送って欲しい」というメールを1件送信したとします。参加候補者たち全員がレジュメを添付してメールに返信したら、届いたレジュメをクラウドにアップロードして、レジュメ1件ごとにデータベースのエントリーを作成する必要があります。

以下はそのためのステップです。

  1. メールを1件ずつチェックする
  2. レジュメをダウンロードする
  3. レジュメをクラウドにアップロードして、レジュメのテーブルにエントリを作成する

ダルそうな作業ですね。

Action Mailboxを導入する

Rails 6では、受信メールを処理するためのAction Mailboxが導入されました。Action Mailboxは、受信メールをコントローラによく似たメールボックスにルーティングしてRailsで処理できるようにします。Mailgun、Mandrill、Postmark、SendGridといった主要なプラットフォームはひととおりサポートされています。

インストールと実装

以下のコマンドを実行して、feedback_collectorという新しいアプリケーションを作ります。

> rails new feedback_collector

rails action_mailbox:installを実行してAction Mailboxをインストールします。このとき、受信メール保存用にActive Storageも同時にインストールされます。メールはここに保存され、処理済みかどうかがトラッキングされます。

続いてActive Jobが読み込まれてメールを処理し、処理が終わったメールは削除されます。削除されたメールについてもidやチェックサムをトラッキングするので、同じメールがまたやってきたときに処理を回避できます。

> cd feedback_collector
> rails action_mailbox:install
#=> Copying application_mailbox.rb to app/mailboxes
#=>      create  app/mailboxes/application_mailbox.rb
#=> Copied migration 20191021075823_create_active_storage_tables.active_storage.rb from active_storage
#=> Copied migration 20191021075824_create_action_mailbox_tables.action_mailbox.rb from action_mailbox

上のコマンドで生成されるマイグレーションは、action_mailbox用とactive_storgage用です。application_mailboxも同時に作成されます。

それではUserProductFeedbackのscaffoldを生成しましょう。

> rails g scaffold User name email
> rails g scaffold Product title
> rails g scaffold Feedback user:references product:references content:text

以下のマイグレーションを実行します。

> rails db:migrate

action_mailboxテーブルのスキーマにはstatusmessage_idmessage_checksumなどのカラムが含まれます。statuspendingprocessingfinishedbouncedのいずれかになります。message_idmessage_checksumは重複防止用です。

ApplicationMailboxクラスは以下のような感じになります。

class ApplicationMailbox < ActionMailbox::Base
  # routing /something/i => :somewhere
end

メールのルーティングは以下のように定義します。

class ApplicationMailbox < ActionMailbox::Base
  # routing /something/i => :somewhere
  routing  :all => :feedbacks
end

すべてのメールをFeedbacksMailboxにリダイレクトするルーティングを1つ指定しました。ルーティングは以下のように正規表現でも指定できます。

class ApplicationMailbox < ActionMailbox::Base
  # routing /something/i => :somewhere
  routing  /feedback\-.+@example.com/i => :feedbacks
end

上の正規表現はfeedback-Ahdhc12@example.comfeedback-5264yYxjg@example.comのようなメールアドレスにマッチします。

次はFeedbacksMailboxを作成します。

> rails g mailbox Feedbacks
#=> Running via Spring preloader in process 623
#=>       create  app/mailboxes/feedbacks_mailbox.rb
#=>       invoke  test_unit
#=>       create    test/mailboxes/feedbacks_mailbox_test.rb

FeedbacksMailboxクラスには、メールを処理するprocessメソッドがあります。このクラスでmailオブジェクトにアクセスできます。

メールを処理する前に何か操作を加えたい場合は、次のようにbefore_processingコールバックででやれます。

class FeedbacksMailbox < ApplicationMailbox
  before_processing :user

  def process
  end

  def user
    @user ||= User.find_by(email: mail.from)
  end
end

mail.fromuserを取れるようになりました。しかし製品へのフィードバックを保存するためにproduct_idが欲しくなります。

product_idを取得するには、返信メールの正規表現を次のようにproduct_idを含む形で指定できます。

  RECIPIENT_FORMAT = /feedback\-(.+)@example.com/i

返信メールがfeedback-1234@example.comの場合、上の正規表現によって1234product_idとして取得できます。

このメールを処理してユーザーからのフィードバックを保存しましょう。

class FeedbacksMailbox < ApplicationMailbox
  RECIPIENT_FORMAT = /feedback\-(.+)@example.com/i

  before_processing :user

  def process
    # フィードバックを作成する
    # mail.decodedはメールがマルチパートでない場合はbodyを返す
    # マルチパートの場合はmail.parts[0].body.decodedを使う
    # ここでは後者がフィードバックを含む
    if mail.parts.present?
      Feedback.create user_id: @user.id, product_id: product_id, content: mail.parts[0].body.decoded
    else
      Feedback.create user_id: @user.id, product_id: product_id, content: mail.decoded
    end
  end

  def user
    @user ||= User.find_by(email: mail.from)
  end

  def product_id
    # recipientsは複数の可能性があるので
    # RECEIPIENT_FORMATにマッチするものを検索

    recipient = mail.recipients.find { |r| RECIPIENT_FORMAT.match?(r) }

    # first_match(つまりproduct_id)を返す
    # 例: recipient = "feedback-1234@example.com"
    # これで1234を返す
    recipient[RECIPIENT_FORMAT, 1]
  end
end

mailオブジェクトにアクセスできるので、マルチパートのメールや添付ファイルありのメールも読み取れます。

development環境でテストする

development環境でテストするなら、http://localhost:3000/rails/conductor/action_mailbox/inbound_emails/newをブラウザで開いて受信メールを配信するだけでやれます。toのメールアドレスに基づいてメールボックスにルーティングされ、メールが処理されます。

production向けの設定

Action Mailboxをproduction環境向けに設定するために、私たちの場合はconfig/environment/production.rbでingressを指定する必要があります。

ingresspostmarkにした場合で考えます。

config.action_mailbox.ingress = :postmark

さらに、Action Mailboxでpostmarkのingressへの認証リクエストに使う強力なパスワードの生成も必要です。私たちの場合、強力なパスワードはingress_passwordに暗号化済みcredentialとして保存する必要があります。

action_mailbox:
  ingress_password: PASSWORD

credentialに保存する代わりに、RAILS_INBOUND_EMAIL_PASSWORD環境変数に保存したパスワードを提供する手もあります。

今度は受信フックを設定して、受信したメールをactionmailboxというユーザー名と先ほど生成したパスワードを用いて/rails/action_mailbox/postmark/inbound_emailsに転送する必要があります。以下は私たちの場合のWebhook URLです。

https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails

動かせるサンプル

たとえば以下のサービスが使えます。

  1. Sendgrid(メールサービス)
  2. Freenom(ドメイン登録サービス)
  3. ngrok(ローカルWebサーバーを公開できるパブリックURLを提供する)

いずれも無料です。

セットアップ

SendGrid、Freenom、ngrokでアカウントを作ります。ngrokのインストールについてはngrokのガイドに記載の手順をご覧ください。

次はfreenomのドメイン登録リンクで無料ドメインを登録します。検索ボックスにactionmailboxなどのドメイン名を入力して「Check Availability」ボタンを登録します。オプション機能は無料なので好きな機能をオンにします。ナビゲーションバーの「Services > My Domain」セクションにドメインが表示されます。

それが終わったら、このドメインをSendGridで認証する必要があります。認証手順は以下のとおりです。

  1. SendGridの sender_authフォームをクリックする。
  2. 「DNS host」の「Other Host (Not Listed)」を選択し、「DNS Host」にfreenomと入力する。
  3. 「Next」をクリック。
  4. 「From Domain」ボックスにドメイン名を入力する。
  5. 「Next」をクリック。
  6. 「CNAME」レコードのフィールドが3つ表示される(「Freenom」ドメイン管理セクションでこれらを追加する必要あり)。
  7. 「My Domains」セクションの新しいタブで「Freenom」を開く。
  8. 「Manage Freenom DNS」タブをクリック。
  9. ここにCNAMEレコードを3つ入力する必要がある。
  10. MXレコードも作成する(「name」は空欄でもよい)。「type」をMX、「target」をmx.sendgrid.net、「priority」を10にそれぞれ設定。
  11. 「SendGrid」タブに戻ってチェックボックスを確認し、「Verify」をクリック。
  12. 失敗した場合は15〜20分ほど待ってから「Verify」を再度クリック。
  13. sender_auth/domainsリンクを開くと、設定したドメインのステータスがVerifiedになっている。

次は、development.rbに以下のSMTP設定を追加します。

config.action_mailer.smtp_settings = {
  :user_name => SENDGRID_USERANME,
  :password => SENDGRID_PASSWORD,
  :domain => OUR DOMAIN,
  :address => 'smtp.sendgrid.net',
  :port => 587,
  :authentication => :plain,
  :enable_starttls_auto => true
}
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = false

rails sを実行してサーバーを起動します。

./ngrok http 3000を実行してブラウザの新しいタブで表示します。

> ./ngrok http 3000
ngrok by @inconshreveable

Session Status                online
Account                       ROMIL MEHTA (Plan: Free)
Version                       2.3.35
Region                        United States (us)
Web Interface                 http://127.0.0.1:4040
Forwarding                    http://386e42cd.ngrok.io -> http://localhost:3000
Forwarding                    https://386e42cd.ngrok.io -> http://localhost:3000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              0       0       0.00    0.00    0.00    0.00

上の「Forwarding」の値を見ると、サーバーのpublic URLが386e42cd.ngrok.ioになっています。この後の例で使いますので、URLをメモしておきます。

今度は、前述の「production向けの設定」で予告した手順を勧めます。

  • 「ingress」にsendgridを追加
config.action_mailbox.ingress = :sendgrid
  • ingressのパスワードを作成してcredentialに追加します。
action_mailbox:
  ingress_password: PASSWORD
  • SendGridの「Inbound Parse」セクションを開いて、「Add Host & URL」をクリックします。

ドメインを選択し、destination URLにhttps://actionmailbox:INGRESS_PASSWORD@SERVER_PUBLIC_URL/rails/action_mailbox/postmark/inbound_emailsを設定して、「POST the raw, full MIME message」チェックボックスをオンにします。

実例

以下を実行して、ユーザーに製品へのフィードバックを促すメイラーを作成します。

> rails g mailer Feedback
Running via Spring preloader in process 48769
      create  app/mailers/feedback_mailer.rb
      invoke  erb
      create    app/views/feedback_mailer
      invoke  test_unit
      create    test/mailers/feedback_mailer_test.rb
      create    test/mailers/previews/feedback_mailer_preview.rb

app/mailers/feedback_mailer.rbに以下のメール送信コードを追加します。

class FeedbackMailer < ApplicationMailer
  default from: FROM_MAIL_ADDRESS

  def send_email
    mail(to: ANY_USERS_EMAIL, reply_to: REPLY_TO_MAIL_ADDRESS, subject: 'Mailbox Test', body: 'Provide feedback for the product by replying to this mail')
  end
end

上のコード例のREPLY_TO_MAIL_ADDRESSにはfeedback-#{PRODUCT_ID}@#{SERVER_PUBLIC_URL}が、PRODUCT_IDには、フィードバックをお願いする製品がそれぞれ入ります。

以下のコマンドを実行すると、フィードバック依頼メールがトリガーされます。

> FeedbackMailer.send_email.deliver_now

上のコマンドを実行後、メールがユーザーに届きます。

feedback mailboxのルーティングは設定済みなので、ユーザーがメールに返信するとFeedbackMailboxprocessメソッドが呼び出されてメールが処理されます。

まとめ

本記事ではAction Mailboxの基礎、インストール方法、実装、設定方法について学びました。また、production環境に近いセットアップ例についても解説いたしました。

関連記事

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


Rails 6: Docker dev環境のconfig.hostsとwhitelisted_ipsについて

$
0
0

こんにちは、hachi8833です。

Evil Martians流のRails 6 Docker dev環境をやっていて迷った点をメモします。macOS環境でのDocker Desktopが前提です。

Rails 6のDocker開発環境構築をEvil Martians流にやってみた

1. Docker dev環境のためのconfig.hostsの追加は通常は不要

DockerのRails 6 development環境では、一部の場合を除いてconfig/environments/development.rbに以下を追加する必要はありませんでした(後述)。

  config.hosts << 'localhost'

Rails 6にDNSリバインディング攻撃防止機能が追加された(翻訳)

上の記事にあるように、development環境のconfig.hostsには既に"localhost"がデフォルトで含まれていますので、ブラウザからhttp://localhost:3000でアクセスできます。ただしdevelopment環境以外のconfig.hostsは空欄になっているので、少なくとも本番環境では別途config.hostsに自ドメインを追加します。

Rails.application.config.hosts = [
  IPAddr.new("0.0.0.0/0"), # All IPv4 addresses.
  IPAddr.new("::/0"),      # All IPv6 addresses.
  "localhost"              # The localhost reserved domain.
]

ngrokなどを使う場合は必要

config/environments/development.rbにconfig.hostsを追加する必要があるのは、たとえばngrokやserveoで外部からdevelopment環境にアクセスする場合です。
ngrokを使う場合はconfig/environments/development.rbに以下を追加します。

config.hosts << '.ngrok.io'

2. web-consoleを使うならwhitelisted_ipsの追加が必要

一方、DockerのRails 6 development環境でweb-consoleを動かすのであれば、config/environments/development.rbに以下のような行を追加する必要があります。

  config.web_console.whitelisted_ips = '0.0.0.0/0'

web-console gemはRailsのdevelopment環境にデフォルトで入りますが、better_errorsやRubyMineなどをデバッグに使っているのであればなくてもいいかもしれません。ところでweb-consoleって実際どのぐらい使われているのでしょう?🤔

参考: web-console を使えば Rails App のデバックが楽になる - Qiita

追記: config.web_console.permissionsについて

上の記事にあるように、現在のweb-console gemは、config.web_console.permissionsconfig.web_console.whitelisted_ipsの順でホワイトリストを参照していますので、どちらに設定してもいいようになっています。デフォルトではどちらにもホワイトリストは設定されていません。

おまけ

Docker環境に限りませんが、ウイルスチェックソフトのパーソナルファイアウォールにDocker development環境サーバーへのアクセスを邪魔されることもあります。ブラウザからアクセスできない場合はパーソナルファイアウォールの設定もチェックしてみましょう。

自分もESETのライセンスを更新したときにESETの設定が初期化されてアクセスできなくなり、上述の2つの設定をあれこれ変えてみたりしたのですが、結局ESETのパーソナルファイアウォールの設定を変えてやっと解決したのでした😅

関連記事

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

docker-composeを便利にするツール「dip」を使ってみた

週刊Railsウォッチ(20200309前編)Webpackerに乗り換えるべき理由25、Railsのindex_byとindex_withは有能、GCPはやっぱりスゴいほか

$
0
0

こんにちは、hachi8833です。生まれてはじめてRailsにプルリク投げて一瞬でマージいただきました😂


つっつきボイス:「お、ついにRailsにプルリク🎉」「土曜に投げたんですが、数分もしたらkamipoさんがマージしててたまげました😳」「kamipoさんの常駐率スゴそう⛩」「セキュリティガイドのmarkdownに&gt;みたいな生のHTML要素や記号が書き込まれていて、普通なら誰もそういうのは気にしないんですけど、GitLocalizeというツールの原文パースがその箇所でぶっ壊れたので、やむなくプルリクしました」「気づいた人が投げるのが一番☺

ドキュメントのようなトリビアなプルリクのタイトル冒頭には[ci skip]を付けてCIに負担をかけないようにするのがマナーと知りました↓(訳したのは自分ですが😅)。

参考: Ruby on Rails に貢献する方法 - Railsガイド

RailsのCI (継続的インテグレーション: Continuous Integration) サーバーの負荷を減らすために、ドキュメント関連のコミットメッセージには[ci skip]と記入してください。こうすることで、コミット時のビルドはスキップされます。[ci skip] は「ドキュメントのみの変更」以外では使用できません。コードの変更には絶対使用しないでください。
railsguides.jpより

GitLocalizeはGitHub上のmarkdownやhtmlドキュメントの多言語翻訳支援ツール(無料)で、なかなかスグレモノです。こちらについては近々記事にしたいと思います😋


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

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

今回は公式の更新情報とコミットリストから見繕いました。

⚓水平シャーディングのサポートが追加

アプリケーションからマルチプルシャーディングに接続してシャーディングを切り替えられるようになった。シャーディングのスワップは引き続き手動である点に注意(今回の変更には自動シャーディング切り替えのAPIは含まれていない)。

# 設定例
production:
  primary:
    database: my_database
  primary_shard_one:
    database: my_database_shard_one
# マルチプルシャーディングへの接続
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to shards: {
    default: { writing: :primary },
    shard_one: { writing: :primary_shard_one }
  }
# コントローラやモデルのコードでシャーディングをスワップする
  ActiveRecord::Base.connected_to(shard: :shard_one) do
    # shard_oneから読むようになる
  end

水平シャーディングAPIではreplicaもサポートされる。詳しくはガイドを参照。
ついでにこのPRでは、ドキュメントのないメソッドをいくつかprivate名前空間に移動し、エラーメッセージやドキュメントにも手を加えた。
同PRより大意


つっつきボイス:「たぶんhorizontal付けなくても普通にシャーディングでよかった気がする😆

つっつき後、verticalなシャーディングもあるらしいという指摘がありました。

参考: How Sharding Works - Jeeyoung Kim - Medium


同記事より

🎉❤がいっぱい付いてました」「マルチDBになってからはシャーディングをRailsでやる方法がしばらくなかったからじゃないかな🤔」「そういえばシャーディングは6.1から対応したいってどこかに書いてあったような」

最初に申し上げておきたいのは、現時点のRailsではシャーディング(sharding)はまだサポートされていないという点です。私たちはRails 6.0でマルチプルデータベースをサポートするために膨大な作業をこなさなければなりませんでした。シャーディングのサポートを忘れていたわけではありませんが、そのために必要な追加作業は6.0では間に合いませんでした。さしあたってシャーディングが必要なのであれば、シャーディングをサポートするさまざまなgemのどれかを引き続き利用するのがおすすめと言えるかもしれません。
railsguides.jpより

「シャーディングを待ち構えてた人は結構いるらしいことがわかった😆」「そんな感じですね」「ま自分はシャーディングやりたくないのでシャーディングじゃない方法で実現したいですけどっ🤣

参考: シャーディングとは、テーブルシャーディングという可能性【水平分割】 | SEO対策なら株式会社ペコプラ

⚓スキーマキャッシュで拡張子.gzipをサポート

スキーマキャッシュのシリアライズ戦略使うYAMLとMarshalの両方でgzipをサポートした。
特に巨大なスキーマキャッシュはKubernetesのデプロイで問題になる可能性がある(ConfigMapの上限は1 * 1024 * 1024)。データベースがこれ以上大きくなると上限を超えるかもしれない。
同PRより大意

# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L11
      def self.load_from(filename)
        return unless File.file?(filename)

-       file = File.read(filename)
-       filename.end_with?(".dump") ? Marshal.load(file) : YAML.load(file)
+       read(filename) do |file|
+         filename.include?(".dump") ? Marshal.load(file) : YAML.load(file)
+       end
+     end
+
+     def self.read(filename, &block)
+       if File.extname(filename) == ".gz"
+         Zlib::GzipReader.open(filename) { |gz|
+           yield gz.read
+         }
+       else
+         yield File.read(filename)
+       end
      end
+     private_class_method :read
...
      def dump_to(filename)
        clear!
        connection.data_sources.each { |table| add(table) }
-       File.atomic_write(filename) { |f|
-         if filename.end_with?(".dump")
+       open(filename) { |f|
+         if filename.include?(".dump")
            f.write(Marshal.dump(self))
          else
            f.write(YAML.dump(self))
          end
        }
      end
...
+       def open(filename)
+         File.atomic_write(filename) do |file|
+           if File.extname(filename) == ".gz"
+             zipper = Zlib::GzipWriter.new file
+             yield zipper
+             zipper.flush
+             zipper.close
+           else
+             yield file
+           end
+         end
+       end

先週のatomic_writeが消えたのかと思って焦りましたが、openに移動してたんですね😋


つっつきボイス:「先週スキーマキャッシュでMarshal使えるようにしてたので(ウォッチ20200302)、その続きっぽいです」「なるほど、gzipのCPUコストは安いですし☺」「拡張子もgzになってるからスキーマ自体をgzで保存できるようになったのか」「PRにもKubernetes環境でスキーマキャッシュがあふれないためとありますね: 実際、コンテナではこういうストレージの割り当てがほとんどないので、そこにでかいファイルを押し込まれると困るという要請があったんでしょうね」「なるほど〜」

⚓Kubernetesとボリュームオプション

「自分たちはその辺を考えたくない😆: docker runコマンドの-v/--volumeに相当することをdocker-composeでやれば回避できるんですけど、くばねてだとその辺ができないということなんでしょうね」「Shopifyではdocker buildに含めているとか何とか下の方に書いてありますね: 場合によるんでしょうけど😆

「それそれ: 自分がKubernetesに踏み切れない理由のひとつが、ボリュームオプションをどうしても使いたいからなんですよ」「わかります☺」「ボリュームオプションが使えないとなるとインフラを相当慎重に作らないといけなくなる😢」「GCPのKubernetes(GKE: Google Kubernetes Engine)ならボリュームマウントやれないかな〜?🤔AWS ECS(Elastic Container Service)は既にできていて、AWSのEKS(Elastic Container Service for Kubernetes)もちょっと前にできるようになったと聞いたような覚えが」

「くばねてほとんど使ってないんでわかりませんけど、たぶんAWSで言うEFS(Elastic File System)に相当するサービスがGCPでも使えるならボリュームマウントできそう😋」「GCPのサービスを一気通貫で使っていればできそうな気がしますね🤔」「AWSのECSもEFSならマウントできるはず: EFSは言ってみればマルチAZの同時接続に対応したNFSなので」「GCPにもEFS的なサービスあるのかどうかはまだ知らない😆

参考: 最強のマネージドKubernetesはどれ? スペシャリストが比較検証したGKS/AKS/EKS – G3 Enterprise
参考: Amazon ECS で Amazon EFS を使用して Docker ボリュームを作成する
参考: Amazon EFS を Amazon EKS で使用する
参考: GCP と AWS サービス対応表・比較表(2019年2月版) | apps-gcp.com
参考: Network File System - Wikipedia

⚓GCPとGoogleの底知れなさ

「GCP、そろそろ勉強しないといけないかな〜って気持ちになってきますね: 最近babaさんがSlackのAWSチャンネルでAWSのここが残念とかいろいろ書いてるのを見てると特に🤣」「ああAWSのorganizationとかが残念な件🤣」「あの書き込みはもしかすると社内インフラをGCPに移行するための布石を打ってたりして🤣」「自分もそんな気がしてる🤣

「そういえば最近某所で某インフラエンジニアと呑みながらちょっと話したんですけど『やっぱGCPはつえぇ〜ですよ💪』って言ってた😆」「へ〜強い人もそう言ってるとは😳」「babaさんとも話したことあるんですけど、AWSは言ってみれば下から積み上げていったボトムアップ的なサービス、一方Googleは研究レベルのものをいきなりぽいっと放り込んで二世代ぐらい先の世界を実現する異世界感がヤバい👽」「スゴさのレベルが😆」「亜空間に連れてかれそう😆

「普通の発想ならプロダクトにまず投入しないような技術をぽいっとリリースするGoogleの底力はやっぱスゴくて、GCPじゃないとできないことが結構多いってその人も言ってましたね」「ふ〜む」「AWSは既存のパラダイムで構築されている分、それに縛られることも多かったりしますし☺」「GCPは3年前にお遊びで使ってみただけですけど相当世界変わってそう😅

「KubernetesはGoogleがオープンソースにしたことで今日の隆盛がありますけど、当然ながらすべてをオープンソースにしたわけではない」「たしかに」「オープンになったのはせいぜいインターフェイス周りですし、もちろんそれで問題なく使えますけど、パフォーマンスやコストに直結する部分はそれよりも下のレイヤにあるわけで、Kubernetesのクラスタを動かすネットワークレイヤとか時刻同期みたいな部分にとんでもない謎技術が集中しているはず💪」「ほぇ〜😳」「だから自分はKubernetesを使うならGKEにすべきって思うんですよ😆」「わかります」「Kubernetesのようなものを自前でホスティングするなんてもってのほか🤣」「そうでしょうね🤣

⚓マルチDB向けのrakeタスクを追加

今回の変更で、シングルデータベースに加えて以下のようなコマンドも使えるようになった。
同PRより大意

    rails db:schema:dump
    rails db:schema:dump:primary
    rails db:schema:dump:animals
    rails db:schema:load
    rails db:schema:load:primary
    rails db:schema:load:animals
    rails db:structure:dump
    rails db:structure:dump:primary
    rails db:structure:dump:animals
    rails db:structure:load
    rails db:structure:load:primary
    rails db:structure:load:animals
    rails db:test:prepare
    rails db:test:prepare:primary
    rails db:test:prepare:animals

つっつきボイス:「公式情報のPRリンクが間違ってたので探しちゃいました😆

⚓each_with_objectindex_byindex_withに置き換えた


つっつきボイス:「このプルリクちょっと面白かったです😋」「each_with_objectはRubyのメソッドで、index_なんちゃらはRailsのメソッドでした」「ほほぅ😋」「後者の方が短く書けて速いみたいです」「index_withって知らなかったけど、index_byはたま〜に使ってた」

参考: Enumerable#each_with_object (Ruby 2.7.0 リファレンスマニュアル)
参考: index_by — Enumerable
参考: index_with — Enumerable

# actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb#L92
-     def fetch_or_cache_partial(cached_partials, template, order_by:)
-       order_by.each_with_object({}) do |cache_key, hash|
-           hash[cache_key] =
-             if content = cached_partials[cache_key]
-               build_rendered_template(content, template)
-             else
-               yield.tap do |rendered_partial|
-                 collection_cache.write(cache_key, rendered_partial.body)
-               end
-             end
+       order_by.index_with do |cache_key|
+         if content = cached_partials[cache_key]
+           build_rendered_template(content, template)
+         else
+           yield.tap do |rendered_partial|
+             collection_cache.write(cache_key, rendered_partial.body)
+           end
          end
+       end
      end

参考: Enumerable#index_by() が標準で欲しい - kなんとかの日記 — 2008年の記事です

index_byがRubyにも欲しいというむか〜しの記事↑を見つけました」「たしかにRubyに標準で入ってもおかしくなさそう☺」「どゆこと?」「index_byは以下で言うとUser.find(:all)したものをindex_byすると、user.idがキーになって、それに該当するユーザーのオブジェクトがその先にいるという」「なるほど、ブロックの値をキーにしたハッシュを取れるのか!まさしくindex_byという名前にふさわしい👍」「たまにこういうのを使いたくなりますね😋

# kwatch.hatenadiary.orgより
module Enumerable
  def index_by()   # 名前は to_hash のほうが好み
    hash = {}
    each do |item|
      key = yield(item)
      hash[key] = item
    end
    return hash
  end
end

## example: キーが user id, 値が User オブジェクトであるような Hash を作る
hash = User.find(:all).index_by {|user| user.id }

「まあうかつにallで取ると死にそうだけど😆」「😆」「find(:all)という今は使わないむか〜し昔の書き方をしていた頃からindex_byがあったのがよくわかった☺」「2008年!」

「そしてindex_withはRails 6から入ったのか(ウォッチ20180608)」「むむ、index_withはブロックの中で評価した式がハッシュの値になって、評価する前のキーがハッシュのキーになるってことか!😳」「そういうことか〜!😳」「ちょっとややこしい😅

# activerecord/lib/active_record/associations/preloader.rb#L175
          def records_by_owner
-           @records_by_owner ||= owners.each_with_object({}) do |owner, result|
-             result[owner] = Array(owner.association(reflection.name).target)
+           @records_by_owner ||= owners.index_with do |owner|
+             Array(owner.association(reflection.name).target)
            end
          end

「つまりたとえば配列Aと配列Bがあったとして、index_withでブロックの中でupcaseかけたとすると、元のlowercaseの方がハッシュのキーになって、upcaseかけた結果がハッシュの値になるという感じ」「凄まじいメソッド😆」「こういうのを使いたい瞬間があるのか〜😆

「ざっくりまとめるとindex_withはハッシュのに影響して、index_byはハッシュのキーに影響するのが違いということかな」「逆転の関係🙃」「オプション引数をこねこねしたくなると欲しいヤツ」「知ってたら使いたくなるメソッドですね」「人の書いたコードで見ると一瞬考えちゃいそうだけど😆」「書いた人以外はすぐわからないところはRubyのtapと似てるかも😆

参考: Object#tap (Ruby 2.7.0 リファレンスマニュアル)

「このプルリクを投げた人はRails用のcopを書いたついでに修正をかけたそうで、rubocop-railsにもcopが反映されました↓」「これはとってもいい流れ!😋

⚓ガイドの追加修正

つっつきボイス:「最後はトリビアでガイドの修正: 1つめはパラレルトランザクションのテスト方法」

パラレルトランザクションをテストする
Railsではどのテストケースも自動的に1つのデータベーストランザクションでラップされ、テスト完了時にロールバックされます。これによってテストの独立性を担保でき、データベースの変更が単独のテストに閉じ込められます。
スレッド内でパラレルトランザクションを実行するコードをテストしたい場合、トランザクションがテストトランザクションの配下でネストしているため、互いにブロックしてしまう可能性があります。
以下のようにテストケースのクラスでself.use_transactional_tests = falseを書くことでトランザクションを無効にできます。

class WorkerTest < ActiveSupport::TestCase
  self.use_transactional_tests = false

  test "parallel transactions" do
    # start some threads that create transactions
  end
end

メモ: トランザクションを無効にしたテストによるデータベースの変更は完了時に自動でロールバックされなくなるため、作成したデータテストをすべてクリーンアップしなければなりません。
testing.mdより大意


「もうひとつはActive StorageガイドのサンプルコードにXSSが発見されたので修正されました」「おっと😆」「要らん変数(${file.name})が埋まってた😆

// direct_uploads.js
addEventListener("direct-upload:initialize", event => {
  const { target, detail } = event
  const { id, file } = detail
  target.insertAdjacentHTML("beforebegin", `
    <div id="direct-upload-${id}" class="direct-upload direct-upload--pending">
      <div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div>
-     <span class="direct-upload__filename">${file.name}</span>
+     <span class="direct-upload__filename"></span>
    </div>
  `)
  target.previousElementSibling.querySelector(`.direct-upload__filename`).textContent = file.name
})
// (略)

参考: クロスサイトスクリプティング - Wikipedia — XSS

⚓Rails

⚓fakeredis: redisサーバーを立てずに開発テスト(Ruby Weeklyより)

# 同リポジトリより
    require "fakeredis"

    redis = Redis.new

    >> redis.set "foo", "bar"
    => "OK"

    >> redis.get "foo"
    => "bar"

つっつきボイス:「これうれしい人いるかなと思って」「なるほど、redisのfaker: CIでぺろっとredisを動かしたいときに役に立つのかな?」「どうだろう〜?今どきはredis立てちゃうんでは?😆」「そう思う😆」「redis立てるのが面倒な人向けかなと」「そのパターンはありそうですが😆

「redisが欲しいけどredisを立てられないときとか?🤔」「BPSのWebチームで内製したSandStarというGitLab CIサーバー↓だとdocker-composeでredis立てられますけど(実際にはredis動かしてませんが😆)、そういうのがない環境だとredis立ち上がらないので、シングルプロセスでredisのfalerが動かせるのはそれなりに価値があるかもしれませんね☺

GitLab 10.6以降でpushイベントのSlack通知が止まったときの対応方法

⚓Webpackerに乗り換えるべき理由25


つっつきボイス:「25も😆」「さよならSprocketsという文字が見えた😆」「Sprocketsからの乗り換えも含んでるみたい☺

「実際Railsプロジェクトの中でJavaScriptのコードも管理するんだったらWebpackerなのかな〜っていう気はしますけど」「今のRailsではJSの管理が公式にWebpackerになりましたね: 画像やCSSの公式な管理はまだですけど」「でも自分はここ最近のRailsプロジェクトではもうSprockets入れてませんし😎」「Sprocketsって今も入っちゃうんでしたっけ?」「rails new--skip-sprocketsって叩かないと入ってきます😅」「ちなみにSprocketsを殺しても、scaffoldするとapp/assets/ディレクトリ作られちゃいました😅


記事ななめ読み:

  • 乗り換えない理由があるとすれば
    • JSを大して使ってない
    • 時間ない
    • 心の準備がまだ
  • 1. WebpackerこそRailsの未来
  • 2. Sprocketsは死んだ、Sprocketsよ永遠なれ
  • 3. JSをうまく書く方法が変わる
  • 4. ESモジュールのパワーを享受できる
  • 5. $JAVASCRIPT_FRAMEWORKが不要になる
  • 6. 別のファイル構成を利用できる
  • 7. 依存関係の管理がしやすくなる
  • 8. jQueryプラグインとおさらばできる(その気があれば)
  • 9. ES2015以降の構文をES5+Babelにコンパイルできる
  • 10. エクスペリメンタル機能もその気になれば使える
  • 11. 特定のブラウザバージョンを対象にできる
  • 12. 新しいブラウザAPIのポリフィルが使える
  • 13. TypeScriptが使える❤
  • 14. 新しい強力なツールの封印が解かれる
  • 15. ソースコードをプログラム的に変更できる
  • 16. require_tree的なこともできる
  • 17. コードの自動静的分割
  • 18. コードの自動動的分割
  • 19. 最新のCSSフレームワークが使える
  • 20. アセットコンパイルがRailsのdevelopmentサーバーの外に出る
  • 21. development環境でページをリロードせずにコードを更新できる
  • 22. source mapオプションが使える
  • 23. パフォーマンスバジェットを実践できる
  • 24. バンドルの中身をチェックできる
  • 25. tree-shaking(不要なコードをビルドから削除する機能)が使える

参考: Google Developers Japan: パフォーマンスバジェットのご紹介 - ウェブパフォーマンスのための予算管理


「お、そろそろゲームにログインする時間なのでおいとまします😆」「😆」「お疲れさまでした〜👋

今回のウォッチつっつき会は、たまたまですが参加者が自分もふくめて全員リモートワーク中だったのでZoomで社内開催しました🔭。Zoomありがたいです😂。なお社内でのつっつき会は途中入場途中退出自由でROM専もOKです。

⚓Let’s Encryptの証明書をRubyで自動更新(RubyFlowより)

# 同記事より
require 'fileutils'
require 'acme-client'
require 'openssl'
apps =
  { 'drgcms' => {
      dir: '/path_to/drgcms', domains: %w[www.drgcms.org tulips.drgcms.org] }
  }

client_key = OpenSSL::PKey::RSA.new( File.read('lets-encrypt.key') )
client = Acme::Client.new(private_key: client_key, directory: 'https://acme-v02.api.letsencrypt.org/directory')

apps.each  do| app, domains |
  p '',"Renewing APP: #{app}"
  order = client.new_order(identifiers: domains[:domains])
  order.authorizations.each do |authorization|
    p ['validating', authorization.domain]
    challenge = authorization.http

    # write challange data to challenge.filename
    FileUtils.mkdir_p( File.join( domains[:dir], 'public', File.dirname( challenge.filename ) ) )
    File.write( File.join( domains[:dir], 'public', challenge.filename), challenge.file_content )

    # validate single domain
    challenge.request_validation
    while challenge.status == 'pending'
      p ['challenge.status', challenge.status]
      sleep(2)
      challenge.reload
    end
    p challenge.status # => 'valid'
  end

  # get certificate
  private_key = OpenSSL::PKey::RSA.new(4096)
  csr         = Acme::Client::CertificateRequest.new(private_key: private_key, names: domains[:domains])
  order.finalize(csr: csr)
  while order.status == 'processing'
    sleep(1)
    challenge.reload
  end
  # save certificate and private key
  if (certificate = order.certificate)
    File.write("#{app}-pkey.pem", private_key.to_pem )
    File.write("#{app}-cert.pem", certificate)
  else
    raise "Error retrieving certificate for application #{app}. Certificate was empty."
  end
end

つっつきボイス:「ACMEv1とかv2って初めて見ました」「これはLet’s Encryptの更新プロトコルです☺」「おぉ、それ用のgemもあるみたいですね」「自分はLet’s Encryptのコマンドで十分ですけど、Rubyでやりたい人もいるんでしょう😆

⚓CarrierWave::Uploader::Baseをうっかり継承しないようにする方法(Ruby Weeklyより)


つっつきボイス:「Uploader::Baseを継承するとすぐに気づかないバグになるみたいですね」「CarrierWaveみたいなでかいライブラリにこういうことするのってあんまりない気がしますけど😆

# 同記事より
module CarrierWave
  module Uploader
    module YourAppName
      class MustInheritFromApplicationUploaderError < StandardError; end
      def initialize(model = nil, mounted_as = nil)
        # これがraiseされると、適用されたアップローダがApplicationUploaderを
        # 継承する必要があることを思い出させてくれる
        # このオブジェクトモデルのファイルではアップローダーを指定していないことがあった
        unless is_a?(ApplicationUploader)
          raise MustInheritFromApplicationUploaderError, "Did you forget to specify an uploader?"
        end
        super
      end
    end
  end
end

「お、上のコードを見るとたしかに直接の継承を明示的に禁止するコードになってる: CarrierWave本体じゃなくてApplicationUploaderを継承せいということね🧐」「MustInheritFromApplicationUploaderErrorってスゴい名前😆」「今のRailsもActiveRecord::BaseではなくApplicationRecordを継承するようになってますし☺


前編は以上です。

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

週刊Railsウォッチ(20200303後編)Ruby 2.7で引数のruby2_keywordsフラグを確認する、fake_apiでAPIプロトタイプ、groupdateで日付をグルーピングほか

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

週刊Railsウォッチ(20200310後編)Flutter+Firebaseでモバイルアプリ開発、命名7つの鉄則、Rubyバージョンを切り替えるchruby gemほか

$
0
0

こんにちは、hachi8833です。こちらのサイトの評判がめちゃいいですね。


stopcovid19.metro.tokyo.lg.jpより

以下はつっつき後に見つけたツイートです。尊敬するオードリー・タン氏❤が対策サイトにプルリク投げてくれたとは😂🎉。GitHubで👍がこんなにたくさん付いてるのを見たのは初めてかもしれません。

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

⚓Ruby

⚓chruby: rbenvのライバル登場か(Ruby Weeklyより)

# 同リポジトリより
$ chruby 1.9.3
$ chruby
 * ruby-1.9.3-p392
   jruby-1.7.0
   rubinius-2.0.0-rc1
$ echo $PATH
/home/hal/.gem/ruby/1.9.3/bin:/opt/rubies/ruby-1.9.3-p392/lib/ruby/gems/1.9.1/bin:/opt/rubies/ruby-1.9.3-p392/bin:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/home/hal/bin:/home/hal/bin
$ gem env
RubyGems Environment:
  - RUBYGEMS VERSION: 1.8.23
  - RUBY VERSION: 1.9.3 (2013-02-22 patchlevel 392) [x86_64-linux]
  - INSTALLATION DIRECTORY: /home/hal/.gem/ruby/1.9.3
  - RUBY EXECUTABLE: /opt/rubies/ruby-1.9.3-p392/bin/ruby
  - EXECUTABLE DIRECTORY: /home/hal/.gem/ruby/1.9.3/bin
  - RUBYGEMS PLATFORMS:
    - ruby
    - x86_64-linux
  - GEM PATHS:
     - /home/hal/.gem/ruby/1.9.3
     - /opt/rubies/ruby-1.9.3-p392/lib/ruby/gems/1.9.1
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :benchmark => false
     - :backtrace => false
     - :bulk_threshold => 1000
     - "gem" => "--no-rdoc"
  - REMOTE SOURCES:
     - http://rubygems.org/

つっつきボイス:「rbenv的なchruby、なるほど: 今のところrbenvで間に合ってますけど☺」「そういえば最近はもっぱらrbenv使ってるのでrvm使わなくなりました」「rvmは少々お行儀が悪かったんですよね〜😢」「chrubyはcdコマンドにフックかけてないってありますね」「そうそう、rvmはcdにフックかけてた気がする: rbenvはどうだったかな〜🤔」「忘れた頃につっかかったりしそうですね😅」「謎の理由でrvmが死んだことはあった😇

⚓Symbol#to_sの挙動


つっつきボイス:「Noah Gibbsさんの記事で、とりあえず手元で一次訳してみました(近日公開予定)」(眺めてみる)

「Ruby 2.7 previewでto_sがfrozen stringを返すように変更してみたら、思った以上に影響が大きそうだったので最終の2.7には入らなかったという話でした」「ははぁ、これはありそう」「#16150でも"foo".freeze.to_s.frozen?とかいろいろやってみてるし😆」「特に元がstringだった場合に元と同じstringを返すので、改変すると元のも変わっちゃったりすることがあったようです」「object_idが同じになっちゃうんでしょうね🤔

「普通に考えればto_sがfrozen stringを返すのは正しそうなんですけどね」「いろんな使われ方されてると難しいんでしょうね😅

⚓命名7つの鉄則


つっつきボイス:「記事のドメイン名がnamingthings.coですって😆」「ネーミングしか興味なさそう😆

「PronounceabilityとかSearchabilityって、ググラビリティみたいな造語の匂いがしますね」「Austerityって何だろ?🤔」「辞書的には『謹厳な』みたいな感じですけど、ニュアンスとしては『ふざけない』『ひねらない』という雰囲気かなと」「はぁ〜なるほど、一部の人間にしか理解できないようなジョークを混ぜたりすると本質とか意図がぼやけちゃうよね、という主張か」「temporary conceptsを使うなというのもそれっぽいですね」

「don’t be cleverのcleverはネガティブなニュアンスでしょうね😆」「cleverは割と『小賢しい』『猪口才な』という匂いになります😆」「教養を盛り込みすぎた挙げ句、知らない人に伝わらなかったら元も子もないというか🧐」「『これがわかるかねキミ😎』みたいな😆」「そういえば日本ではそういう意味でのcleverすぎる変数名って見ない気がする🤔」「この記事なかなかよさそう👍」「やった😋


記事ななめ読み:

  • 一貫性(consistency)
  • 理解のしやすさ(understandability)
  • 特定性(specificity) — あまりに特定しすぎてもいかん
  • 長すぎず短すぎない(brevity)
  • 検索しやすさ(searchability)
  • 発音のしやすさ(pronounceability)
  • 素直さ(austerity)

⚓その他Ruby


つっつきボイス:「『RubyをWindowsでまともに動かす』ということをそもそもしたくない😆」「😆」「WSLでも結局Windows Subsystem for Linuxで動いてるのでWindowsなのか?というのは疑問ではある」「それLinuxでは感😆」「そういえばこんなツイートも↓」

「ああこれ、自分も大学でRubyInstaller使ってWindowsでRails教えてましたけど、とにかくgemが軒並み動かなくなります😇」「あ〜😳」「Rails本体はぎりぎり動くんですけど、サードパーティgemがたいてい動かなくて😭: pgとかmysql2みたいなアダプタを使おうとするだけでdllを入れないといけないし、SQLite使おうとしてもSQLiteやdllをインストールするところから始めないといけないとか」「何にもできませんね😇」「か、悲しい😅」「これじゃつらすぎですよもう😭

参考: WindowsでRailsTutorialするときに気をつけること - Qiita

⚓DB

⚓Joe Bot: PostgreSQLクエリ最適化をアシストするAI(Postgres Weeklyより)


postgres.aiより


つっつきボイス:「クエリ最適化アシスタント」「普通にCIに常駐するようなボットっぽいですけど」「クエリオプティマイザにシナリオを何回も流し込んで…みたいなのは昔からあるけど、これは外からやるのかな?🤔

(アニメーションGIFのデモ画像を見て)「このJoe Botくんが『こうしてみたらどう?』って教えてくれて、Slackbot形式でやりとりができるというものみたい😋」「おぉ〜」「PostgreSQLの内蔵クエリオプティマイザを改善するというよりは、人間が書くSQL文のオプティマイズをサポートするボットでしょうね☺」「あったら使うかなどうかな〜、DBAな人ならうれしいかも」「これは遅いかもみたいな雰囲気だけでやりとりするんじゃなくて、エビデンスベースでやりとりできて記録がSlackに残るのはよさそう👍

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

⚓CloudWatchの改善


つっつきボイス:「お〜複数アラーム組み合わせができるようになってる😋」「今までは単品のアラームしかできなかったんですね」「元々CloudWatchはあまり凝ったことはできませんし、sensing intervalとかも融通が効かないし😆」「ははぁ」

babaさんとも話したことありますけど、CloudWatchを高度な監視のために積極的に使う理由ってあんまりなくて、あるとすればAWSにビルトインされていて何のセットアップもしないで使えるってことぐらい😆」「ないよりはマシなぐらいでしょうか?」「同じものを自分で立てるのはしんどいので、まったくないと困るけど、でも不便😢

「とは言え、複数アラームを組み合わせて条件付けして『こういうときは無視していい』とかできるようになったのは割とありがたい👍

参考: CloudWatch 複合アラームが利用可能になりました!! | Developers.IO

⚓KubernetesのHelmとは

つっつきボイス:「Helmっていうとemacsとかで入力支援するツールを思い出す😆」「VimでもHelm使えるのかな?」「名前ぐらいは聞いたことあるかも(Vimmerなの❤)」「あとはzshとかでシェルを超リッチにしてる人たちが使ってそう」

参考: 初心者〜初級者のためのEmacs-Helm事始め : 前編 - Qiita

「でさくらの記事のHelmは、ほんとにKubernetesのマネージャか」「くばねて、触ってます?」「触ろうと思いつつまだですね〜😆」「Kubernetes入門みたいな本買って読んでるけど、手を動かしながらじゃないとなかなかわからんな〜📕

「ところでKubernetesの対抗馬って今のところなさそうですね」「まあ強いて言うならAWSのECSとか」「たぶんですけど、ECSはくばねてのような方向性にはならないだろうという確信が自分の中では得られました😆」「ECSはコンテナオーケストレーションの先駆けですから😆」「あとDocker Swarm😆」「Docker Swarm!たぶんKubernetesにはもう勝てないでしょう⚔

「今だとどんなオルタナティブがあるんだろか?」「この辺の記事↓かな〜」「PrometheusにDocker Composeに…」「Prometheusは違うのでは😆」「違う気がする😆

参考: 9 Best Orchestration Open Source Docker Tools in 2018

「こっちも見てみよう↓」「ああMinikubeもあるな〜」「Minikubeは割とすんなり入りますね😋: でもKubernetes使いたいレベルの案件だったらKubernetes動かさないと意味ないかもって自分は思いますけど😆」「😆」「そうそうRancherってのもあった」

参考: 14 Best Docker orchestration tools as of 2020 - Slant

「ざっと眺めた感じ、とりあえずKubernetes勉強しとくのがいいのかな〜🤔」「自分たちはDocker Composeで収まる範囲でやってることがほとんどだけど、Kubernetesが必要になりそうなレベルになったら代用品じゃなくてKubernetesでやらないとキツいかも☺

「今更ですけど、Docker Composeってオーケストレーションツールなんですね」「あれは確かにオーケストレーションしてますね🧐: シングルホストだけど」「気づかずに使ってました…😅

⚓その他クラウド

つっつきボイス:「そうそう、これはWSL2の話」「へ〜、Windows HomeでDocker Desktopが使える?」「順番としてはWSL2がWindows Homeに来たからそれを使ってDocker Desktopが動くようになるということかと」「WSL2ってヤツがWindows Professionalじゃなくても動くようになった?」「Hyper-Vのサブセット的なWSL2がWindows Homeでも動くようになるということですね☺」「Professional版とは何ぞや感😆」「まあHyper-Vの機能がすべてWSL2で使えるわけではないらしいので」「あ、そうなのか😅」「Windows Homeを最新に上げれば使えるのかな?まだ上げてないからわからんけど😆

参考: WSL2で劇的に変わるあなたのWebアプリケーション開発環境【その2:導入編】 | SIOS Tech. Lab
参考: Windows 10 での Hyper-V の有効化 | Microsoft Docs

⚓JavaScript

⚓FlutterとFirebase


flutter.devより



firebase.google.comより


つっつきボイス:「FlutterとFirebaseをつい取り違えそうな自分でした😅」「たぶん今の世の中で、iOSアプリとAndroidアプリを最速で公開する最もお手軽な方法といえば、FlutterとFirebaseの組み合わせって感じですかね〜☺」「でしょうね: 何しろサーバーサイドも含めて全部お膳立てされてますし」「後はDartさえ書けば🤣」「それな🤣」「ダ〜トか〜😅

参考: 【Flutter入門】開発言語Dartの特徴を解説するよ | TECHRISE

「あと同じ記事の中で、BLoCとかいう設計方法についてのリンク↓があったので開いてみたら『自分はもうBLoC使ってません』と書いてありました😇」「今はいろいろ過渡期ですから😆

参考: FlutterのBLoC(Business Logic Component)のライフサイクルを正確に管理して提供するbloc_providerパッケージの解説

「ともあれ、個人でモバイルアプリをとりあえず作ってみたいというレベルであれば、FlutterとFirebaseの組み合わせは全然ありだと思います👍」「よ〜し😋」「とにかくこういうのはぱぱっと作って運用してみることですね: 後は投資を集めて会社を大きくするなり売却するなりしてもいいし、運用が回ってくればエンジニアも集まってくるので、じゃあロックインされた部分を切り離そうかみたいなこともできるようになるでしょうし」「ですね☺」「スタートにはこういうのがいいと思います❤

「少なくともReact Nativeでやるよりはよさそう😆」「😆」「React Nativeみたいなゴツいフレームワークは、計画がみっちり立っていてがっつり投資集めて作ることが決まってるような案件でないと、重たそう😆」「React Nativeでないとできないような内容なら別だけど😆


reactnative.devより

追いかけボイス: 「React NativeとFlutterどっちがゴツいといったらどっちも十分ごついと思う人です」(社内Flutter勢より)

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

⚓Googleが検索をモバイルファーストへ


つっつきボイス:「お、まだ終わってなかった?」「前からやるぞって宣言してましたけど、まだだったとは」「とりあえずオレオレRailsアプリのモバイル対応がまだ不十分なので気になってました😅

⚓その他フロントエンド

⚓言語・ツール

⚓文字について


つっつきボイス:「昔からあるサイトですが、古い記事もちゃんと更新されているのがいいなと思って」「出たな異体字セレクタ」「DTPわかんない😇」「DTPの世界はWebと背景や文脈が異なるので、Web屋が気軽に手を出しにくいですね☺

⚓その他言語


つっつきボイス:「自分は見かけなかったな〜これ」「理由があるのかないのかもわからん😆」「システム屋がガチガチの要求受けて作ったらこうなりそう」「縦は絶対揃えろとか…ってこれプロポーショナルだから揃ってないし😆

⚓その他

⚓読み方も大事

情報系の用語は英語由来のワードが多いですし、日本語でも「引数」のように日常生活ではなかなか使わない単語が登場します。独学だとどうしても自己流の読み方となってしまい、実際に就職したりプログラミング系のコミュニティに参加したりするときにコミュニケーションのハードルになってしまう可能性があります。(恥ずかしながら、私自身も中学生時代は「引数」を「いんすう」と読んでしまっていた苦い経験があります。)
そのため、N予備校では可能な限り、初出の単語や英単語には括弧書きで以下の画像のように読み方を付けています。
同記事より


つっつきボイス:「ドワンゴのN予備校頑張ってるな〜」「N予備校も今教材を無料開放してるみたいですね」「開校した当初は一部の人たちには注目されつつも一般からは遠巻きに見られていた感じでしたけど、今回の休校騒ぎで評判が変わってくるかも🤔」「私ちょっと興味ありました😋


nnn.ed.nicoより

⚓電子書籍期間限定無償公開の嵐


つっつきボイス:「こちらも続いて無償公開のエントリです」「ko1さんの『Rubyのウラガワ』が期間限定で無償で読める!」

「無償コンテンツ公開はありがたいけど、世の保護者が今最も欲しいのは子どもから目を離さず世話をしてくれる大人でしょうね」「そうなんですよね😢」「自分もマンションの自治会で『家で仕事してるならちょっと見といてもらうことってできますか?』って聞かれましたし😅

「ついでといいますか、以下はZoomで参加可能なイベントです」「Zoomで400人!」「やれるもんなんですね」「Zoomのウェビナーっていう機能を使うと大人数で参加できるんですよ」「へぇ〜!😳」「ただし別料金💰」「パネリストも100人いると排他で話すの大変そう…」


追いかけボイス: 3/19の銀座Rails#19もZoomでのリモート参加形式になったそうです。「出張!Railsウォッチ」もあります。

⚓番外

⚓「密集恐怖症」って呼んでた


つっつきボイス:「ああこれ😆、なんちゃらフォビア系でこういうのありますね」「自分は昔から適当に密集恐怖症って呼んでました: こういう集合体恐怖症のケってあります?」「蓮コラ的な?自分はそんなにないかな😆」「自分も特にないんですが苦手な人はとことん苦手なのを何度も目にしました」「こういう根源的な恐怖は生理的なものだから理由とか関係ないでしょうし」「恐怖って脳のかなり下の層からきてるみたいですし」

参考: トライポフォビア - Wikipedia


後編は以上です。

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

週刊Railsウォッチ(20200303後編)Ruby 2.7で引数のruby2_keywordsフラグを確認する、fake_apiでAPIプロトタイプ、groupdateで日付をグルーピングほか

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Rails: Active Recordメソッドのパフォーマンス改善とN+1問題の克服(翻訳)

$
0
0

概要

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

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

Rails: Active Recordメソッドのパフォーマンス改善とN+1問題の克服(翻訳)

Railsアプリケーションのパフォーマンスは多くの変数に依存していますが、その中のひとつは、アクション完了のために実行されるクエリ数です。データベース呼び出しの回数が少ないほどメモリアロケーションが削減され、ひいては操作完了に要する時間も削減できます。

そうした問題のひとつがN+1クエリ問題です。projectsテーブルとcommitsテーブルがあるときに、projectを2件読み込んでからそれらのprojectのすべてのcommitsを読み込むと、projectを読み込むクエリが1件と、projectごとにcommitsをフェッチするクエリがN件生成されます。追加操作は可換なので、1+NともN+1とも書けます。

# projects = Project.where(id: [1, 2])
> SELECT "projects".* FROM "projects" WHERE "projects"."id" IN (1, 2)

# projects.map { |project| project.commits }
> SELECT "commits".* FROM "commits" WHERE "commits"."project_id" = $1  [["project_id", 1]]
> SELECT "commits".* FROM "commits" WHERE "commits"."project_id" = $1  [["project_id", 2]]
....

N+1クエリを解決する一般的な方法は、includesを用いて関連付けをeager loadingすることです。

includesのしくみ

includespreloadeager_loadのショートハンドです。

preloadは2つのクエリを開始します。最初のクエリはメインのモデルをフェッチし、次のクエリは関連付けられているモデルをフェッチします。eager_loadはLEFT JOINを行い、メインのモデルと関連付けられているモデルの両方をフェッチする1つのクエリを開始します。

preloadはメモリ使用量の点でeager_loadよりずっと有利です。eager_loadで使われる他の戦略を強制しない限り、preloadActive Recordのデフォルト戦略です。理由についてはActiveRecord::Associations::Preloaderクラスに記載されています。

rubydocにある昔のRails 4.1.7のPreloaderドキュメントはもっと参考になります。

Authorモデルに’name’カラムと’age’カラム、Bookモデルに’name’カラムと’sales’カラムがあるとします。Active Recordはこの戦略に基づいて、以下のように1件のクエリで1件のauthorとその全bookをすべて取り出そうとします。

SELECT * FROM authors
LEFT OUTER JOIN books ON authors.id = books.author_id
WHERE authors.name = 'Ken Akamatsu'

しかしこれでは余分なデータを含んだ行が大量に返される可能性があります。最初の行が返った時点で、Authorオブジェクトをインスタンス化するのに十分なデータがあります。以後の多数の行で有用なのは、JOINされた’books’テーブルのデータだけあり、JOINされた’authors’のデータは冗長であるにもかかわらずメモリとCPU時間を食います。この問題はeager loadingのレベルが増すに連れてたちまち悪化します(Active Recordがアソシエーションの関連付けもeager loadingする)。

preloadのしくみ

preloadは、preloadするすべての関連付けをループして、関連付け1件ごとにクエリを作成します。

# Project.preload(:commits)
> Project Load (1.8ms)  SELECT "projects".* FROM "projects"
> Commit Load (128.3ms)  SELECT "commits".* FROM "commits" WHERE "commits"."project_id" IN ($1, $2) [["project_id", 1], ["project_id", 2]]

eager_loadのしくみ

eager_loadは、LEFT OUTER JOINでレコードをeager loadingします。JOINのRIGHT側とマッチするかどうかにかかわらずJOINのLEFT側のすべての行を保持するのではありません。

eager_loadJoinDependencyの内部に実装されています。返される結果では、LEFT側のテーブルの各レコードについて1行しか含まれません。

# Project.eager_load(:commits)
> SELECT "projects"."id" AS t0_r0, "projects"."name" AS t0_r1, "projects"."org" AS t0_r2, "projects"."connected_branch" AS t0_r3,
  "projects"."enabled_by" AS t0_r4, "projects"."created_at" AS t0_r5, "projects"."updated_at" AS t0_r6, "projects"."permalink" AS t0_r7,
  "projects"."domain" AS t0_r8, "commits"."id" AS t1_r0, "commits"."author" AS t1_r1, "commits"."committer" AS t1_r2, "commits"."message"
  AS t1_r3, "commits"."sha" AS t1_r4, "commits"."parents" AS t1_r5, "commits"."project_id" AS t1_r6, "commits"."created_at" AS t1_r7,
  "commits"."updated_at" AS t1_r8, "commits"."status" AS t1_r9, "commits"."committed_at" AS t1_r10 FROM "projects"
  LEFT OUTER JOIN "commits" ON "commits"."project_id" = "projects"."id"

# Project.includes(:commits).references(:commits).count
# includesにreferencesを追加することでもeager_loadがトリガされる
> ... (上と同じクエリ) ...

eager_loadcountを組み合わせるとDISTINCTキーワードが自動で追加されることに気づいた方もいるでしょう。

このDISTINCTは、eager loadingおよびCOUNT SQL操作が検出されたときにActive Recordによって追加されます。

# Project.eager_load(:commits).count
# 'DISTINCT'が自動で追加されたことがわかる
  SELECT COUNT(DISTINCT "projects"."id") FROM "projects" LEFT OUTER JOIN "commits" ON "commits"."project_id" = "projects"."id"
> 2

関連付けのサブセットをeager loadingする

場合によっては、キューに乗ったすべてのcommitなどのデータで、データのサブセットだけをeager loadingしたいことがあります。これについては後述の「メソッドをスコープにチェインしてしまう」や「スコープはeager loadingできないので関連付けに変換する」で解説しています。

動的な条件に基づいてeager loadingする

上のセクションの続きです。フィルタの基準が動的に決定されるデータのサブセットをeager loadingしたいときはどうすればよいでしょうか。たとえば、現在ログイン中のユーザーのcommitをすべてeager loadingしたいとします。それには、ActiveRecord::Associations::PreloaderというドキュメントのないAPIを使う必要があります。

# projects = Project.all.to_a
> SELECT "projects".* FROM "projects"

# ActiveRecord::Associations::Preloader.new.preload(
  projects,
  :commits,
  Commit.where("author ->> 'email' = ?", current_user.email)
  )
> SELECT "commits".* FROM "commits" WHERE (author ->> 'email' = > 'current_user_email')

メモ: この機能は今のところRails 6では動かなくなっています(#36638

集計クエリをeager loadingする

Railsには組み込みのカウンタキャッシュがあり、COUNT集計関数の問題解決に利用できます。カウンタキャッシュを追加してメンテしたくないのであれば、activerecord-precounterなどのgemを利用できます。

その他のAVGSUMなどの集計関数については、eager_group gemを利用できます。

また、余分な依存関係を追加せずに解決する方法もあります。

# projects.map {|project| project.commits.count}
> SELECT COUNT(*) FROM "commits" WHERE "commits"."project_id" = $1  [["project_id", 1]]
> SELECT COUNT(*) FROM "commits" WHERE "commits"."project_id" = $1  [["project_id", 2]]

# Commit.where(project_id: projects).group(:project_id).count
> SELECT COUNT(*) AS count_all, "commits"."project_id" AS
  commits_project_id FROM "commits" WHERE "commits"."project_id"
  IN (SELECT "projects"."id" FROM "projects" WHERE "projects"."id" IN ($1, $2))
  GROUP BY "commits"."project_id"  [["id", 1], ["id", 2]]

# Commit.where(project_id: projects).group(:project_id).sum(:comments)
> SELECT SUM(comments) AS sum_comments, "commits"."project_id" AS
  commits_project_id FROM "commits" WHERE "commits"."project_id"
  IN (SELECT "projects"."id" FROM "projects" WHERE "projects"."id" IN ($1, $2))
  GROUP BY "commits"."project_id"  [["id", 1], ["id", 2]]

eager loadingのよくある間違い

⚓1. 誤ったオーナーで関連付けがeager loadingされる

次の例をご覧ください。

class Project < ApplicationRecord
  has_many :commits
  has_many :posts, through: :commits
end

class Commit < ApplicationRecord
  belongs_to :project
  has_one :post
end

class Post < ApplicationRecord
  belongs_to :commit
end

Projectでpostsをeager loadingしたのにcommitでpostを呼び出すと、データベースへのクエリが発生します。このposts(ターゲット)はproject(オーナー)で読み込まれるので、このprojectで読み込まれたpostsしかフェッチできなくなります。

# projects = Project.includes(:commits, :posts)
> SELECT "projects".* FROM "projects"
> SELECT "commits".* FROM "commits" WHERE "commits"."project_id" IN ($1, $2, ...)
> SELECT "posts".* FROM "posts" WHERE "posts"."commit_id" IN ($1, $2, ...)

# projects.first.commits.first.post
>

postを個別のcommitで呼ぶと、そのcommitでeager loadingしなければならなくなります。

Project.includes(commits: :post)
> ...

projects.first.commits.first.post

⚓2. selectではなくpluckを使ってしまう

以下の例を見てみましょう。指定の組織にあるprojectのcommitをすべて読み込みたいとします。

# Commit.where(project_id: Project.where(org: 'rails').pluck(:id))
> SELECT "projects"."id" FROM "projects" WHERE "projects"."org" = $1  [["org", "rails"]]
> SELECT "commits".* FROM "commits" WHERE "commits"."project_id" IN ($1, $2)  [["project_id", 1], ["project_id", 2]]

上のコードではクエリが2件発生します。最初のクエリではprojectsのidをSELECTし、次でcommitsをフェッチします。ここでpluckselectに置き換えてみると、サブクエリのおかげでクエリが1件だけになります。

# Commit.where(project_id: Project.where(org: 'rails').select(:id))
> SELECT "commits".* FROM "commits" WHERE "commits"."project_id" IN (SELECT "projects"."id" FROM "projects" WHERE "projects"."org" = $1)  [["org", "rails"]]

メモ: pluckは、行全体ではなく値のサブセットをSELECTしたい場合に使いましょう。

⚓3. メソッドをスコープにチェインしてしまう

あるprojectのすべてのpostsを、読み込み済みのpostsのサブセット(公開済みのpostsのみ、など)に応じて読み込みたい場合、結果セットが小さいのであれば、メモリ上でフィルタすることでデータベースにクエリを2件送信することを回避できます。

projects.posts # DBクエリ1件
projects.posts.published # DBクエリ1件

# 以下ならデータベースへのクエリが1件で済む
projects.posts.select {|p| p.status.eql?('published') }

⚓4. スコープはeager loadingできないので関連付けに変換する

class Commit < ApplicationRecord
  ...

  scope :queued, -> { where(status: :queued) }

  ...
end

projects = Project.includes(:commits)

# キューに乗ったcommitをフェッチするクエリを送信する
projects.commits.queued

commitsをすべてeager loadingするのではなく、キューに乗ったcommits(commitsのサブセット)だけをeager loadingしたい場合は、スコープ付きの関連付けを作成してからeager loadingしなければなりません。

class Project < ApplicationRecord
  ...

  has_many :queued_commits, -> { where(status: :queued) }, class_name: 'Commit'

  ...
end

projects = Project.includes(:queued_commits)
projects.queued_commits

⚓5. プリロードしたデータでexists?を呼んでしまう

exists?を使うと常にデータベースクエリが発生します。データが読み込み済みであれば、レコードが空でないかどうかの確認にはpresent?を使うべきです。別の方法として、存在チェック後にデータが読み込まれることがわかっていれば、単にレコードを読み込んでから存在チェックします。

⚓6. countではなくsizeを使おう

countを使うと常にデータベースクエリが発生します。データが読み込み済みであれば、sizeを呼べば関連付けのサイズを取得できます。常にsizeを使うようにすれば、データベースクエリは関連付けが読み込まれていない場合にのみ発生します。

まとめ

本記事では、よくあるN+1問題を克服するのに使えるActive Recordのメソッドをいくつかご紹介いたしました。シナリオによっては、大量のオブジェクト読み込みやクエリ実行を削減するヒントをRailsに提供できます。Railsアプリケーションのスピードアップについては今後の記事でフォローする予定です。

関連記事

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

Rails: N+1クエリを「バッチング」で解決するBatchLoader gem(翻訳)

Rails向け高機能カウンタキャッシュ gem ‘counter_culture’ README(翻訳)

週刊Railsウォッチ(20200316前編)Webpackerの対抗馬Simpackerはいかが、Hanami::APIは高速性をフィーチャー、N+1と戦うgemほか

$
0
0

こんにちは、hachi8833です。こちらはオンラインイベントや無料コンテンツのまとめサイトだそうです↓。


onlinefesjapan.comより


つっつきボイス:「音楽ライブ系の情報多いですね🎶」「フェスだからいろいろあるかも」「漫画や書籍が無料で読める系も📓」「毎週のようにライブ見に行ってる知り合いも、ここ最近イベント中止が相次いで週末やることなくなったって言ってますし😆」「毎週のようにってスゴい😳」「自分の回りのJリーグファンもみんな気落ちしてますし😭

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

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

今回はコミットリストから見繕いました。

⚓perform_enqueued_jobsがリトライしないよう修正

ActiveJob::TestCase#perform_enqueued_jobsは今後リトライしないようになる。
perform_enqueued_jobsをブロックなしで呼び出すと、アダプタは既にキューに入っているジョブを実行するようになった。キューの中にある、後に完了することが決まっているジョブは実行されなくなる。
この変更が影響するのは、perform_enqueued_jobsにブロックを渡さなかった場合に限られる。
Changelogより大意

# activejob/lib/active_job/test_helper.rb#L602
      def jobs_with(jobs, only: nil, except: nil, queue: nil, at: nil)
        validate_option(only: only, except: except)

-       jobs.count do |job|
+       jobs.dup.count do |job|
          job_class = job.fetch(:job)

          if only
            next false unless filter_as_proc(only).call(job)
          elsif except
            next false if filter_as_proc(except).call(job)
          end
          if queue
            next false unless queue.to_s == job.fetch(:queue, job_class.queue_name)
          end
          if at && job[:at]
            next false if job[:at] > at.to_f
          end
          yield job if block_given?
          true
        end
      end

つっつきボイス:「プルリクのreeenqueueってeが一個多い気がする😆」「ホントだ😆」「そういえばGitHubにはスペルチェッカーありませんね☺

問題
perform_enqueued_jobsをブロック無しで実行すると、リトライメカニズムで自分自身を再度キューに乗せるジョブが即座に動き出してしまう。
この動作はperform_enqueued_jobsにブロックを渡すのであれば理解できる。
しかし自分が期待するのは、perform_enqueued_jobsにブロックを渡さない場合は、後でキューに乗るジョブを実行するのではなく、既にキューに乗っているジョブを実行することである。
解決法
ジョブの配列をdupして今後改変されないようにする。
同PRより大意

「あーよくある操作かも: ジョブを実行した後にそのジョブリストを取り出すのか、実行する前にジョブリストを取り出すのかというタイミングをちゃんと使い分けできるようにしたのかな🤔」「ジョブをdupして解決したってありますね」「dupしないとジョブのタイミングによってうまくいかないことがあったのかも🤔

これは#33626の意図にも合致する。
しかしこれは重要な変更ではあるが微妙な変更でもある。元のメソッドは「そのブロックを実行中に有効になったジョブが実行対象となる」というような意味だが、メソッド名にブロックを渡さない場合の文字どおりの意味としては「それまでにキューに乗っていた特定のジョブを実行する」ということになる。そしておそらく「既存のジョブだけではなく後続のジョブも実行してしまう」というバグが存在していた。
これらを別のメソッドに分割して振る舞いを区別できるようにする手もある。つまり「マッチするジョブを、ブロックの実行中に実行する」と「キューで待ち状態のジョブだけを実行する」を区別する。
ジョブのリトライをテストする場合、後続のジョブをジョブ階層的にキューに乗せ、スケジュールされたジョブは第2のブロック形式呼び出しでラップする必要があるだろうか?
同PRコメントより

# 同PRコメントより
# キューに乗ったジョブAを実行する。ジョブAはジョブBもキューに乗せる。
A.perform_later

# ここで実行すればアサーションで期待どおりの出力が得られるはず
perform_enqueued_jobs only: [ A, B ]

# あれ、ジョブAがキューに乗せたジョブBが動かなかったぞ
assert some_b_thing # => failed

# キューに乗ったジョブを実行し、その実行中にキューに乗ったジョブを実行する?
perform_enqueued_jobs do
  perform_enqueued_jobs
end

perform_enqueued_jobsが入れ子になってる😆」「変な書き方😆」「コメントを見た感じではブロック回りの話のようだ」

「ちなみに現場レベルだと1回実行するとうまくいかないのにもう1回実行するとなぜかうまくいくバグってあったりしますよね🤣」「あるある🤣」「実行するとなぜか1個余っちゃって、念のためもう1回実行すると消えるみたいな🤣」「理由わからないけど現場はそれで解決しようみたいな🤣」「# こうするとちゃんと動くとかコメントが付くヤツ🤣」「# たまにうまくいかないけど2回動かせば大丈夫とか🤣」「冪等になってればいいという考え方もありますけど☺

⚓IPAddrをlocalhostと比較したときに毎回例外をrescueしていたのをやめた

# railties/lib/rails/application/configuration.rb#L36
       @hosts                                   = Array(([IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0"), ".localhost"] if Rails.env.development?))
        @hosts                                   = Array(([".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")] if Rails.env.development?))

つっつきボイス:「IPAddrをlocalhostと比較すると例外が発生していたんですね😳」「localhostはIPアドレスじゃないしな〜☺

「比較の順序を変えただけ?」「IPAddr構造体と比較するより先にlocalhostと比較するようにしたようだけど」

「これ見ると↓一番よく使うのがlocalhostなのにlocalhostの比較がトップに来ないのは遅いって😆コメント)」「あ〜そういうことね😆」「リクエストのたんびに例外をrescueするのをやめてパフォーマンスを改善しようってことだと思いました☺」「HostAuthorizationミドルウェアで毎回ここを通ってたのか、わかる☺

MRI以外のプラットフォームでは例外のraiseがかなり遅いので、例外を制御フローに使うのは可能な限り許すべきではないと個人的には思う。
同コメントより大意

⚓localhostで使ったファビコンが残ってしまう問題

「Rackミドルウェアは必ず通るし」「まあ最近は開発環境でlocalhostを指定することあんまりありませんけど😆」「それな😆」「localhostを複数の案件で使っちゃうと、ブラウザ表示で他の案件のファビコンが表示されちゃったりしてやりづらいんですよ😇」「そうそう、スクショとか撮りづらくなる😆」「だからなんちゃら.lvh.meとかにして、他の案件のファビコンが出てこないようにしてます😆

「それ気になってました😳: localhostを指定してブラウザ表示すると、普段よく表示しているオレオレアプリのファビコンが他のアプリにも表示されちゃうことが多いのはなぜなんだろうって」「もともとブラウザはファビコンについてはかなりアグレッシブにキャッシュしちゃうんですよ: 本来はメタタグにファビコンパスの定義を書けるんですけど、そういう定義がなくてもブラウザが『ここにファビコンがあるはず』って勝手に取りに行っちゃう変な文化があります」「えぇ〜、そうだったんですか😅

参考: 正しいfaviconの設定方法を対応ブラウザ別にまとめる | ブログ | Glatch(グラッチ) - 夫婦で活動するフリーランスWeb制作ユニット
参考: Google Chromeでブックマークのfaviconがおかしいのを直す方法 | N★Typeブログ

「おすすめの対処法は、localhostの代わりに上で言ってるなんちゃら.lvh.meみたいに案件ごとにドメインを明示的に 分けることでしょうね🧐」「なるほど!」「IPアドレス直打ちとかすると、ごくまれにうまく行かなかったりすることもありますので、lvh.meみたいなループバックドメインのサブドメインにしておく方が余分な面倒を減らせます☺」「自社アプリだけ開発しているなら気にしなくていいんでしょうけど😆」「うちみたいな受託ソルジャーでは必要ですね😆

参考: 【Rails】ローカル環境の開発でサブドメインがある場合「localhost」ではなく「lvh.me」を使う - FujiYasuの日記

⚓RubyのIPアドレス処理

「そういえばRubyのデフォルトのIPアドレス機能って、CIDR構成ネットワークのinclusionチェックはできるけど人間が読めるstring形式にさっと戻せないとか、結構融通効かないんですよね😢」「たしかにイマイチ」「サイダー?」「IPアドレスに続けて/26とか書くアレですね☺」「任意の箇所でIPアドレスを区切るヤツ🧐」「ああ思い出しました😅」「Rubyのデフォルト機能には、サブネットマスクを取ってコネコネするみたいなのがないんですよね〜😢

参考: Classless Inter-Domain Routing - Wikipedia — CIDR
参考: class IPAddr (Ruby 2.7.0 リファレンスマニュアル)

「かといって今どきIPアドレス比較のビット演算処理を手作りしたくありませんし😇」「そんな生々しい処理😆」「現代の俺たちがやるべきことじゃない気がする😆

「いつだったかkamipoさんも『今の時代にIPアドレス処理するはめになるとは』みたいなことをつぶやいてましたね」「そうそう、ipaddressとかいうもっと高機能なgemを入れてました」「引数で渡されたものがサブネットの中にあるかどうかみたいなコードを今どき自分で書きたくない🤣」「同意🤣

以下のgemがそれかどうかわかりませんが参考までに。

⚓stringのアロケーションを削減

# actionpack/lib/abstract_controller/helpers.rb#L60
      def helper_method(*methods)
        methods.flatten!
        self._helper_methods += methods
        location = caller_locations(1, 1).first
        file, line = location.path, location.lineno

        methods.each do |method|
          _helpers.class_eval <<-ruby_eval, file, line
-           def #{method}(*args, &blk)                     # def current_user(*args, &blk)
-             controller.send(%(#{method}), *args, &blk)   #   controller.send(:current_user, *args, &blk)
-           end                                            # end
-           ruby2_keywords(%(#{method})) if respond_to?(:ruby2_keywords, true)
+           def #{method}(*args, &block)                    # def current_user(*args, &block)
+             controller.send(:'#{method}', *args, &block)  #   controller.send(:'current_user', *args, &block)
+           end                                             # end
+           ruby2_keywords(:'#{method}') if respond_to?(:ruby2_keywords, true)
          ruby_eval
        end
      end

つっつきボイス:「なるほど、stringをシンボルに変えたと」「ブロック変数も&blkから&blockに」

⚓Rails.envでStringInquirerのサブクラスを最適化して使うようにした

# activesupport/lib/active_support/core_ext/string/inquiry.rb#L3
require "active_support/string_inquirer"
+require "active_support/environment_inquirer"
# activesupport/lib/active_support/environment_inquirer.rb
module ActiveSupport
  # StringInquirerの特殊用途
  # 環境文字列に基づいてコンストラクション時にデフォルトの3つの環境を定義する
  class EnvironmentInquirer < StringInquirer
    DEFAULT_ENVIRONMENTS = ["development", "test", "production"]
    def initialize(env)
      super(env)

      DEFAULT_ENVIRONMENTS.each do |default_env|
        singleton_class.define_method(:"#{env}?", (env == default_env).method(:itself))
      end
    end
  end
end

つっつきボイス:「すとりんぐいんくわいあら〜?」「初めて見ました」「何をするヤツなんだろう?」「Active Supportなのか」

「ああなるほど: Rails.env.production?みたいなbooleanなカラムをチェックする述語メソッドを生やすヤツか」「なるほど」「StringInquirerからenvチェックを切り離してる」

「たぶんenvのdevelopmentとtestとproductionみたいによく使うものをいちいちmethod_missingで呼んでたら非効率だから、シングルトンメソッドをあらかじめ定義しちゃおうぜということかなと😋」「ははぁ、なるほど」「stagingみたいにデフォルトにないenvはmethod_missingで遅い呼び出しでもええやろと」「パフォーマンスも10倍ぐらい速くなってるぞと⚡」「method_missingの実装は重いからな〜🏋🏻‍♀️

参考: BasicObject#method_missing (Ruby 2.7.0 リファレンスマニュアル)

Warming up --------------------------------------
      StringInquirer   145.978k i/100ms
 EnvironmentInquirer   367.087k i/100ms
Calculating -------------------------------------
      StringInquirer      2.332M (±10.8%) i/s -     11.532M in   5.019755s
 EnvironmentInquirer     27.333M (± 3.4%) i/s -    136.556M in   5.001554s

⚓ドキュメント修正

つっつきボイス:「お、またindex_byウォッチ20200309)」「今度はAPIドキュメントが追加されてました」「ああなるほど☺」「修正後はindex_byとindex_withの説明が相補的になってる😋」「index_byはともかくindex_withの説明は欲しい👍」「サンプルとともに」

index_by: enumerableをハッシュに変換する。ブロックの結果をハッシュのキーとし、そのときのelementをハッシュの値とする。
index_with: enumerableをハッシュに変換する。そのときのelementをハッシュのキーとし、ブロックの結果をハッシュの値とする。
同PRより大意


つっつきボイス:「こちらはGetting Startedガイドの修正で、RailsInstallerの記述が削除されました」「RailsInstaller、懐かしい〜: 随分前に更新されなくなってますよねこれ」「更新後のドキュメントにもありますけど、今はWindowsだとRubyInstaller for Windows使って、さらにSQLiteもインストールする」「まさに先週した話ですね(ウォッチ20200310)」「つらい作業😭

⚓Rails

⚓ Simpacker: Webpackerのオルタナティブ

Webpackerというプロダクトは、Webpackのことを知らなくても簡単に使えるし、他にも便利な機能を提供しているところがよい。Webpackerのつらみは、WebpackをWebpacker固有のDSLやwebpacker.ymlでコンフィグしないといけない点だ。既にWebpackのコンフィグ方法を知っている人は、それをWebpackerのコンフィグに置き換える必要がある。自分はwebpack.config.jsを直接設定したいの!
Simpackerは、webpackのmanifest.json出力を参照してjavascript_pack_tag経由でscriptタグを作成する最小限の機能のみを提供する。その分Webpackの機能を知る必要があるが、Simpackerについてはほとんど何も知らなくてよい。
ただし、Webpackerにあるyarn統合やリクエスト編集のようなお便利機能はSimpackerでは利用できない。
同リポジトリより大意


つっつきボイス:「ああクックパッドさんのSimpacker」「こんなのあったんだ〜😳」「考えてみたら、WebpackerってRailsエンジニア側の発想ですし😆」「😆」「Webpackでやりたいフロントエンジニア側のことはあんまり考えてない感」「WebpackerではWebpackに触らせないんでしたっけ?」「というより、あくまでWebpackerからWebpackを制御するという感覚ですね☺: Webpackを直接触りたいフロントエンジニアはSimpackを使うと直接やれるようになるということらしい」「ふ〜む」

参考: Webpackerはもう要らない〜 Simpacker - Qiita

「ほほぅ、SimpackerではWebpackとwebpacker-cliは入るけど、webpacker-dev-serverは自分で入れないといけないのか」「webpacker-dev-serverは入ってないと結構つらそう」「このQiita記事を見た感じでは、普通にWebpackとReactを設定してる雰囲気👍」「使う機会があったらやってみてもいいかも😋

⚓N+1と戦うツール2点


つっつきボイス:「この間公開した翻訳記事↓でこの2つのgemが紹介されていたので」

Rails: Active Recordメソッドのパフォーマンス改善とN+1問題の克服(翻訳)

「activerecord-precounterはk0kubunさんのgemか」「これもk0kubunさんのactiverecord-precountより設計のきれいな、counter cacheのオルタナティブ」「わかる: Active Recordにパッチを当てるgemって結構コワイし👺

# 同リポジトリより
tweets = Tweet.all
ActiveRecord::Precounter.new(tweets).precount(:favorites)
tweets.each do |tweet|
  p tweet.favorites_count
end
# SELECT `tweets`.* FROM `tweets`
# SELECT COUNT(`favorites`.`tweet_id`), `favorites`.`tweet_id` FROM `favorites` WHERE `favorites`.`tweet_id` IN (1, 2, 3, 4, 5) GROUP BY `favorites`.`tweet_id`

「もうひとつのeager_group gemは集計関数を扱うヤツみたいです」

-- 同リポジトリより
SELECT "posts".* FROM "posts";
SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."status" = 'approved'
SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 2 AND "comments"."status" = 'approved'
SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = 3 AND "comments"."status" = 'approved'

-- ↓ 
SELECT "posts".* FROM "posts";
SELECT COUNT(*) AS count_all, post_id AS post_id FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3) AND "comments"."status" = 'approved' GROUP BY post_id;

「なるほど、1つのSQLに集約してくれるのか」「きれいに動いてくれてればいいけど、scopedとかでおかしなことになったりしないかな😅

# 同リポジトリより
class Post < ActiveRecord::Base
  has_many :comments

  define_eager_group :comments_average_rating, :comments, :average, :rating
  define_eager_group :approved_comments_count, :comments, :count, :*, -> { approved }
end

class Comment < ActiveRecord::Base
  belongs_to :post

  scope :approved, -> { where(status: 'approved') }
end

「こういうふうに↑define_eager_groupと書かなければ勝手に動き出さないということらしい↓: 自分はこういうときに生SQL書くことが多いけど、同じようなコードが何度も出てくるような状況ならこういうのを使ってもいいかな😋」「なるほど」「生SQLも似たようなものをいくつも書くことになるの多いですし😆」「たしかに😆」「使うならscopedでは避けたい気がしますね: joinしてscopedなものにこのgemをかけると変なことになったりして😆

「このgemの作者としては、こうやってオブジェクトの属性の1個になるように書けるのがいいということなんでしょうね☺」「たしかにシンプルですね😋」「ハッシュが登場してハッシュをループで回したりするのは気持ちよくないでしょうし」「それにQuery Objectにするにしても何個も似たようなQuery Objectができるのはあんまりうれしくないし」「こう書きたい気持ちはワカル」

⚓Rails 6に入るstrict_loading

「N+1といえば、count系とは違いますけどstrict_loadingっていうN+1を殺す場所を探すのに便利な機能がRailsでデフォルトで入るってこないだのウォッチに載ってましたね(ウォッチ20200302)」「おぉそういえば」「includesみたいなeager loadingメソッドをを叩かない状態でhas_manyリレーションにアクセスすると落ちてくれるので、N+1を絶対殺したいときに使える標準機能😇」「エグい😆: strict_loadingモードで調べられるってことなのね」「lazy loadさせないための機能」

strict_loadingって名前がイマイチな気がしますけど😆: force_eager_loadingとかすればいいのに」「たしかに😆」「#37400にいいねが多いのもわかる気がしました」「みんなN+1に苦しめられてますし😆」「strict_loading使うのはちょいめんどくさいですけど😆」「N+1が出るときはあっても実用上問題ないレベルなら使えるのがRailsのよさという考えもあるので、悩ましい😆」「まあ業務アプリではこういう機能がある方が確実にいいですね👍

⚓Hanami::APIは速度をフィーチャー(Ruby Weeklyより)


つっつきボイス:「この間取り上げようと思って漏れてしまいましたが、HanamiがAPIはじめました〼ということだそうです」「うん、HanamiがAPIサーバーの方向に向かうのは正しい気がしますね👍」「言われてみればAPIの方が向いてるかも?」「Railsは昔から完全にフルスタックな方向に向かっていますし、Railsと棲み分けるのであれば、Hanamiはビューやりませんぐらいのスタンスにしてみるのもありだと思います😋」「自分もHanamiがAPIに向かうのは全然ありだと思います😋

「ところでこのベンチマークのSinatraの遅さにビビったんですけど: Railsより遅いのかと😆」「😆」「ああ、下の方にルーティング10,000とか書いてますけど、Sinatraはルーティング増えるとてきめんに遅くなるんですよ😆」「そういえばそんな話ありましたね😆


同アナウンスより

「メモリーフットプリントはそこまで差は開いてませんね↓」


同アナウンスより

「そもそもルーティングが10,000もあるようなアプリって作りませんよね😆」「😆」「とりあえずこの比較はSinatraに不利すぎてうのみにできないけど、HanamiがAPIの高速化に専念するのは方向性として有望だと思いますし、何ならうちらで使ってみてもいいかも😋

⚓chaskiq: オープンソースのキャンペーンプラットフォーム(Ruby Weeklyより)


chaskiq.ioより

最近conversational marketingという言い方が流行ってるんでしょうか🤔


つっつきボイス:「IntercomやZendeskやDriftのオルタナティブか: Driftは聞いたことある気がするけど」「それっぽいキャンペーンサイトをRailsで立ち上げられる全部盛りキットみたいな感じでした」「ああ、そのまますぐ使えるようなヤツね: ノリとしてはRedmineみたいな感じかな☺」「このchaskiq.ioみたいなLP(ランディングページ)的なサイトが作れるんでしょうね」「だと思います」

「それがRailsである必要がどこまであるかというのは思いますけど😆」「😆」「まあWordPressみたいに、つよつよのエンジニアなしでもとりあえず立ち上げられるところまでやれるなら価値あるかも?」「どうだろう、Railsという時点で割とハードル高かったりして😆」「一応Dockerとか使ってるみたいだし、うまいことパッケージングしてすっと使えるようにしてるかも🤔」「Rails 6.0.2だから最新ですね」「金もリソースもこれからみたいなベンチャーや個人が週末までにRailsでサイト立ち上げたいみたいな用途に使えそうですね」「そういうときはスピードが一番貴重だったりしますし、ダッシュボード的なものも付いててやってます感アピールできますし、スタートアップにはそういうのが本当に大事☺」「やらずに済むことをやらずに済ませるというのが大事😋


前編は以上です。

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

週刊Railsウォッチ(20200310後編)Flutter+Firebaseでモバイルアプリ開発、命名7つの鉄則、Rubyバージョンを切り替えるchruby gemほか

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

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

Rails公式ニュース

Ruby Weekly

Viewing all 1381 articles
Browse latest View live