週刊Railsウォッチ(20190701)RMagickのメモリ使用量が劇的に改善、インスタンス変数の定義順で速度が変わる?、GitLab CIランナーをローカルで回すほか

こんにちは、hachi8833です。kazzさんのアバター画像が変わったことにお気づきでしょうか。

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

今回のつっつき録画に音声が入っていなかったので、今回は分割せず軽量版とします。申し訳ありません🙇

お知らせ: 第12回公開つっつき会(無料)

開始以来ついに1年目を迎える第12回目公開つっつき会は、今週7月4日(木)19:30〜にBPS会議スペースにて開催されます。引き続き皆さまのお気軽なご参加をお待ちしております🙇。

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

スキーマダンプのstringをintegerに変更

# activerecord/lib/active_record/migration.rb#L1072
    def get_all_versions
-     if schema_migration.table_exists?
+       schema_migration.all_versions
        schema_migration.all_versions.map(&:to_i)
      else
        []
      end
    end
...
    def load_migrated
-     @migrated_versions = Set.new(@schema_migration.all_versions)
+     @migrated_versions = Set.new(@schema_migration.all_versions.map(&:to_i))
    end
# activerecord/lib/active_record/schema_migration.rb#L47
      def all_versions
-       order(:version).pluck(:version).map(&:to_i)
+       order(:version).pluck(:version)
      end

ここは変更すべきと思われるが、既にRC版に入っているので6.0-stableではやらない方がいいかと思い、アプリ側の変更を必要としない変更だけに留めようとしてみた。
これは、すべてのスキーママイグレーションのバージョン番号をintegerとしてダンプしていた#36439の一部を元に戻す。この変更は振る舞いが変わらないので別段問題ではない。6.1で改めてこれを非推奨化するべき。
cc/ @rafaelfranca そちらから本変更を求められ、structureファイルが変更されている。大きな問題ではないので、GitHubの現在の更新はそのままでよいと思うが、この振る舞いの変更の方が心配なので、いったんロールバックして6.1で改めて非推奨化しようかと思う。
同PRより大意


つっつきボイス:「プルリクメッセージを見ると、これは修正すべきだけど6.0-stableじゃない方がいいかもとあったのが、Oracleアダプタやってるyahondaさんがこの修正欲しいと言って入ったようです」「to_iする方が長さが短くなってよさそうではあるけど、他に何か不都合があったのかも?🤔」

init_withのスキーマキャッシュをdeep_deduplicate

# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L37
      def init_with(coder)
-       @columns          = coder["columns"]
-       @columns_hash     = {}
-       @primary_keys     = coder["primary_keys"]
-       @data_sources     = coder["data_sources"]
-       @indexes          = coder["indexes"] || {}
-       @columns          = deep_deduplicate(coder["columns"])
+       @columns_hash     = @columns.transform_values { |columns| columns.index_by(&:name) }
+       @primary_keys     = deep_deduplicate(coder["primary_keys"])
+       @data_sources     = deep_deduplicate(coder["data_sources"])
+       @indexes          = deep_deduplicate(coder["indexes"] || {})
        @version          = coder["version"]
        @database_version = coder["database_version"]
      end

つっつきボイス:「deep_duplicate?」「いえ、deep_deduplicateですね」「ででゅぷりけーと!」「新しいみたいでググっても出てきません😢」


同コミットより

「GitHub上で例のコードジャンプ↑が効くぞ😋:なるほど」「-valueって何だっけ?」「stringをfreezeするショートハンドですね」「あ〜そうそう」

        def deep_deduplicate(value)
          case value
          when Hash
            value.transform_keys { |k| deep_deduplicate(k) }.transform_values { |v| deep_deduplicate(v) }
          when Array
            value.map { |i| deep_deduplicate(i) }
          when String, Deduplicable
            -value
          else
            value
          end
        end

参考: instance method String#-@ (Ruby 2.6.0)

self が freeze されている文字列の場合、self を返します。 freeze されていない場合は元の文字列の freeze された (できる限り既存の) 複製を返します。
docs.ruby-lang.orgより

robotstxt.orgのURLを修正

