Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

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

こんにちは、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@福岡) -- 延期

追記(2020/01/27): 今週1/30に開催予定だった福岡でのリモート公開つっつき会は、新型コロナウイルス対策のため延期となりました。🙇

上に先んじて、来週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パーサー?」

訂正(2020/01/21): rexmlが入ったのはbundled gemsの変更とのご指摘をいただきました🙇。
* Feature #16485: Make rexml, rss to the bundled gems - Ruby master - Ruby Issue Tracking System

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


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。