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

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

こんにちは、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


CONTACT

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