# guides/source/configuring.md#L1543
To block just specific pages, it's necessary to use a more complex syntax. Learn
- it on the [official documentation](http://www.robotstxt.org/robotstxt.html).
+ it on the [official documentation](https://www.robotstxt.org/robotstxt.html).

つっつきボイス:「ドキュメントの小さな修正ですが、robotstxt.orgっていうのがあるって初めて知ったので」「ほほ〜☺️」

参考: The Web Robots Pages


robotstxt.orgより

参考: 結局「robots.txt」ってなに?使う理由と基本の仕組みを解説|ferret [フェレット]

uniquenessでない場合にsave!の失敗がエラーにならない問題を修正

#35528の修正。
#35528は、親レコード1件を#save!するときに関連する子レコードがuniqueness制約に違反した場合にのみ発生する。これまでのところ、presenceやinclusionなどの他のバリデーションが失敗した場合はActiveRecord::RecordInvalidが(期待どおり)発生するが、uniquness制約が失敗した場合はActiveRecord::RecordInvalidが発生せず、単にnilが返されてしまっていた。
なお、トランザクションをサイレントにロールバックする機能はこの修正の影響を受けず、期待どおり動作する。
同PRより大意


つっつきボイス:「は〜こんなバグがあったとは😳」「トランザクションをサイレントにロールバックする機能というのがあるんですね」「この修正はテスト↓が重要な部分かな😋」

# activerecord/test/cases/autosave_association_test.rb#L1702
+ test "rollbacks whole transaction and raises ActiveRecord::RecordInvalid when associations fail to #save! due to uniqueness validation failure" do
+   author_count_before_save = Author.count
+   book_count_before_save = Book.count
+
+   assert_no_difference "Author.count" do
+     assert_no_difference "Book.count" do
+       exception = assert_raises(ActiveRecord::RecordInvalid) do
+         @author.save!
+       end
+
+       assert_equal("Validation failed: Published books is invalid", exception.message)
+     end
+   end
+
+   assert_equal(author_count_before_save, Author.count)
+   assert_equal(book_count_before_save, Book.count)
+ end
+
+ test "rollbacks whole transaction when associations fail to #save due to uniqueness validation failure" do
+   author_count_before_save = Author.count
+   book_count_before_save = Book.count
+
+   assert_no_difference "Author.count" do
+     assert_no_difference "Book.count" do
+       assert_nothing_raised do
+         result = @author.save
+
+         assert_not(result)
+       end
+     end
+   end
+
+   assert_equal(author_count_before_save, Author.count)
+   assert_equal(book_count_before_save, Book.count)
+ end

番外: elsif

# actionview/lib/action_view/template/resolver.rb#L272
      def extract_handler_and_format_and_variant(path)
        pieces = File.basename(path).split(".")
        pieces.shift
        extension = pieces.pop
        handler = Template.handler_for_extension(extension)
        format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
        format = if format
          Template::Types[format]&.ref
-       else
-         if handler.respond_to?(:default_format) # default_format can return nil
-           handler.default_format
-         else
-           nil
-         end
+       elsif handler.respond_to?(:default_format) # default_format can return nil
+         handler.default_format
        end

つっつきボイス:「以下のツイートで見つけたんですが、elseifが続いていたのをamatsudaさんがelsifに変えていました」「あ、たしかにこれはelsifの方が簡潔に書ける!」「うまい具合にnilも書かなくて済みますね」「コミットメッセージのタイトルが⛳なのはコードゴルフ?😆」

参考: コードゴルフ - Wikipedia

「まだオープン中ですが、それに関連して以下のRuboCop設定更新もリクエストされてました」「IfInsideElseは入れてもいい気がするし😋」「ちょい厳しいかもという声もありますね」

「でRuboCopの方では、これに絡んで後置のifをありにできるオプションをリクエストしてて、こちらはマージされてました❤️」

参考: Class: RuboCop::Cop::Style::IfInsideElse — Documentation for rubocop (0.71.0)

追記(2019/07/02)

情報ありがとうございます!🙇

Rails

active_record_in_cache gem


つっつきボイス:「神速さんの作ったgemですが、Rails.cacheを初めて知りました😅: これはビューのキャッシュとかとは違うんでしょうか?」「Rails.cacheはクエリをキャッシュしたりできるヤツだったかな」「ほんとだ↓」

参考: Rails のキャッシュ: 概要 - Rails ガイド

ビューのフラグメントをキャッシュするのではなく、特定の値やクエリ結果だけをキャッシュしたいことがあります。Railsのキャッシュメカニズムでは、どんな情報でもキャッシュに保存できます。
railsguides.jpより

参考: Rails.cacheについて | 酒と涙とRubyとRailsと

Ruby on Railsで特定の値やクエリ結果をキャッシュするしくみとしてRails.cacheを紹介します。
この機能を使うとや有効期限を設定したり、キャッシュ内容を圧縮できます。
morizyun.github.ioより

「日本語記事に『中身は3行』とあるけどgemのコアは本当に3行だけなんですね↓😳」「はは〜なるほど、メモリに乗れば次からはデータベースを叩かなくて済むと: 記事ではinclude ActiveRecordInCache::MethodsApplicationRecordでやってるけど、自分なら特定のモデルでピンポイントにやるかな〜」

# 同記事より
def in_cache(column = :updated_at, options = {}, &block)
  value = block_given? ? all.instance_exec(&block) : all.maximum(column)
  name = "#{all.to_sql}_#{value}"
  Rails.cache.fetch(name, options) { all.to_a }
end

RMagickのメモリ使用量改善


つっつきボイス:「@Watsonさんのこの記事スゴいなと思って」「お、これRubyKaigi 2019で発表してたヤツだし!」「そういえば見たって言ってましたね(私見られなかった😢)」「あれはとてもいい発表でした❤️」

「上の記事によると、RubyKaigiの時点でリークは修正されたけどGCがなかなか発動しなかったので今回はRMagickとImageMagickの両方に手を入れたそうです」「おお〜、以前のメモリ使用量は青で、修正後は赤↓、まるで違うし」「ものすごい改善ですね💪」「今までは鍋の底に大穴開いてた感😆」「お風呂の栓がちゃんとしまってなかったというか😆」


同記事より

「以前なりゆきでImageMagickとMiniMagick使ったけど、画像系はいろいろ大変でしたよ〜😅」「そういえば記事書いてましたね↓」

[Rails] MiniMagickでPDFのページ数を取得するときはフォントエラーに注意!

GitLab CIランナーをローカルで回す


つっつきボイス:「GitLab CIランナーをローカルで動かすという少し前の記事なんですけど、ローカルでCI回せるとうれしいものでしょうか?」「おお、そりゃもちろん😍: GitLabにpushした後さんざん待たされた末にエラーとかつらいし😭」「GitLab CIランナーをDocker化してローカルで動かすか、以下からランナーのバイナリを取ってきて動かせばやれるみたいです😋」

参考: docs/install/bleeding-edge.md · master · GitLab.org / gitlab-runner · GitLab

なお記事の最後に、ローカルだとジョブのキャッシュが保存できないらしく、毎回スクラッチで回るので注意とあります。それでもローカルだと速いとも。

GitLab自社運用のための注意点とノウハウ(2018/06版)

リードレプリカのテスト


つっつきボイス:「@kamipoさんの記事ですけど、これと似たようなことってある気がして↓」「こういうつらみ、ありますね〜😢: キャッシュが効いているかどうかのテストの難しさとか思い出しちゃう😇」

あたかもマスターで更新が起きたっぽいときにリードレプリカにも更新が伝搬しているかのように見せかけるため、Active Recordはテストのときデフォルトですべてのコネクションプールをマスターのコネクションプールにすり替えるということで対処している。コミットが起きないんだったら全部マスターに接続してテストすればいいじゃないってやつです。しかしこれをされるとマスターではなくリードレプリカからデータを読んでることをテストしたいときにめっちゃこまるという話である。
同記事より(強調は編集部)

Ruby

Sorbetについて


つっつきボイス:「Steep gemをやっているsoutaroさんのブログです」「ああ、サブタイピングの困難とかsigのオーバーライドの問題とか、いろいろわかりみあります」「Rubyの柔軟さを保ちながら型チェックでカバーするのって大変そう…」

以前RubyKaigi 2019のレポート記事↓にも書きましたが、Rubyの型チェックはLevel-1とLevel-2に分けて進められていて、SteepはSorbetやRDLと同じくLevel-2に該当するそうです。

キーワードで振り返るRubyKaigi 2019@博多(#1)

インスタンス変数のパフォーマンスを調べてみた(Hacklinesより)


tendelovemaking.comより


つっつきボイス:「@tenderloveさんの記事なんですけど、インスタンス変数の定義順序で実行速度が変わることがあるみたいです」「マジで?😆」

何らかの理由で、インスタンス変数を逆方向に定義する方が、インスタンス変数を順方向に定義するよりも高速です。この記事ではその理由について説明しますが、さしあたってパフォーマンスの高いコードが必要なら常にインスタンス変数を逆方向に定義してください(ジョークにつき実際にはやらないでね)。
同記事より大意(強調は編集部)

「アニメーションGIF↓まで作ってくれてます🙏」「何というカリカリチューニング: まあそもそもインスタンス変数ってこんなにゴロゴロ作るもんじゃないし😆」「やっぱり😆」「それにしてもtenderloveさんよくこんなの見つけたし😳」


同記事より

DB

私家版「SQLスタイルガイド」(DB Weeklyより)


つっつきボイス:「opinionatedとあるので独断と偏見のSQLスタイルガイドということだそうです」「ほほ〜☺️」

「しょっぱながいきなり『SQL句は小文字で書け』?」「え〜そうかな〜?😅」「『Shiftキー押し続けるのがつらいから』みたいです😆」

-- Good
select * from users

-- Bad
SELECT * FROM users

-- Bad
Select * From users

「他のは『行頭にカンマ置くな』とか割と初歩的なスタイルへの言及に終止してますね☺️」

「『引用符はシングルクォートを使え』?」「むむ、PostgreSQLだったか、シングルクォートとダブルクォートで意味がちょっと違ってた気がするナ🤔」

-- Good
select *
from users
where email = 'example@domain.com'

-- Bad
select *
from users
where email = "example@domain.com"

(ひとしきりググる)「そうそうこれ↓」「あ〜ほんとだ!😳」「むしろMySQLが例外だったとは…」

参考: えっ、まだPostgreSQLで「”」使ってるの? - Qiita

PostgreSQLなどの標準SQLでは、
* シングルクォーテーションで囲う:文字列定数として扱う
* ダブルクォーテーションで囲う:カラム名として扱う
という仕様になっている。
(中略)
しかしMySQLだけは独自の仕様を持っています。MySQLでは、
* シングルクォーテーション「’」で囲う:文字列定数として扱う
* ダブルクォーテーション「”」で囲う:文字列定数として扱う
* バッククォート「`」で囲う:カラム名として扱う
という仕様になっています。
Qiita記事より

「もうひとつ見つけましたけど↓、こっちではSQL標準でそうなってるとありますね😳」

参考: sql - What is the difference between single quotes and double quotes in PostgreSQL? - Stack Overflow

二重引用符はテーブル名やフィールド名に用いられるが、省略できることもある。一重引用符は文字列定数に用いる。これはSQL標準である。質問文のクエリを冗長に書くと次のような感じになる↓。
stackoverflow.comより大意

select * from "employee" where "employee_name"='elina';

後でSQL標準の該当箇所を探してみたのですが、うまく見つけられませんでした😇。

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

この頃人気のCSSプロパティなど


同サイトより

「アンケートベースのCSS調査結果サイトで、表示とかめちゃめちゃ凝ってます」「Bootstrapの知名度つえ〜😆」


2019.stateofcss.com/technologies/css-frameworks/より

「GridとFlexboxは知名度は同じぐらいだけど実際に使われてるのはFlexboxっぽい」


2019.stateofcss.com/features/layout/より

参考: 2019年、CSSのプロパティ・機能やツールについて使用状況や認知度を徹底調査 -The State of CSS 2019 | コリス

というわけで、以下の実測データサイトと比べてみるとまたよいと思います。なおFlorianとは京都在住のW3Cのメンバーのことです。

参考: Chrome Platform Status


chromestatus.com/metrics/css/popularityより

番外

ナノグラフェン


つっつきボイス:「グラフェンって炭素なので、将来は炭素でLSIを作れるようになるかも?」「夢ある〜🥰」「30年ぐらいかかりそうですけど😅」

参考: ナノグラフェン - Wikipedia
参考: “夢の物質” 炭素素材の製造技術の開発に成功 名古屋大学 | NHKニュース

以下の動画は上とは別のグラフェンナノリボンの紹介です。


今回は以上です。

バックナンバー(2019年度第2四半期)

週刊Railsウォッチ(20190625-2/2後編)「Webpack入門」は秀逸、「システム設計入門」、Envoy Mobile登場、Docker Desktop for WSL 2ほか

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

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

Rails公式ニュース

Hacklines

Hacklines

DB Weekly

db_weekly_banner

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

夏のTechRachoフェア2019

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