- 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年が推奨値になってるんですね😳」
「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
で効く、と」
サブパーティショニングと呼ばれる方法を使って、パーティションそれ自体をパーティションテーブルとして定義することができます。 パーティションには、他のパーティションとは別に独自のインデックス、制約、デフォルト値を定義できます。 インデックスは各パーティションで別々に作成されなければなりません。
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
⚓concerning
にprepend: true
を指定できるようになった
- commit: concerning learns how to prepend the concern · rails/rails@8543974
- commit: Concerns learn to be prepended · rails/rails@ba2bea5
# 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すべきと指示できれば有用なことがある。
concerning
にprepend: true
を指定することでこれが可能になった(デフォルトはfalse)。
@8543974より
つっつきボイス:「concerningでprependできるとうれしいことって何でしょう?」「そもそもレアケースではないかと😆」「kazzさんに話してみたら使いみちで考え込んでました」「prepend: false
が今までの挙動だったのはわかるけど、何が問題だったとかどう使いたいとかが具体的に書いてないし🤣」「このコミットたちをまとめるプルリクってないの?」「それがどうも見当たらなくて、コミットだけみたいです😅」
「concernってそもそもあんまりやりたくないし😆: 使うと何となくDRYになったような気がするけど、育ってくると読みにくさが半端なくなる😢」「例の定番記事↓でも言ってるヤツですね」「gemの形にでもなれば違うかもしれませんけど」
後で気づきましたが、以下のコミットにドキュメントが少し追加されていました。
ActiveSupport::Concern
でprepend
をサポート。
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)
- PR: Fix load_additional_types handling of large OIDs by rockymeza · Pull Request #38425 · rails/rails
「なので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_path
にPathname
が渡されていた場合に対応したそうです」「ああRubyのPathname
オブジェクトを渡すと動かなかったのか: そりゃ渡したいよね😆」
require 'pathname'
Pathname.new("foo/bar") # => #<Pathname:foo/bar>
Pathname.new("foo/bar").to_s # => "foo/bar"
⚓オプション引数退治は続く
- commit: Make message encryptor/verifier initializer takes keyword arguments · rails/rails@6708f3a
- commit: Make `translate` helper takes keyword arguments the same with `I18n.t… · rails/rails@6c02fee
- commit: Make `localize` helper takes keyword arguments the same with `I18n.lo… · rails/rails@b803ed0
- commit: `each` block cannot take keyword arguments · rails/rails@44cc2e7
- commit: Fix `with_options` to allow string key options · rails/rails@a55620f
つっつきボイス:「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
のかっこありなしの違いの話↓をちょっとだけ思い出しました。
⚓番外: 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より)
- リポジトリ: rocketjob/semantic_logger: Semantic Logger is a feature rich logging framework, and replacement for existing Ruby & Rails loggers.
- サイト: Semantic Logger for Ruby or Rails. Supports Graylog, Bugsnag, MongoDB, Splunk, Syslog, NewRelic.
# 同リポジトリより
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より
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からかな?」
- イベント: 技術書典8
2月29日(土)、3月1日(日) #技術書典 8開催について、お知らせを公開いたしました。こちらのお知らせの対象は、660以上のサークル出展者のみなさま、15社の協賛企業のみなさま、そして技術書との出会いを楽しみにして下さっている来場者のみなさまです。どうぞご確認ください。https://t.co/MXWhI4DPFm
— 技術書典公式アカウント (@techbookfest) February 15, 2020
「人混みと行列苦手なので、技術書典はマジでVR参加したい🤣」「例のOriHime↓にお願いできたらいいのに😆」「私も技術書典は行ってますけど人多くてうんざりです😆」「同じじゃないですか🤣」
- サイト: 分身ロボット「OriHime」
そういえばOriHimeクリエイターの芳藤さんはこの間NHKの『逆転人生』にも出演されてましたね。
本日は
吉藤オリィさんも
OriHimeで登場されます!
楽しみです✨#OriHime #エンターテインメント万博 https://t.co/kvdPUwMtWH
— エンターテインメント万博 (@entamexpo) February 15, 2020
追記(2020/02/19)
残念ながら今回は中止に...
【#技術書典 8 開催中止のお知らせ】
誠に残念ですが、先週末の新型コロナウイルス感染症に関わる状況の急激な変化を鑑み、2月29日(土)、3月1日(日) に予定しておりました技術書典8の中止、およびオンライン開催「技術書典 応援祭」への変更をお知らせいたします。https://t.co/MXWhI4DPFm— 技術書典公式アカウント (@techbookfest) February 17, 2020
⚓Railsの名前付けカンペ(Hacklinesより)
つっつきボイス:「おそらく自分用のチートシートというか、Railsに慣れてる人には今更かなと思いつつ」「ああRailsの命名コンベンション☺️: それ以前にRubyにも命名コンベンションありますし」
後で見つけた以下のGist↓はRubyの名前付けについても載っていますね。
- Gist: Alex's Rails Cheat Sheet
Stack OverflowでもRuboCopのRubyスタイルガイドしか触れられていませんでした。
参考: Ruby naming conventions? - Stack Overflow
⚓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は日本法人あるんですね😳」
If you're headed to #RSAC 2020 at Moscone, Feb 24-28, make sure you stop by the Sumo Logic booth #252 and chat about continuous intelligence! https://t.co/nYhwgZY7Nr @rsaconference pic.twitter.com/GJ96Zi4S8H
— Sumo Logic (@SumoLogic) February 16, 2020
参考: 日本に本格進出したSumo Logicに関する、知らない人は知らない意外な事実 - @IT
前編は以上です。
バックナンバー(2020年度第1四半期)
週刊Railsウォッチ(20200212後編)Rubyistが解説するUnicodeとUTF-8、Sorbetが速い理由、CSSの歴史、2019年の脆弱性まとめほか
- 20200210前編 Railsのベンチマークジェネレータ、長いバックグラウンドジョブと戦う、Timestamp切り詰めの謎、Open APIツールほか
- 20200204後編 Ruby3.0の他のbreaking change、Rubyのシリアライザ、GitHubのcode ownersほか
- 20200203前編 Railsの各種高速化コミット、OpenAPIの使い所、パンくずリストgem loaf、Railsビュー最適化ほか
- 20200128後編 もう一つのgemマネージャgel、”Did you mean”の仕組みを追う、DXOpalでブラウザゲームほか
- 20200127前編 Railsでキーワード引数warning退治始まる、ライブラリとフレームワークの違い、ShopifyのRails高速化記事ほか
- 20200121後編 RubyKaigi 2020受付開始、RubyGemsとBundlerの今後、ファイル同期ツールMutagenほか
- 20200120前編 福岡でも公開つっつき会、Railsのconnection_specification_nameでprimaryという名前が非推奨に、structure.sqlとschema.rbほか
- 20200115後編 Ruby 2.7関連情報、Bootstrap 5は今年前半リリースか、PostgreSQLでやってはいけないリストほか
- 20200114前編 config_forのbreaking change、Active Storage variantをDBでトラッキング、SprocketsとWebpackの違いほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp Slackなど)です。