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

今月の『WEB+DB PRESS』Vol.111「詳解Rails 6」は読んでおきたい!

$
0
0

訂正(2019/06/27): 冒頭のパラグラフを訂正いたしました🙇

こんにちは、hachi8833です。先週金曜日の銀座Rails#10の二次会の席でWEB+DB PRESS Vol.111でRails 6の特集があることを知りました。だからというわけではありませんが、遅まきながら思い出して昨日Kindle版で同誌を購入しました。

特集「詳解Rails 6」もくじ

  • 第1章:Rails 6が指し示す未来(松田 明)
    DHHの構想と,大規模サービスからの機能還元
  • 第2章:新しいコンポーネント(y-yagi)
    Active Storage,Action Text,Action Mailbox

  • 第3章:フロントエンド開発の変化(y-yagi)
    WebpackerによるJavaScriptエコシステムとの共存

  • 第4章:大規模サービス向け新機能(松田 明)
    複数データベース接続,テストの並列実行

  • 第5章:そのほかの主要な新機能(松田 明)(y-yagi)
    オートロードシステムの刷新,Active RecordとAction Viewの改善

  • 第6章:Rails 6実践チュートリアル(y-yagi)
    ステップバイステップで作って学ぶ新機能

読んでみて

「新機能の数々をコミッターが最速解説!」と表紙にあるとおり、Railsコミッターのa_matsudaさんとy-yagiさんがRails 6について6つの章を手分けして執筆しています。これまで週刊Railsウォッチなどで断片的にお伝えしていたさまざまな内容も改めて確認できましたし、詳しくは本誌でご覧頂きたいのですが、ウォッチで見逃してたこともいろいろ記載されています😅

特にa_matsudaさんの第1章「Rails 6が指し示す未来」は、Railsのこれまでの歴史をおさらいしつつ、Action TextやAction Mailboxといった新機能が導入されたいきさつなどがわかりやすく解説されています。RailsにおけるJavaScript(Webpack、Webpacker)の位置づけについてもいろいろ腑に落ちました。Rails 6のマニフェスト的なものを個人的に感じました。

なるようになるブログでおなじみのy-yagiさんも、JavaScriptとWebpackerに絡めたRailsのフロントエンド開発や現時点でのRails 6のチュートリアルなどについて解説しています。

Rails 6の単なる新機能紹介にとどまらず、おおっと思うような注意点なども盛り込まれており、来るべきRails 6の大きな流れを知るのにとても役立ちました。今回のWEB+DB PRESSは、@kamipoさんの一連のブログなどと合わせて今のうちに読んでおきたいですね。

参考: Rails 6.0の複数DBでリードレプリカのテストするのたぶん大変 - かみぽわーる

ネタバレにならないように書くの、大変…。


つか実は以下の箇所がどこなのか気になって買ったのでした。すみませんすみません。


なお、同誌冒頭の@kazuhoさん(H2Oの作者)インタビューがいきなり面白くて先にそっちを読んでしまいました。よくみるとインタビュアーは@mizchiさんでした。

「Julia特集」の方も、Red Data Toolsなどでおなじみの@mrknさんががっつりJuliaを解説してました。Jupyter NotebookでJulia動くとか、Juliaでメタプロできるって知らんかった…

そして@_ko1さんの連載「Rubyのウラガワ」第2回はMRIのGCの話でした。また買おうっと😋

関連記事

Rails 6のB面に隠れている地味にうれしい機能たち(翻訳)

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


2019年前半の「JavaScriptをちゃんとやるための地図」

$
0
0

こんにちは、hachi8833です。BPS社内勉強会の発表を元に、社内JavaScript勢の皆さまのお力を得て記事を書きました。

目的

JavaScriptをたまにしか使わない私ですが、それもあってなおさら書くときに迷いに迷います。

  • ネットの情報多すぎ、動き激しすぎ、選り分けるにしても指針が欲しい
  • 古いコードや毒入りコードを拾って食あたりや、泥舟に乗って沈没するのを少しでも避けたい
  • 最初の一歩をどこから踏み出すかの手がかりが欲しい

そこで、2019年前半と区切って、一度JavaScript世界について門外漢ならではの「大きな絵」が欲しかったのでした。ここに書いたことの中には来年には古くなっているものあるかもしれません。

ただ、社内のJS勢の話を総合すると、JavaScript世界は数年前に比べるとかなり落ち着きつつあるとのことです。この機会にということで、主に自分のために現在のJavaScript世界で遭難しないための地図を作りました。そのうえで、こういうもっとごつい本↓あたりに進んでみようと思っています。

対象

本記事は以下の読者を念頭に置いています。プログラミング自体が初めての方orそれに近い方は対象に含めていません🙇

  • 他の言語をある程度やってるがES2015以降は初めての人
  • ES2015以前にJavaScriptをやってたが再入門したい人
  • 最近のJavaScriptの動向を大づかみしたい人

最初に押さえておきたいもの

2019年6月現在、最初に読むのにふさわしいと思える良質なオンライン記事/ドキュメント/書籍を以下に絞り込みました。今後何をするにしても、これらを読んでからにしたいと思います。

1. JavaScript界隈を大づかみする地図


同記事より

mizchiさんがTypeScriptを始めるうえでの心得について書いた記事です。網羅的ではありませんが、要点を的確に押さえていて、急いでいる方が最初に読むのにとてもよいと思います。


こちらの書籍は勉強会の後に知りました。本記事でまとめようとしていたJavaScriptの概要や歴史や動向、開発の要点などを遥かに上のレベルで見事に網羅していて脱帽しました。特定のトランスパイラやフレームワークに偏らないようなるべく中立に書かれているように見える点にも好感を持ちました。何より文章の質がとても高いと思います❤。Ruby on Railsにも言及しています。

JavaScriptの文法入門書ではありませんが、最初に読んでおくと自分の中に「大きな地図」を作るうえでとても役立つと思います。少なくとも今の自分には助かりました。

初版は2017/08/11ですが、私が買ったKindle版は2019/04/05にVer.1.1になっており、現時点の最新の動向も反映されています。

2. オンライン入門書


jsprimer.netより

js-primerは、azuさんを中心に今も書き直されている、ES2015以降を対象としてJavaScriptというプログラミングを1から学べる再入門書です。同書が何を目的とし、何を目的としていないかについてははじめににも書かれていますので、そちらをご覧ください。

3. Webpack入門


knowledge.sakura.ad.jpより

週刊Railsウォッチでもご紹介しましたが、「Webpackとは何か」を含めて詳細に解説した、さくらインターネットの中の人の力作Webpack入門です。

4. リファレンス

上を読んだら、MozillaのJavaScriptドキュメントを一次情報として裏付けを取りながら、ググるなり本を買うなりするとよいと思います。

JavaScriptを書くときに最小限押さえておきたい点

きりがありませんので相当絞り込んであります。

1. 'use strict';を冒頭に必ず書く

コードの冒頭に'use strict';と書くことで、ES2015より前の非推奨の書き方をエラーにするなどの制約がかかり、よくない書き方を避けられます。

参考: Strict モード - JavaScript | MDN

なお少々細かくなりますが、JavaScriptのモジュールでは、以下のように必ずstrictモードになるので書かなくても大丈夫だそうです。

  • モジュール対応インタプリタはstrictモードで動く
  • モジュールバンドラを使う場合は適宜'use strict';が挿入される

2. varは使わないこと

基本はconstで変更できなくしておきます。変更が必要な場合はletを使います。

varは一切使わなくてもコードは書けますし、varには望ましくない挙動があるので使うべきではありません。

ネットで検索するときにも、varを使っているサンプルコードはそれだけで古いと判断できますので、選別の手がかりにできます。

3. 早いうちに非同期処理をPromiseとasync/awaitで書けるようにする

Promiseとasync/awaitはわかりにくいのですが、非同期処理は重要な概念でもありますし、いずれ多用することになりますので、早いうちに身体を慣れさせておくのがよいと思います。

最後に

実際の勉強会ではReactやNode.jsなどもっと手広く扱ったのですが、上述の『最新JavaScript開発』のまとめがあまりにも良すぎるので、本記事では割愛いたしました。

関連記事

JavaScript: 5分でわかるPromiseの基礎(翻訳)

JavaScriptスタイルガイド 1〜8: 型、参照、オブジェクト、配列、関数ほか (翻訳)

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

$
0
0

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

Rails: 高速リアルタイム検索API「algolia-search-rails」gem README(翻訳)

$
0
0

概要

MITライセンスに基づいて翻訳・公開いたします。


algolia.comより

Rails: algolia-search-rails gem README(翻訳)

Algolia Searchは、最初のキーストロークを入力した時点でリアルタイムで結果を返せる、ホスト型検索エンジンです。

このgemはalgoliasearch-client-rubyを元に作られたもので、Algolia Search APIを自分好みのORMに簡単に統合できます。

Rails 3.x、4.x、5.xはすべてサポート対象です。

algoliasearch-rails-exampleサイトで、autocomplete.jsベースのオートコンプリート機能やInstantSearch.jsベースのインスタント検索結果ページをご覧いただけますので、ご興味がありましたらどうぞ。

API ドキュメント

完全なリファレンスはAlgoliaのWebサイトで参照いただけます。

訳注: 目次は省略しました

セットアップ

インストール方法

gem install algoliasearch-rails

Gemfileに以下を追加します。

gem "algoliasearch-rails"

続いて以下を実行します。

bundle install

設定

config/initializers/algoliasearch.rbファイルを作成し、APPLICATION_IDAPI_KEYをセットアップします。

AlgoliaSearch.configuration = { application_id: 'YourApplicationID', api_key: 'YourAPIKey' }

このgemは、ActiveRecordMongoidSequelと互換性があります。

タイムアウト

初期化時に以下のオプションを設定することで、さまざまなタイムアウトスレッショルドを設定できます。

AlgoliaSearch.configuration = {
  application_id: 'YourApplicationID',
  api_key: 'YourAPIKey',
  connect_timeout: 2,
  receive_timeout: 30,
  send_timeout: 30,
  batch_timeout: 120,
  search_timeout: 5
}

注意

このgemでは、インデックス作成タスクのトリガーにRailsのコールバックを多用しています。after_validationbefore_saveafter_commitといったコールバックをバイパスするメソッドが使われていると、変更がインデックスに反映されません。たとえば、update_attributeメソッドはバリデーションチェックを行いません。アップデート時にバリデーションを行うには、update_attributesをお使いください。

AlgoliaSearchモジュールによって注入されるメソッド名の冒頭にはすべてalgolia_が追加され、それらに関連する短いエイリアス名も追加されます(定義されていない場合)。

Contact.algolia_reindex! # <=> Contact.reindex!

Contact.algolia_search("jon doe") # <=> Contact.search("jon doe")

利用法

インデックスのスキーマ

以下のコードは、Contactインデックスを作成してContactモデルに検索機能を追加します。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    attribute :first_name, :last_name, :email
  end
end

送信する属性を指定する(ここでは:first_name:last_name:emailに限定します)ことも、指定しない(この場合すべての属性が送信される)こともできます。

class Product < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    # すべての属性が送信される
  end
end

add_attributeメソッドを用いて、モデルのすべての属性に加えて別の属性を送信することもできます。

class Product < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    # すべての属性の他にextra_attrも送信される
    add_attribute :extra_attr
  end

  def extra_attr
    "extra_val"
  end
end

関連性の高さ

私たちの提供する設定では、インデックス全体の関連性の高さ(relevancy)をチューニングするさまざまな方法が使えます。その中でも最も重要性が高いのは、「検索可能な属性(searchable attributes)」と、「レコードの人気(record popularity)」を反映するいくつかの属性です。

class Product < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    # Algoliaレコードのビルドに使う属性のリスト
    attributes :title, :subtitle, :description, :likes_count, :seller_name

    # 検索したい属性を`searchableAttributes`設定で定義する
    # (旧attributesToIndex)(ここでは`title`、`subtitle`、`description`)。
    # 重要性の高い順にリストアップする必要がある。
    # `description`に`unordered`とタグ付けすることでその属性のマッチ位置への影響を回避している。
    searchableAttributes ['title', 'subtitle', 'unordered(description)']

    # `customRanking`設定はランキングの基準(criteria)を定義するもので、
    # 2つのレコードのテキスト関連性が等しいかどうかを比較するのに用いられる。
    # これはそのレコードの人気(popularity)を反映する。
    customRanking ['desc(likes_count)']
  end

end

インデックス化

特定のモデルをインデックス化するには、そのクラスで単にreindexを呼び出します。

Product.reindex

すべてのモデルをインデックス化する場合は以下のようにします。

Rails.application.eager_load! # 全モデルが読み込み済みであること(development環境では必須)

algolia_models = ActiveRecord::Base.descendants.select{ |model| model.respond_to?(:reindex) }

algolia_models.each(&:reindex)

フロントエンド検索(リアルタイムエクスペリエンス)

従来の検索ロジックや機能は、バックエンドで実装される傾向がありました。この方法は、ユーザーが検索クエリを手入力して検索を実行し、結果ページにリダイレクトするという検索エクスペリエンスであれば事足りました。

検索をバックエンドで実装する必然性はもはやありません。現実には、ほとんどの場合ネットワークの遅延や処理の遅延が重なってパフォーマンスが悪化します。そこで、私たちが開発したJavaScript API Clientの利用を強くおすすめします。あらゆる検索リクエストをユーザーのブラウザやスマートフォンやクライアントから直接発行することで、トータルの検索遅延を削減しつつ、サーバーの負荷も同時に軽減します。

私たちのJS APIクライアントはgemに組み込まれているので、JavaScriptマニフェストの手頃な場所(Rails 3.1以降ならapplication.jsなど)でalgolia/v3/algoliasearch.minrequireするだけで準備できます。

//= require algolia/v3/algoliasearch.min

あとは以下のようなJavaScriptコードでできます。

var client = algoliasearch(ApplicationID, Search-Only-API-Key);
var index = client.initIndex('YourIndexName');
index.search('something', { hitsPerPage: 10, page: 0 })
  .then(function searchDone(content) {
    console.log(content)
  })
  .catch(function searchFailure(err) {
    console.error(err);
  });

先ごろ(2015年3月)JavaScriptクライアントの新しいバージョン(V3)をリリースしました。V2をお使いの方は移行ガイドをお読みください

バックエンド検索

注意: クエリをサーバーから送信せずにエンドユーザーのブラウザから直接クエリ送信するのであれば、JavaScript API Clientを使うことをおすすめします。

1件の検索はORMに沿ったオブジェクトを返しますが、そのときにデータベースからの再読み込みが発生します。トータルの遅延とサーバーの負荷を削減するためにも、クエリ実行はJavaScript API Clientで行うことをおすすめします。

hits =  Contact.search("jon doe")
p hits
p hits.raw_answer # 元の生JSON answerを取得する

各ORMオブジェクトにはhighlight_result属性が1つずつ追加されます。

hits[0].highlight_result['first_name']['value']

データベースからのオブジェクト再読み込みを行わずにAPIから生JSON answerを取り出したい場合は、次の方法が使えます。

json_answer = Contact.raw_search("jon doe")
p json_answer
p json_answer['hits']
p json_answer['facets']

検索パラメータは、インデックス設定から静的に指定することも、または検索時にsearchメソッドの第2引数でsearch parametersを動的に指定することもできます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    attribute :first_name, :last_name, :email

    # インデックス設定に保存されているデフォルトの検索パラメータ
    minWordSizefor1Typo 4
    minWordSizefor2Typos 8
    hitsPerPage 42
  end
end
# 動的な検索パラメータ
p Contact.raw_search('jon doe', { hitsPerPage: 5, page: 2 })

バックエンドのページネーション

私たちは、あらゆる検索の実行(すなわちページネーションも)をフロントエンドのJavaScriptで行うことを強くおすすめしていますが、ページネーションのバックエンドとしてwill_paginatekaminariもサポートします。

:will_paginateを用いる場合は以下のように:pagination_backendで指定します。

AlgoliaSearch.configuration = { application_id: 'YourApplicationID', api_key: 'YourAPIKey', pagination_backend: :will_paginate }

これで、searchメソッドを呼び出せばたちどころにページネーションされた結果が表示されます。

# コントローラ
@results = MyModel.search('foo', hitsPerPage: 10)

# ビュー(will_paginateを使う場合)
<%= will_paginate @results %>

# ビュー(kaminariを使う場合)
<%= paginate @results %>

タグ付け

tagsメソッドで以下のようにレコードにタグを追加できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    tags ['trusted']
  end
end

以下のように動的な値も使えます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    tags do
      [first_name.blank? || last_name.blank? ? 'partial' : 'full', has_valid_email? ? 'valid_email' : 'invalid_email']
    end
  end
end

結果セットを特定のタグで絞り込むには、クエリ発行時に{ tagFilters: 'tagvalue' }または{ tagFilters: ['tagvalue1', 'tagvalue2'] }を検索パラメータとして指定します。

ファセット

検索結果でさらにfacetsメソッドを呼ぶことで、ファセットを取得できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    # [...]

    # ファセットで使える属性のリストを指定する
    attributesForFaceting [:company, :zip_code]
  end
end
hits = Contact.search('jon doe', { facets: '*' })
p hits                    # ORM-compliant array of objects
p hits.facets             # extra method added to retrieve facets
p hits.facets['company']  # facet values+count of facet 'company'
p hits.facets['zip_code'] # facet values+count of facet 'zip_code'
raw_json = Contact.raw_search('jon doe', { facets: '*' })
p raw_json['facets']

ファセットの検索

以下のようにファセットの値も検索できます。

Product.search_for_facet_values('category', 'Headphones') # {value, highlighted, count}の配列

このメソッドには、クエリで使える任意のパラメータを渡せます。これによって、そのクエリにマッチしそうな結果だけを返すように調整できます。

# 「red Apple products」(およびそれらの個数のみ)を含むカテゴリだけを返す
Product.search_for_facet_values('category', 'phone', {
  query: 'red',
  filters: 'brand:Apple'
}) # 「red Apple products」にリンクするphoneカテゴリの配列

グループ化(group by)

グループ化をdistinctに行う方法について詳しくはこちらをご覧ください。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    # [...]

    # レコードをグループ化する属性を指定する
    # (ここではcompanyでレコードをグループ化する)
    attributeForDistinct "company"
  end
end

地理的な検索(geo-search)

レコードの地理上の位置で絞り込むにはgeolocメソッドを使います。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    geoloc :lat_attr, :lng_attr
  end
end

結果セットをSan Joseの周囲50km以内に絞り込むには、クエリ発行時に{ aroundLatLng: "37.33, -121.89", aroundRadius: 50000 }を検索パラメータとして指定します。

オプション

自動インデックスと非同期実行

インデックスは、レコードが1件保存されるたびに「非同期的に」反映され、レコードが1件削除(destroy)されるたびにインデックスから「非同期に」削除されます。具体的には、ADDやDELETEを伴うネットワーク呼び出しは同期的にAlgolia APIに送信されますが、Algoliaのエンジンでの処理は非同期的に行われます。つまり、直後だと結果が反映されない可能性があります。

自動インデックスやインデックスからの自動削除の設定は、以下のオプションで無効にできます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch auto_index: false, auto_remove: false do
    attribute :first_name, :last_name, :email
  end
end

自動インデックスを一時的に無効にする

自動インデックスは、without_auto_indexスコープで一時的に無効にできます。これはパフォーマンス上の理由でよく使われます。

Contact.delete_all
Contact.without_auto_index do
  1.upto(10000) { Contact.create! attributes } # このブロック内では自動インデックスが動かない
end
Contact.reindex! # バッチ操作を用いる

キューとバックグラウンドジョブ

自動インデックスや自動削除の処理を設定することで、キューを用いてこれらの処理をバックグラウンド実行できます。デフォルトではActive Job(Rails 4.2以降)のキューが用いられますが、独自のキューイングメカニズムを定義することもできます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch enqueue: true do # ActiveJobは`algoliasearch`キューでトリガされる
    attribute :first_name, :last_name, :email
  end
end

考慮すべき点

更新や削除をバックグラウンドで行う場合、ジョブの実際の実行時より前のタイミングでデータベースにレコードの削除がコミットされる可能性があります。万一、レコードを削除するためにレコードをデータベースから読み込むと、ActiveRecord#findがRecordNotFoundで失敗します。

このような場合は、ActiveRecordからのレコード読み込みをバイパスしてインデックスを直接操作する方法があります。

class MySidekiqWorker
  def perform(id, remove)
    if remove
      # レコードがデータベースから削除された可能性があれば
      # ActiveRecord#findで読み込めない
      index = Algolia::Index.new("index_name")
      index.delete_object(id)
    else
      # レコードは存在するはず
      c = Contact.find(id)
      c.index!
    end
  end
end

Sidekiqの場合

Sidekiqの場合は次のようにします。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch enqueue: :trigger_sidekiq_worker do
    attribute :first_name, :last_name, :email
  end

  def self.trigger_sidekiq_worker(record, remove)
    MySidekiqWorker.perform_async(record.id, remove)
  end
end

class MySidekiqWorker
  def perform(id, remove)
    if remove
      # レコードがデータベースから削除された可能性があるので
      # ActiveRecord#findで読み込めない
      index = Algolia::Index.new("index_name")
      index.delete_object(id)
    else
      # レコードは存在するはず
      c = Contact.find(id)
      c.index!
    end
  end
end

DelayedJobの場合

delayed_jobの場合は次のようにします。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch enqueue: :trigger_delayed_job do
    attribute :first_name, :last_name, :email
  end

  def self.trigger_delayed_job(record, remove)
    if remove
      record.delay.remove_from_index!
    else
      record.delay.index!
    end
  end
end

同期処理とテストについて

次のオプションを設定することで、インデックス化とインデックスからの削除を同期的に行うよう強制できます(この場合、gemはwait_taskメソッドを呼ぶことで、メソッドから戻ったときにこの操作に対応します)。ただし、この操作は非推奨です(テスト目的を除く)。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch synchronous: true do
    attribute :first_name, :last_name, :email
  end
end

インデックス名をカスタマイズする

デフォルトではクラス名がインデックス名に使われます(「Contact」など)。index_nameオプションでインデックス名をカスタマイズできます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch index_name: "MyCustomName" do
    attribute :first_name, :last_name, :email
  end
end

インデックス名に環境を追加する

以下のオプションを用いて、Railsの現在の環境をインデックス名の末尾に追加できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch per_environment: true do # インデックス名は"Contact_#{Rails.env}"となる
    attribute :first_name, :last_name, :email
  end
end

属性定義のカスタマイズ

複雑な属性値をブロックで指定できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    attribute :email
    attribute :full_name do
      "#{first_name} #{last_name}"
    end
    add_attribute :full_name2
  end

  def full_name2
    "#{first_name} #{last_name}"
  end
end

注意: この種のコードを用いて属性を追加で定義すると、その直後から属性の変更をこのgemで検出不可能になってしまいます(このgemではRailsの#{attribute}_changed?メソッドで変更を検出しています)。その結果、レコードの属性が変更されていない場合にもレコードがAPIにプッシュされます。次のように_changed?メソッドを作成することでこの振る舞いを回避できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch do
    attribute :email
    attribute :full_name do
      "#{first_name} #{last_name}"
    end
  end

  def full_name_changed?
    first_name_changed? || last_name_changed?
  end
end

ネステッドオブジェクトやネステッドリレーションについて

リレーションシップの定義

追加の属性を定義し、JSONに沿った任意のオブジェクト(配列、ハッシュ、配列とハッシュの組み合わせのいずれか)を返すネステッドオブジェクトを簡単に埋め込むことができます。

class Profile < ActiveRecord::Base
  include AlgoliaSearch

  belongs_to :user
  has_many :specializations

  algoliasearch do
    attribute :user do
      # ネステッド"user"オブジェクトを`name` + `email`に制限
      { name: user.name, email: user.email }
    end
    attribute :public_specializations do
      # public specializationの配列をビルド(`title`と`another_attr`のみを含む)
      specializations.select { |s| s.public? }.map do |s|
        { title: s.title, another_attr: s.another_attr }
      end
    end
  end

end

ネステッドな子オブジェクトの変更を反映させる

Active Recordの場合

Active Recordでは、touchafter_touchで行います。

# app/models/app.rb
class App < ApplicationRecord
  include AlgoliaSearch

  belongs_to :author, class_name: :User
  after_touch :index!

  algoliasearch do
    attribute :title
    attribute :author do
      author.as_json
    end
  end
end

# app/models/user.rb
class User < ApplicationRecord
  # belongs_to関連付けを使う場合は
  # - `touch: true`を使うこと
  # - `after_save`フックは定義しないこと
  has_many :apps, foreign_key: :author_id

  after_save { apps.each(&:touch) }
end
Sequelの場合

Sequelではtouchプラグインで変更を反映できます。

# app/models/app.rb
class App < Sequel::Model
  include AlgoliaSearch

  many_to_one :author, class: :User

  plugin :timestamps
  plugin :touch

  algoliasearch do
    attribute :title
    attribute :author do
      author.to_hash
    end
  end
end

# app/models/user.rb
class User < Sequel::Model
  one_to_many :apps, key: :author_id

  plugin :timestamps
  # この関連付けは利用不可(これはafter_saveをトリガしない)
  plugin :touch

  # ここでtouchされる必要のある関連付けを定義する
  # 効率はよくないが、after_saveをトリガできるようになる
  def touch_associations
    apps.map(&:touch)
  end

  def touch
    super
    touch_associations
  end
end

カスタムobjectID

objectIDは、デフォルトではそのレコードのidに基づきます。:idオプションを指定すればこの振る舞いを変更できます(ただしuniqフィールドを使うこと)。

class UniqUser < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch id: :uniq_name do
  end
end

制約でデータのサブセットのみをインデックス化する

:ifオプションや:unlessオプションを用いて、レコードのインデックス化に制約を追加できます。

これによって、条件付きインデックス化や、ドキュメントごとのインデックス化ができるようになります。

class Post < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch if: :published?, unless: :deleted? do
  end

  def published?
    # [...]
  end

  def deleted?
    # [...]
  end
end

注意: これらの制約を使うと、インデックスをデータベースと同期するために直ちにaddObjects呼び出しやdeleteObjects呼び出しが実行されるようになります。その場合、ステートレスなgemからはオブジェクトが制約とマッチするかどうかを認識できなくなるか、一切マッチしなくなるので、私たちはADD操作やDELETE操作を送信するよう強制しています。_changed?メソッドを作成することでこの振る舞いを変更できます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch if: :published do
  end

  def published
    # trueかfalseを返す
  end

  def published_changed?
    # 「published」ステートが変更された場合にのみtrueを返す
  end
end

以下のいずれかの方法で、レコードのサブセットをインデックス化できます。

# will generate batch API calls (recommended)
MyModel.where('updated_at > ?', 10.minutes.ago).reindex!
MyModel.index_objects MyModel.limit(5)

サニタイザ

sanitizeオプションで属性をすべてサニタイズできます。属性に含まれるHTMLタグはすべて取り除かれます。

class User < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch per_environment: true, sanitize: true do
    attributes :name, :email, :company
  end
end

Rails 4.2以降をご利用の場合は、rails-html-sanitizerへの依存も必要です。

gem 'rails-html-sanitizer'

UTF-8エンコーディング

force_utf8_encodingオプションで属性をすべて強制的にUTF-8エンコーディングにできます。

class User < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch force_utf8_encoding: true do
    attributes :name, :email, :company
  end
end

注意: このオプションはRuby 1.8と互換性がありません。

例外処理

raise_on_failureオプションで、Algolia APIへのアクセスを試行中にraiseされる可能性のある例外を無効にできます。

class Contact < ActiveRecord::Base
  include AlgoliaSearch

  # development環境でのみ例外をraiseする
  algoliasearch raise_on_failure: Rails.env.development? do
    attribute :first_name, :last_name, :email
  end
end

設定例

以下は、実際に使われている設定例です(HN Searchより)。

class Item < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch per_environment: true do
    # the list of attributes sent to Algolia's API
    attribute :created_at, :title, :url, :author, :points, :story_text, :comment_text, :author, :num_comments, :story_id, :story_title

    # integer version of the created_at datetime field, to use numerical filtering
    attribute :created_at_i do
      created_at.to_i
    end

    # `title` is more important than `{story,comment}_text`, `{story,comment}_text` more than `url`, `url` more than `author`
    # btw, do not take into account position in most fields to avoid first word match boost
    searchableAttributes ['unordered(title)', 'unordered(story_text)', 'unordered(comment_text)', 'unordered(url)', 'author']

    # tags used for filtering
    tags do
      [item_type, "author_#{author}", "story_#{story_id}"]
    end

    # use associated number of HN points to sort results (last sort criteria)
    customRanking ['desc(points)', 'desc(num_comments)']

    # google+, $1.5M raises, C#: we love you
    separatorsToIndex '+#$'
  end

  def story_text
    item_type_cd != Item.comment ? text : nil
  end

  def story_title
    comment? && story ? story.title : nil
  end

  def story_url
    comment? && story ? story.url : nil
  end

  def comment_text
    comment? ? text : nil
  end

  def comment?
    item_type_cd == Item.comment
  end

  # [...]
end

インデックス

手動でのインデックス化

index!インスタンスメソッドでインデクス化をトリガできます。

c = Contact.create!(params[:contact])
c.index!

インデックスからの手動削除

remove_from_index!インスタンスメソッドでインデックスからの削除をトリガできます。

c.remove_from_index!
c.destroy

再インデックス化

このgemでは、全オブジェクトの再インデックス化方法を2とおり提供しています。

アトミックな再インデックス化

reindexクラスメソッドは、該当の全オブジェクトを<INDEX_NAME>.tmpという一時インデックスを作成してから、この一時インデックスを(アトミックにインデックス化完了した)最終インデックスに移動することによって、(削除済みオブジェクトも考慮に入れて)全レコードを再インデックス化します。これは、全コンテンツを再インデックス化する最も安全な方法です。

Contact.reindex

注意: インデックス固有のAPIキーを利用している場合は、<INDEX_NAME><INDEX_NAME>.tmpの両方を許可してください。

警告: このようなアトミックな再インデックス化は、モデルのスコープやフィルタがかかっている状態で行うべきではありません。理由は、この操作によってインデックス全体が置き換わり、フィルタされたオブジェクトだけが残ってしまうためです。例: MyModel.where(...).reindexではなくMyModel.where(...).reindex!とすること(末尾の!は必ず付けること!!!)。

正規の再インデックス化

対象の全オブジェクトを(一時インデックスを使わず、除外されたオブジェクトを削除することもなく)インプレースで再インデックス化するには、reindex!クラスメソッドを使います。

Contact.reindex!

インデックスをクリアする

インデックスをクリアするには、clear_index!クラスメソッドを使います。

Contact.clear_index!

背後のインデックスにアクセスする

indexクラスメソッドを呼び出すことで、背後のindexオブジェクトにアクセスできます。

index = Contact.index
# index.get_settings, index.partial_update_object, ...

primary/replica

add_replicaメソッドを使ってreplicaインデックスを定義できます。primary設定をreplicaで継承したい場合はreplicaのブロックでinherit: trueをお使いください。

class Book < ActiveRecord::Base
  attr_protected

  include AlgoliaSearch

  algoliasearch per_environment: true do
    searchableAttributes [:name, :author, :editor]

    # `author`のみで検索する目的でreplicaインデックスを定義する
    add_replica 'Book_by_author', per_environment: true do
      searchableAttributes [:author]
    end

    # 他はメインブロックと同じで並び順だけカスタマイズした
    # replicaインデックスを定義する
    add_replica 'Book_custom_order', inherit: true, per_environment: true do
      customRanking ['asc(rank)']
    end
  end

end

replicaで検索するには以下のコードを使います。

Book.raw_search 'foo bar', replica: 'Book_by_editor'
# または
Book.search 'foo bar', replica: 'Book_by_editor'

単一のインデックスを共有する

1つのインデックスを複数のモデルで共有するのがよいこともあります。これを実装するには、背後のどのモデルでも決してobjectIDがコンフリクトしないようにする必要が生じます。

class Student < ActiveRecord::Base
  attr_protected

  include AlgoliaSearch

  algoliasearch index_name: 'people', id: :algolia_id do
    # [...]
  end

  private
  def algolia_id
    "student_#{id}" # teacherとstudentのIDがコンフリクトしないようにすること
  end
end

class Teacher < ActiveRecord::Base
  attr_protected

  include AlgoliaSearch

  algoliasearch index_name: 'people', id: :algolia_id do
    # [...]
  end

  private
  def algolia_id
    "teacher_#{id}" # teacherとstudentのIDがコンフリクトしないようにすること
  end
end

注意: 複数のモデルを元にした1つのインデックスを対象とする場合、MyModel.reindexは絶対に使わないでください。使うのはMyModel.reindex!だけです。reindexメソッドは、再インデックス化をアトミックに行う目的で一時インデックスを用います。これが使われると、生成されるインデックスにはモデルの現在のレコードしか含まれなくなってしまいます(他のレコードが再インデックス化されません)。

複数のインデックスを対象に設定する

add_indexメソッドを用いることで、1つのレコードを複数のインデックスでインデックス化できます。

class Book < ActiveRecord::Base
  attr_protected

  include AlgoliaSearch

  PUBLIC_INDEX_NAME  = "Book_#{Rails.env}"
  SECURED_INDEX_NAME = "SecuredBook_#{Rails.env}"

  # すべての本を'SECURED_INDEX_NAME'インデックスに保存する
  algoliasearch index_name: SECURED_INDEX_NAME do
    searchableAttributes [:name, :author]
    # securityをタグに変換する
    tags do
      [released ? 'public' : 'private', premium ? 'premium' : 'standard']
    end

    # publicな(つまりreleasedだがpremiumではない)本を
    # 'PUBLIC_INDEX_NAME'インデックスに保存する
    add_index PUBLIC_INDEX_NAME, if: :public? do
      searchableAttributes [:name, :author]
    end
  end

  private
  def public?
    released && !premium
  end

end

追加のインデックスで検索するには、次のコードを使います。

Book.raw_search 'foo bar', index: 'Book_by_editor'
# or
Book.search 'foo bar', index: 'Book_by_editor'

テスト

テストの注意点

specを実行するために、ALGOLIA_APPLICATION_IDALGOLIA_API_KEYの環境変数を設定してください。テストで作成および削除したインデックスは、productionアカウントでは決して使わないでください

可能なら次のようにdisable_indexingオプションを設定し、API呼び出しでインデックス化操作(追加/更新/削除)をすべて無効にしておきましょう。

class User < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch per_environment: true, disable_indexing: Rails.env.test? do
  end
end

class User < ActiveRecord::Base
  include AlgoliaSearch

  algoliasearch per_environment: true, disable_indexing: Proc.new { Rails.env.test? || more_complex_condition } do
  end
end

またはAlgolia API呼び出しをモック(mock)にしてもよいでしょう。私たちは、algolia/webmockを使えるサンプル設定をWebMockで提供しています。

require 'algolia/webmock'

describe 'With a mocked client' do

  before(:each) do
    WebMock.enable!
  end

  it "ここでは一切APIを呼び出してはならない" do
    User.create(name: 'My Indexed User')  # モック化済み(APIは呼び出されない)
    User.search('').should == {}          # モック化済み(APIは呼び出されない)
  end

  after(:each) do
    WebMock.disable!
  end

end

関連記事

インタビュー: 超高速リアルタイム検索APIサービス「Algolia」の作者が語る高速化の秘訣(翻訳)

週刊Railsウォッチ(20190708-1/2前編)ActiveRecord::FixtureSetがめちゃ強くなってた、MacだとRubyが遅い理由、Puma 4登場ほか

$
0
0

こんにちは、hachi8833です。「👨‍🦲」という絵文字をSlackに貼ったらこんなふうにぶっ壊れたことで合字だということを知りました。


つっつきボイス:「Bald?」「人間の顔の絵文字にズラのコンポーネントをかぶせてたことが判明しました😆」「😆

参考: 👨‍🦲 Man: Bald Emoji
参考: 🦲 Emoji Component Bald Emoji

「そうそう😆、Unicodeってこんなふうに複数の文字を組み合わせて合字が作れるんですよね☺」「4人家族もパパとママと子ども2人を悪魔合体っぽく作ったりしてますね👨‍👩‍👧‍👦」「こういうのに長けたUnicode職人がいるんですよきっと😆」「こういう字は1文字でもバイト数めっちゃ多かったりするので、データベースにぶちこむと溢れたりすることがまれによくあるという😆

参考: 合字 - Wikipedia
参考: 絵文字一覧(家族:family)👪 | Let’s EMOJI

「そういえば最近のRubyだとなんとかlengthみたいなので取れますね」

後でやってみたらscan(/\X/).lengthで取れました。\Xがgraphemeの境界にマッチする正規表現だそうです。

# Ruby 2.6.3
» str = "👨‍👩‍👧‍👦"
» str.each_char do |i|
»   p i
» end
"👨"
"‍"
"👩"
"‍"
"👧"
"‍"
"👦"
#» "👨‍👩‍👧‍👦"
» str.length
#» 7
» str.bytesize
#» 25
» str.scan(/\X/).length
#» 1

参考: Ruby: 書記素クラスターを考慮して文字数を求める - Sarabande.jp

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

今回は7/4(木)に開催された「公開つっつき会#12」を元にお送りいたします。おかげさまで1周年にして満員御礼となりました。お集まりいただいた皆さま、ありがとうございます!😂

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

今回は公式更新情報コミットリストの両方からみつくろいました。公式情報が夏っぽいですね☀🏖🏄‍♀️

⚓Active Recordスキーマキャッシュのcolumns_hashのシリアライズとパースを止めた

#35891の続き。
columns_hashは、名前でインデックス化されたものを単純にcolumnsしているので、単にカラムのarrayからカラムを再インデックスした場合よりもシリアライズとデシリアライズがずっと遅い。
同PRより大意


つっつきボイス:「とりあえずコード見るのが早そう↓」

# activerecord/lib/active_record/connection_adapters/schema_cache.rb#L28
      def encode_with(coder)
        coder["columns"]          = @columns
-       coder["columns_hash"]     = @columns_hash
        coder["primary_keys"]     = @primary_keys
        coder["data_sources"]     = @data_sources
        coder["indexes"]          = @indexes
        coder["version"]          = connection.migration_context.current_version
        coder["database_version"] = database_version
      end

      def init_with(coder)
        @columns          = coder["columns"]
-       @columns_hash     = coder["columns_hash"]
+       @columns_hash     = {}
        @primary_keys     = coder["primary_keys"]
        @data_sources     = coder["data_sources"]
        @indexes          = coder["indexes"] || {}
        @version          = coder["version"]
        @database_version = coder["database_version"]
      end
...
      def columns_hash(table_name)
-       @columns_hash[table_name] ||= Hash[columns(table_name).map { |col|
-         [col.name, col]
-       }]
+       @columns_hash[table_name] ||= columns(table_name).index_by(&:name)
      end

「こういうHash[columns(table_name).mapみたいなのって遅くなりそうな印象あるし」「高速化系の改修ですね☺

⚓time.getutcの呼び出しを削減


つっつきボイス:「まさにタイトル通り: タイムゾーンが既にUTCだったらtime.getutcを呼ばないと」「わかりやすい〜🥰」「時刻変換ってどれぐらい重いんだろう🤔

# activemodel/lib/active_model/type/helpers/time_value.rb#L6
module ActiveModel
  module Type
    module Helpers # :nodoc: all
      module TimeValue
        def serialize(value)
          value = apply_seconds_precision(value)

          if value.acts_like?(:time)
-           zone_conversion_method = is_utc? ? :getutc : :getlocal
-
-           if value.respond_to?(zone_conversion_method)
-             value = value.send(zone_conversion_method)
+           if is_utc?
+             value = value.getutc if value.respond_to?(:getutc) && !value.utc?
+           else
+             value = value.getlocal if value.respond_to?(:getlocal)
            end
          end

          value
        end

「ベンチマークはついてないけど、実行命令数レベルでは減ってる😆」「7行から5行に🎉」「Railsはこういう地道な修正も多いですね☺


同PRより

⚓TranslationHelper#translateのハッシュデフォルト値の扱いを修正

translateメソッドがHashを1つ返すことを期待する場合がある(たとえばnumber.formatキーの場合)。
そういう人は次のようにデフォルトのHashを指定する必要があるかもしれない。
translate(:'some.format', default: { separator: '.', delimiter: ',' })
このやり方はI18n.translateメソッドで期待どおり動作するが、TranslationHelper#translateではデフォルト値にArray()を適用しているのでデフォルト値が最終的に[:separator, '.', :delimiter, ','](というarray)になってしまう。
同PRより大意

# actionview/lib/action_view/helpers/translation_helper.rb#L60
      def translate(key, options = {})
        options = options.dup
        if options.has_key?(:default)
-         remaining_defaults = Array(options.delete(:default)).compact
+         remaining_defaults = Array.wrap(options.delete(:default)).compact
          options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol)
        end

つっつきボイス:「Array.wrapってActive Supportのヤツ?」「…arrayだったら何もしない的なメソッドだったはず↓」「やっぱAPIdock見やすい〜😋


apidock.comより

# apidock.comより
Array.wrap(nil)       # => []
Array.wrap([1, 2, 3]) # => [1, 2, 3]
Array.wrap(0)         # => [0]

「あ〜なるほど、arrayでなければ必ずarrayでラップしてくれるのね❤

「エッジなバグでもあったのかな?」「…このテストケースに対応してる↓: 前はハッシュが単なるarrayになってた」「おかしな変換が残ってたのね☺

# 同PRより
  def test_hash_default
    default = { separator: ".", delimiter: "," }
    assert_equal default, translate(:'special.number.format', default: default)
  end

「(内職中ふと顔を上げて)ははぁ、Array()にハッシュを食べさせるとハッシュがarrayに分解されちゃうけど、Array.wrapすればハッシュのままarrayに入ってくれるのね😋」「それだ」「arrayの中にハッシュが入ってるのが欲しかったと☺」「translateするならそうあって欲しいし」「よく踏んだなこれ」

後でArray.wraprails cで試してみました↓。

development» hash = { separator: ":", delimiter: "," }
#» {:separator=>":", :delimiter=>","}
development» Array(hash)
#» [[:separator, ":"], [:delimiter, ","]]
development» Array.wrap(hash)
#» [{:separator=>":", :delimiter=>","}]

⚓URL破損チェックを修正

# activerecord/lib/active_record/database_configurations.rb#L137
      def build_db_config_from_hash(env_name, spec_name, config)
        if config.has_key?("url")
          url = config["url"]
          config_without_url = config.dup
          config_without_url.delete "url"

          ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url)
-       elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String })
+       elsif config["database"] || config["adapter"] || ENV["DATABASE_URL"]
          ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
        else
          config.each_pair.map do |sub_spec_name, sub_config|
            walk_configs(env_name, sub_spec_name, sub_config)
          end
        end
      end

つっつきボイス:「ほほぅ、URL形式のデータベースコンフィグが反映されてないことがあったと☺」「database.ymlって実はいろんな形式を食わせることができるんですけど、たしか前もウォッチで話した気がする(ウォッチ20181105)」「そうでした!」「ODBCなんかでよく使うmysql://なんちゃらみたいなヤツもRailsでは食えるんですけど、あの手のオレオレURLみたいなのって何か名前あるんですかね?🤔」「URLスキームっぽいといえばそうだけど、どうだろね〜、なさそう😆」「たぶんRFCとかにはないんじゃないかなって😆」「雰囲気URLスキーム🤣」「このプルリクは、そういうのをパースするあたりを修正したんでしょうね☺

参考: URL schemeの一覧


wiki.suikawiki.orgより

「そういえばRails 6の例のマルチプルデータベースで、複数DBをURL形式で指定した場合ってどうなるんだ?みたいな話をどっかで聞いたんですけど」「定義ごとにURL持つみたいな形になるだろうからどうってことなさそうだけど?🤔」「自分はdatabase.ymlでURL形式で書くときに別途ユーザー名とかパスワードとか書かないんですよね😆」「あ、Rails 6からdatabase.ymlが1階層増えるから、ユーザー名とかパスワードをバラして書かずにURLにまとめて書いたときに個別のユーザー名とかパスワードとかがどう扱われるかってことか😳」「まあそんな感じで☺

「今の話を簡単に説明すると、Rails 6でマルチプルデータベースにしたときのdatabase.ymlでURL形式で書いたときにどうなるかという話です: 今Sublimeで記憶ベースで要点部分だけ書いてみますけど、普通はこんな感じ↓」

production:
  primary: 
    host: 'hoge'
    port: '3306'
    user: 'user1'
    password: 'pass1'
  replica:
    host: 'huga'
    port: '3306'
    user: 'user1'
    password: 'pass1'

「それをたとえば以下↓みたいに書いたときにどう扱われるか、みたいな話です」

production:
  primary: 'mysql://user1:pass1@hoge:3306/test'
  replica: 'mysql://user2:pass2@huga:3306/test'

「まあ移行するときに頑張れば済むんですけどー」「そうそう、書き換えれば頑張れる😆」「以上雑談でした😆

⚓オートロードガイドに追記

# guides/source/upgrading_ruby_on_rails.md#L273
+#### Having `app` in the autoload paths

+Some projects want something like `app/api/base.rb` to define `API::Base`, and add `app` to the autoload paths to accomplish that in `classic` mode. Since Rails adds all subdirectories of `app` to the autoload paths automatically, we have another situation in which there are nested root directories, so that setup no longer works. Similar principle we explained above with `concerns`.

+If you want to keep that structure, you'll need to delete the subdirectory from the autoload paths in an initializer:

+ActiveSupport::Dependencies.autoload_paths.delete("#{Rails.root}/app/api")

+In `classic` mode, if `app/models/foo.rb` defines `Bar`, you won't be able to autoload that file, but eager loading will work because it loads files recursively blindly. This can be a source of errors if you test things first eager loading, execution may fail later autoloading.

+In `zeitwerk` mode both loading modes are consistent, they fail and err in the same files.

つっつきボイス:「例のZeitwerkの記述がちょっと増えたようです」「そういやZeitwerkって、おれドイツ人じゃないから発音わかんないんでホットロードだったか何か別の名前になるんじゃないかってRubyKaigiあたりの発表で誰か言ってたような気が😆」「マジで😆」「まだ名前変わってないかな😆」「ツァイトヴェルクってそんなに言いにくいかな〜😆

「ちなみにZeitwerkについて簡単に説明すると、Rails 6でオートローダー周りがZeitwerkというものに変わるんですね: 一応従来のローダーも使えるけど多少コンフィグが必要で、あとSTI使ってると場合によっては読み込み順を明示的に指定しないとエラーになることがある、みたいなことが移行ガイドに書いてますね」

参考: 「シングルテーブル継承 (STI)」Active Record の関連付け (アソシエーション) - Rails ガイド

Rails 6 Beta2時点のZeitwerk情報(要訳)

そういえばKraftwerkもドイツ語ならクラフトヴェルク(=発電所)ですが、例のバンド名はみんなクラフトワークって英語風に呼んでるっぽいですね。

⚓Rails

⚓Puma 4が登場(Ruby Weeklyより)


つっつきボイス:「お〜Puma 4出ましたか!」「@schneemsさんの記事なんですが、ご本人のツイートのコラ動画が何だかかわいくて↓😍」「字幕が『ずっと一緒だよ』って😆」「そして3.42と4.0が泣き別れ😆」「3系はサヨナラと😆」「ズッ友って言ったのにって😆」「Reactorとかいう部分が変更されてAPIがちょい変わったみたい」

⚓RailsのWebサーバーあれこれ

「ついでにゲストの皆さんにお伺いしますけど、Railsで使ってるWebサーバーは何ですか?: じゃあなさそうなところから、Mongrelの人!」「え何それ?😆」「いないか😆、じゃWEBrickの人」「これもいないかな😆」「じゃPassengerの人!」「お〜1人いますね」「じゃUnicornの人!」「1人、いや2人😆」「じゃPumaの人!」「お〜多い」「…Thinは?」「そういえばThinってありましたね😆」「これもなつかしい😆」「Thin使ったことあった😆

参考: WEBrick互換の軽量WebサーバMongrel - [Ruby on Rails/Ruby] ぺんたん info
参考: library webrick (Ruby 2.6.0)
参考: Passenger - Enterprise grade web app server for Ruby, Node.js, Python
参考: unicorn: Rack HTTP server for fast clients and Unix
参考: macournoyer/thin: A very fast & simple Ruby web server

「やっぱり今だとデフォルトのPumaですねよ: Pumaもマルチプロセス・マルチスレッドで設定すれば全然速いですし苦労しなくなったし😋」「みんながPumaを使うようになったことでスレッドセーフに書くことを心がけるようになったであろうというか😆」「みんなどこまでスレッドセーフって意識してるのかな、なんて😆

「ついでに、Railsアプリでマルチスレッド周りのバグを踏んだことのある方は?Railsそのものじゃなくてアプリケーションのコードを書いたときに」「お、意外にいない😆」「オレはあるっ🤣: あれを一度でも踏むと、クラスインスタンス変数とかを見かけたときにコイツメ〜という気持ちになりますよもう🤣」「🤣」「🤣

参考: 【まとめ】インスタンス変数、クラス変数、クラスインスタンス変数 - Qiita

「そういえばUnicornのgraceful restartがどうやってもグレースフルになってくれなかったことあるし😇」「間違った方向に苦労してたりとか?😆」「手動でシグナル送ったりもういろいろやりましたよ〜😭、Capistrano書き換えてstop/startしたり」「そういうときは念のため数秒待つ🤣」「そうそう🤣、そしてstopしたらpsでマスタープロセスがちゃんと死んだのを確認してからリスタートするコードとか書きましたし😭」「Unicornだとちゃんと死んでくれないマスターっていたな〜😆」「Pumaにしてからまったくそういう経験ないっすね😋」「Pumaが突然死したんでUnicornに替えたことならあるといえばあるけど😇、今ならインスタンス増やすとかすればいいんだろうし」「今はメモリだけ軽く監視しとけばいいかみたいな☺

⚓Action TextをJSなしでテストする


つっつきボイス:「Action TextはRails 6で入ることになっているWYSIWYGエディタですね」「BasecampのTrixエディタを使ってるヤツ」「そのAction TextをJSなしでテストすると」「has_rich_text :content😆」「そんな感じのDSLが入ってたかも」「Action Textまだ使ったことないし😆

参考: Trix: A rich text editor for everyday writing


trix-editor.orgより

週刊Railsウォッチ(20181009)Rails 6の新機能:WYSIWYGエディタ「Action Text」、Rails 6の青写真スライド、Apache POIはスゴイほか

「ところでRailsでWYSIWIGエディタやるときって皆さん何をお使いですか?」「redcarpetかな〜」「redcarpetはよく使われますよね☺」「く、このリポジトリにデモがない…😇」「デモがないあたりが昔のgemっぽい😆

例のRailscasts↓に動画チュートリアルがありました。

参考: #272 Markdown with Redcarpet - RailsCasts

「他のWYSIWIGエディタを使ってる方は?」「んーと、フロアーラみたいな名前のWYSIWIGエディタをこないだ社内で使ってたかな」「お、これですね↓」「そうそう、Froala Editor」「うん、ひととおりのリッチテキスト使えるっぽい😋



froala.comより

参考: イカすwysiwygエディタFroalaをRailsに5分で導入

「Floala、新し目でオシャレ感あるし😘」「エディタでUndo使えるし!」「有能👍」「ところでこの記事↑でもFloala使ってるけど、gem 'wysiwyg-rails'でインストールって😆」「なんつう名前😆」「どーなんだろう、他のWYSIWIGをもラップする神gemな匂いありそうだけど😆」「この名前空間取るってどうよ😆」「これは攻めてる⚡」「攻めてる🔫

「Floalaのリポジトリ、2 years agoみたいなのがそこそこ目につく😆」「Floalaは一応商用がメインでライセンス料払うみたいな形式なんだけど、オープンの方の更新は微妙かも?🤔」「ということはRails専用とかではないわけですね😋」「まあnpmとか使わずに入れられるという感じなんだけど、今なら無理して使わなくてもいいんじゃね?って😆

「…CKEditorってのもあります😆」「CKEditorなら使ってもいいかな〜: でもWYSIWIGって外部ライブラリを使うと結局サーバー側でアップロードとかの実装を自分でやらないといけないのが面倒なんですよね😭」「昔はCKEditorぐらいしかまともなのがなかった🧐


ckeditor.comより

「こういうライブラリはどこまでまともなHTMLを吐いてくれるかが気になりますし😆」「自分の職場のiOSエンジニアがですね、世の中にはIMEで変換中にうまく動かないWYSIWIGエディタが多いってボヤいてましたよ」「うお〜それはキツそう😅」「もうね、気の毒すぎてお疲れさま以上の言葉をかけられないっすよ😆」「尊い犠牲😆」「WYSIWIGって入れようとするとホント大変だし、家帰りたくなるし🏠」「何回車輪を再発明したら気が済むんだの世界😆

「Floala、割と良さそうな印象❤」「wysiwyg-railsって名前を除けば🤣」「そうそう🤣

⚓hanmoto: 静的ページをhamlなどで書けるgem

// 同リポジトリより
// app/views/public_pages/404.html.haml:
- provide(:title, 'Not found')
%h1 Not found
%p This webpage is not found.
%p= link_to 'Home', root_path

つっつきボイス:「@junchitoさんの会社で使われてるhanmoto(版元)というgemだそうです」「お、こういうの以前にも見たような?🤔」「昔からあるといえばある☺」「hanmotoのリポジトリで、gakubuchi↓にヒントを得たってありますね😋」「自分が覚えてたのはgakubuchiじゃなかったけど、こういうのはみんな一度はやりたくなるヤツ😆

「Railsのエラーページを動的にやろうとすると『Railsが死んだときにエラーページを出せない』という問題がありますよね😆」「😆」「なのでエラーページは静的にする必要があるんですが、でもエラーページでCSSとかJSとかも読み込もうとするとアセットのファイル名についてるシグネチャとかをちゃんと扱えないと困る: hanmoto gemはそこらへんをいい感じにやってくれるんでしょうね☺」「Rails自身はそういう仕組みってないのかな…😅」「Rackが死んだときに表示するエラーはRailsでは対応できませんね🧐」「Railsで404.erbみたいなのが欲しいってこと?」「ああ、エラーページのerbをプリコンパイルするのをRails自身が持ってればいいのにってことね: まああればあったでいいかも☺

後で探してみたら、hanmotoと少し似た感じのrambulanceというgemもありました。

参考: 【動的VS静的】Railsの404/500エラーページ 静的の勝利 - 珈琲駆動開発

「まあエラーページを何もカスタマイズしてないRailsアプリが本番でデフォルトのエラーページを表示すると、なかなかショッキングな色合いが目に飛び込みますし🤣」「赤い赤い真っ赤っ赤🤣」「そしてむっちゃ電話かかってくる📞」「赤は刺激が強いかも😅」「その意味で、必要以上にユーザーを驚かせないエラーページを作っておくというのは大事だと思います🧐」「それが身を助けますね☺」「いつだったかAmazon Payでいつもの真っ赤な画面が出てましたし↓」「😆

「ユーザーがあの赤い画面を見ると『オレ、壊しちゃった?😱』って気持ちになりがちですし😆」「『大丈夫だよ』がユーザーに伝わるエラー画面が重要😆」(以下延々)

⚓初心者向けRSpecの書き方


つっつきボイス:「メドピアさんの記事ですけど、例のRSpecえかきうたと合わせて読むとちょうどいいのかなと思って」

「『describe/context/itのフォーマットが統一されていない』のコード例は確かに悲しい😭

# 同記事より
# contextが日本語だったり英語だったり式だったり
describe 'valid?' do
  context '非公開のとき'
  context 'publish'
  context 'expired < Time.now'  
end

# itに条件が書かれている
describe 'valid?' do
  it '◯◯で△△のとき、回答が登録されること'
end

「『letで定義した変数名が、何を表しているかわからない』、自分はlet否定派だけど😆」「なぬう😆」「とはいえ変数名を考えるのってめっちゃ時間かかるし、件数が多くなるとどんどんつらくなってくるし😇

「『beforeブロック内でテストを行う』、これはやっちゃダメでしょ😆」「書こうと思えば書けちゃうのか😱

「『テスト対象が同じ、複数のテストケースで、subjectが使われていない』、subjectを使うと読みやすくなるのかというと個人的には疑問ありますが🤣」「subjectがきれいにキマるといいんだけどね〜😎」「キマるとかっこいいけど、今のsubjectってどれ?っていちいち上の方見に行かないといけないのってどうかな〜って😆」「subjectが遠くなるとつらくなる😭」「結局ネストするとつらくなってくるし😅」「ネストとともに地獄感高まる👹

⚓みんなでうたおう『RSpecえかきうた』

「うちのメンバーが書いてくれた『RSpecえかきうた』記事↓、自分もイチオシなので😆読んだことのない方はぜひ一度どうぞ」

RSpecえかきうた

「前半は普通にitでテストケースを並べて、expect書いて、前提条件を書いて、ってやってくわけですね」「どきどき😋」「で後半は『やっぱ離れてると読みにくいよね』って途中からitをまとめてって、contextitに合流させて、するとsubjectがないのにitって何だかヘンだからitやめてtestって名前にしましょって😝」「するとRSpecがあっという間にかわいいアレのようになってしまうわけですよ🤣

「こ、これは🤣」「mとiとnとiがつくテストに🤣」「えかきうたがいつの間にか替え歌🤣」「そりゃもうアンチでしょ〜😛」「自分は好きっ🤣」「業務のテストコードってdescribeとかきれいに書けないことの方が多いくらいだし、もうこれでいいんじゃね?って思うことは、 多々ある😆

「このRSpecえかきうた記事、Twitterでもはてブでも『最高です』というコメントだけが付いてました😆」「もうアンチしかいない🤣」「みんなアンチ🤣」「RSpecで疲れた人にひとときの安らぎを☺」「一服の清涼剤🍹

とっても引用したい動画があったのですが、やめときます😅。察してください。

「ちなみにRSpecでテスト書いてる人は?」「やっぱRSpec多い」「minitestの人は?」「お、いますね〜😳」「他のテストフレームワーク使ってる人は?」「さすがにいない😆」「ZenTestとかは?」「今は亡きZenTest😆」「いないかやはり〜😆

⚓ActiveRecord::FixtureSetがスゴくなってた

↑もともと上の記事を取り上げようとエントリしてたのですが、FixtureSetの話があまりによかったので組み替えました。


つっつきボイス:「そうそう、今日のミーティングで出た話なんですけど、テストデータをFactoryで管理するのとFixtureで管理するのとでそれぞれメリットとデメリットがあるよねって」

⚓fixtureとfactoryについて

「fixtureとfactoryは皆さんご存知ということでいいでしょうか?」「fixtureは知らない人いるかも?🤔」「fixtureはRailsが最初から持っている仕組みで、fixturesというフォルダの下にyamlファイルを置いておくとテスト時にそれがいい感じに1行ずつ展開されてデータベースに入ってくるヤツで、生データを置いているような感覚に近い」

factoryは、基本的な定義はfactoryに書くんですけど、実際のデータはコードで作ります: factory createとかtraitとかを使ったりすることもあります」

FactoryGirlでtraitを使うとintegration test書くのが捗るという話

「ついでに皆さんどっちをお使いでしょうか?、まずはfixtureの人」「ほいっ」「おお、fixtureなんですか?」「オレfixtureでしか書かない😆」「へ〜、以前factory_bot(当時はfactory_girl)の記事とか書いてたのに😆

「factory_botって、テストが増えてくるとすごく遅くなるのが問題なんですよ🐢」「あ〜」「一方fixtureを全部グローバルなfixtureでやれると、最初にガッと全部読み込んで後はトランザクションでやれるから速い🐰: その代り、そのガッと読み込みを実現するまでの作業はつらい😇

「整合性のあるfixtureを書くのってつらくないですか?」「そこはまあそんなに大変でもないんですけど、大変なのはグローバルfixtureだと全部入ってくるんで本番データでテストしているみたいな感じになってくるところで、User.firstとかUser.lastみたいなのが気軽に使えなかったりとか」「なるほど〜」

「fixtureでやるときは、考え方をfactoryのときとは変えた方がいいというのはありますね🧐: たとえばファイル名を変えてそのときだけロードするみたいな設計をある程度最初から考えとかないといかないとか」「このテストコードではこっちのfixture、あちらでは別のfixtureみたいな感じですね」

「でもfixtureって自由度高く書けるから、昔ほどアンチにすることもないかな〜って: 初期段階からやれるんならfixtureでやってみるといいんじゃないでしょうか😋

「続いてfactoryの方は?やっぱりいますね: 反論オーケーですよ😆」「そうですね、fixtureだとどんなふうにデータが入ってくるかとかって、特にチームに初めて参加する人はfixtureを全部見ないとわかりにくいかなと思うんですよね」「たしかにfixtureだとidも生idが入ったりしますし🤔

⚓ActiveRecord::FixtureSetがいつの間にかスゴくなってた

「それがですね、ここだけの話、実はfixtureにはActiveRecord::FixtureSetといういいものがあるんですよ奥さん❤」「ほほぅ?」「多くの人はfactory_girlにやられる前のfixtureしか知らないかもしれないんですけど、以下のサンプルコード↓を見るとだいたいわかります」

参考: ActiveRecord::FixtureSet

「え、ダイナミックfixtureとかある?」「ナウいfixutureにはあるんですよ〜😆

<!-- api.rubyonrails.org より -->
<%Q1.upto(1000) do |i| %Q<
fix_<%= i %>:
  id: <%= i %>
  name: guy_<%= 1 %>
<% end %>

「つかもうちょっと下のアドバンストfixtureの方がむしろ普段づかいするところで、たとえばidなんか自動生成してくれるし↓」

george:
  id: 1
  name: George the Monkey

reginald:
  id: 2
  name: Reginald the Pirate
george: # generated id: 503576764
  name: George the Monkey

reginald: # generated id: 324201669
  name: Reginald the Pirate

belongs_toなんかもこうやって扱えるし↓」

### in pirates.yml

reginald:
  id: 1
  name: Reginald the Pirate
  monkey_id: 1

### in monkeys.yml

george:
  id: 1
  name: George the Monkey
  pirate_id: 1

「おお、末尾のpirate: reginaldreginald:を参照してくれるのか😳」「だからfactory_botとかでリレーション作るよりは楽ですね〜😋」「たしかに〜😍

### in pirates.yml

reginald:
  name: Reginald the Pirate
  monkey: george

### in monkeys.yml

george:
  name: George the Monkey
  pirate: reginald

「結局factory_botの何がつらいって、複雑なリレーション入れ子になったデータを作るのがつらいんですよね😭」「相互参照も地獄感あるし😈」「それ、誰かが作ってくれてればいいんですけど😆、自分でつくるのはホントしんどいし、あったらあったで今度はそれが要らなくなったときに困るんですよ😭」「それを消すと?」「他のテストに影響出ますね〜😢」「createすると思わぬところで使われたりとか😅

「fixtureの続きを見ると、ポリモーフィックbelongs_toもやれるし!」

### in fruit.rb

belongs_to :eater, :polymorphic => true
### in fruits.yml

apple:
  id: 1
  name: apple
  eater_id: 1
  eater_type: Monkey

「has_and_belongs_to_manyもやれますし😋

### in monkeys.yml

george:
  id: 1
  name: George the Monkey
  fruits: apple, orange, grape

### in fruits.yml

apple:
  name: apple

orange:
  name: orange

grape:
  name: grape

ラベルのinterpolation(式展開)もやれますし😋: まあこのラベル名が何やら一意の整数値になってるみたいなんで、スペルミスするといつまでたっても関連付けられなくて他のが関連付けられちゃう、みたいなつらいこともありますけど😆」「何だかマクロ的に展開される?」「まあそんな感じで」

geeksomnia:
  name: Geeksomnia's Account
  subdomain: $LABEL
george_reginald:
  monkey_id: <%= ActiveRecord::Fixtures.identify(:reginald) %>
  pirate_id: <%= ActiveRecord::Fixtures.identify(:george) %>

「YAMLのdefaultsも使えますよ〜😋」「おぉっ!」「ただこれは大文字のDEFAULTSにしないと普通のラベルとして解釈されちゃうとか、arrayでしか使えないみたいなのがあって、fixture使っている人が少ないせいか、ドキュメントにそうやってポロポロ穴開いてますし😆」「なるほど、DEFAULTSが予約語になってると」「これはもっと使ってフィードバックしないと!」

DEFAULTS: &DEFAULTS
  created_on: <%= 3.weeks.ago.to_s(:db) %>

first:
  name: Smurf
  *DEFAULTS

second:
  name: Fraggle
  *DEFAULTS

fixture model classもアンスコ付きの_fixture:でやれるし: これを使ってたとえばusersとerror_usersみたいにファイルを用意しておいて、specごとにfixtureコマンドで呼ぶことでテストケースを分けたりできますし」「は〜なるほど!、fixtureはデフォルトで実在のモデル名を使うけど、モデル名じゃないのを_fixture:で定義しておくと読み込めると」「読み込んで、さらにグローバルfixtureとは別にこっちを使ってねって指定できる」

_fixture:
  model_class: User
david:
  name: David

「いやだ何これスゴい」「やべ〜、こんな賢いfixtureが書けるなんてっ🥰」「fixtureでここまでできるとfactoryってあんまり要らなくて、むしろトランザクションだけでたいてのテストが書けるっつーか☺

「しかしいつの間にこんなにスゴくなったんですか?」「まあ少しずつ増えてたみたいで、Rails 5の頃にはだいたい今のような感じになってたかな〜😋」「何だか『眼鏡を取ったらスゴい美人だった』みたいな😆

「そういうのを調べるにはやっぱりAPIdock↓に限る💪」「なるほど、4.0.2から入ったと」「APIdockだとその機能がいつから入ったかがわかるのがとってもいい😘」「ラベルのinterpolationとかERB直接扱えるようになったのは割と最近だった印象ですけど」


apidock.comより

「fixtureは、たとえばステージングより前の段階でQA用のサンプルデータを作ったりするのにもよかったりしますね❤: fixtureってステージングでもdevelopment環境でも使えるんで、fixtureでテストケースをばばっと書き並べておくとか、本番からfixtureにダンプしてそこから削っていくとかみたいな用途ならfactory_botよりもいいかなって🥰

「あとfixtureのデータの整合性の問題ですけど、いったん全部ぶっこんでからvalidかどうかで流せばいいだけなんで大したことないと思うし」「たしかに!整合性はたまにチェックすればいいぐらいかと😋」「そうそう☺

「そういうわけで皆さん一度ActiveRecord::FixtureSetはチェックしてみるとよいかと🕶」「少なくともDHHはfixture派だろうし、そこは譲らないだろうし😆」「今日からfixture派!😍」「きっとCIが早く終わって地球に優しいっすよホレホレ🌎」「このAPI翻訳しようかな✨

⚓その他Rails

つっつきボイス:「Deviseの動きをトラックするgemみたいです」「どちらかというと証跡ログっぽいかな?」「Geocodingとかもあるから、こいつはどこからログインしたのかみたいなのをデータベースに入れてくみたいな」「上の方のHow It Worksあたりに書いてあるかな〜: ログインの成功/失敗とかIPアドレスとかを記録できるから、やっぱり証跡ログかな」「この手のログは提出を求められることがよくあるし、とりあえず入れとけ感あるかも☺」「GeoIPとか入れとくと、やたら中国からアクセスあるなみたいなのも取れるし😆

参考: GeoIP2 Databases | MaxMind


つっつきボイス:「これは来週渋谷で開催されるTokyo Rubyist Meetupという英語話者を前提としたミートアップなんですけど、誘われたのでちょっくら行ってきます↓」「そういえば前からやってますね〜☺

scenicか久しぶりに聞いたな〜」「この間の銀座Rails↓でもscenicのこと話しましたし😆」「そうそう😆」「ミートアップのお題もdatabase viewsだから怖いくらい既視感ありますね😆」「やっぱデータベースでやった方が速いことはデータベースでやろうよって🤣

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


⚓Ruby

⚓impersonator: オブジェクトのやりとりを記録/再生(Ruby Weeklyより)

# 同リポジトリより
calculator = Impersonator.impersonate(:add, :divide) { Calculator.new }

つっつきボイス:「impersonateって『偽装する』でしたっけ」「なるほど、オブジェクトインタラクションを記録して再生すると」「なんかVCRって文字見えたし😆」「VCRってVideo Cassette Recorderだからビデオデッキでしたっけ」「VCRってgemがあるんですよ↓」「そうそう、HTTPリクエストを記録して再生するヤツ」「HTTPSpyなんてのもあったような」「impersonateはそういうののメソッド版というか」

「しかし何でまたこういうの作ったんだろ🤣」「やりたかったからとか😆」「VCRにインスパイアされたってあるけどどうしてこうなったというか😆」「使いみち何かあるかな〜🤔

⚓MacだとRubyが遅い理由(Ruby Weeklyより)


つっつきボイス:「最近Mac使ってないからわかんないけど😆Ruby遅いの?」「Discourseが出してたベンチマークでもMacが遅いって出てたの見たな〜: WindowsでVM使うより遅いって」「えぇそんなに遅い?😅」「お、記事にもまさにそのツイートが↓」

「リストの上の方がMacで独占されてるし🤣」「だっはっは🤣(マカーだけど)」「これはひどい🤣」「Win10のVMwareで動いてるLinuxにすら負けてるし😇」「か、悲しい😭」「どうしてこうなった😇

「下の速い方を見るとさすがに素のLinuxのRubyが速いですね」「しかしディストリで差が出ているのはなぜ?って思うし😆」「デフォルトのカーネルパラメータとかが関係してそう」「果たしてMacのSSDが原因なのか、それとも何なのか」「上と下でダブルスコアぐらい違ってますし😆」「@samsaffronさんの記事↓はタイトルではWindowsの話かと思ったらMacの方が遥かにやべーじゃねーかって😆」「WindowsのHypervisorはベンチ入ってないのかな?」

参考: Why I stuck with Windows for 6 years while developing Discourse

「MacのRubyが遅いのは、果たしてMach-Oバイナリだからなのか、それともmacOSのHypervisor層が遅いのか、いろいろご意見が出そうではありますね🤔」「どうなんだろね〜?」「もしMach-Oが原因なら、Hypervisor層でLinux動かしてそこで動かせばもう少しマシになったりして」「どっちにしろ遅い🤣」「とほほ🤣」「それにしても負けすぎにもほどがあるというか😆

参考: Mach-O - Wikipedia

「というわけで皆さんWindowsマシンに乗り換えましょう」「🤣」「🤣」「おやこんなところにWindows機が😝

追記(2019/07/08)

上の測定結果はあくまで雑談の参考どまりとお考えください。

⚓Rubyの「Direct Instruction」


同記事より

つっつきボイス:「@tenderloveことAaron Pattersonさんの記事ですが、いつもの自分のサイトではなくGitHubのブログに書いてますね」「こういうアニメーション↑作ってるのはすげー」「@tenderloveさん、最近GIFアニメに入れ込んでますね: 自分もTechRacho記事でこういうのやりたいなと思いつつ😅」「どんなツール使ってるんだろ?」「聞いてみたら案外パワポかも😝」「🤣」「案外その方が楽だったりして😆

「ASTツリーとかもみっちり書いてる↓」「Rubyは新しい技術をこうやって解説してくれる人がいるからいいですね〜😋」「Linuxだと最新技術を学べる書籍がそもそも最近なくなってるし😇」「Linuxの最新カーネルなんかだと、最近は大学の研究室なのか誰なのか結構みっちりドキュメント書いてたりするんですけど、どれを信用していいのかよくわからないという🤣」「🤣


同記事より

⚓その他Ruby


つっつきボイス:「最近流行りのRuby型チェッカー」「まだちゃんと読んでませんが、型チェックについて思うところを書き連ねてますね」

「Rubyの型チェッカーをJetBrainsのIDEとかが採用するようになるとまた世の中が変わってくるかなって」「JetBrainsのIDEなら、型チェッカーがない今でもめちゃうまく回ってるんじゃないかって😆」「ですよね、今でもIDEがYardのドキュメントとかを参照して引数とか型のヒントとか出してくれますし😋」「取りあえず押せば何か出てくる😆」「Yardでちゃんと書かれてさえいればヒント出してくれるとかホント神がかってるし⛩」「Rubyの型チェッカーはむしろVSCodeとかの方がうれしいんじゃ?」



つっつきボイス:「@k0kubunさんのメモ書きだそうです」「そういえばRubyのVMを書くためにJVMを書いてみることにしたとかスゴいことをやってるらしき」「マジで😆

↓こちらのようです。

参考: セルフホストで学ぶJVM入門 - k0kubun’s blog

⚓Quoraより


つっつきボイス:「最近QuoraでMatzの回答を読むのが楽しみで😋」「Rubyのグルがこうやって一次回答を示してくれるのはいいですね〜☺」「しかも日本語で🇯🇵」「Quoraだと英語ロケールは自動翻訳されるのかな?🤔

「Rubyのブロック、そういえば他でほとんど見かけないですね🤔」「Rubyのブロックのよさは、何といってもブロックを1個しか取らないと決めたことでしょうね〜」「それはある!👍」「おかげで記法が複雑にならずに済んだし😋


前編は以上です。

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

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

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

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

Rails公式ニュース

Ruby Weekly

Rails API: `ActiveRecord::FixtureSet`(翻訳)

$
0
0

概要

MITライセンスに基づいて翻訳・公開いたします。


以下も参考にどうぞ。

Rails API: ActiveRecord::FixtureSet(翻訳)

fixtureとは、テストの対象としたいデータ(つまりサンプルデータ)を組み立てる手法のひとつです。

fixtureはYAMLファイルに保存されます。1つのYAMLファイルが1つのモデルに対応し、ActiveSupport::TestCase.fixture_path=(パス)で指定されたディレクトリに保存されます(このディレクトリはRailsで自動設定されますので、自分のRailsアプリ/test/fixtures/に保存できます)。fixtureファイル名の末尾には.yml拡張子が付きます(例: 自分のRailsアプリ/test/fixtures/web_sites.yml)。

fixtureファイルのフォーマットは次のような感じになります。

rubyonrails:
  id: 1
  name: Ruby on Rails
  url: http://www.rubyonrails.org

google:
  id: 2
  name: Google
  url: http://www.google.com

上のfixtureファイルにはfixtureが2件含まれています。各YAML fixture(レコードなどを表す)に名前が与えられ、続く行をインデントして、キーバリューペアを「キー: 値」の形式で記述します。レコードとレコードの間には見やすくするための空行が入ります。

注意: fixture同士の間には順序関係がありません。fixtureを順序付けしたい場合は、omapというYAMLをお使いください。omapの仕様についてはyaml.org/type/omap.htmlをどうぞ。同じテーブルのキーに外部キー制約を付ける場合は、順序ありのfixtureが必要になります。これは主に次のようなツリー構造で必要となります。

--- !omap
- parent:
    id:         1
    parent_id:  NULL
    title:      Parent
- child:
    id:         2
    parent_id:  1
    title:      Child

テストケースでfixtureを使う

fixtureはテストの構成要素なので、fixtureは単体テストや機能テストで用いられます。fixtureの利用法は2とおりありますが、まずは単体テストのサンプルを見てみましょう。

require 'test_helper'

class WebSiteTest < ActiveSupport::TestCase
  test "web_site_count" do
    assert_equal 2, WebSite.count
  end
end

test_helper.rbはデフォルトですべてのfixtureをテストデータベースに読み込みます。したがって、このテストは成功します。

test環境では、各テストの実行前にすべてのfixtureが自動的にデータベースに読み込まれます。データの一貫性を保つために、fixtureは読み込みが実行される前に環境によって削除されます。

fixtureはデータベースでも利用できますが、その他にモデルと同じ名前の特殊な動的メソッドを介してアクセスすることもできます。

この動的メソッドにfixture名を渡すと、その名前と一致するfixtureを返します。

test "find one" do
  assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
end

fixture名を複数渡すと、それらの名前に一致するfixtureをすべて返します。

test "find all by name" do
  assert_equal 2, web_sites(:rubyonrails, :google).length
end

引数を渡さない場合、すべてのfixtureを返します。

test "find all" do
  assert_equal 2, web_sites.length
end

存在しないfixture名を渡すとStandardErrorが発生します。

test "find by name that does not exist" do
  assert_raise(StandardError) { web_sites(:reddit) }
end

別の方法として、fixtureデータの自動インスタンス化を有効にすることもできます。たとえば次のテストがあるとします。

test "find_alt_method_1" do
  assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
end

test "find_alt_method_2" do
  assert_equal "Ruby on Rails", @rubyonrails.name
end

テストケース内でこれらの手法を用いてfixtureデータにアクセスするには、ActiveSupport::TestCaseの派生クラスで以下のいずれか1つを指定しなければなりません。

  • インスタンス化されたfixtureをすべて有効にする(上のalt_method_1とalt_method_2を有効にする)には以下のようにします。
self.use_instantiated_fixtures = true
  • fixtureのハッシュだけを作成し、各インスタンスをfindしない(alt_method_1のみを有効にする)ようにするには以下のようにします。
self.use_instantiated_fixtures = :no_instances

これらの別の方法のいずれかを使うと、パフォーマンスに悪影響が生じます。理由は、fixtureのハッシュやインスタンス変数を作成するために、データベース内のfixture化されたデータをフルスキャンしなければならないためです。そのため、fixture化されたデータセットが大規模になるとコストがかさみます。

ERBで動的fixtureを使う

fixtureの量が重要だが、fixtureの中身はさほど重要ではない場合があります。そのような場合はYAMLのfixtureでERBを併用することで、以下のようにテストの読み込みで大量のfixtureを作成できます。

<% 1.upto(1000) do |i| %>
fix_<%= i %>:
  id: <%= i %>
  name: guy_<%= i %>
<% end %>

上はきわめてシンプルなfixtureを1000件作成します。

ERBを用いることで、<%= Date.today.strftime("%Y-%m-%d") %>などをのような動的な値をfixtureに注入できます。ただしこの機能には若干の注意点があります。fixtureは、それがfixtureの形であれば予測可能なサンプルデータの安定的な単位となりますが、そこに動的な値を注入する必要があると思えてきた場合は、おそらくアプリケーションが正しくテスト可能な状態であるかどうかを再点検する必要があるでしょう。つまり、fixtureの中で動的な値を使うことはすなわち「コードの匂い」と考えるべきです。

あるfixture内で定義されたヘルパーメソッドは、他のfixtureでは利用できません。これは、テスト同士をまたがる依存関係を意図に反して作り出さないためです。複数のfixtureで用いるメソッドは、::context_classincludeされる何らかのモジュール内で定義すべきです。

  • test_helper.rbでヘルパーメソッドを定義する
module FixtureFileHelpers
  def file_sha(path)
    Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
  end
end
ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers
  • fixtureの中でヘルパーメソッドを定義する
photo:
  name: kitten.png
  sha: <%= file_sha 'files/kitten.png' %>

トランザクショナルテスト

テストケースでは、テストケースごとにdeleteとinsertを使う代わりに、beginとrollbackを用いてデータベース変更を分離できます。

class FooTest < ActiveSupport::TestCase
  self.use_transactional_tests = true

  test "godzilla" do
    assert_not_empty Foo.all
    Foo.destroy_all
    assert_empty Foo.all
  end

  test "godzilla aftermath" do
    assert_not_empty Foo.all
  end
end

(おそらくrakeタスクで)すべてのfixtureデータをテストデータベースにプリロードしてトランザクショナルテスト(transactional test)を行うと、テストケース内のすべてのfixture宣言を省略できることがあります。理由は、全データが既に存在し、すべてのケースの変更がロールバックされるからです。

インスタンス化されたfixtureでプリロード済みデータを用いるには、self.pre_loaded_fixturesをtrueに設定してください。これにより、fixture経由で読み込まれたすべてのテーブルのfixtureデータにアクセスできるようになります(この挙動はuse_instantiated_fixturesの値によって変わります)。

以下の場合はトランザクショナルテストを使いません

  1. トランザクションが正しく動いているかどうかをテストする場合。ネストしたトランザクションは、すべての親トランザクションがコミットされるまでコミットされません(特にfixtureのトランザクションがセットアップで開始され、ティアダウン(teardown)でロールバックする場合)。つまり、Active Recordがネステッドトランザクションまたはsavepointsをサポートするまではトランザクションの結果を検証できません(これについては作業中)。
  2. データベースがトランザクションをサポートしていない場合。MySQLのMyISAMをのぞくあらゆるActive Recordデータベースはトランザクションをサポートしています。その場合はInnoDBかMaxDBかNDBをお使いください。

アドバンストfixture

IDを指定しないfixtureにはいくつかの機能を利用できます。

  • 安定な自動生成ID
  • 関連付けをラベルで参照する(belongs_to、has_one、has_many)
  • HABTM関連付けをインラインリストとして用いる

訳注: 現在のRailsでHABTM(has_and_belongs_to_many)を使ったリレーションは一般に悪手とされています。現在のRailsでは代わりにhas_many :through関連付けを使うのが一般的です。
* 参考: HABTMリレーションシップは悪であるという論争 | A-Listers
* 参考: has_and_belongs_to_many関連付け — Active Record の関連付け - Rails ガイド
* 参考: has_many :through関連付け — Active Record の関連付け - Rails ガイド

IDを指定した場合でも利用できる機能がいくつかあります。

  • Timestampカラム値の自動入力
  • fixtureラベルの式展開(interpolation)
  • YAML defaultsのサポート

安定な自動生成ID

以下のおさるfixtureがあるとします。

george:
  id: 1
  name: George the Monkey

reginald:
  id: 2
  name: Reginald the Pirate

それぞれのfixtureには一意のIDが2種類あります。2つのIDのうち、1つはデータベース用で、もう1つは人間用です。そうする代わりに、主キーを生成できるとよいでしょう。それぞれのfixtureのラベルがハッシュ化されて一貫したIDが生成されます。

george: # 生成されたID: 503576764
  name: George the Monkey

reginald: # 生成されたID: 324201669
  name: Reginald the Pirate

Active Recordはこのfixtureのモデルクラスを参照して正しい主キーを見つけ、fixtureがデータベースに挿入される直前に主キーを生成します。

指定したラベルで生成されるIDは定数なので、ラベルがわかっていれば、読み込みなしに任意のfixture IDを見つけられます。

関連付けをラベルで参照する(belongs_to、has_one、has_many)

fixtureで外部キーを指定すると非常にもろくなる可能性があり、しかも読みにくくなります(言うまでもありませんが)。Active Recordは任意のfixtureのIDをラベルで特定できるので、外部キーを(IDではなく)ラベルで指定できます。

belongs_to

おさると海賊でもう少しやってみましょう。

### pirates.yml

reginald:
  id: 1
  name: Reginald the Pirate
  monkey_id: 1

### monkeys.yml

george:
  id: 1
  name: George the Monkey
  pirate_id: 1

この調子でおさると海賊を増やして複数のファイルに分割すると、追いかけるのがつらくなってきます。IDをやめてラベルを使ってみましょう。

### pirates.yml

reginald:
  name: Reginald the Pirate
  monkey: george

### monkeys.yml

george:
  name: George the Monkey
  pirate: reginald

見事にすっきりしました。このfixtureのモデルクラスはActive Recordに反映されてすべてのbelongs_to関連付けが検索されるようになり、(外部キーのターゲットIDではなく)関連付けmonkey: george)のターゲットラベルを指定できるようになります。

ポリモーフィックbelongs_to

ポリモーフィックなリレーションシップのサポートは少し複雑です。理由は、関連付けが指す先の型をActive Recordが認識する必要があるためです。次のような例ならわかりやすいでしょう。

### fruit.rb

belongs_to :eater, polymorphic: true
### fruits.yml

apple:
  id: 1
  name: apple
  eater_id: 1
  eater_type: Monkey

もっとよくできそうですね。

apple:
  eater: george (Monkey)

ポリモーフィック関連付けのターゲットの型を指定するだけで、後はActive Recordが面倒を見てくれます。

has_and_belongs_to_many

おさるにフルーツをあげる時間になりました。

### monkeys.yml

george:
  id: 1
  name: George the Monkey

### fruits.yml

apple:
  id: 1
  name: apple

orange:
  id: 2
  name: orange

grape:
  id: 3
  name: grape

### fruits_monkeys.yml

apple_george:
  fruit_id: 1
  monkey_id: 1

orange_george:
  fruit_id: 2
  monkey_id: 1

grape_george:
  fruit_id: 3
  monkey_id: 1

このHABTM fixtureを消し去りましょう。

### monkeys.yml

george:
  id: 1
  name: George the Monkey
  fruits: apple, orange, grape

### fruits.yml

apple:
  name: apple

orange:
  name: orange

grape:
  name: grape

fruits_monkeys.ymlファイルが不要になりました!ここではジョージのfixtureでフルーツのリストを指定しましたが、フルーツごとにおさるのリストを指定することも簡単です。fixtureのモデルクラスがbelongs_toによってActive Recordに反映され、has_and_belongs_to_many関連付けが見つかるようになります。

Timestampカラム値の自動入力

Active Recordの標準的なタイムスタンプカラム(created_atcreated_onupdated_atupdated_on)のいずれかがテーブルやモデルで指定される場合は、自動的にTime.nowで設定されます。

特定の値が設定済みの場合は何もしません。

fixtureラベルの式展開

現在のfixtureのラベルは、カラムの値としていつでも参照できます。

geeksomnia:
  name: Geeksomnia's Account
  subdomain: $LABEL
  email: $LABEL@email.com

また、指定のラベルでIDを保持できるようにする必要が生じることがあります(古い結合テーブルfixtureを移植する場合など)。そんなときはERBが役に立ちます。

george_reginald:
  monkey_id: <%= ActiveRecord::FixtureSet.identify(:reginald) %>
  pirate_id: <%= ActiveRecord::FixtureSet.identify(:george) %>

YAML defaultsのサポート

fixtureのYAMLファイルでデフォルト値を設定して再利用できます。これはdatabase.ymlでデフォルトを指定するのに使われる手法と同じです。

訳注: この場合DEFAULTSはすべて大文字にする必要があります。

DEFAULTS: &DEFAULTS
  created_on: <%= 3.weeks.ago.to_s(:db) %>

first:
  name: Smurf
  <<: *DEFAULTS

second:
  name: Fraggle
  <<: *DEFAULTS

ラベルが「DEFAULTS」のfixtureはすべて安全に無視されます。

fixtureのモデルクラスを設定する

fixtureのモデルクラスをYAMLファイルで直接指定できます。これは、fixtureがテストの外部で読み込まれ、かつset_fixture_classが利用できない場合(rails db:fixtures:loadの実行時など)に有用です。

_fixture:
  model_class: User
david:
  name: David

ラベルが「_fixture」のfixtureはすべて安全に無視されます。

関連記事

[Rails 5.1] ‘form_with’ APIドキュメント完全翻訳

週刊Railsウォッチ(20190716-1/2前編)Railsアプリの最適化テクニック、あなたの知らなそうなRuby 2.7の変更点、Stripe向けRailsエンジンほか

$
0
0

こんにちは、hachi8833です。高気圧の到来を割と本気で待ち望んでます。

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

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

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

⚓MySQLのenumsetカラムのダンプを修正

# activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb#L40
          def schema_type(column)
            case column.sql_type
            when /\Atimestamp\b/
              :timestamp
+           when /\A(?:enum|set)\b/
+             column.sql_type
            else 
              super
            end
          end

          def schema_limit(column)
-           super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type)
+           super unless /\A(?:enum|set|(?:tiny|medium|long)?(?:text|blob))\b/.match?(column.sql_type)
          end

enumset:stringとして型キャストされるが、現時点の:string型がスキーマダンプで誤って再利用されている。
カラムのキャスト型はsql_typeと同じとは限らない。この修正では、enumsetカラムのスキーマダンプでtypeではなくsql_typeを正しく使うようになる。
同PRより大意


つっつきボイス:「お、これ@kamipoさんがつぶやいてたヤツか: MySQLのenumdb:schema:dumpに対応したとか何とか」「後で探してみます」

おそらくこれかなと↓。

「最近MySQLとスキーマ周りの修正が目につきますね」「この修正はいいと思う🥰」「データベースの機能を使うべき」「今までstringになっちゃってた?」「ぽすぐれだと完全にenum型になるんだけど、MySQLのこの辺よくわからん😆」「バグとまではいかないけど適切ではなかったっぽい🤔」「今まではstringにしてたけど、本来はenum値がstringでないこともあったとかなんでしょうね: このテストのdiffに出てる'time'とか↓」

# activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb#L42
        def test_enum_type_with_value_matching_other_type
-         assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')"
+         assert_lookup_type :string, "ENUM('unicode', '8bit', 'none', 'time')"
        end

⚓DB作成時にyamlを読み取れなかった場合にwarningを出す

# activerecord/lib/active_record/railties/databases.rake#L23
-   ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+   ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
      desc "Create #{spec_name} database for current environment"
      task spec_name => :load_config do
        db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
        ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
      end
    end
  end

つっつきボイス:「for_each(databases)に変わったところからしてマルチプルDB絡みっぽい」「マルチプルDB関連の修正がこれまで相当ありましたけど、やっぱり大きな改造なんですね…😅」「かなり変わってきてますからね〜: 普段ほぼ誰も使ってないような機能まで影響受けたりするだろうから、見落としもいろいろ出てきそう🥺

⚓MySQLのエラーチェックをエラー番号ベースに

# activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb#L9
module ActiveRecord
  module ConnectionHandling # :nodoc:
+   ER_BAD_DB_ERROR = 1049
+
    # Establishes a connection to the database that's used by all Active Record objects.
    def mysql2_connection(config)
      config = config.symbolize_keys
      config[:flags] ||= 0
      if config[:flags].kind_of? Array
        config[:flags].push "FOUND_ROWS"
      else
        config[:flags] |= Mysql2::Client::FOUND_ROWS
      end
      client = Mysql2::Client.new(config)
      ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config)
    rescue Mysql2::Error => error
-     if error.message.include?("Unknown database")
+     if error.error_number == ER_BAD_DB_ERROR
        raise ActiveRecord::NoDatabaseError
      else
        raise
      end
    end
  end

つっつきボイス:「これは見たまんまの修正😆」「今までエラーメッセージを文字列リテラルで比較してたんですね」「文字列で比較しちゃうと関係ないところでマッチしちゃう可能性もありますし」「こういうのはエラーメッセージテーブルとかから一括で引っ張ってまとめられるといいな〜☺

参考: MySQL :: MySQL 8.0 Reference Manual :: B.3.1 Server Error Message Reference


以下は直接関係ありませんが、つっつき後にたまたま見かけたツイートです。

⚓HTTP Feature-Policyの設定をサポート

Feature-Policy: geolocation ‘none’; autoplay https://example.com

# config/initializers/feature_policy.rb
Rails.application.config.feature_policy do |f|
  f.geolocation :none
  f.camera      :none
  f.payment     "https://secure.example.com"
  f.fullscreen  :self
end

つっつきボイス:「またHTTPヘッダーが増える?😅」「見た感じ、ブラウザがサーバーに『この機能ならあるから、使うのはやぶさかではない』ことを伝えるヤツなんでしょうね」「おー」「そうすれば、たとえばブラウザでGeolocationが使えるならそれに応じたレスポンスを返せるし、ブラウザのカメラを使うことは相成らぬということであればカメラのUI自体をオミットすることもできる、という感じかな😆」「あと、これを使ってRails側でライブラリを選択的にロードすることで軽くできるなんてこともできそうですし」「あ〜」

参考: Feature Policy

⚓番外: システムテストでhttpsセッションがやりづらい

  1. システムテストでSSLを使うようPumaを設定
    Capybara.server = :puma, { Host: "ssl://#{Capybara.server_host}?key=<key_file>&cert=<cert_file>"

2. アプリのホストでHTTPSを使うよう設定

   Capybara.app_host = "https://#{Capybara.server_host}" 

3. 簡単なシステムテストを回す-> Chromeブラウザは起動するがテストが詰まってタイムアウトで落ちる

it 'shows correct version of home page' do
    visit root_path
    expect(page).to have_content('anything')
end

つっつきボイス:「プルリクではなくissueを拾ってみました」「HTTPSであることをそうやってテストしようとすればそうなるし😆: HTTPSでないとできないテストは確かにめんどくさい😭」「とりあえず設定でapp_hostを上書きして回避したそうです↓」

  config.before :each, type: :system do
    Capybara.app_host = "https://#{Capybara.server_host}"
  end

「HTTPSのテストって、証明書をどうする問題とか、そもそもignore_ssl=trueしていいのかどうか問題とか、考えないといけない部分多いし😆」「そうそう、issueにもあるけど、DEFAULT_HOSThttp://127.0.0.1がハードコードされるとできないとか😇」「ありゃ〜😆」「まあ証明書を無視しちゃえばやれますけど🤣」「🤣

「それ以前に、HTTPSをRails自身が解釈すべきなのかどうかというのはありますね: だいたいどの人も間にNginxとかELBとかを挟んでそこでHTTPS化したりするので🧐」「今ここは想像だけど、Railsは『HTTPSの部分はSSLアクセラレータ的なものに任せる』という考え方だったりするんじゃないかな〜って😆」「たしかにその方がもろもろラクになりますよね😋」「ただ、HTTPSのときにこういうヘッダーが付くかどうかみたいなテストとか、クライアント証明書の中にあるパラメータを取ってきて動かすテストみたいなのは、どうしてもこういう形でやらざるを得ないんですよ😢」「これはもうしょうがないですね😅

⚓Rails

⚓Stripe::Rails: stripeを統合したRailsエンジン(Ruby Weeklyより)

# 同リポジトリより
Stripe.plan :silver do |plan|
  plan.name = 'ACME Silver'
  plan.amount = 699 # $6.99
  plan.interval = 'month'
end

Stripe.plan :gold do |plan|
  plan.name = 'ACME Gold'
  plan.amount = 999 # $9.99
  plan.interval = 'month'
end

Stripe.plan :bronze do |plan|
  # Use an existing product id to prevent a new plan from
  # getting created
  plan.product_id = 'prod_XXXXXXXXXXXXXX'
  plan.amount = 999 # $9.99
  plan.interval = 'month'
end

つっつきボイス:「Stripeを扱うための単なるgemとかではなくてRailsエンジンの形なんだそうです」「StripeのAPIをRailsにマウントできるようにしたとかそういう感じかな?🤔」「Stripeというサービスには元々管理画面がありますけど、そういうStripeの管理画面相当のことをRailsでもやれるとかなんでしょうね」「READMEにいくつか書いてますね↓」

  • Stripeの設定を一箇所にまとめて管理
  • stripe.jsをアセットパイプラインで使える
  • プランやクーポンをアプリ内で管理
  • StripeからのWebhookを受け取ったりバリデーションしたりが簡単にやれる

「なるほど、Stripeサービスの管理画面をポチポチする部分を、このエンジンをずごっと入れることで、Stripeの管理画面を使わなくてもRailsでやれるようになると😋

「Stripeなら自分で管理画面作らなくてもStripeサービスの管理画面でやれるのがうれしい部分なのかなと思ってましたけど」「そこはアプリの運用によっては一部をRailsでやりたい場合もあるでしょうね」「そっか〜」「特にプランの変更なんかは、Railsのモデルと連動させたいこともあるでしょうね🧐」「それありそう!」「そういうのをやりたいときにこのエンジンがあるといいのでわっ😆

Stripe決済を自社サービスに導入してわかった5つの利点と2つの惜しい点

⚓Railsアプリの最適化テクニック(Ruby Weeklyより)


つっつきボイス:「測定して、データベースを最適化して、キャッシュを最適化して、HTTPを最適化して、バックグラウンドに移して、DRYにして…と、まあ普通に行えるテクニック集😆」「つまり定番の最適化ですね😋」「DRYはやりすぎると逆に遅くなることありますけどっ🤣

同記事見出しより:

  • まずは測定
  • データベースの最適化
    • N+1をなくす
    • インデックスをちゃんと付ける
    • ORMクエリを生SQLに書き換えてみる
    • データベースの正規化度合いを下げる
    • INSERTをトランザクションで一括でやる
  • キャッシュの最適化
    • ビューのキャッシュ
    • DBクエリのキャッシュ
    • 「あえてキャッシュしない」テクニック
  • HTTPの最適化
    • アセットのキャッシュ
    • 画像の最適化
    • JavaScriptコードの分割
  • ロジックをバックグラウンドワーカーに移す
  • コードをDRYに
  • 他にもいろいろあるからね

⚓スベってるRSpecテストの実例(Ruby Weeklyより)


つっつきボイス:「割と短い記事かな」「『アソシエーションがあるかどうかのテスト』のような意味のないテストの例が」「タイトルのpointlessってそういうことね☺

# 同記事より
it { expect(profile).to belong_to(:user) }
it { expect(user).to have_one(:profile }

has_manyと書いてあるかどうかのテストとか、モデルで自動生成されるメソッドのテストとかは普通意味ないですし😆」「そういうテストを書いちゃう人がいるんでしょうね😆」「まあ書けばテストの件数は増えますけどっ😆」「あ〜カバレッジ増やすためだけのテストみたいな😆

同記事の続きはこちら↓だそうです。

参考: A repeatable, step-by-step process for writing Rails integration tests with Capybara - Code with Jason

⚓12-Factorに沿ったRailアプリ(RubyFlowより)


つっつきボイス:「12-Factorといえばこれですね↓」「12-Factorに沿ってRailsアプリを構築する方法を考える記事らしき🤔


12factor.netより

「Railsの場合、12-Factorのためにやることはそんなになさそう?」「まあこういうENV切り出し↓とかは基本ですね」

# 同記事より
default: &default
  secret_key_base: <%= ENV.fetch('SECRET_KEY_BASE', 'some-default') %>
  host: <%= ENV.fetch('HOST', 'localhost:3000') %>

  s3:
    assets_bucket: <%= ENV.fetch('S3_BUCKET') %>
    access_key_id: <%= ENV.fetch('S3_ACCESS_KEY') %>
    secret_access_key: <%= ENV.fetch('S3_SECRET_ACCESS_KEY') %>
    region: <%= ENV.fetch('S3_REGION', 'us-west-1') %>

development:
  <<: *default
test:
  <<: *default
production:
  <<: *default

「記事のその先では『dotenv使うのってどうよ?』みたいな話が😆」「dotenvじゃなくても単に環境変数読み込めばよくね?みたいな😆」「dotenvをどうやって連携するかだけ最初にきちっと考えておけばいいかなという気もするけど🤔」「記事には『dotenvはアプリの設定を管理するgemではなく、.envから環境変数を読み出すgemでしかない』ってありますね」「たしかにアプリの設定管理用じゃない😆

記事の「dotenvを使わないやり方のメリット」↓です。

  • 素のRailsでやれるのでgem追加不要
  • アプリ起動時に設定がバリデーションされる
  • 必要な変数が不足していればアプリの起動が失敗してくれる
  • 開発者がproductionの設定にアクセスできないようにできる
  • 必要な設定のマニフェストを得られる
    同記事より大意

「そういう意味では、環境変数とかで『アプリの振る舞いの設定』と秘密鍵のような『(振る舞いではない)単にアプリで必要な情報』をごっちゃに扱う人ってたしかにいる!🤣」「いるいる!🤣」「そういったものってきちんと分けておくべき😤」「そういう設定が2000行とか超えたり😭」「何とかモードとDBパスワードがおんなじファイルに入っているとう〜ん😇って気持ちになるし」「混ぜるな危険🚫」「一度ぐちょぐちょに混ざっちゃうと誰も解体できなくなるし😭

⚓Rails 6のbefore?after?の日本人にとっての微妙感(Ruby Weeklyより)

↑先ほどこのサイトを開くと「問題があるため開けません」と表示されましたが、その後正常に復帰したようです。


つっつきボイス:「BigBinaryのRails 6記事なんですけど、記事にはないRuby Weeklyの見出しで『no more confusing < comparison』とありました」「今までなかったのね☺」「英語よくわかんないけど😆、これちょっとわかりにくいというか、before?after?のレシーバーと引数って英語的に合ってる?🤔」「英語圏の人にとってどうなんだろ?」「んん〜、どうやら英語的には合ってるみたいですが日本人にはややこしい😅」「before?<の、after?>のエイリアスみたいです」「どっちが主体か割とわかりにくい…😅

# 同記事より
Date.new(2019, 3, 31).before?(Date.new(2019, 4, 1))
# => true

「beforeって動詞じゃないですよね?」「そのはずです」「Weblio辞書見ても動詞はないですね〜」「接続詞、前置詞、副詞はある」「動詞じゃないメソッドが出てくると割と考え込んじゃう😆」「副詞的用法なら引数いらなさそうだし😆」「そこらへんが何か違和感残るし😅」「スペルアウトしてless_than?とかgreater_than?とかの方がよかったりして😆」「それはそれでありそうだけど😆

参考: Rails6 のちょい足しな新機能を試す4 (Date#before? Date#after? 編) - Qiita

# Qiita.comより
# 今日の後は昨日? => この解釈が間違い。
irb(main):001:0> Date.today.after?(Date.today.yesterday)
=> true

Qiitaの記事によると、当初は#32185alias_methodで定義されていたのが、#32398でリファクタリングされたようです。

# Qiita.comよりcalculation.rb
    # Returns true if the date/time falls before <tt>date_or_time</tt>.
    def before?(date_or_time)
      self < date_or_time
    end

    # Returns true if the date/time falls after <tt>date_or_time</tt>.
    def after?(date_or_time)
      self > date_or_time
    end

⚓その他Rails


つっつきボイス:「バンド関連だと『当方ボーカル。全パート募集』やんなるほど見かけます😆」「学生の起業なんかでも超よくありますし🤣」「🤣」「🤣

参考: 「当方ボーカル、他全パート募集!」実際どれくらいある? 憧れのボーカリスト1位はオアシス&ワンオク - エキサイトニュース

⚓Ruby

⚓あなたの知らなそうなRuby 2.7の変更点6つ(Ruby Weeklyより)

クックパッドの中の人の記事です。ラテン系の方で、RubyConf Colombia↓の主催者でもあるそうです。


つっつきボイス:「最初は『IRBで複数行再表示できる』、これってk0kubunさんが入れたヤツかなと思ったらあれはインクリメンタルシンタックスハイライトでした😅


同記事より

「次は『Module#constant_source_locationが入った』」「source_locationは2.6にも入ってるけど、2.7でさらに拡張!」

参考: Module#constant_source_location [Feature #10771] · ruby/ruby@9384383
参考: source_location — Class: Method (Ruby 2.6.3)

「次は『FrozenError#receiverが入った』」「エラー起こしたヤツのレシーバーのを取れるんですね😋

参考: Add FrozenError#receiver · ruby/ruby@39eadca

「次は『キーの非シンボルとシンボルの混在がまた許されるようになった』」「againだからいっとき許されなくなってたのね😆

# 同記事より
method_with_keyword_args(a: 1, "b" => 2, "c" => 3)
#=> {:a=>1}

# 2.6.0
method_with_keyword_args(a: 1, "b" => 2, "c" => 3)
#=> ArgumentError (non-symbol key in keyword arguments: "b")

参考: リビジョン 64358 - non-symbol keys in kwargs * class.c (separate_symbol): [EXPERIMENTAL] non-sy... - Ruby master - Ruby Issue Tracking System

「次は『ブロックなしのProcやlambdaの扱い』」「Procがwarningになって、lambdaがエラーに😳」「ブロックなしがやれると嬉しい場合があったようなそうでもないような😅」「Procやlambda周りを整理しようとしてるのかも🤔

# 同記事より
def proc_without_block
  proc
end

# 2.7以降
proc_without_block { "in here!" }.call
# => warning: Capturing the given block using Proc.new is deprecated; use `&block` instead
# => "in here!"
# 同記事より
def lambda_without_block
  lambda
end

# 2.7以降
lambda_without_block { :in_here }
ArgumentError (tried to create Proc object without a block)

「そして『$;$,がdeprecation warningに』😆」「Perl由来の特殊変数にも手を付け始めた😆」「あの読みづらい$系を減らしていく方向なのかな😆」「よりリーダブルになるのは賛成😋」「そうやって記号が使えるようになったらまた新しい記法に使ったりして🤣

# 同記事より
$; = " "
#=> warning: non-nil $; will be deprecate
#=> " "

"hello world!".split
#=> warning: $; is set to non-nil value
#=> ["hello", "world!"]

$, = " "
#=> warning: non-nil $, will be deprecated
#=> " "

["hello", "world!"].join
#=> warning: non-nil $, will be deprecated
#=> "hello world!"

[Ruby] Kernelの特殊変数をできるだけ$記号なしで書いてみる

Ruby 2.7の変更点について詳しくは以下で。

参考: ruby/NEWS at master · ruby/ruby

⚓ソルベってみた話(Ruby Weeklyより)

Evil Martiansの中の人です。


つっつきボイス:「Sorbetのオープンソース化直前の記事だそうです」「前からSorbetはライブラリ向きかもみたいな話は聞きますね☺」「小さいライブラリなんかはSorbetでやれるといいかも😋」「記事の結論は『まだ始まったばかり』だそうです」

速報: Ruby向け型チェッカー「Sorbet」をStripeがオープンソース化

⚓slop: 軽量なコマンドオプションパーサー(Ruby Weeklyより)


つっつきボイス:「オプションの処理といえばRubyには前から優秀なoptparseがありますけどね(ウォッチ20180518☺」「slopだと何か嬉しい点があるのかしら?」「こんなふうに↓型も指定できるあたりがそうかも」「あ〜」「またしかにoptparseでやれないようなリッチなことがやれそう😋

# 同リポジトリより
o.string  #=> Slop::StringOption, expects an argument
o.bool    #=> Slop::BoolOption, no argument, aliased to BooleanOption
o.integer #=> Slop::IntegerOption, expects an argument, aliased to IntOption
o.float   #=> Slop::FloatOption, expects an argument
o.array   #=> Slop::ArrayOption, expects an argument
o.regexp  #=> Slop::RegexpOption, expects an argument
o.null    #=> Slop::NullOption, no argument and ignored from `to_hash`
o.on      #=> alias for o.null

⚓その他Ruby

つっつきボイス:「RubyWorld Conference、日にちもう決まってましたっけ?」

とうに決まってました↓😅

「技術トピックがそれほど多くないこともあって最近はRubyWorld Conferenceそれほど行ってなかったな〜😅」「私も最近ご無沙汰してます😅」「松江のうまい飯を食いに行くというのはありますけど🍽🍶」「😋


参考: サークル詳細 | Kishima Craft Works | 技術書典

⚓Ruby Trunkより

⚓新機能はデフォルトでオフにして欲しい — 却下(Ruby Weeklyより)

もっと長期間議論してから入れてもいいのでは、というニュアンス。--enable-experimental=...フラグとかでやって欲しいと。


つっつきボイス:「このissueがなぜかRubyWeeklyに載ってました」「experimentalな機能についてこう思う気持ちはわからなくもないけど🤔」「issueの最後でMatzが『experimentalフラグは入れたくない』とジャッジを下していました↓」

  • 「フラグがあればリリースが安定する」について: ユーザーが新機能をオプトインすることは可能だが、先延ばしの期間がさらに延びるだけだし、リリース前に決定できないならいずれにしろマージすべきではない。
  • 「フラグがあれば多くの人に使ってもらえる」について: trunkとpreviewで十分だと思う。experimentalな機能をリリース後に無効にする人は、経験上ほぼいない。
    同issueより大意

「これはホントそのとおりで、そのためのRCやらpreviewだと思いますし😆」「experimentalは最終的に使って欲しいものでしょうし😆」「リリースでstableにするときにはexperimentalな機能を整理すべきだと思うし、『入れるけどデフォルトでオフ』とかはしない方がいいでしょうし」「デフォルトでオフにしないといけないような環境でtrunkを使ってくれるなと😆」「😆


前編は以上です。

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

週刊Railsウォッチ(20190709-2/2後編)strong_password v0.0.7がハイジャックされていた、TerraformとCloudFormation、CSSの設計ミスリストほか

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

週刊Railsウォッチ(20190722-1/2前編)Rails 6エラー画面の改良点、Dateを四捨五入できるtime_calc、Rackミドルウェアのデザインパターンほか

$
0
0

こんにちは、hachi8833です。7年近く肌身離さず使い続けたMacbook Pro 2013 LateのSSDのメインパーティションが、先週金曜日に天に召されました😇

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

⚓お知らせ2件

⚓「出張Railsウォッチ in 銀座Rails」のお知らせ

morimorihogeからのお知らせ:

7/24開催予定の銀座Rails#11 @リンクアンドモチベーションにRailsウォッチが出張予定です。
都合により後半からの参加予定ですが、懇親会枠には間に合いますのでよろしければお声かけください。

⚓週刊Railsウォッチ「公開つっつき会」第13回のお知らせ(無料)

1年目を越えた第13回目公開つっつき会は、8月1日(木)19:30〜にBPS会議スペースにて開催されます。皆さまのお気軽なご参加をお待ちしております🙇

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

⚓database_exists?をadapterに追加

# activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L267
+     # Does the database for this adapter exist?
+     def self.database_exists?(config)
+       raise NotImplementedError
+     end
# activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb#L45
+     def self.database_exists?(config)
+       !!ActiveRecord::Base.mysql2_connection(config)
+     rescue ActiveRecord::NoDatabaseError
+       false
+     end

あえてpublicメソッドにしたそうです。


つっつきボイス:「まああれば使うかな☺」「MySQL向けかな?」「abstract_adapterに足されてるから全部のadapterに足されますね」「あ、そうか😅

「これもマルチプルDB対応でしょうか?」「どうだろう🤔、シングルDBなら最初に接続したときに落ちていればすぐわかるけど、マルチDBだと途中でconnection_adaptersでアクセスしているときに落ちる可能性もあるから、もしかするとこういうので対応するようにしたのかなあ」「とりあえずあっていい機能だと思いますね😋」「マルチDB環境で、複数の異なる種類のDBに接続するケースの対応を意図しているのかも?: MySQLAdapterとPostgresqlAdapterをそれぞれ接続した場合に、ちゃんとそれぞれのdatabase_exists?実装で動いて欲しい、的な」※捕捉参照

SQLiteは接続時にデータベースが存在しないと、こっそりデータベースを作成する。この振る舞いはアダプタ間で異なるので他の問題を引き起こす。このコミットはdatabase_exists?メソッドを追加して、データベースを作成せずにデータベースをチェックできるようにした。問題解決の第一歩としてこれを行った。
同コミットより大意

morimorihoge捕捉(2019/07/23 02:25):
その後Twitterにて@kamipoさんより本件は複数DB対応とは関係ない話であるとのご指摘いただきました。
指摘頂いた内容を追いかけ直してみると、#36383db:prepareの挙動がPostgreSQL AdapterとSQLite3 Adapterで異なる点について解説されていました。

背景として、db:prepareは元々db:migrateが実行され、もしDBがなければAR::NoDatabaseErrorがraiseされることを経由することで、本来DBがなければdb:setupが実行されることを期待されていました。
しかし、SQLite3 AdapterではDBがない状態でもAR::NoDatabaseErrorをraiseせず、暗黙的にDBファイルの作成を行ってしまうことから、db:setupで実行されるはずのschemaとseedsの読み込みが行われていませんでした。

このあたりをdatabase_exists?メソッドに処理を統合しつつ、SQLite3 Adapterでのチェックもファイル存在チェックからメソッドによるチェックに統合することで、SQLite3環境での挙動が本来期待するものになっていなかったのを修正するという流れであった、と思われます。

# kamipoさん、ご指摘ありがとうございました!

⚓コネクション間で引用符付きカラムとテーブル名のキャッシュを共有

# activerecord/lib/active_record/connection_adapters/mysql/quoting.rb#L5
    module MySQL
      module Quoting # :nodoc:
        def quote_column_name(name)
-         @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
+         self.class.quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
        end

        def quote_table_name(name)
-         @quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
+         self.class.quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
        end
# activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb#L
        def quote_table_name(name) # :nodoc:
-         @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
+         self.class.quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
        end
...
        def quote_column_name(name) # :nodoc:
-         @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
+         self.class.quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
        end

つっつきボイス:「quote_column_nameなんてあるんですね」「ありますあります、MySQLだとバッククォート`でやります🧐」「バッククォートで?!」「ぽすぐれはダブルクォートだったかな、データベースによってこのあたりが微妙に違っているという😆」「PostgreSQLはUtils.extract_schema_qualified_nameというメソッドが生えてる☺

「この改修は、こういうquoted columnsをコネクション間で共有する方がいいよみたいなヤツなのかな🤔」「名前をキャッシュするということ?」「定義は基本的に変わらないものですし☺」「だからメモ化しようぜと」「コネクションアダプタは複数になってもここは変わらないでしょと」「SQL Standard的にはダブルクォートがカラムのクオート文字↓」

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

⚓コネクション共有時のクエリキャッシュを修正

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L428
      def connection
-       @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout
+       @thread_cached_conns[connection_cache_key(current_thread)] ||= checkout
      end

      # Returns true if there is an open connection being used for the current thread.
      #
      # This method only works for connections that have been obtained through
      # #connection or #with_connection methods. Connections obtained through
      # #checkout will not be detected by #active_connection?
      def active_connection?
-       @thread_cached_conns[connection_cache_key(Thread.current)]
+       @thread_cached_conns[connection_cache_key(current_thread)]
      end
...
+       def current_thread
+         @lock_thread || Thread.current
+       end
# activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb#L35
        def enable_query_cache!
-         @query_cache_enabled[connection_cache_key(Thread.current)] = true
+         @query_cache_enabled[connection_cache_key(current_thread)] = true
          connection.enable_query_cache! if active_connection?
        end

        def disable_query_cache!
-         @query_cache_enabled.delete connection_cache_key(Thread.current)
+         @query_cache_enabled.delete connection_cache_key(current_thread)
          connection.disable_query_cache! if active_connection?
        end

        def query_cache_enabled
-         @query_cache_enabled[connection_cache_key(Thread.current)]
+         @query_cache_enabled[connection_cache_key(current_thread)]
        end
      end

スレッドをまたがる共有コネクションが有効な場合にクエリキャッシュが正しいコネクションで利くようにした。
同PRより大意


つっつきボイス:「これもコネクションの共有周りか」「スレッドになってる」「current_threadにまとめたと」「これで詰まったりしないのかな?」「クエリキャッシュだし、そんなに遅くなったりはしなさそうだけど🤔

⚓relation_exists?を修正

# activerecord/lib/active_record/relation/finder_methods.rb#L353
      def construct_relation_for_exists(conditions)
        conditions = sanitize_forbidden_attributes(conditions)

        if distinct_value && offset_value
-         relation = limit(1)
+         relation = except(:order).limit(1)
        else
          relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
        end
        case conditions
        when Array, Hash
          relation.where!(conditions) unless conditions.empty?
        else
          relation.where!(primary_key => conditions) unless conditions == :none
        end
        relation
      end

つっつきボイス:「JOINしたテーブルでdistinctoffsetorderを指定するとrelation.exists?でエラーになったと」「exceptというのは?」「:orderのスコープをあえてexceptで外していますね」「PostgreSQLでorderがつくとエラーになってたのか」「えぇ〜」「ぽすぐれだと『使ってないカラムがあるよ』みたいなエラーがよく起きるからそれかも?」「あ〜」「このややこしい組み合わせのときに起きるエラーっぽい」

⚓issue: ActionController::Baseのdeprecation warningが紛らわしい

DEPRECATION WARNING: Initialization autoloaded the constants ActionText::ContentHelper and ActionText::TagHelper.

Being able to do this is deprecated. Autoloading during initialization is going
to be an error condition in future versions of Rails.

Reloading does not reboot the application, and therefore code executed during
initialization does not run again. So, if you reload ActionText::ContentHelper, for example,
the expected changes won't be reflected in that stale Module object.

These autoloaded constants have been unloaded.

Please, check the "Autoloading and Reloading Constants" guide for solutions.
 (called from <main> at /Users/
USER/Projects/rails-app-test/config/environment.rb:5)

つっつきボイス:「こちらはPRじゃなくてissueです」「出なくていいwarningが出るのね」「warningが鳴ったら気にするけど、あまりに多かったりすると見てられなくなったりしますし😆

⚓Rails

⚓Rails 6のエラー表示を目にして思ったこと(Ruby Weeklyより)


同記事より


つっつきボイス:「Rails 6のエラー画面の嬉しい点と残念な点と見苦しい点はこれとこれ、みたいな記事です😆」「Rails 6のエラー画面って地道に改良されてますね😋」「エラーがハイライトされるようになって、トレースもフィルタできるようになって、コンテキスト情報もリッチになって🎂」「でもオートサジェスチョンがうまくいかないことがあるとか😆」「あとこれ↓が出てもうれしくないとか😆」「Railsあるある😆」「Railsやってればどこでも出てきますし😆


同記事より

直接関係ありませんが、記事タイトルの「The Good, The Bad, The Ugly」といえばエンニオ・モリコーネ作曲の一度聴いたら忘れられないこのテーマソング。

参考: 続・夕陽のガンマン - Wikipedia

⚓その名はRack::CORSRuby Weeklyより)

# 同記事より
config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'example.com', 'localhost:3000'
    resource '/publicStuff/*', headers: :any, methods: [:get, :post]
    resource '/myStuff/*', headers: :any, methods: :any
  end
end

つっつきボイス:「Rack::CORS、取り上げたことがありそうでなかったので」「Cross-Origin Resource Sharing」「割と細かく設定できそうなgem😋

参考: オリジン間リソース共有 (CORS) - HTTP | MDN

Rack::CORSは★も2600とかなり多いですけど、どんなときに使うんでしょう?」「APIサーバーとかやるときはこういうのをやらないと動きません」「あ〜」「クロスドメインのリクエストをJavaScriptで投げようとすると、そのままでは制約に引っかかりますね」

「Chromeだったかな、リクエストはかろうじて渡せたけど中身が空っぽになってたときはえぇ〜?!って気持ちになりましたし😆」「😆」「むしろ動かずにエラー表示して欲しいんだけど〜😅」「CORSの話はちょいちょい出てきますね☺

参考: Ruby on Rails 5 アプリにあとから API 機能(JWT, CORS 対応)を追加する - 無印吉澤

⚓Rails 6のActive Jobリトライ/discardフック(Ruby Weeklyより)

# 同記事より
class Container::DeleteJob < ActiveJob::Base
  retry_on Timeout::Error, wait: 2.seconds, attempts: 3

  def perform(container_id)
    Container::DeleteService(container_id).process

    # Will raise Timeout::Error when the remote API is not responding.
  end
end

つっつきボイス:「Rails 6の新機能なんですけど、おそらく今まで取り上げてなかった😅」「Active Jobのretrydiscardか」「へぇ〜!」「やったことあった気もするけど?」「とりあえず見当たりませんでした…」

「必要ですね☺」「必要」「discardとか特に欲しいし🥰」「ね😆」「ゴミファイルが無限にたまらないようにしたりとか」「むしろほっとくとオートクリーンされちゃうファイルを追いかけるときとか😆」「たしかにたしかに」

⚓ビューでbyebugを使うコツ(Hacklinesより)


つっつきボイス:「めっちゃ短い記事です」「<%# a = 1; byebug; b = 2; %>とか書くとbyebugがビューで動くと」「こういう適当な代入文ではさまないとbyebugがするっと通り抜けちゃうらしいです😆」「そうなんだ!」

「ちょうど今日のWebチームミーティングでRailsのビュー周りの発表がありましたけど、ビューだとプリコンパイルしてメソッド化されてからみたいなことをやってたりするから、そういうのもbyebugに影響したりするのかな?☺」「そんな予感はしますね🤔」「自分はビューだとbyebugよりも適当にraiseして調べますけどっ😆」「私はprintデバッグしかしてないけどっ😆

Railsのテンプレートレンダリングを分解調査する#1探索編(翻訳)

⚓Railsでunscopedしない方法

unscopedはしない方がいいっすね〜: マジ事故るから🤣」「そういえば前に翻訳した記事でもそんな主張してました😆」「自分がデータベースVIEW↓をおすすめする理由がまさにそれ😋: データベースVIEWならunscopedしてもデフォルトスコープが外れても大丈夫ですし👍

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

⚓Rackミドルウェアで使われているデザインパターンを探る(Ruby Weeklyより)


同記事より


つっつきボイス:「Rackミドルウェアのデザインパターンか」「この辺のcallメソッド↓とかはよく言及されますね」「callメソッドさえ実装すればRackアプリになれるとか☺

# 同記事より
class MyApp
  def call(request)
     [ 200, {"Content-type" => "text/plain"}, ["hello world"]]
  end
end
app = MyApp.new

参考: Rails と Rack - Rails ガイド
参考: 10分でわかる Rack ミドルウェアの作り方 | MMMブログ

「なんだかLISPっぽいコードが、と思ったらClojureだった😆

(defn wrap-content-type [handler content-type]
  (fn [request]
    ;; call the next handler, then add a header to the response
    ;; before returning it to our caller
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

参考: Concepts · ring-clojure/ring Wiki
参考: Clojure - Wikipedia


clojure.orgより

「たしかにRackはChain of Responsibilityパターンだなぁ」「Chain of ResponsibilityパターンってGoFでしたっけ?」「マルチスレッド編にあったかな?」「(内職中顔を上げて)GoFにあるっ、たしか基本の23パターンにあったと思うし」

参考: Chain of Responsibility パターン - Wikipedia

「Chain of Responsibilityパターンということは、ミドルウェア間のinとoutで示し合わせてチェインできるようにするとそういうヤツでしたっけ」「そうそう、自分が処理できるなら自分のcallメソッドを使うけど、自分で処理できないならそのまんま次のチェインにリクエストごと渡すと☺」「後は任せたっと😆」「もうぽいっと投げる🏈」「で、チェインの最後に何でも食べるヤツを番兵的に置いて引き取ってもらうと🎖

参考: 番兵 - Wikipedia

「記事にはDecoratorパターンもあるし」「RackはPipelineパターンも使ってるとあるけど聞いたことない…」「それ知らね〜😆」「少なくともGoFではない😆

参考: Decorator パターン - Wikipedia
参考: The Pipeline Pattern — for fun and profit - Aaron Weatherall - Medium

「この辺↓とかたしかにパイプラインっぽいし👀: 戻ってこないけど😆」「パイプラインなら戻らなくてもよさそうだけど😆」「またしかに😆

Tools::Pipeline.new .add_step(Tools::GetPolicyHistory)
.add_step(Tools::WithLoggedOutcome, log) 
.add_step(Tools::RejectIfUnsupportedCheckPolicyHistory) 
.add_step(Tools::FlagWithinRetentionIfEL) 
.add_step(Tools::RejectIfNotFlaggedWithinRetention)
.add_step(Tools::RejectIfActiveChainNotYetMigrated)
.add_step(Tools::GetCustomerFromRails, rolodex_api)
.add_step(Tools::MakeChain, pimms_api)
.add_step(Tools::AssociateCustomerWithChain, rolodex_api)
.add_step(Tools::MarkCompletedAfterwards, log, pimms_api)
.add_step(Tools::MigratePoliciesAndNotes, log, pimms_api, notes_api)
.call(old_policy_number: old_policy_number)

⚓VCR gemの使いこなし10(Ruby Weeklyより)

# 同記事より
VCR.configure do |c|
  vcr_mode = ENV['VCR_MODE'] =~ /rec/i ? :all : :once

  c.default_cassette_options = {
    record: vcr_mode,
    match_requests_on: %i[method uri body]
  }
end

つっつきボイス:「先週ちらっと触れたVCR gem(ウォッチ20190708)の使いこなし記事です」「use_cassetteとかありますね☺」「やっぱりVCRだけにカセットなんですね😆

「VCRはハードウェアのテストとかやるのに向いてる感じですね😋」「あんまりスタブ化されてないサービスなんかをカセットにするといいかも」「そういえばFaradayしか使えないこととかあった😆」「Faradayはちょい重いんだよな〜😅

参考: lostisland/faraday: Simple, but flexible HTTP client library, with support for multiple backends.

「そのときはVCRでカセットに記録してそれでテストしましたし」「そっちの方が結局テスト書きやすいですし😋」「ぼくは雑だからスタブ使っちゃいますが😆」「スタブって個人的に何となく信用しきれないところがあって、生リクエスト作りたくなっちゃうな〜」「自分もそうしますけど😆」「スタブは軽いしテスト範囲を限定できるから、いいといえばいいんですが」

ソフトウェアテストでstubを使うコストを考える(翻訳)

⚓RailsアプリのDockerコンテナをビルドしてみた(Hacklinesより)

つっつきボイス:「Dockerfileとdocker-composeでRailsというみんなやってそうな記事ですが、どうやってるのかなと思って😆」「RailsでDockerを使ってる人って、どの人も微妙〜に使い方違うんですよね🤣」「nodejsをapt-getでインストールしてるあたり↓とかも人によって違うし😆」「何が入ってくるのかわかりにくいヤツ😆

FROM ruby:2.6.2
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client

「本当に小さいコンテナを作りたければ本チャンのWebサーバーにはnodeはいらないはずだし: Webpackerをコンパイルするときだけあればいいんじゃね?って😆

「おや、この記事のdocker-composeはvolumeの下にcachedがついてないじゃないですか🤣」「そうそう、DockerfileもRUN apt-get updateした後にaptのキャッシュを消してない🤣」「マサカリ飛んできたゾ🤣

「ちょっと前にdocker-composeのvolumeにcachedとつけると速くなることを学びましたよ〜😂」「最近入ってきた機能ですよね☺」「たしかこれやらないと、特にDocker for Macで遅くなる」「付ける分にはどのDockerでもいいんでしょうか?」「今はどれでも使えると思います☺

参考: Docker for Mac のボリュームの遅さを cached オプションで解消 - Qiita

「この記事の人ももしかするとLinux環境なのかもしれないし😆」「そこは情状酌量で😆


後でDocker公式ドキュメントのベストプラクティスをmorimorihogeさんが見つけてくれました。

参考: Dockerfile のベストプラクティス — Docker-docs-ja 1.9.0b ドキュメント

以下は よく練られた RUN 命令であり、推奨する apt-get の使い方の全てを示すものです。

# docs.docker.jpより
RUN apt-get update && apt-get install -y \
    aufs-tools \
    automake \
    build-essential \
    curl \
    dpkg-sig \
    libcap-dev \
    libsqlite3-dev \
    lxc=1.0* \
    mercurial \
    reprepro \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

付け加えると、apt キャッシュをクリーンにし、 /var/lib/apt/lits を削除することで、イメージのサイズを減らします。 RUN 命令は apt-get update から開始されるので、 apt-get install でインストールされるパッケージは、常に新鮮なものです。
docs.docker.jpより

⚓その他Rails


つっつきボイス:「おっ、MySQLの内部表現か😋

「これ↓は…なんだbeforeか😆

YEAR
1バイトinteger
DATE
3バイトintegerをYYYY×16×32 + MM×32 + DDをパック
TIME
3バイトintegerをDD×24×3600 + HH×3600 + MM×60 + SSをパック
TIMESTAMP
4バイトinteger、epoch (‘1970-01-01 00:00:00’ UTC)以降のUTC秒を表す
DATETIME
8バイト、うち4バイトintegerはYYYY×10000 + MM×100 + DDをパックしたdate、 4バイトはHH×10000 + MM×100 + SSをパックしたtime

「そしてafterはこれ↓」「hourが10ビット」「minuteとsecondはそれぞれ6ビットに😳」「DATETIMEはyear*13+month、あ、month表現でyearを持つってことか」「へぇ〜😳

# TIME
 1 bit sign    (1= non-negative, 0= negative)
 1 bit unused  (reserved for future extensions)
10 bits hour   (0-838)
 6 bits minute (0-59) 
 6 bits second (0-59) 
---------------------
24 bits = 3 bytes

# TIMESTAMP: エンディアンがlittleからbigになった他は同じ

# DATETIME
 1 bit  sign           (1= non-negative, 0= negative)
17 bits year*13+month  (year 0-9999, month 0-12)
 5 bits day            (0-31)
 5 bits hour           (0-23)
 6 bits minute         (0-59)
 6 bits second         (0-59)
---------------------------
40 bits = 5 bytes

「これはきれいに寄せたな〜😋」「きつきつのパンツみたくなりましたね😆

⚓Ruby

⚓time_calc: 新しい日時算出ライブラリ(Ruby Weeklyより)

# 同リポジトリより
require 'time_calc'

TC = TimeCalc

t = Time.parse('2019-03-14 08:06:15')

TC.(t).+(3, :hours)
# => 2019-03-14 11:06:15 +0200
TC.(t).round(:week)
# => 2019-03-11 00:00:00 +0200

# TimeCalc.call(Time.now) shortcut:
TC.now.floor(:day)
# => beginning of the today

「モンキーパッチなし」「マジックなし」「チェイン可能」だそうです。


つっつきボイス:「zverokさんが新たに日時算出ライブラリを作ったそうなんですが、どの辺がうれしいかなと思って」「ああ、日時のround(四捨五入)とかfloor(切り下げ)とかceil(切り上げ)ってたしかに欲しいときある!」「おお!」「floorceilまであるって😆

「こういうのやりたくなったりしません?週単位の切り上げとか、月単位の切り捨てとか😆」「Dateでroundの場合って、どう丸めるんだろう?🤔」「水曜日のある時点を越えたときだったりして😆」「その辺をカスタマイズできたりすると業務的にかなりうれしいかも😋」「こういうの割と自力で書いたりしてますもんね😆」「stepしたりチェインしたりできるし😳

# 同リポジトリより
TC.(t).step(2, :weeks)
# => #<TimeCalc::Sequence (2019-03-14 08:06:15 +0200 - ...):step(2 weeks)>
TC.(t).step(2, :weeks).first(3)
# => [2019-03-14 08:06:15 +0200, 2019-03-28 08:06:15 +0200, 2019-04-11 09:06:15 +0300]
TC.(t).to(Time.parse('2019-04-30 16:30')).step(3, :weeks).to_a
# => [2019-03-14 08:06:15 +0200, 2019-04-04 09:06:15 +0300, 2019-04-25 09:06:15 +0300]
TC.(t).for(3, :months).step(4, :weeks).to_a
# => [2019-03-14 08:06:15 +0200, 2019-04-11 09:06:15 +0300, 2019-05-09 09:06:15 +0300, 2019-06-06 09:06:15 +0300]

「Ruby本家とかActive Supportに影響を与えそうな気がちょっとしてきました」「Active Supportに入っててもおかしくない機能かも😍」「この実装をそのまま使うかどうかはわからないけど😆

⚓その他Ruby

つっつきボイス:「bundle execを入力したくなくてやってみた記事だそうです」「binスタブ使うとかじゃなくて?」「記事ではzshでやってますね」「Oh My Zsh↓を入れるといい感じにできるぞ!みたいな😆」「そんなのあったとは😆


ohmyz.shより

「いっとき、Railsコマンドを実行するのにbundle execをいちいち入力したくない人たちがいろいろやって不具合出したりしてましたね」「beあたりをbashのエイリアスにすればいいんじゃね?☺」「私もそれで十分だと思います😆


⚓Quoraより


つっつきボイス:「『お金があればやれる』という結論が沁みました😂」「インタプリタだしな〜しょうがないかな〜」



つっつきボイス:「Matzの回答でとても勇気が湧きました😂」「集中力15分😆」「よくて30分」「普通8時間も集中力とか続かないってホント😆

「8時間も集中を持続するなんて、せいぜい数年に一度、それも爆発炎上🔥したときぐらいだし😆」「それも明け方とかに😆


前編は以上です。

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

週刊Railsウォッチ(20190604-2/2後編)Cloudflare Workers KVの可能性、PostgreSQL 12 Beta 1、Bootstrap 5でjQuery廃止ほか

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines


週刊Railsウォッチ(20190729-1/2前編)Rails 6のリリースは近そう?、Evil MartiansのRails+Docker記事、Railsパフォーマンス測定ほか

$
0
0

こんにちは、hachi8833です。夏がやってまいりました。

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

⚓週刊Railsウォッチ「公開つっつき会」第13回のお知らせ(無料)

1年目を越えた第13回目公開つっつき会は、8月1日(木)19:30〜にBPS会議スペースにて開催されます。引き続き皆さまのお気軽なご参加をお待ちしております🙇

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

今回は公式情報からです。


つっつきボイス:「多くの人はrc2が取れてから使いはじめてバグを踏むから😆、rc2で動かして見つけてくれないと出ちゃうよ、と」「rc2と最終リリース、ほぼほぼ変わらないですし☺

「この後でも取り上げますが、Rails 6が近々リリースされるかもしれないそうです」「お、そういえば7月末だか8月第一週ぐらいにリリースされるかもって聞いたし」「いよいよ出るのか〜(感無量)」「6.0の後早々に6.0.1を出さずに済むためにも、やれる人は今のうちにrc2でやってみましょう〜☺」「社内プロジェクトでも上げられそうなものがあったらやってみますか😆

なお、以下はつっつき後の日曜のツイートです。どうぞお大事に。

「6.0の新機能といえば、Action TextにAction Mailbox、それとツァイトなんとか😆」「Zeitwerkですね」「Action Textあまり語られてない感が😆

Rails 6 Beta2時点のZeitwerk情報(要訳)

⚓Active Storageにファイルを追加するとhas_many_attachedの元からあったファイルを消すかどうかの振る舞いを選択可能に

期待する振る舞い:
元のファイルアップロードは保存されるべき。上の再現手順の最後のステップでは、モデルのインスタンスには元のファイルと追加アップロードしたファイルの両方が保存されるべき。
実際の振る舞い:
追加のファイルをアップロードすると元のファイルが吹っ飛び、新しいファイルしか残らない。
注意: 現在の振る舞いは機能するために必要な方法であるという議論もあるだろう。しかし現在の6.0.0rc1の振る舞いは、5.2.2.1で観察される振る舞いとは違うので、これが新たなバグを呼び込むかもしれないという気がする。
同issueより大意

5.2ではattachmentsのコレクションを代入するとコレクションに追加される。この振る舞いに依存している既存の5.2アプリを6.0にアップグレードしても壊れないようになった。
6.0以降に生成した新しいアプリでは、代入するとコレクション内の既存のattachmentsは置き換わる。既存のattachmentsを消さずにコレクションにattachmentsを新たに追加するのであれば#attachを使うべき。
自分は6.1では古い振る舞いがdeprecateされることを期待している。
同PRより大意

# activestorage/lib/active_storage/attached/model.rb#L95
          def #{name}=(attachables)
-           attachment_changes["#{name}"] =
-             if attachables.nil? || Array(attachables).none?
-               ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
-             else
-               ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
+           if ActiveStorage.replace_on_assign_to_many
+             attachment_changes["#{name}"] =
+               if attachables.nil? || Array(attachables).none?
+                 ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
+               else
+                 ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
+               end
+           else
+             if !attachables.nil? || Array(attachables).any?
+               attachment_changes["#{name}"] =
+                 ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables)
              end
+           end
          end
        CODE

つっつきボイス:「もしかしてマルチプルアップロードを誰も使ってなかった?😆」「んなこたーない😆」「RedMineとかでもめっちゃ使いますし☺」「6.0でマルチプルアップデートの振る舞いが変わるけど、このままだと5.2とかで現在の振る舞いを当てにしているコードがコケるから、それを回避するということみたいです」

「変更後の仕様の方が本来ということか: 1つのhas_manyなcollectionが対象なんだから、再アップロードしたら洗い替えするものでしょうって」「洗い替えか〜」「複数ファイルの組み合わせの完全性を保証したいというときもあるから、洗い替えするかどうかを選べるオプションは一時的にあってもいいかな」「APIとしてはbreaking changesになるから、追加でやりたい人は#attachを使えと」「ぐぉっ、またPCがフリーズした😇」「モニタを私のに差し替えましょう」

「古い振る舞いは6.1でdeprecateしようとPRにあります」「PRもマージされてissueも閉じたからそういう流れか」「config.active_storage.replace_on_assign_to_manyという設定も追加されてますね」「falseにすれば従来の振る舞いになるという、いつもの延命措置😆」「知らずにアップグレードするとコケると😆


その後私がRailsガイドを6.0向けに更新翻訳しているときに、アップグレードガイドで該当の記述をたまたま見つけました。Active Recordの挙動に合わせたんですね。プルリクにも目的を書いて欲しいです😢

Rails 6.0のデフォルト設定では、添付ファイルのコレクションへの代入は、追加ではなく既存ファイルの置き換え操作になります。これにより、Active Recordでコレクションの関連付けに代入するときの振る舞いと一貫するようになります
アップグレードガイドのドラフトより(強調は編集部)


「ところで上のコードの末尾にあるCODEって何かしら?🤔」「たぶんヒアドキュメントの終わり部分では?😆」「あ、そのちょっと上に開始部分があった↓😅」「そもそもメソッド名に#{name}みたいなのが書かれてる時点でヒアドキュメントっぽいですし😆

      def has_many_attached(name, dependent: :purge_later)
        generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
          def #{name}
...
       CODE

⚓新機能: ハッシュの条件で開始値なしのrangeをサポート

Ruby 2.7で導入予定の開始値なしrangeをいち早く取り入れたようです。

# 同PRより
Order.where(created_at: ..1.year.ago)

class Users < ApplicationRecord
  scope :unruly, -> { where(karma: ...0) }
# activerecord/lib/arel/predications.rb#L37
    def between(other)
      if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1
        self.in([])
-     elsif open_ended?(other.begin)
+     elsif other.begin.nil? || open_ended?(other.begin)
        if other.end.nil? || open_ended?(other.end)
          not_in([])
        elsif other.exclude_end?
          lt(other.end)
        else
          lteq(other.end)
        end
      elsif other.end.nil? || open_ended?(other.end)
        gteq(other.begin)
      elsif other.exclude_end?
        gteq(other.begin).and(lt(other.end))
      else
        left = quoted_node(other.begin)
        right = quoted_node(other.end)
        Nodes::Between.new(self, left.and(right))
      end
    end

つっつきボイス:「PC再起動完了」「開始値なしのrangeって不思議感ありありだし😆」「Ruby 2.7で入るんですね」「これどういうクエリになるんだろうか?🤔」「大なり小なりとかでやるのかも?」「なんか微妙に読みづらくなりそう😅

参考: カルマ値とは - はてなキーワード

⚓新機能: SMSリンクを作成するsms_toヘルパーメソッド

<%= sms_to "15155555785", "Text me", body: "I have a question.." %>
# actionview/lib/action_view/helpers/url_helper.rb#L606
+     def sms_to(phone_number, name = nil, html_options = {}, &block)
+       html_options, name = name, nil if block_given?
+       html_options = (html_options || {}).stringify_keys
+
+       extras = %w{ body }.map! { |item|
+         option = html_options.delete(item).presence || next
+         "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
+       }.compact
+       extras = extras.empty? ? "" : "?&" + extras.join("&")
+
+       encoded_phone_number = ERB::Util.url_encode(phone_number)
+       html_options["href"] = "sms:#{encoded_phone_number};#{extras}"
+
+       content_tag("a", name || phone_number, html_options, &block)
+     end

つっつきボイス:「リンクを叩くとSMSメッセージ画面↓がPC/Android/iOSのどれでも表示されるそうです」「sms:なんちゃらのショートハンドなのね」「hrefにsms:って書けるって知らなかったし😆


同PRより

<a href="sms:5155555785;?body=Hello%20Jim%20I%20have%20a%20question%20about%20your%20product">Text me</a>

⚓新機能: ActiveRecord::QueryAbortedスーパークラスを追加

あらゆるタイムアウトをrescueするには(ただしそれ以外のクエリ例外はrescueしないとする)、rescue ActiveRecord::QueryCancelledActiveRecord::StatementTimeoutActiveRecord::AdapterTimeoutを行わなければならない。これでは、どれかひとつ足し忘れて欲しいものをrescueできなくなりがち。
そこで、すべてのタイムアウトエラーを簡単にrescue ActiveRecord::QueryAbortedできるよう、スーパークラスを1つ導入してみたいと思う。
同PRより大意

# activerecord/lib/active_record/errors.rb#L357
+ class QueryAborted < StatementInvalid
+ end
+
  # LockWaitTimeout will be raised when lock wait timeout exceeded.
  class LockWaitTimeout < StatementInvalid
  end

  # StatementTimeout will be raised when statement timeout exceeded.
- class StatementTimeout < StatementInvalid
+ class StatementTimeout < QueryAborted
  end

  # QueryCanceled will be raised when canceling statement due to user request.
- class QueryCanceled < StatementInvalid
+ class QueryCanceled < QueryAborted
  end

  # AdapterTimeout will be raised when database clients times out while waiting from the server.
- class AdapterTimeout < StatementInvalid
+ class AdapterTimeout < QueryAborted
  end

つっつきボイス:「クエリのタイムアウトをrescueするときにQueryAborted一発で囲んで集約したいということか」「クエリをパースするタイミングで発生するエラーを同一視したいというか」「クエリを投げたときに失敗する可能性のあるエラーをすべて1個のスーパークラスにしたというか」

StatementTimeoutって見た目だけで推測すると、SELECTを打ってから返ってくるまでの時間が長すぎた場合にRDBMSの何かのパラメータに基づいてタイムアウトする、とかかな〜😆」「それっぽいかも☺

QueryCanceledはどういうタイミングで出るのかな?🤔」「たぶんですが、トランザクション分離レベルで待つとか?」「と思ったらコードの上に説明書いてあった🤣」「QueryCanceledはユーザー側でキャンセルした場合か」「BEGIN TRANSACTIONとかの中でキャンセルできた気がするからそういう場合なのか、それともぽすぐれがマルチスレッドクエリを使えた覚えがあるからそっちかな?とも思うけど、とりあえず推測ですし😆

「とにかくこういうタイムアウトたちを一回で取れるようにしたいと😆」「こういうの今後も増えてきそうですし😆」「その基盤はできたと☺

「こういう処理って、こうやってスーパークラスを作ってやる場合もあれば、query aborted typesみたいな感じで配列に入れてincludes?で評価するやり方もあったりしますよね」「rescueするときはスーパークラスの方がやりやすそうかな〜☺

⚓新機能: ジェネレーターのskip-collision-checkオプション

y-yagiさんのPRです。

Rails 5.2までは同じ名前のジェネレータをdestroyせずに複数回実行できたが、Rails 6.0とZeitwerkではできない。クラス名のコリジョンチェックでエラーになる。
このチェックではconst_defined?↓を使っているが、これはオートロードオブジェクトも定義されていることが前提になっている。
この部分はRails 5.2まで機能していなかったが、Zeitwerkならアプリケーションのコードを正しくチェックできそうに見える。
ただし、たとえば間違えた属性名を修正するためにジェネレータを再実行する(その場合事前にdestroyしておく必要がある)ときに少々不便。
PRはこの点を解消するためにコリジョンチェックをスキップするオプションを追加する。このオプションを使えばRails 5.2までと同様ファイルを上書きできるようになる。
同PRより大意

# railties/lib/rails/generators/base.rb#L252
        def class_collisions(*class_names)
          return unless behavior == :invoke
+         return if options.skip_collision_check?

          class_names.flatten.each do |class_name|
            class_name = class_name.to_s
            next if class_name.strip.empty?
            # Split the class from its module nesting
            nesting = class_name.split("::")
            last_name = nesting.pop
            last = extract_last_module(nesting)

            if last && last.const_defined?(last_name.camelize, false)
              raise Error, "The name '#{class_name}' is either already used in your application " \
-                          "or reserved by Ruby on Rails. Please choose an alternative and run "  \
-                          "this generator again."
+                          "or reserved by Ruby on Rails. Please choose an alternative or use --skip-collision-check "  \
+                          "to skip this check and run this generator again."
            end
          end
        end

参考: Class: Module (Ruby 2.6.3)


つっつきボイス:「これはどのジェネレータ?」「rails gのジェネレータか」「multiple timesはジェネレータを複数回実行するってことなのね」「ぱっと見同時実行かと思った😆」「ジェネレータでフィールド名間違えたりしたときにbashの履歴から修正して再実行するときとかなんでしょうね」「destroyしなくてもやれるようにしたと」

rails gって自分はマイグレーションのときぐらいしか使わないけど😆、マイグレーションを再実行したらどうなるんだろう?」「マイグレーションファイルにはタイムスタンプが付くからマイグレーションは再実行してもかぶらなさそうだけど🤔」「でも同じクラスがあったらそれこそコリジョンしそうですし😆」「上のコードのメッセージにこう書いてある↓ぐらいだから、今はコリジョンするようになってるんですね☺

The name ‘#{class_name}’ either already used in your application or reserved by Ruby on Rails. Please choose an alternative and run this generator again.

「ということはこのskip-collision-checkはマイグレーションでも使えるということかな?」「ということでしょうね」「Railsチュートリアルとかだとワンライナーでマイグレーションを作れるのは便利そうですけど☺

⚓issue: Rails 6 rc2のマルチプルDBでテストが通らない

rails db:create
rails db:migrate
rails test

つっつきボイス:「こちらはPRではなくissueなんですが、rc2が出たときは空になっていたRC2のマイルストーンを見に行ったら、これが加わっていました(その後3つに増えています)」「rc1とrc2って相当差が出ましたからね〜」「マルチDBの設定が必要なのか」「これはまさにrc2でテストやってくれてありがとうというissueですね☺」「再現用のリポジトリとかも作ってくれてるとか有能」「kamipoさんが望んでいるのはまさにこれ」「私も試しに取り急ぎローカルで再現しようとしたら、なぜかポスグレのgemが自分の環境でつっかえたのでそこどまりです😅

なお、つっつきの後で伸びたスレで、#36439でのSchemaMigrationの移動が影響しているらしいという書き込みがありました。

参考: Rails 6 multiple DB test Issue - PG::ConnectionBad: connection is closed error · Issue #36743 · rails/rails

⚓Rails

⚓Railsのどこで時間がかかっているか(Hacklinesより)

#同記事より抜粋
==================================
  Mode: cpu(1000)
  Samples: 4293 (0.00% miss rate)
  GC: 254 (5.92%)
==================================
SAMPLES    (pct)     FRAME
    206   (4.8%)     ActiveRecord::Attribute#initialize
    189   (4.4%)     ActiveRecord::LazyAttributeHash#[]
    122   (2.8%)     block (4 levels) in class_attribute
     98   (2.3%)     ActiveModel::Serializer::Associations::Config#option
     91   (2.1%)     block (2 levels) in class_attribute
     90   (2.1%)     ActiveSupport::PerThreadRegistry#instance
     85   (2.0%)     ThreadSafe::NonConcurrentCacheBackend#[]
     79   (1.8%)     String#to_json_with_active_support_encoder

つっつきボイス:「パフォーマンス系でおなじみNoah Gibbsさんの記事です」「まずはプロファイリングをinstrumentationでやるか、サンプリングでやるか」

Ruby 2.5.0はどれだけ高速化したか(翻訳)

「シングルスレッドとマルチスレッドでそれぞれ測定してますね」「Noah Gibbsさんのこの記事は、どういう戦略でいくかを示してから実際に測定して切り分けていくという順序で進んでいくのがいいですね〜❤」「おぉ〜」「『推測するな。計測せよ。』を実践してる人☺

参考: UNIX哲学 - Wikipedia — 「推測するな。計測せよ。」の出典

「結論↓は普通っぽい😆」「でも測定で裏付けたという」「数値でちゃんと出したのが重要☺

  • Railsをきちんと設定したとしても、Rails CRUDアプリの時間はDBクエリやActive Recordやシリアライズにかなり食われる
  • GCはRuby 1.9よりかなりよくなっているが、それでも少なからぬ時間を食われてる → ごみオブジェクトを減らせ
  • DBのオーバーヘッドに加えて、Active Recordもかなりオーバーヘッドがある → Sequelなどのオルタナティブを検討
  • StackProfは使いやすくていいヤツなのでRubyアプリでやってみる価値あり

「Sequelか〜😅」「StackProfってNoah Gibbsさんが作ったような気がしてたけど違った😅」「@tmm1さんか」「tackProfのcontributor見ると@tenderloveさんとかいろんな人がやってるし😳」「そういえばCSVが遅遅だったときに StackProf使ったことあったわ: 今ググったら検索結果で自分がこのリポジトリをクリックした跡残ってるし😆

参考: tmm1/stackprof: a sampling call-stack profiler for ruby 2.1+

⚓LefthookとCrystalballとGitのマジックで楽に開発しよう(Ruby Weeklyより)

おなじみEvil Martiansの記事です。


lefthandリポジトリより


つっつきボイス:「crystalballはRubyKaigiにも登場してましたね→」

「それとLefthook」「お〜Gitフックマネージャか」「ボクシングの左フックにかけてるんでしょうね😆」「Overcommit↓と似たようなものをEvil Martiansが作ったと」「しかもGo言語で」「LefthookでフックをかけてCrystalballを動かすみたいな」

「Crystalballって何でしたっけ?」「影響しそうな部分だけテストするみたいなgem」「いわゆる占いの水晶玉🔮

「こういう組み合わせで速くなるといいですね〜: RSpecがGitのフックで動くともうストレスでしかないし😭」「みんなローカルでテストしてからにしてねと言いたいけど、ローカルで回しても時間かかりますし🕗」「回してる間に余裕でメシ食えるし😆」「昔のLinuxカーネルとかX11とか、帰る前にコンパイルを走らせたりしましたね〜😇」「そして翌日も終わってなかったり😆」「この記事翻訳したいです😋

⚓Evil MartiansのDocker記事


つっつきボイス:「Evil MartiansがRails+Docker記事も出してますね」「Evil Martiansはこうやったと」「たしかRailsConfでRailsアップグレードをテラフォーミングになぞらえた話をしたときのサイドストーリーということね」

「先週のDocker記事よりはイケてるかな?」「Aptfileって?」「これをcatして出てきたリストをapt-getするという😆」「あ〜なるほどっ」「大文字で始まってるから何かのパッケージかと思ったら、それをやってるだけ😆」「わかる〜😍、こういうのってちょいちょい更新したくなるし」

# 同記事より
# We use an external Aptfile for that, stay tuned
COPY .dockerdev/Aptfile /tmp/Aptfile
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    build-essential \
    postgresql-client-$PG_MAJOR \
    nodejs \
    yarn=$YARN_VERSION-1 \
    $(cat /tmp/Aptfile | xargs) && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
    truncate -s 0 /var/log/*log

「こちらのdocker-composeはちゃんとcachedやってますね😋、と思ったら記事の中でもcached説明してるし」

volumes:
  - .:/app:cached
  - bundle:/bundle
  - rails_cache:/app/tmp/cache
  - node_modules:/app/node_modules
  - packs:/app/public/packs
  - .dockerdev/.psqlrc:/root/.psqlrc:ro

「ぽすぐれのメジャーバージョンをアップグレードするなんてのはめったに起きないから最初にやっとくと」「NodeやYarnもそんなにしょっちゅう変わらないだろうし」「その次のbuild-essential、新しいのを使いたいから入れてるんじゃないかなと」「お、node_modulesをボリュームにするのか」

ARG RUBY_VERSION
# See explanation below
FROM ruby:$RUBY_VERSION

ARG PG_MAJOR
ARG NODE_MAJOR
ARG BUNDLER_VERSION
ARG YARN_VERSION

# Add PostgreSQL to sources list
RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
  && echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list

# Add NodeJS to sources list
RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash -

# Add Yarn to the sources list
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
  && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

# Install dependencies
# We use an external Aptfile for that, stay tuned
COPY .dockerdev/Aptfile /tmp/Aptfile
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade && \
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
    build-essential \
    postgresql-client-$PG_MAJOR \
    nodejs \
    yarn=$YARN_VERSION-1 \
    $(cat /tmp/Aptfile | xargs) && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
    truncate -s 0 /var/log/*log

「記事の後半でコンフィグをステップバイステップでめっちゃ丁寧に説明してくれてるし!」「エライ!🎉」「超エライ👍」「これも翻訳したいです😋

⚓docker-composeで便利そうなdip

「記事の最後にdipというツールが紹介されてますね: docker-composeで設定したアプリをCLIでインタラクティブに操作するみたいです」

asciicast

「どういうつくりなのかな?」「docker-composeを利用したときに、docker-compose runで使いたいものをgulp buildみたいにまとめられるんじゃないかしら: 以下のdip.ymlを見た感じではそれっぽいし↓」「ははぁ〜」

version: '2'

environment:
  RAILS_ENV: development

compose:
  files:
    - docker-compose.yml

interaction:
  bash:
    service: backend
    command: /bin/bash
    compose_run_options: [no-deps]

  bundle:
    service: backend
    command: bundle
    compose_run_options: [no-deps]

  rake:
    service: backend
    command: bundle exec rake

  rails:
    service: backend
    command: bundle exec rails
    subcommands:
      s:
        service: rails
        compose_run_options: [service-ports]

  yarn:
    service: backend
    command: yarn
    compose_run_options: [no-deps]

  rspec:
    service: backend
    environment:
      RAILS_ENV: test
    command: bundle exec rspec

  rubocop:
    service: backend
    command: bundle exec rubocop
    compose_run_options: [no-deps]

  psql:
    service: postgres
    command: psql -h postgres -U postgres -d example_app_dev

  'redis-cli':
    service: redis
    command: redis-cli -h redis

provision:
  - dip compose down --volumes
  - dip compose up -d postgres redis
  - dip yarn install
  - dip bash -c ./bin/setup

「interactionの下にbashとかbundleとかrailsとかyarnとかrspecとか書いてあるから、コマンド実行した後にいちいちRUNで作ったりしなくてもdip run なんちゃらでやれるとかそういう感じかなと🤔」「ちゃんと読まないとわからないけど何だかよさそう😋

「docker-composeコマンドで延々入力したくない人向けかも」「たしかにdocker-composeってコマンド名自体が長いですよね😆」「docあたりでTab補完してもdockerで止まっちゃうし😆

# 同リポジトリより
dip run rails c
dip run rake db:migrate

dip ssh upとかdip nginx upなんてのもできるみたいですし」「dip ssh upってまさにコンテナにsshするときのショートハンドか」「docker-compose.ymlにこんなふうに↓書けばこのショートハンドが使えるのね😳

services:
  web:
    environment:
      - SSH_AUTH_SOCK=/ssh/auth/sock
    volumes:
      - ssh-data:/ssh:ro

volumes:
  ssh-data:
    external:
      name: ssh_data

dip nginx upの設定もnetworksの下にexternal:を指定してるから、別のdocker-composeからアクセスできるんだろうな」「nginxプロキシを差し込めると」「frontendの方でこれに合わせておいてくれればnginxをそっちに向けるよ、と」「dip dns upなんてのもできるし😍」「これ欲しかった人いるんじゃ?😆

version: '2'

services:
  foo-web:
    image: company/foo_image
    environment:
      - VIRTUAL_HOST=*.bar-app.docker
      - VIRTUAL_PATH=/
    networks:
      - default
      - frontend
    dns: $DIP_DNS

networks:
  frontend:
    external:
      name: frontend

「たしかにdocker-composeで一度作っちゃった後でsshで入ろうとするのも閉じるのも面倒くさかったりするけど、こういう感じで一種の『付け外しできるバックドア的なツール』として使えるのかなと」「全部の機能を残らず使うかというと疑問ですけど😆」「知ってれば便利に使えそう❤」「sshとDNSとnginxプロキシ、どれもやれたらうれしいヤツだし😋」「後でちゃんと見てみようかな☺

⚓pg_search: PostgreSQLの全文検索をActive Recordで(Ruby Weeklyより)

# 同リポジトリより
class AntipatternExample
  include PgSearch::Model
  multisearchable against: [:contents],
                  if: :published?

  def published?
    published_at < Time.now
  end
end

problematic_record = AntipatternExample.create!(
  contents: "Using :if with a timestamp",
  published_at: 10.minutes.from_now
)

problematic_record.published?     # => false
PgSearch.multisearch("timestamp") # => No results

sleep 20.minutes

problematic_record.published?     # => true
PgSearch.multisearch("timestamp") # => No results

problematic_record.save!

problematic_record.published?     # => true
PgSearch.multisearch("timestamp") # => Includes problematic_record

casebookというニューヨークの会社が作っているgemです。


casebook.netより


つっつきボイス:「pg_searchは前からあるgemで、ずっと前にウォッチでちょっとだけ取り上げたことがありました」「ぽすぐれのフルテキストサーチですね☺」「名前は聞いたことありますし☺」「multisearchableとな😆」「こういう感じの名前って昔のgemによくありましたね」「最近流行りませんけど😆

# 同リポジトリより
class EpicPoem < ActiveRecord::Base
  include PgSearch::Model
  multisearchable against: [:title, :author]
end

class Flower < ActiveRecord::Base
  include PgSearch::Model
  multisearchable against: :color
end

「pg_searchはPostgreSQLに何か拡張とか入れる必要あるのかな?」「READMEにはPostgreSQLの全文検索機能を利用すると書いてあるから、なしでもできそうですね」

「Ransackとかとは違うヤツでしょうか?」「全然違いますね😆」「ransackは全文検索とはアルゴリズムがまるで違いますし」「そうでしたか😅」「言ってみればRansackは、Arelを組み立てやすくするためのgemですから」「検索条件を組み立てやすくするものなんですね」「もしかするとpg_searchもRansackと併用できるかもしれないけど」「わかんないけどできそう😆


より

⚓better_errorsを高速化

とても短い記事です。


つっつきボイス:「better_errorsが遅いとかあまり思ったことないけど😆」「でっかいオブジェクトを巻き込んじゃうと遅くなったりするみたいですね」「そんなこともあるのかと😆

「あ〜なるほど、inspectがすごくでっかくなっちゃうときか」「そういうときは早々と諦めると😆」「if inspected.size > 20_000のときに諦めるのね😆

⚓その他Rails


つっつきボイス:「エントリ増やしすぎないようにと思いつつ貼りました😅」「Active RecordをやめてROM(Ruby Object Mapper)にしてみたと」「まあたしかにActive Recordはヤバいくらい多機能ですけどね☺

  • サイト: ROM


rom-rb.orgより

⚓勉強会・書籍


つっつきボイス:「『Rails 6リリースするかも?Party at Speee?』の特別ゲストのリストを見ると、先日の銀座Rails#11↓のときにいた人が全員いるというあたりで既視感が蘇ってきました🤣」「そうそう、全員いましたし🤣」「へぇ〜!」


roppongirb.connpass.comより

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

「Meguro.rbはSlackを開設してるので取りあえず入ってみました😋

「3つ目の書籍は、これも銀座Rails#11で発表していたken1flanさんが出してる本だそうで、新人教育でセキュリティホールの空いたアプリを触ってもらって実地に学ぶというのをやってるそうです」「『ちゃんと作らないとこうなるよ』と😆」「セキュリティはこうやって実際に見せるとわかりやすいですよね☺


前編は以上です。

おたより発掘

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

週刊Railsウォッチ(20190722-1/2前編)Rails 6エラー画面の改良点、Dateを四捨五入できるtime_calc、Rackミドルウェアのデザインパターンほか

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines

週刊Railsウォッチ(20190805-1/2前編)Rails 6のActive Recordは速くなった、Windows WSL2+VSCodeでのRails開発、Martin Fowler記事ほか

$
0
0

こんにちは、hachi8833です。7payが9月に静かに息を引き取ることが先週決まったそうです。

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

今回は「公開つっつき会」第13回を元にしています。ご参加いただいた皆さま、ありがとうございます!

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

6.0.0リリースが迫りつつありますね。今回は主に以下から見繕いました。

6-0-stableブランチはもう立っていますね。


つっつきボイス:「Rails 6、8月になったしもうそろそろかな😋」「マイルストーンはいったんゼロになったんですが、今日見たら6つになってました(その後5つになりました)」「なるほど、rc2でissue増えましたか」「@kamipoさんが『rc2のうちに使ってみてね』と呼びかけてたのが功を奏した感じ🙏」「以下は基本的にmasterへのマージですが、ものによってはバックポートされるかなと」「致命的なissueならバックポートされるかもですね☺

⚓レスポンスにVary: Acceptヘッダーを追加

/users/1のようなリクエストでは、返すべきものを決定するのにAcceptヘッダーを用いる。そのレスポンスヘッダーにVaryを追加しないと、ブラウザが誤って別の種類のコンテンツをキャッシュする可能性があり、コンテンツの代わりにJavaScriptコードが露出するかもしれない。このプルリクは、そのようなクエストにVary: Acceptを追加することで修正する。詳しくは#36213を参照。
Stan Lo
changelogより大意

# actionpack/lib/abstract_controller/rendering.rb#L17
  module Rendering
    extend ActiveSupport::Concern
    include ActionView::ViewPaths
    # Normalizes arguments, options and then delegates render_to_body and
    # sticks the result in <tt>self.response_body</tt>.
    def render(*args, &block)
      options = _normalize_render(*args, &block)
      rendered_body = render_to_body(options)
      if options[:html]
        _set_html_content_type
      else
        _set_rendered_content_type rendered_format
      end
+     _set_vary_header
      self.response_body = rendered_body
    end
# actionpack/lib/action_controller/metal/rendering.rb#L80
+     def _set_vary_header
+       self.headers["Vary"] = "Accept" if request.should_apply_vary_header?
+     end
# actionpack/lib/action_dispatch/http/mime_negotiation.rb#L150
+     def should_apply_vary_header?
+       !params_readable? && use_accept_header && valid_accept_header
+     end
...
      private
...
+       def params_readable? # :doc:
+         parameters[:format]
+       rescue *RESCUABLE_MIME_FORMAT_ERRORS
+         false
+       end

Rails 4.2.1+Ruby 2.1.7p400でAcceptヘッダーを追加すると、レスポンスにVary: Acceptヘッダーが含まれるはずが、すべてのレスポンスがuser agent(ブラウザ)側から等しいとみなされてキャッシュの問題が発生する。以下も参照。
94369 - Backing doesn’t handle Vary header correctly - chromium - Monorail
issueより大意

Goby言語の@st0012ことStan Loさんによるコミットです。


同リポジトリより


つっつきボイス:「Varyヘッダーってそういえばあったかも🤔」「こんなのがあったとは↓」

参考: RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content
日本語: RFC 7231 — HTTP/1.1: Semantics and Content (日本語訳)

「解説記事探す方が早そう↓」

参考: Vary - HTTP | MDN

Vary HTTP レスポンスヘッダは、オリジンサーバから新しく要求するのではなく、キャッシュされたレスポンスを使用できるかどうかを決定するために将来のリクエストヘッダをどのように一致させるかを決定します。これは、コンテンツネゴシエーションアルゴリズムでリソースの表現を選択するときにどのヘッダを使用したかを示すためにサーバによって使用されます。
developer.mozilla.orgより

「もしかすると、AWS Cloudfrontとかでキャッシュキーに指定すると参照されるのかも?」「この記事がまさにそれっぽい↓」

参考: コンテンツキャッシュとVaryヘッダとnginx - Qiita
参考: Amazon CloudFront(グローバルなコンテンツ配信ネットワーク)| AWS


aws.amazon.comより

「スマホ向けとか?」「スマホもそうでしょうし、Accept-Languageヘッダーで言語別にキャッシュしたりとかもあるでしょうね☺: URLは同じだけど言語が違う場合にもキャッシュを効かせたいときとか」「Varyで指定したヘッダーの組み合わせをキャッシュのキーに使うというもののようだ」

参考: Accept-Language - HTTP | MDN

「そしてプルリク#36213はというと、『/users/1のようなリクエストでは、返すべきものを決定するのにAcceptヘッダーを用いる』、あ〜たしかに!」「おぉ?」「Railsの通常のリクエストフォーマットだと、Acceptヘッダーで指定した内容に基づいて、JSONを返したりHTMLを返したりします: そしてVaryを指定しないと違うものをキャッシュしちゃうかもしれないので、このプルリクでVary: Acceptを追加したと」「これはごもっともな修正!」

「issueを見ると、Rails 4で見つかったとありますね」「全員ではないにしても、ハマってた人はおそらく以前からいたでしょうね😅」「あ〜」「主にどこで問題になるのかですけど、たぶんブラウザというよりはコンテンツプロキシの方じゃないかな〜?🤔

後にst0012さんに尋ねたところ「主にブラウザが対象のつもりだった」とのことでした。

参考: カスタムオリジンの場合のリクエストとレスポンスの動作 - Amazon CloudFront

オリジンが応答で Vary:* を返し、対応するキャッシュ動作の [Minimum TTL] の値が [0] の場合、CloudFront はオブジェクトをキャッシュしますが、そのオブジェクトの後続のすべてのリクエストをオリジンに転送して、オブジェクトの最新バージョンが含まれていることを確認します。CloudFront には、If-None-Match や If-Modified-Since などの条件付きヘッダーは含まれません。その結果、オリジンはすべてのリクエストに応じて CloudFront にオブジェクトを返します。
オリジンが応答で Vary:* を返し、レスポンスで返される、対応するキャッシュ動作の [Minimum TTL] の値が別の値になっている場合、CloudFront は「CloudFront が削除または更新する HTTP レスポンスヘッダー」に記述されている方法で Vary ヘッダーを処理します。
docs.aws.amazon.comより

「CloudFrontはVaryヘッダー対応しているのかな?と思ったら『対応してない』という2015年のブクマコメント↓が目に止まったけど、実際どうなんだろう?🤔

※コメントを直接埋め込めなかったので、「〜のコメント」をクリックしてください。

コンテンツキャッシュとVaryヘッダとnginx – Qiita

ちなみにCloudFrontはVaryヘッダについて感知しない。Accept-EncodingとForward Headers設定単位でキャッシュする。

2015/12/23 22:05

その後社内での記事見直しで、『CloudFrontはあくまでCDNに対して「どのヘッダをキャッシュキーにするか」を設定する必要があるので「Originサーバーが任意にキャッシュキーを設定できる」Varyヘッダの動きはしてくれないであろう』と推測されましたが、このあたりに詳しい方の情報お待ちしています🙇

「もしかすると、ウサギさんアイコンでおなじみのVarnishあたりならVaryに対応してるかな?」「以前ウォッチで取り上げましたね🐰」「Varnishは爆速コンテンツキャッシュサーバーですけど、最近はあまり自前では建てなくなったかな〜」「ともあれVaryヘッダーはその種のコンテンツキャッシュサーバーとかnginxとかに関連するヤツですね」

参考: Vary — Varnish version 3.0.7 documentation


varnish-cache.orgより

「コミットした@st0012さんも『自分的にクリーンヒットだったと思う』と言ってました」「このissueは知らずに踏んだら正体不明になりがち😆」「原因突き止めるのめちゃ大変そう…😅

⚓MatchDataを作らないMime::Type\#match?を追加

# actionpack/lib/action_dispatch/http/mime_type.rb#L286
+   def match?(mime_type)
+     return false unless mime_type
+     regexp = Regexp.new(Regexp.quote(mime_type.to_s))
+     @synonyms.any? { |synonym| synonym.to_s.match?(regexp) } || @string.match?(regexp)
+   end

つっつきボイス:「amatsudaさんのコミットなんですが、次の2つは最初のに関連しているのかなと思って」「=~をやめてmatch?メソッドに変えたのね」

# actionpack/lib/action_dispatch/http/mime_type.rb#L204
      def parse_data_with_trailing_star(type)
-       Mime::SET.select { |m| m =~ type }
+       Mime::SET.select { |m| m.match?(type) }
      end

「これは高速化かしら?」「コミットには特に説明ありませんね😅」「たしか=~はグローバル変数を書き換えるけどmatch?は書き換えない分速いとかがあったような?」「そういえば前にウォッチでも取り上げたような」

こちらでした↓。2.4で入ったんですね。

参考: Ruby 2.4 implements Regexp#match? without polluting global variables | BigBinary Blog

=~は自分が見つけた記事では2.6でdeprecatedになってる?」「おお?」「もうそこまで進んでた?」「自分、割と使ってますけど」「便利ですよね😋」「=~の順序、どっちが先かわからなくなりがちですけど😆」「順序変わると意味変わりますし☺」「それにしても記号はググりにくいの〜😅

参考: サンプルコードでわかる!Ruby 2.6の主な新機能と変更点 - Qiita

「おや、でも記事にnil=~は明示的に実装されたとありますし、どうやら2.6でdeprecatedになったのはObject#=~だけらしい」「あ、そういうことか!」「Object#=~はなまじnilを返すんじゃなくて、NoMethod errorを出して欲しいと」「なので=~自体はなくならない😋」「@mameさんのチケット↓を見ても、Object#=~の話はあってもRegexは直接関係なさそうだし」

参考: Feature #15231: Remove `Object#=~` - Ruby master - Ruby Issue Tracking System

⚓frozen文字列がtransliterateされたときのエラーを防止

ActiveSupport::Inflector.transliterateはエンコードが変更されると文字列を改変する。#36702から本コミットまでの間は、frozen文字列を渡すとFrozenErrorが発生することがあった。transliterateする前にはfrozen文字列をdupするよう変更した。
同PRより大意

# activesupport/lib/active_support/inflector/transliterate.rb#L63
    def transliterate(string, replacement = "?", locale: nil)
+     string = string.dup if string.frozen?
      raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)


つっつきボイス:「修正は文字列がfreezeされてた場合に対処したと」

「ところで今のRubyはもうfrozen文字列ってデフォルトになったんだっけ?」「えっとまだだったかな?3.0で入るんだったかな?😅」「こないだRuboCopに怒られたような気が😆

参考: [Ruby] Ruby 3.0 の特大の非互換について - まめめも — 2015年の記事です

その後BPS社内Slackで、「最終的にRuby 3ではfrozen-string-literalsをデフォルトにしないことになった」↓と指摘をもらいました。

参考: Ruby3.0とRails6.0では主にどのような変更や新機能が加わりますか?登場の際、開発、保守、運用、コミュニティ、その他言語とのポジション(特にPython2と3)はどんな変化が考えられますか? - Quora
参考: Feature #11473: Immutable String literal in Ruby 3 - Ruby master - Ruby Issue Tracking System

I consider this for years. I REALLY like the idea but I am sure introducing this could cause HUGE compatibility issue, even bigger than Ruby 1.9. So I officially abandon making frozen-string-literals default (for Ruby3).
This does not mean we are going to remove the frozen-string-literal feature that can be specified by magic comments.
Matz.
#11473より

⚓transliterateよもやま

「しかしtransliterateってそもそも何やねん🤣」「えっと、Rubyをルビーと書くとか、改善をKaizenと書くみたいに外国語の音を写し取ることを一般にtransliterateって言いますね」「へぇ〜」「そんなメソッドがあったとは😆

transliterate v. (他国語の文字などに)書き直す; 音訳[字訳]する.

参考: ActiveSupport::Inflector — なお、APIdockでは文字化けしてます😇

transliterate('Ærøskøbing')
# => "AEroskobing"

「ロシア語を英語表記にするみたいなときに使いそうですね」「APIには『非ASCII文字をASCII文字で近似したりする』とありますね」「なるほど、アクセント記号付きのアルファベットをアクセント記号なしにするとか」

参考: ダイアクリティカルマーク - Wikipedia

transliterate、その気になればジャパニーズ実装もできそう😋」「yamlファイルでできるみたいですね」「関西弁オプションとか🤣」「🤣」「日本語のローマ字、大きく分けただけでもヘボン式とか文科省がやってるヤツとかいろいろバラついてて難しそう😅」「transliterate、日本人にはピンとこないけどヨーロッパの人はとても欲しいでしょうね」「使うシチュエーションがあまり思いつきませんしっ😆

参考: ヘボン式ローマ字 - Wikipedia
参考: 日本式および訓令式 - Wikipedia

⚓Azureにもファイル名とdispositionをアップロード

# activestorage/lib/active_storage/service/azure_storage_service.rb#L20
-   def upload(key, io, checksum: nil, content_type: nil, **)
+   def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
      instrument :upload, key: key, checksum: checksum do
        handle_errors do
-         blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type)
+         content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
+
+         blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition)
        end
      end
    end


つっつきボイス:「dispositionはContent-Dispositionヘッダで合ってます?」「合ってると思います: IEではこれを付けないと…😇というヤツ」「それかっ!」「PR自体はAzureのStorageにアップロードするコードにmeta情報を付けられるようにしたということみたい」

参考: BK通信 - ブラウザのバッドノウハウ コンテンツ編

「IEはContent-Dispositionでファイル名を渡すんですが、FirefoxやChromeは普通にfilenameで渡せばUTF-8文字列でもちゃんと食べてくれる😋: Content-Dispositionが本来何をするものなのかよくわかってませんが😆」「IE対応という印象しかないというか、IEのファイルダウンロードはこれを設定しないとだいたいおかしくなる😢

参考: Content-Disposition - HTTP | MDN

通常の HTTP レスポンスにおける Content-Disposition レスポンスヘッダーは、コンテンツがブラウザでインラインで表示されることを求められているか、つまり、Webページとして表示するか、Webページの一部として表示するか、ダウンロードしてローカルに保存する添付ファイルとするかを示します。
developer.mozilla.orgより

Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"

「今更MDNを見てみると↑、コンテンツをインラインで表示するか添付ファイルとしてダウンロードするかを指定するものだったのね😆」「PDFでinlineを指定するとブラウザの中でPDFが開いたり、attachmentを指定するとファイルとして保存したり、ということをやるためと」「そしてfilename=というオプションが使えると: IEはこっちを優先しちゃってたりするのかもな〜」

「これも関連してそうですね↓」「こっちはActive Storageのストレージごとのオプションを実装したと」「こんな実装なのか↓: 引数最後にある**が、以後の引数を全部捨てにかかってる感😆」「互換性維持とかのために、知らないオプションがやってきてもよしなに握りつぶす😆」「もしかすると**でやれば仮引数代入が発生しない分ちょっぴり速くなったりするかな〜、なんて😆」「どちらかというと、指定にない引数を付け呼び出してもArgumentErrorが出ないで処理してくれる方が重要だったりするかも🤔

# activestorage/lib/active_storage/service/s3_service.rb#L23
-   def upload(key, io, checksum: nil, content_type: nil, **)
+   def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
      instrument :upload, key: key, checksum: checksum do
+       content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
+
        if io.size < multipart_upload_threshold
-         upload_with_single_part key, io, checksum: checksum, content_type: content_type
+         upload_with_single_part key, io, checksum: checksum, content_type: content_type, content_disposition: content_disposition
        else
-         upload_with_multipart key, io, content_type: content_type
+         upload_with_multipart key, io, content_type: content_type, content_disposition: content_disposition
        end
      end
    end

「そういう書き方ってしてなかったので参考になる〜😋」「他人が書いたのを見ないと知らないままになりそう」

⚓Issue: Sidekiqがclassicオートローダーでコケる


つっつきボイス:「6.0.0のマイルストーンから1つだけissueを拾ってみました」「Zeitwerkからクラシックオートローダーに切り替えると、よりによってAction Textでデッドロックする!」「よく見つけたな〜これ😳

「ぱっと見、Action Textだからビューに関連してるかと思ったけど、Sidekiqジョブを投入する側のオートローダーとジョブを処理するワーカー側のオートローダーが異なるとおかしくなるらしい」「スレを見ると、productionでは起きずdevelopmentで起きるとか、Sidekiqプロセスのコンカレンシーを1にすると回避できるとありますね」

Sidekiqログ: https://gist.githubusercontent.com/langsharpe//Sidekiq.log

「これがSidekiqのログ↑」「冒頭に絵が入ってくるんですね😆

$ bundle exec sidekiq


         m,
         `$b
    .ss,  $$:         .,d$
    `$$P,d$P'    .,md$P"'
     ,$$$$$bmmd$$$P^'
   .d$$$$$$$$$$P'
   $$^' `"^$$$'       ____  _     _      _    _
   $:     ,$$:       / ___|(_) __| | ___| | _(_) __ _
   `b     :$$        \___ \| |/ _` |/ _ \ |/ / |/ _` |
          $$:         ___) | | (_| |  __/   <| | (_| |
          $$         |____/|_|\__,_|\___|_|\_\_|\__, |
        .d$$                                       |_|

2019-07-26T06:55:53.814Z 17273 TID-oxdgbihcp INFO: Running in ruby 2.6.2p47 (2019-03-13 revision 67232) [x86_64-darwin18]
2019-07-26T06:55:53.814Z 17273 TID-oxdgbihcp INFO: See LICENSE and the LGPL-3.0 for licensing details.
2019-07-26T06:55:53.815Z 17273 TID-oxdgbihcp INFO: Upgrade to Sidekiq Pro for more features and support: http://sidekiq.org
2019-07-26T06:55:53.815Z 17273 TID-oxdgbihcp INFO: Booting Sidekiq 5.2.7 with redis options {:id=>"Sidekiq-server-PID-17273", :url=>nil}
2019-07-26T06:55:53.872Z 17273 TID-oxdgbihcp INFO: Starting processing, hit Ctrl-C to stop
...

「再現手順をチラ見した感じでは割と特殊なことやってるっぽいけど😆」「Zeitwerkの状態でジョブを実行してコンプリートしてから、クラッシックオートローダーに切り替えると、起きると」「逆はやってないけどやったらどうなるんだろう😆」「よくぞ見つけた👍」「6にアップグレードするときはSidekiqあたりに注意が必要かも」

⚓amatsudaさんがアロケーションなどを削減

ほとんどが細かなリファクタリングです。

# activesupport/lib/active_support/inflector/methods.rb#L378
-       parts.reverse.inject(last) do |acc, part|
+       parts.reverse!.inject(last) do |acc, part|
          part.empty? ? acc : "#{part}(::#{acc})?"
        end

つっつきボイス:「@amatsudaさんがアロケーションを削減したりした細かなコミットがたくさんあって、それぞれやってることが少しずつ違ってて参考になりそうだったので」「ひたすら黙々と」「こういう感じで書くことでメモリコピーが減るので、やって大丈夫であれば有効でしょうね」「たくさん並べましたが、今つっつきで全部開けると時間かかりそうなので適当なところまででいいです😅

「こういうふうに*で置き換えたりも↓」「引数を単独の*にするのって意味的にはどういうものでしたっけ?」「メソッドシグネチャとしては受けられるけど中では使わない、ということでしょうね」「さっきの**もそうですし」「渡してもinvalid argumentとかにはならないけど、使われない」

# actionpack/lib/abstract_controller/callbacks.rb#L40
-   def process_action(*args)
+   def process_action(*)
      run_callbacks(:process_action) do
        super
      end
    end

⚓Rails


つっつきボイス:「上のツイートは本日(公開つっつき会の日)のついさっき見つけました」「自分もさっき社に戻る途中で見た😋」「@kamipoさん半端ないわやっぱ!」「Rails 6だいぶ速くなったみたいですし」「オブジェクトアロケーション系の操作って遅くなりやすいから、そういうところなんかも速くなるのはうれしいですね😂」「このベンチ結果を眺めながらrc2で遊ぶとはかどりそう❤


rubybench.orgより

⚓ポリモーフィック関連付けでのeager loading

2017年の記事です。


つっつきボイス:「コードが9割を占めている感じの記事でした」

流れ:

  • ポリモーフィック関連付けのeager loadingでN+1を避けるのは面倒
  • (普通のサンプル)
  • 動かすとN+1が発生
  • preloadで即解決
  • (ポリモーフィック関連付けのサンプル)
  • ちょい面倒
  • ActiveRecord::Association::Preloaderを書いて解決
  • ActiveRecord::Association::Preloaderpreloadと組み合わせて解決

「この辺、最近のRailsだともうちょっとよくなってたような覚えがうっすらと」「N+1が起きなくなるんでしょうか?」「いや、起きるんだけど効率がもう少しよくなってた気がする: あれはどこでやってたかな〜?🤔

「普通にやってN+1が起きたらpreloadでIN展開できる」

SELECT "films".* FROM "films"
SELECT "directors".* FROM "directors" WHERE "directors"."id" IN (...)

「ポリモーフィック関連付けではPreloaderを手作りしてる」「え、こういう書き方してるのか〜😳」「でもちゃんとINに展開されてますね」

class Like::Preloader
  def self.preload(likes)
    preloader = ActiveRecord::Associations::Preloader.new
    preloader.preload(likes.select { |like| like.resource_type.eql?(Book.name) }, { resource: :author })
    preloader.preload(likes.select { |like| like.resource_type.eql?(Film.name) }, { resource: :director })
  end
end
SELECT "likes".* FROM "likes"
SELECT "books".* FROM "books" WHERE "books"."id" IN (...)
SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (...)
SELECT "films".* FROM "films" WHERE "films"."id" IN (...)
SELECT "directors".* FROM "directors" WHERE "directors"."id" IN (...)

「たぶんですけど、ポリモーフィック関連付けだけの問題じゃないんでしょうね: Bookだとauthor.nameを取ってFilmだとdirector.nameを取るとかしないといけなさそうですし、ポリモーフィック関連付けをポリモーフィックなまま1つのpreloadでやれるようにするということのようだ🤔」「ポリモーフィックでのN+1、自分も以前踏んだことあったような😅

profiles = Profile.preload(:likes)
Profile::Preloader.preload(profiles)
profiles.each do |like|
  puts profile.name
  profile.likes.each do |like|
    puts like.resource.name
    puts case like.resource
    when Book like.resource.author.name
    when Film like.resource.director.name
    end
  end
end

「まあここまでやるか?という気もちょっとしますけど😆」「諦めてクエリを複数に分けるとかする手も」

⚓高品質ソフトウェアのためにコストをかける価値はあるか

Martin Fowlerさんの記事です。


つっつきボイス:「今回はMartin Fowlerさんが自分で書いてますね☺」「お、この図どっかで見たことあるし↓: 図だけで思い出すと、コードをきちんと書いてリファクタリングもしておくと、全体のコード量はあまり進まなくても将来新機能をラクに追加できるようになるので、時間をかけてでもちゃんと書く方が長い目で見て結果的に時間を節約できる、みたいな説明だったかな」「たしかに〜」「深みがある」


同記事より

「cruit…?」「英辞郎によると、cruitは新兵とか新人という意味で、recruitを略したものらしいけど、ここでは作るのに時間のかかる新機能という意味で使われてるっぽい」

参考: cruitの意味・使い方|英辞郎 on the WEB:アルク

「この図↓とかまさにそれで、この変曲点を超えるかどうかという話」「品質の低いコードを急いで書いても、時間をかけて品質の高いコードを書く方が将来機能が増えたときの品質も上がるし開発速度も上がるよと、その変わり目となる変曲点ですね😋


同記事より

「もうひとつ、手掛けているサービスがこの分岐点を超えるほど生きながらえるのかという問題とのバランスですよね🤣」「🤣」「🤣」「まあでも実際そうで、製品がリリースされるまでに開発費用が底をついたらおしまいですし💸、どんなに目も当てられないようなコードであってもまずはリリースできなければ価値ゼロですし」「このさじ加減がなかなか難しい😅」「冒頭の、9月に終わるサービスみたいな😆」「特にベンチャーがVCから資金を受けて開発しているような場合はタイムリミットを越えたら投資を引き上げられるので、そういう条件下だと別の力学が働き始めるといいますか😆」「そういう場合でなければこの図のように考えたいですね☺

⚓Railsマイグレーションのちょっとしたコツ(Hacklinesより)

短い記事です。


つっつきボイス:「schema_migrationsテーブルを操作するためのつらい方法とラクな方法」「delete from schema_migrationsとか普通やらなくない?🤣」「db:rollbackでやりましょう🤣

# 同記事より
delete from schema_migrations where version in (20190726133807, 20190726191446);

「ラクな方はdb:rollbackでやると記事にあるけど先に言われちゃいましたね😆

rake db:rollback

「うっかりブランチ切り替えたときなんかに、殺したくないテーブルがあるからそれを避けて手動で消すとかありそうですけど😆」「ありがちなのは、マイグレーションコードの中にエラーを仕込んで途中で止まっちゃったときとか、add columnsされたものを手動でちまちま消さないと次のマイグレーションができなくてイラつくとか😆

「そうそう、db:migrate:downでバージョン指定できる↓…ってこれRailsガイドに書いてあるんじゃ?😆」「ありますね〜😆」「tipsでもtrickでもなく、仕様でした😆」「hard wayの方は…」「hard wayはつらいヨ、とだけ言っておきます😭

rake db:migrate:down VERSION=20190726133807

参考: 4.4 特定のマイグレーションのみを実行する Active Record マイグレーション - Rails ガイド

⚓WSL2とVScodeでのRails開発はイイゾ(Hacklinesより)


つっつきボイス:「お、WSL2」「中身まだ読んでませんが、喜びが伝わってくる感じです」「今の自分は、WindowsのDockerコンテナでRailsを動かして、RubyMineで書いたコードをそこにコピーして普通にやれてます☺」「そういえば今日Docker for Windowsの新しいstable版も出てDockerのバージョンも上がったし、Docker DesktopのDockerバージョンも19.03になったので、やった幸せになれるという感じ❤」「先週話題にした19.03ですね(ウォッチ20190731)」「docker-composeのバージョンが上がったのが特にありがたい🙏

参考: Docker Desktop for Mac and Windows | Docker

「もうWindowsでもDocker動かせばRails開発できますヨ😋」「最近のVSCodeにはRubyMineみたいなオートデプロイメント機能があったと思いますし」

「ちなみにWSL2を使ってる人はこの中にいらっしゃいます?」「お、まだ少ない」「使ってみたいとは思ってるんですけど😅」「Windows 10のHomを使ってる人ならWSL2欲しいですよね😋: ProならHyper-V使えますけど」

参考: Windows 10 Home と Pro はどう違うの?比較表などで解説するよっ! | Tanweb.net


前編は以上です。

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

週刊Railsウォッチ(20190730-2/2後編)Docker 19.03の新機能に注目、ngrokはスゴい、redis-namespaceほか

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

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

Rails公式ニュース

Hacklines

Hacklines

週刊Railsウォッチ(20190806-2/2後編)RSpec CopのLeakyConstantDeclaration、serveoでゼロコンフィグ公開、RuboCopのPerformance/RegexpMatch改修ほか

$
0
0

こんにちは、hachi8833です。来週の週刊Railsウォッチはお盆休みのためお休みをいただきます🙇。先祖の供養を忘れずに。

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

今回は「公開つっつき会」第13回を元にしています。ご参加いただいた皆さま、ありがとうございます!

⚓Ruby

⚓awesome-ruby.com: 巨大なおすすめgemリストサイト(Ruby Weeklyより)


同サイトより

巨大すぎてつっつき中にチェックしきれないのは確実です。Awesome Rubyという別のサイト↓もあるのでややこしいですね。


つっつきボイス:「Awesomeなんちゃらって流行ってましたよね😆」「特にGitHub Trendingにやんなるほどありますね😆

「とりあえずこういうのやるなら年度付けて欲しい😆」「たしかに〜😆」「こういうのは、いつのAwesomeほげかですごく変わりますし😆」「年と、あと月も欲しいです」

後で気づきましたが、リポジトリは★10000越えてますね😳。issueには追加リクエストがずらりと並んでいます。


同リポジトリより

⚓RuboCop1.4.1のPerformance/RegexpMatch改修

#5589のフォロー。
このPRは、ガード条件のif分岐でMatchDataが検出されない場合にPerformance/RegexpMatchが以下のようにfalse negative(偽陰性)になる問題を修正する。
この問題はこのコードで検出された。@kamipoさんからのフィードバックに感謝。

% cat example.rb
# frozen_string_literal: true

raise unless ex.message =~ /can't modify frozen IOError/

% bundle exec rubocop --only Performance/RegexpMatch
Inspecting 1 file
.

1 file inspected, no offenses detected

他に#5569のコメントもフォローした。
このPRは、ガード条件の後にMatchDataが存在する場合にPerformance/RegexpMatchがfalse negative(偽陽性)になる問題を防止する。以下はfalse negativeの例。

def foo
  return if /re/ =~ str

  # `Regexp.last_match[1]` cannot be referenced if changed from `=~` to `match?`.
  do_something(Regexp.last_match[1])
end

#73より大意


つっつきボイス:「@kamipoさんが喜んでるツイートを見かけたので拾いました」「performance copのfalse negativeとfalse positiveを修正したのね😋」「こういうのが前は通ってたのか〜」「どこにあるのかなと思って最初うっかりRuboCop本家を探しちゃったんですけど、rubocop-performanceにありました😅」「今のRuboCopは分かれてますね☺」「rubocop-performanceを-aでざざっとオートコレクトするのは便利だったりしますよね」「怒られた箇所を全部修正するのは大変ですけど😅

現在の主なRuboCopリポジトリを拾ってみました。

⚓ブロック内の定数がトップレベルから参照できる問題(Ruby Weeklyより)

# 同記事より
    A = Class.new do
      B = 1
    end
    A::B # NameError: uninitialized constant A::B
    B # => 1

つっつきボイス:「leaky constants?」「ブロックの中で定数を宣言するとトップレベルの定数になるんだそうです」「つかClass.newってブロック取れるんだ!😳」「Module.newとかClass.newブロック取れたと思いますけど、Object.newだとブロック取れるかな?」

なお、後でRuby 2.6.3+Pryで試すとObject.newもブロックを取ることは一応できました↓。ただしObject.newのブロック内で宣言した定数にはトップレベル/名前空間どちらからもアクセスできませんでした。

» E = Object.new do
»   F = 3
» end
#» #<Object:0x000055f0d6492290>
» E::F
TypeError: #<Object:0x000055f0d6492290> is not a class/module
from (pry):14:in `__pry__'
» F
NameError: uninitialized constant F
from (pry):15:in `__pry__'

Class.newの中で宣言したモジュール名やクラス名もトップレベルになると↓」

# 同記事より
    A = Class.new do
      module B
      end
      class C
      end
    end
    A::B # NameError: uninitialized constant A::B
    A::C # NameError: uninitialized constant A::C

「そしてモジュールの中でClass.newすると、トップレベルではなくそのモジュールのレベルになる↓…」「うひょ〜😅

# 同記事より
    module M
      A = Class.new do
        B = 1
      end
    end
    M::A::B # NameError: uninitialized constant M::A::B
    M::B # => 1

「この辺の挙動がRSpecで問題になることがあるっぽいです」「このRSpecコード↓だと名前空間がかぶるということかな」「記事では上と下が逆順で実行されるとsuperclass mismatchが起きるそうです😇」「特定のテストだけで使うモックの名前がどちらもトップレベルでかぶると上書きしちゃうみたいな」「ありそう〜😅

# 同記事より
    # spec/a_spec.rb
    RSpec.describe A do
      class DummyClass < described_class
      end

      it { ... }
    end

    # spec/b_spec.rb
    RSpec.describe A do
      class DummyClass
      end

      it { ... }
    end

「たしかにこれ知らずに踏みそう😇」「ぱっと見にわかりづらいけど😅」「実行順序をランダムにしておくとたまに起きて、実行順序を固定すると起きなくなるみたいな😅」「しかも、たとえテストが動いたとしても実はちゃんとテストできてないという😆

「これ、仕様としてどうなんでしょう?🤔」「どうなんでしょう〜😅」「モジュールの中とかではなくて.newブロックの中だからいいのかなという気もしますけど😅: 定義スコープではないところでの定数宣言ですし」「単なるブロックの中での定数宣言だから、グローバルな定数という扱いになるというか」「インデントだけ見ると軽くギョッとしますけど😆

「というような問題をRSpec CopのLeakyConstantDeclaration↓で検出できるようになったと記事にありますね😋」「ほほぉ〜😍

⚓valvat: VAT番号をバリデーション(Ruby Weeklyより)

# 同リポジトリより
Valvat.new("DE345789003").valid?
=> true or false

Valvat.new("DE345789003").exists?
=> true or false or nil

Valvat::Syntax.validate("DE345789003")
=> true or false

つっつきボイス:「VAT番号ってこれで初めて知りました」「VATはヨーロッパの国で使われてるヤツですね☺」「納税のために企業に付けられる番号だとか」「海外旅行とかでレシートを集めて申請すると後で還付されるとかそういう感じだったかな?」「自分らに関係あるとしたらそういうときぐらいかも🤔」「あとは個人輸入するときにこういうのを税務署申請するとかもありそう」「ヨーロッパの人ならこういうgem欲しいでしょうね☺」「EU圏内に限った話のようですし」

参考: 付加価値税登録番号(VAT number) - Wikipedia
参考: 海外通販では、VAT(付加価値税)を請求されるの!? | HUNADE EPA/FTA/貿易ガイダンス


「ついでに日本にもマイナンバーのgemあるかなと思って探したんですが、意外にも以下のtsubakiというgemぐらいしか見つからなくて😅」「このgemはシンプルなマイナンバーの番号チェッカーのようですね☺

参考: マイナンバーを検証する gem を作った - Qiita
* リポジトリ: kufu/tsubaki: My Number & Corporate Number validator


同リポジトリより

# 同リポジトリより
# Verifies the format and its check digit with `strict` option:
validates :digits, my_number: { strict: true, allow_blank: true }

# Without strict option, it verifies only the length of the digits:
validates :digits, my_number: true

# Or if a My Number contains any dividers, specify it:
validates :digits, my_number: { strict: true, divider: '-' } # => 4652-8126-6333 should be valid

なお、同リポジトリに以下のサイトも紹介されていました。

参考: 擬似マイナンバーくん

⚓その他Ruby

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

⚓ECSがロードバランサーのターゲットグループを複数サポート

はてブで見つけました。

Amazon ECS services now support multiple load balancer target groups

これのせいで多段リバプロをしていたな

2019/07/31 08:54

参考: Application Load Balancer を作成し ECS タスクを自動登録する


つっつきボイス:「この記事も含めて、最近いくつかの記事情報を社内Slackで寄せてもらってとってもうれしいです😂」「タイトルのmultiple load balancer target groupsですけど、これはmultipleがtarget groupsにかかってるのでは?」「あ、仮タイトルが『マルチプルロードバランサーのターゲットグループ』という直球のままでした😅(今は修正済み)」「この辺は知らないとわかりにくいですし☺

⚓今度はserveo登場


同サイトより


つっつきボイス:「serveoも先週あたりに話題になってましたね: 先週話題にしたngrok(ウォッチ20190731)のオープンソース版的な」「サーヴェオ」

「つまりngrokはオープンソースではない?」「ngrokはバイナリが配布されていて、フリーでも使えるけどお金払わないと自分のドメインが使えないなどの違いがありますね」「なるほど!」


ngrok.comより

「今回お集まりの皆さんの中でngrokお使いの方は?」「まだほとんどいないか…」

「ngrokは知らないよりは知っておいた方がいい便利ツールです👍」「特にスマホでテストするときに、自分のローカル環境で立ち上げて接続するときにいちいちプライベートIPアドレスをチェックするのって面倒じゃないですか: そこでngrokとかこのserveoを使うと、普通にインターネット上にURLをHTTPSで公開できるので便利ですよぉ〜❤」「Railsでなくてもいいんですよね?」「普通にポートをつないでくれるので、だいたい何でも公開できます」「おぉ〜」

「たとえばローカルでRailsサーバーを立てて、顧客とSlackでやりとりしながらURLを渡して、こっちで編集しながら変更点をその場で見てもらえたりできますし😋」「今までだとたとえばcapistranoデプロイとかしないと見せられなかったのがコマンド一発で公開できますし💪」「『顧客が本当に必要だったもの』感😆

「最初はserveoがどういう仕組みでやってるのかと思ったけど基本的にngrokと同じようなことやってますし😆」「なるほど!」「ngrokは公式のバイナリを取ってきて実行するんですけど、serveoはこんなふうに↓sshでポートフォワーディングすればできるのでツールをインストールする必要すらない😋

$ ssh -R 80:localhost:3000 serveo.net

参考: ポートフォワーディングとは - IT用語辞典 e-Words

⚓その他Linux


つっつきボイス:「始まる前の雑談でもこの話出てましたね☺」「今のLinuxでも/dev/fd0とかデフォルトで使えてるのかな?…だめだ、自分の環境だと見えない😇: 物理Linuxサーバーを見ればわかりそうだけど」「どっちにしろ今後フロッピー使いそうにないですし😆」「FDドライバがなくなるとかじゃなくて、単にメンテする人がいなくなったと」

参考: FD/CD のマウント

I’m not sentimental. Good riddance.
同コミットより

Good riddanceが「せいせいした」なんですね。

⚓JavaScript

⚓Vue・React・Angular、どれがパフォーマンスいい?


同記事より


つっつきボイス:「フロントエンド寄りのパフォーマンス比較をあまり見たことがなかったので」「あ、パーティクルをばらまくアニメーションで比較してるのね😳

今回の検証のように大量のオブジェクトをリアルタイムで描画更新するようなケースは、ReactやAngularではほとんどありません。フレームレートとメモリ使用量については、極限の状態においての負荷を検証したので、小規模なプロダクトにおいて負荷が問題になる場面が少ないことは補足しておきます
ライブラリの特性を知ったうえで技術選定をすることはリスクの早期発見につながるはずです。本記事が技術選定の一助になれば幸いです。
同記事より

「まあ自分たちだとパーティクルを動かすのにAngularとかReactとか使わないかな😆、どのぐらい実用的なベンチマークなんだろう?」「記事の最後のまとめにこう↑書いてあるし😋」「実用的かどうかは別にして、ひとつのベンチマークというか」「RubyのベンチマークをOptCarrotでやってるのにもちょっと似ているというか、OptcarrotでGCが速くなってもRailsが速くなるとは限らないみたいな😆」「Optcarrotはファミコンのエミュレータでしたね」

「上の記事を書いた方はこういうサイト↓をやっているそうです」「すごい凝ってる…!」「フロント強そう」

つっつき終了後の親睦会で、こちらのサイトを環境映像的に映しました😋

⚓言語・ツール

⚓Pythonのregexの挙動が変わる😨

RubyのOnigmoはどうなんだろう…

Python 3.7まで(VERSION0)の主な古い振る舞い:

  • ゼロ幅マッチの扱いが正しくなかった
    • .splitはゼロ幅マッチの箇所でsplitしない
    • .subはゼロ幅マッチの箇所から1文字先へ進む
  • Unicode文字のcase-insensitiveマッチはデフォルトでsimple case-folding

新しい振る舞い(VERSION1):

  • ゼロ幅マッチが正しく扱われるようになった
  • Unicode文字のcase-insensitiveはデフォルトでfull case-foldingになる

つっつきボイス:「Pythonで正規表現の仕様が微妙に修正されるそうで、Rubyではこの辺ってどう扱われているのかなと思って拾ってみました」「ちなみにPythonやってる方ってこの中にいます?」「やはりいないか〜😆」「じゃPythonは深追いしないことに🤣

「上はその中で個人的に気になった部分をピックアップしたんですけど、Case FoldingとかCase Mappinngとか知らない用語が続々出てきたので以下にひとまずリンクを貼りました😅

参考: www.unicode.org/Public/UCD/latest/ucd/SpecialCasing.txt
参考: W3C Case folding - Internationalization
参考: toLowerCaseの落とし穴とCase Foldingの話 - LINE ENGINEERING

一方で、我々は toLowerCase() や toUpperCase() といった操作を別の目的で使うこともよくあります。それは「大文字・小文字の違いを無視して文字列を比較したい」という目的のために文字列を一旦どちらかの文字種に統一する、というようなものです。Unicode標準は、このような目的のために “Case Folding” という操作を定義しています。Case Foldingは小文字へのCase Mappingとよく似ていますが、「caseless matchingを言語独立に行う」ために最適化されており、例えばギリシア文字の Σ は単語内の位置に関わらず常に σ (U+03C3) に変換されます。
engineering.linecorp.comより

「上の記事を急いで見た範囲だと、大文字と小文字の変換方法が言語によって違うという問題に関連してるようです」「あ〜その辺の問題か」「以前RubyのUnicodeマルチリンガル絡みで、トルコ語などで大文字小文字の変換が一律にはできないみたいな話を読んだことがあって、そのあたりに絡んでそうです」「このあたりは自然言語の強者に任せたっ😆

参考: instance method String#downcase (Ruby 2.6.0)
参考: Feature #10085: Add non-ASCII case conversion to String#upcase/downcase/swapcase/capitalize - CommonRuby - Ruby Issue Tracking System — Ruby 2.4で非ASCIIの大文字小文字変換が拡張されました

⚓その他言語


つっつきボイス:「自分はEmacs LISPでコンフィグ書く以上のことをしたことないんですけど😆、LISPやったことある方はいます?」「…少ないけどいますね」「大学で、チョット、ヤッタ😆」「かっこが多い言語🤣

「ツイートで紹介されているこの『n月刊ラムダノート』という雑誌↓が、好きな人はすごく好きそうだったので😆」「1,500円!」「冒頭のnは誤植じゃなかった😆」「川合史朗さんとかも書いてますね」「そうそう、『ハッカーと画家』で『エンジニアはLISPをやれ』みたいなことめっちゃ書いてた〜😆」「LISP 1.5とか言われてもわがんね😆」「わからない〜😆

参考: 『n月刊ラムダノート Vol.1, No.2』を読むべき1つめの理由 - golden-luckyの日記

「LISPを今から真面目に始めるかと言われると、ね😆」「でもLISPは言語の歴史では必ず出てくる言語ですし、『本当の』プログラマーというとLISPやってる印象ってちょっとありますね☺」「たしかMatzもLISPから影響受けたりしてましたね」「言語系をガチでやってる人にLISP多そう」

参考: まつもとゆきひろさん,Rubyに影響を与えた言語とRuby開発初期を語る。 ~ RubyKaigi 2013 基調講演 1日目:RubyKaigi 2013 レポート|gihyo.jp … 技術評論社

⚓その他

⚓GitHub CEOの嘆き


つっつきボイス:「最初上のツイートをたまたま見かけて、何の話だろうと思っていたらツイート主はGitHubのCEOで、以下の話のことだったそうです↓」「あ〜この話ね: 実際にはイランだけじゃなくて他にも対象になった国があるらしいですけど」「パブリックなリポジトリは閉じないけど、プライベートリポジトリが閉じるって聞きました」「そうそう、そうらしい」「『やりたくてやってるんじゃない』というのが切実ですね」「米国企業がこれを跳ね返すのは難しそう…」

参考: GitHubがイランなどからアクセス不可に、米国の経済制裁により。CEOのフリードマン氏「望んでやっているのではない」 - Publickey

「で、その動きに反対するリポジトリもできてました↓」「オープンソースに貢献したIranian developerのリストも載ってる」


同リポジトリより

↑今日見たらarchivedになってました😢

⚓番外

⚓頭に輪っかはめる?


つっつきボイス:「どういう種類の電磁刺激なのかな😆」「効果が持続するならいいけど一時的にしか持たなかったら😆」「四六時中装着しないと😆」「時計じかけのオレンジ的な世界🍊」「それ〜😆」「眠くなったら通電とか⚡

参考: 脳に電磁刺激を与えれば、高齢者の記憶力が“若者並み”に改善される:研究結果|WIRED.jp

参考: 時計じかけのオレンジ - Wikipedia

原作で特徴的なのは、主にロシア語をもじる形で人工的に作られた若者言葉「Nadsat」だそうです。「トルチョック」だけ覚えています。

参考: ナッドサット - Wikipedia


後編は以上です。

おたより発掘

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

週刊Railsウォッチ(20190805-1/2前編)Rails 6のActive Recordは速くなった、Windows WSL2+VSCodeでのRails開発、Martin Fowler記事ほか

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

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

Ruby Weekly

BPSに入社してからの約10年を会社の成長とともに振り返る(前編)

$
0
0

新卒でBPSに入社してからもうそろそろで10年となります。
入社してからいろいろありましたが、今では8人を率いるチームリーダーとなり、評価面談や採用面談、チームの売上管理など技術的な部分以外でも様々なことをやるようになりました(実装もたまにする)。

今回の振り返りを書くことで、今BPSにいる方やBPSに応募を考えてる方、エンジニアを目指す方の参考にでもなればよいかなと思ってます。

本記事は3編構成となります。
各編では以下のようなことを書いていこうと思います。

■前編(インターン時代~入社3年目)
本記事の内容になります。
荒波を超えた時代。入社3年目までどのようなことをやってきたのかを書いています。

■中編(入社4年目~入社6年目)
このあたりの年にあることを目的としてアプリチームに異動しました。
なぜアプリに異動しようとしたのか、何が得られたのかなどを中心に書いていこうと思います。

■後編(入社7年目~現在)
このあたりからチームを本格的に作り始めます。
どのようなことをやってどのように作ってきたのかを中心に書こうと思っています。

インターン&アルバイト時代

2009年の10月あたりから1ヵ月程度インターンしていました。
その時の事務所は神奈川県の中央林間で自宅から片道3時間かけてほぼ毎日通ってました。今思うとよくやってたなと思います。

インターンではPHPでプロジェクト管理サイトのようなものの作成で一覧にあるプロジェクトのデプロイボタンを押すとrsyncが走って各環境に最新版が反映されるようなものを作りました。
情報系の大学院を出ているのでプログラミングは多少やってたのですが、PHPはあまり触れたことなく、PHPの本やCakePHPのチュートリアルを見ながらやっていました。

インターンを経て内定をもらって入社を決めたわけなのですが、入社前の1ヵ月前はアルバイトをしていました。
アルバイトでは今も取引させていただいているお客さんのサイトの保守対応をしていました。
機能ロジックちょっと修正したりとか見た目変えたりとか簡単なものの対応。
言語としてはPHPでEthnaとかCakePHPをフレームワークとして使ってました。

ちなみにアルバイト時代は既に会社の近くに引っ越していたので3時間かけて通うとかはしてません。

入社1年目(2010年)

今までで一番大変だった年。

BPSとしては新卒を初めてとった年でこの時入社したのが私含めて3人でした。
BPSのメンバとしてはこの時7人くらい。

6月くらいからはお客さんとの打ち合わせに同行するようになり、別会社の社長や役員の方が同席するような打合せにも参加したりしたんですが、オーラというか覇気というか雰囲気というかそういったものが全然違うなと感じたのをよく覚えてます。

6月、9月で新卒中心でやった案件が2つあって、それぞれで窓口をやっていたんですが、新卒が途中でやめて、その人が担当だった開発もやってでかなり大変でした(今まで一番大変だった案件1,2)。
決済あるシステムをよく新卒でやったなとか思う。

1年目はWordPressも含めて保守でやっていた案件が全部PHPだったので触っていたのはほとんどPHPでした。
この年で新卒2人やめて残った新卒は私1人になりました。

この年は技術的な部分以外にもプライベートでいろいろ勉強した年で会社のしくみや労働基準法、株の勉強(この後、大震災の洗礼をうけることになる)をしだしたりしました。

ちなみにTechRachoが生まれたのもこの年なのでもうそろそろで10年になります。

入社2年目(2011年)

HTML/CSS, Ruby on Railsをやり始めた年。
BPSはこの年に中央林間から新宿に移転しました。

2011年度が始まって数カ月は生のHTML/CSSばかりやっていて、PSDからHTMLに書き出すということを始めてやりました。

今まで既存機能の小改修やビューの小改修だったので一からHTML/CSSを書くというのがこの時初めてでした。
ちなみにこのころはまだIE6とか7の対応もしてたりしてました!

2011年度半ばからは私としては初のRails案件で結構大き目なシステムでBPSの体制としては7人くらいは関わってたようなプロジェクトに関わりました。

役割としてはフロントでHTML/CSSの作成や組み込み、JS周りだったんですが、後半、人が足りないとかで結局バックエンドの機能開発もゴリゴリやっていました。
SVNからGitに変わったのもこのころ。

このころはHTML/CSS, PHPもRubyも大体Eclipse使って書いてました。

入社3年目(2012年)

新卒の採用や中途の採用を行って、この年で大体社員十数名、バイト入れると20名くらいになりました。

この年当たりくらいからアプリとWebでチームが分かれ始めてきたかなと思います。
Webのほうはこの時チームをさらに分けるみたいな話があって作ったのですが、いつの間にか空中分解していました。
体制も能力もまだこの時は備わってなかったなという印象です。

BPSはこの年に再度移転して今の場所になりました。

↑移転時の家具が全くない時の写真

私はというと今も保守している業務系システムのリニューアルとかやっていました。
体制とてはPM1人、開発者3人でしたが、設計含めてメインで開発して多分6割くらい作ったかなと思います。
その他にも3名のチームを率いて小規模のRails案件をやったり、打ち合わせに1人で行ったりするようにもなりました。

この年くらいからほぼほぼ開発はRailsになったように思います。

入社3年目までを振り返って

10年通してみても1年目の経験が一番影響が大きいかなと思っています。

業務的に一番大変な時期だったので、その後も大変なことはいろいろありましたが、この時と比べれば大体楽だなと思えます。

またプライベートで学び始めた株とか会社のしくみなどもかなり今につながっていると思っていて、業界・市場動向、費用対効果、投資などなど単純な開発・実装という枠組み以外から仕事やエンジニアというのを考えられるようになったのはこのあたりの影響が大きいかなと思っています。

技術的なところで見るとこの当時はIE6,7の対応とかもしていてCSSハックしていたりもしましたが、現在では古いブラウザへの対応もほとんどなくなってきてブラウザへの対応や確認が楽になったなという印象です。

この当時はまだtableタグでHTMLが作られているようなのをたまに見かけましたが、今では完全に見なくなりました。
横並びもこの当時は主にfloatで頑張ったりJSで幅計算したりしていましたが、今では等幅な横並びもFlexbox使えば簡単にできるので良い時代になったなという印象です。

Railsでのシステムもこの当時作成したものはRails5にあげたんですがStrong Parametersの対応がかなり大変でした。

関連記事

TechRachoをリニューアルしました(TechRacho誕生秘話付き)

週刊Railsウォッチ(20190819-1/2前編)祝: Rails 6がついにリリース、RailsガイドもRails 6に対応、Arelはpublicだったかほか

$
0
0

こんにちは、hachi8833です。休日はトイレの壁紙を剥がして珪藻土を塗り塗りする作業で終わりました。

参考: 【これさえ読めば大丈夫】はじめての漆喰・珪藻土 塗り壁DIY完全ガイド


つっつきボイス:「自宅の壁?😆」「冬になるたびにトイレの壁にめちゃくちゃ結露してカビの温床になってました🦠」「構造上結露しちゃう家とかありますよね☺」「団地ともお的な昭和な団地なので冬場はコンクリから外の冷気がもろに伝わってきます😅

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

⚓臨時ニュース: Rails 6がついにリリース🎉㊗

つっつき会の翌日の金曜日に再び6.0.0マイルストーンを見てみるとissueがゼロになっていたので、もしかするとウォッチ公開前にリリースされたりして、と思っていたら週末にリリース情報が出ました。

そういえば例の画面も変わってましたね。


早くも@jnchitoさんががっつりRails 6記事を公開しています。他にもPublic Keyなどで続々速報が出ています。

私もRails 6 rc2をちょっといじり始めていたのですが、RailsガイドをRails 6に対応させる更新翻訳(次の記事参照)がせいいっぱいでした😅

参考までに、Evil Martiansの少し前のRails 6記事を再掲します。

Rails 6のB面に隠れている地味にうれしい機能たち(翻訳)

⚓RailsガイドがRails 6に対応

YassLabの安川さんのお力添えで、RailsガイドをひとまずRails 6に対応いたしました。ありがとうございます!😂
上の記事で紹介されているように、今回はZeitwerkのガイドなども追加されています。見落としや誤りなどありましたら、Railsガイドまでフィードバックをお願いします🙇

眼力頼りの差分翻訳を今後何とかしたいです😅


というわけで、ここから下はRails 6リリースより前のつっつきを元にしています。ご了承ください🙇

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

今回は主に最近の6-0-stableブランチから見繕いました。ドキュメントの修正が増えています。
なお6.0.0マイルストーンはつっつき時点で4件ありました。


つっつきボイス:「ある程度予想はしてたけど、やっぱりリリースまでには時間かかってますね☺」「マイルストーンに残ってるissueはデグレっぽい感じですし」

⚓aggregate_aliasの余分な”_”を削除

これは6.0.0マイルストーンにあったissueのようです。

# activerecord/lib/active_record/relation/calculations.rb#L308
      def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
        group_fields = group_values
        if group_fields.size == 1 && group_fields.first.respond_to?(:to_sym)
          association  = klass._reflect_on_association(group_fields.first)
          associated   = association && association.belongs_to? # only count belongs_to associations
          group_fields = Array(association.foreign_key) if associated
        end
        group_fields = arel_columns(group_fields)
        group_aliases = group_fields.map { |field|
          field = connection.visitor.compile(field) if Arel.arel_node?(field)
          column_alias_for(field.to_s.downcase)
        }
        group_columns = group_aliases.zip(group_fields)

-       aggregate_alias = column_alias_for("#{operation}_#{column_name.to_s.downcase}")
+       aggregate_alias = column_alias_for("#{operation} #{column_name.to_s.downcase}")
# activerecord/lib/active_record/relation/calculations.rb#L377
      def column_alias_for(field)
-       return field if field.match?(/\A\w{,#{connection.table_alias_length}}\z/)
-
        column_alias = +field
        column_alias.gsub!(/\*/, "all")
        column_alias.gsub!(/\W+/, " ")
        column_alias.strip!
        column_alias.gsub!(/ +/, "_")
        connection.table_alias_for(column_alias)
      end

つっつきボイス:「コードに一箇所余分な_があったのを修正したそうです」「以前の修正でデグレってたのが解決された🎉

⚓Zeitwerk関連コミット

# railties/lib/rails/autoloaders.rb#L39
+     def log!
+       each(&:log!)
+     end

つっつきボイス:「Zeitwerkデバッグ用の#log!を追加して、Railsガイドでこれを使ったトラブルシューティング方法が追記されてました」「Rails 6のZeitwerkでハマる人もいそうなので、こういうサポートはありがたいですね😋

「ところで上にポツンとあるeachって何でしょう?」「更新箇所のすぐ上にeachが定義されている↓からそれではないかと😆」「あ、これでしたか😅」「あまり見かけない書き方ですけどね☺mainonceは単独で使えそうな感じでそれをまとめて呼び出すショートハンド的なメソッドかなと」

# railties/lib/rails/autoloaders.rb#10
      def main
        if zeitwerk_enabled?
          @main ||= Zeitwerk::Loader.new.tap do |loader|
            loader.tag = "rails.main"
            loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
          end
        end
      end

      def once
        if zeitwerk_enabled?
          @once ||= Zeitwerk::Loader.new.tap do |loader|
            loader.tag = "rails.once"
            loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
          end
        end
      end

      def each
        if zeitwerk_enabled?
          yield main
          yield once
        end
      end

      def logger=(logger)
        each { |loader| loader.logger = logger }
      end

⚓ActionDispatch::Responseのcontent_typeの内部でmedia_typeを使うよう変更

#36490でdeprecateされたcontent_type呼び出しが、アップグレードしたアプリでトリガーされていた。同PRのコメントで報告されていた。
media_typeを使えば、あらゆるケースでdeprecationを回避できる。
同PRより大意

# actionpack/lib/action_controller/metal/renderers.rb#L156
    add :json do |json, options|
      json = json.to_json(options) unless json.kind_of?(String)
      if options[:callback].present?
        if media_type.nil? || media_type == Mime[:json]
          self.content_type = Mime[:js]
        end

        "/**/#{options[:callback]}(#{json})"
      else
-       self.content_type ||= Mime[:json]
+       self.content_type = Mime[:json] if media_type.nil?
        json
      end
    end

    add :js do |js, options|
-     self.content_type ||= Mime[:js]
+     self.content_type = Mime[:js] if media_type.nil?
      js.respond_to?(:to_js) ? js.to_js(options) : js
    end

    add :xml do |xml, options|
-     self.content_type ||= Mime[:xml]
+     self.content_type = Mime[:xml] if media_type.nil?
      xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
    end

つっつきボイス:「@y-yagiさんの立てたPRがこのPRに反映されたそうです」「media_typeなんてのがあったとは😳: MDNを見るとContent-Typeの項目の中にmedia-typeってあるな↓」「ディレクティブですか」

参考: Content-Type - HTTP | MDN


/developer.mozilla.orgより

「これを見る限りでは、Content-Typeの中でmedia-type=なんちゃらMIME typeみたいにMIME typeを書けるらしいけどどういうふうに使うのかな…?🤔」「むむ😅」「media_typeがあればそっちを使って、なければContent-Typeを使うとかなという気もするけど…」「詳しい人の情報求む🙏

その後もう少し追ってみました。

#36034ActionDispatch::Response#content_typeの戻り値を変えたが、#36034でアップグレードの邪魔になっているらしき。
そこでActionDispatch::Response#content_typeの振る舞いを5.2に戻しつつ、古い振る舞いをdeprecatedにした。さらに、振る舞いをconfigで制御できるようにした。
#36490より大意

# #36490より
# actionpack/lib/action_dispatch/http/response.rb#L89
+  cattr_accessor :return_only_media_type_on_content_type, default: false
...
    def content_type
-     super.presence
+     if self.class.return_only_media_type_on_content_type
+       ActiveSupport::Deprecation.warn(
+         "Rails 6.1 will return Content-Type header without modification." \
+         " If you want just the MIME type, please use `#media_type` instead."
+       )
+
+       content_type = super
+       content_type ? content_type.split(/;\s*charset=/)[0].presence : content_type
+     else
+       super.presence
+     end
    end

さらに#36034のコメントを引用します。

#36034の変更がGitHub上で最新の6-0-stable更新に入ったが、breaking changeが発生していろいろfailすることがわかった。content_typeへの変更はたぶん正当だと思うものの、同時に5.2でもアプリを動かしている自分たちにとっては切り替え困難。#36034の変更の意図としては、5.2と6.0ではcontent_typeの戻り値が異なっていて、少なからぬ影響が生じる。この変更をひとまず戻してアプリで従来のcontent_type値を取れるアップグレードパスを整備するのがよさそう?

さらに遡ると、#35709の「content_typecharset以外のパラメータがRailsで無視される」というissueが始まりだったようです。それを修正するために、#36034ActionDispatch::Response#content_typeがContent-Typeヘッダーをすべて返すようにしたところ、既存の5.2アプリでMIME typeの取得などで不具合が生じることがあったので、段階的に移行することになった、冒頭の#36854はそれに伴う変更…という理解で合ってるかしら。

⚓prevent_writesのスレッド安全性を修正

#36830で追加したテストやコードで示すように、prevent_writesはスレッドセーフではなかった。あるスレッドが読み出し、他のスレッドが書き込んでからもう一度読み出すと、最初の書き込みができなくなる。
この変更は、コネクションハンドラのインスタンス変数を削除してThread.current[:prevent_writes]のゲッター/セッターに変え、書き込みが許されるかどうかを設定するようにした。
同PRより大意

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L1006
-     attr_reader :prevent_writes

      def initialize
        # These caches are keyed by spec.name (ConnectionSpecification#name).
        @owner_to_pool = ConnectionHandler.create_owner_to_pool
-       @prevent_writes = false

        # Backup finalizer: if the forked child never needed a pool, the above
        # early discard has not occurred
        ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool)
      end

+     def prevent_writes # :nodoc:
+       Thread.current[:prevent_writes]
+     end
+
+     def prevent_writes=(prevent_writes) # :nodoc:
+       Thread.current[:prevent_writes] = prevent_writes
+     end
+
      def while_preventing_writes(enabled = true)
-       original, @prevent_writes = @prevent_writes, enabled
+       original, self.prevent_writes = self.prevent_writes, enabled
        yield
      ensure
-       @prevent_writes = original
+       self.prevent_writes = original
      end

つっつきボイス:「マルチプルDBでのスレッド関連の問題を修正したみたいですね」

⚓パーシャルレンダリングAPIの古い記述を削除

# actionview/lib/action_view/renderer/partial_renderer.rb#L106
  #   <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
  #
- # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
- # just keep domain objects, like Active Records, in there.
- #
  # == \Rendering shared partials

#36897によるとActionView::PartialRendererに以下のような記述がある:

後方互換性の懸念上、このコレクションにはハッシュを使えない。通常はActive Recordと同様、そこにドメインオブジェクトも保持する。

上の記述が追加されたのは2005年のことで、どこが懸念点だったかという情報は見当たらないものの、#36897で報告されているとおり、実際はできるらしい↓。自分もRails 6.0 rc2で試したところ、ハッシュを渡せた。
同PRより大意

= render :partial => "info_row", :collection => my_collection, :as => :d
# my_collectionはハッシュの配列

⚓Rails

⚓Arelはかつてpublicだったか?


つっつきボイス:「『先週の改修』でissueを追っていたら、このissueのスレがArelの話題でかなり伸びていて気になったので、手元で雑に訳してみました」

参考: File: README — Documentation for rails/arel (master)
参考: ActiveRecordを支える技術 - Arelとは何者なのか? (全5回) その1 - TIM Labs


以下は#36761の大雑把なまとめです。

  • (元々このissueは、threddedというフォーラムエンジンのあるクエリがRC2で期待どおり動かなくなったという報告で始まった)
  • (やりとりの中でArelに言及されていて、Arelがかつてpublicだったことがあったかどうかという議論になった)
    • publicあった説: 3.2のRailsガイドarel_tableを使ったコード例がある、publicではなかったとしても、publicだったと思ってた人はかなり多い
    • publicなかった説: Arel gemはかつて外部gemだったがArelのAPIはpublicになったことはないはずだし、APIにはArelは出てこない
    • publicあった説: APIdockには今もpublicと表示されている
    • publicなかった説: APIdockは公式ではないし、Arelはやはりprivateであり予告なしに変更される可能性がある
  • Arelを使っている人は実際多いようなので、もしかするとArelをpublic APIとして再検討するタイミングなのかもしれない?
    • ActiveRecord::RelationでできないことはArelでやるべし(stringでやるのは禁止)」というプロジェクトに携わったこともある
    • stringでやる方法だとコンポジションもパラメータ化もできず、DB互換性もなくなるのでよくない
    • Arelはリリースのたびに改善されている(5.1はともかく)
    • (「Arelをpublicにすべき」説が後半で飛び交う)
    • 以下↓はArelでないとこんなに簡潔に書けない
scope :started, -> {
  where(arel_table[:valid_from].lteq(Date.current))
}

最終的にこの#36761はcloseされています。なお本来の問題は別途@kamipoさんが#36805で修正しました。


「(抄訳を見ながら)お〜なるほどなるほど、たしかにArelを生で使う人って以前は普通に見かけましたね☺」「やはり〜」「RailsガイドにもArelを使った例があったとは(今はありませんが)」「あったあった、自分もRails 3の頃だったか、英語版Railsガイド見てやりましたもん😆」「😆」「ArelがprivateなAPIだったら、DB触りたいというだけでprivateなAPIをわざわざ使いたくないですし、使った覚えがあるということはガイドに書いてあったはずですし😆」「APIdock見に行くと今でも『public』って出ちゃってますし」「ありゃ〜😆

「そういうこともあったりしたので『Arel、publicだったんでしょ?』という意見が出たのかもですね」「ま気持ちはわかる😆」「ArelってActive Recordに取り込まれる前は別gemだったんですね」「元々汎用的なORMビルダーみたいな位置づけでしたし」

「まあ公式としてはArelをpublic APIとみなしたことはないという見解になるでしょうね: ArelをArelとしてナマで使う人をどうこうするつもりもないでしょうけど」「その分いつAPIが変わっても仕方がないですね」「じっくり読んだらなかなか面白そうなissueではある😋」「issueを全文翻訳しようかとも思いましたが取りあえずやめときます😅

⚓StravaはRailsを使っている


同サイトより


つっつきボイス:「Matzのツイートで見かけたので拾ってみました: フィットネスSNS的なサービスのようです」「この辺は国とか地域で流行り方が違ったりしますね☺」「同社のブログでRailsを5.0に上げたという記事があったのでRails使ってるようです」「2018年末で5.0、GitHubもその頃にRailsアップグレードを進めてましたね: どちらもユーザー数めちゃ多そうだから大変そう😅

⚓Rails 6のマルチDB応用編(Hacklinesより)

production:
  primary:
    adapter: postgresql
  animals:
    migrations_paths: db/animal_migrate
    adapter: postgresql
    url: <%= ENV["HEROKU_POSTGRESQL_OLIVE_URL"] %>
  animals_replica:
    adapter: postgresql
    url: <%= ENV["HEROKU_POSTGRESQL_PURPLE_URL"] %>
    replica: true
  • replicaから読む
  • replica読み取りとDB書き込みを自動切り替え
  • DB読み取りと書き込みを手動で切り替える

つっつきボイス:「Rails 6でやってくるマルチプルDBの機能紹介という感じかな」「タイトルにadvancedとあるけど基本機能っぽい😆

config.active_record.database_selector = 2.seconds

「お、この2秒という数値↑、なるほど感」「というと?」「masterというかprimaryに書き込んだ後でそれがreplicaに複製されるまでにはタイムラグがあるので、タイムラグの上限を保証する的な設定なんでしょうね、2秒経過すればreplicaからも最新のデータを取れると」「database_selector、新しいガイドにあったかな…?🤔

ありました↓。

参考: rails/active_record_multiple_databases.md at 98a57aa5f610bc66af31af409c72173cdeeb3c9e · rails/rails

### active_record_multiple_databases.mdより
config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session

「この辺の数値って意識しておかないとマズいときがあるんですよ: レプリケーションが詰まったりするとデータの参照に失敗してIDがかぶっちゃうとかありますし」「あ〜」「まあIDをデータベースで生成させていれば問題ないんですけど、別のものを使ってIDを生成しているとIDかぶりで落ちるとか普通に起きますので🥶」「なるほど!」「RailsのマルチプルDBがその辺もちゃんと面倒見てくれるとしたら賢い!記事の方も、マルチプルDBについて取りあえずどのぐらい大丈夫なのかとかを知るのによさそうですね👍

⚓Rails 6のAction Textのファイルアップロードを分解調査する

DHHがこの記事をリツイートしてました。


つっつきボイス:「これは例のst0012さんが書いた記事で、既に翻訳済みなので近々公開しますけど、オチはAction Textのファイルアップロード機能でN+1クエリ問題を見つけたというものでした😆#36177)」「まあAction Textも機能でかそうだからそういうのありそうですし😆」「data-trix-attachmentとかにTrixエディタの名残りがありますね〜」

「Action Textの情報がなかなか見当たらなかったのでst0012さんは仕方なく自分で調べたそうです」「このあたりは内部実装だから、今はこうでも今後どう変わるかわかりませんけど☺」「Linuxカーネルの解説書なんかもすぐ古くなりますしっ😆

「記事によると、Action Textでは以前もつっつきで話題に出た例のglobalidを使ってるようです(ウォッチ)」「DHHなら使いそう😆」「しかもSignedGlobalID使ってる」「不正アクセス防止用」「まだ保存されてないデータでグローバルなIDを使いたい場合はやっぱりglobalid欲しいでしょうね☺

参考: rails/globalid: Identify app models with a URI

⚓combustion: Railsエンジンを楽にテスト(Ruby Weeklyより)


つっつきボイス:「combustionって内燃機関の『燃焼』だから、Railsエンジンにひっかけた命名っぽいですね」「これは何がうれしい点かな?」「RailsエンジンをテストするためにRailsアプリを丸ごと作成しなくてもいいようにするとかそういう感じみたいです」「ははぁなるほど😋、Railsのエンジンをデフォルト設定で作ると確かエンジンテスト用のRailsアプリが作られるという覚えが」「Railsエンジンのリポジトリを作ったときに、中にまるっとRailsアプリが入ってるのはちょい冗長な気はしますね」

「ところでRailsエンジンって、fullとmountableとかあってそれそれでちょっと違ってたような」「え、エンジンに種類があったんですか😅」「Railsガイドにありますね↓」

参考: Rails エンジン入門 - Rails ガイド
参考: Getting Started with Engines — Ruby on Rails Guides

--mountableだとマウンタブルエンジンで、--fullだとフルプラグインということか」「あ、Railsガイドの訳がちょい間違ってた😅修正します(その後修正済み)」「原文だとfullにさらにオプションを追加したものがmountableということになりますけど↓、まあfullよりmountableの方が機能が多いって普通思わないですし😆」「直感に反してる😆」「ある種のワナ?😆」「原文も単語がちょっと足りないような(負け惜しみ😅)」

The –mountable option will add to the –full option:
guides.rubyonrails.orgより

「mountableの方が独立性が高くて単独のミニRailsアプリに近そう」「でもfullでも丸ごとRailsが入るのは同じといえば同じかな」「fullオプションだと名前空間化まではやらないらしい」「fullオプションって使いみちあるんだろか?🤣

参考: Gem、Railtieプラグイン、Engine(full/mountable)の違いとそれぞれの基礎情報 - Qiita

⚓その他Rails


つっつきボイス:「ポイントは『RC1でアップグレードするのがいいよ』ということでした」「RC1で機能がほぼ固まるし、RC2の修正ってそんなに多くは発生しないでしょうし☺


前編は以上です。

おたより発掘

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

週刊Railsウォッチ(20190806-2/2後編)RSpec CopのLeakyConstantDeclaration、serveoでゼロコンフィグ公開、RuboCopのPerformance/RegexpMatch改修ほか

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines

ActiveRecord::QueryMethodsのselectメソッドについて深掘りしてみた

$
0
0

こんにちは。BPSに入社してちょうど1年になりましたshin1rokです。

入社時に目標にしていた「TechRachoに技術系の記事を投稿する」を果たすべく、ActiveRecord::QueryMethodsselectメソッドを深掘りしてみます。

環境

  • Ruby: 2.6.3
  • Rails: 5.2.3

ローカルにRailsを読むためだけの小さいアプリを作り、RubyMineのコードジャンプとブレークポイントを駆使して探索しました。

そもそも(および深掘りの視点)

selectメソッドはこのようにModelを拡張する形でAttribute(?)を追加することができます。

※アソシエーションはUser has_many posts

irb(main):014:0> user = User.joins(:posts).select('users.id, posts.id as post_id').first
  User Load (0.6ms)  SELECT  users.id, posts.id as post_id FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1]]
=> #<User id: 1, post_id: 30>
irb(main):016:0> user.post_id
=> 30

このときuserインスタンスにidメソッドは定義されていますが、post_idメソッドは定義されていません。

irb(main):023:0> user.methods.find { |m| m == :id }
=> :id
irb(main):024:0> user.methods.find { |m| m == :post_id }
=> nil

なぜだろうか?と思いつつも、仕事でコードを書くときには意識しなくていい部分なので、無視していました。

夏のTechRachフェアが開催され良い機会なので、Railsのコードを読んで post_idメソッドがどこで定義されているのか 探してみようと思います。

Railsダンジョンへ

以下を実行した時のselectメソッド以降を呼び出し順に探索していきます。

class UsersController < ApplicationController
  def index
    puts User.joins(:posts).select('users.id, posts.id as post_id').limit(2)
  end
end

※件数を減らすためにlimit(2)しています。

selectメソッド

def select(*fields)
  if block_given?
    if fields.any?
      raise ArgumentError, "`select' with block doesn't take arguments."
    end

    return super()
  end

  raise ArgumentError, "Call `select' with at least one field" if fields.empty?
  spawn._select!(*fields)
end

rails/query_methods.rb#L220-L231 · rails/railsより

selectメソッドはArrayに対しても使うことができるのですが、その部分がif block_given?です。

ActiveRecordのselectspawn._select!(*fields)の部分です。
spawnは生み出すという意味があるので、spawn._select!(*fields)でSQLのSELECT文を生成しようとしているように見えます。

spawnメソッド

def spawn #:nodoc:
  @delegate_to_klass ? klass.all : clone
end

rails/spawn_methods.rb#L10-L12 at 5-2-stable · rails/railsより

spawnselectのレシーバ(User.joins(:posts))のようです。

_select!メソッド

def _select!(*fields) # :nodoc:
  fields.flatten!
  self.select_values += fields
  self
end

rails/query_methods.rb#L233-L237 · rails/railsより

引数をレシーバのselect_valuesに設定して、selfを返しています。

recordsメソッド

def records # :nodoc:
  load
  @records
end

rails/relation.rb#L199-L202 · rails/railsより

_select!のあとなんやかんやがあってrecordsメソッドのloadが呼ばれます。

注) なんやかんや: ActiveRecord::Relationは遅延評価されるため。なんやかんやの部分は追いきれませんでした。

loadメソッド

def load(&block)
  exec_queries(&block) unless loaded?

  self
end

rails/relation.rb#L421-L425 · rails/railsより

まだloadしていないのでexec_queries(&block)が実行されます。

exec_queriesメソッド

def exec_queries(&block)
  skip_query_cache_if_necessary do
    @records =
      if eager_loading?
        apply_join_dependency do |relation, join_dependency|
          if ActiveRecord::NullRelation === relation
            []
          else
            relation = join_dependency.apply_column_aliases(relation)
            rows = connection.select_all(relation.arel, "SQL")
            join_dependency.instantiate(rows, &block)
          end.freeze
        end
      else
        klass.find_by_sql(arel, &block).freeze
      end

    preload = preload_values
    preload += includes_values unless eager_loading?
    preloader = nil
    preload.each do |associations|
      preloader ||= build_preloader
      preloader.preload @records, associations
    end

    @records.each(&:readonly!) if readonly_value

    @loaded = true
    @records
  end
end

rails/relation.rb#L546-L576 · rails/railsより

急に長くなったので面食らいますが、注目するところはklass.find_by_sql(arel, &block).freezeです。
eager_loadとpreloadは今回は関係ないので無視すると、@recordsklass.find_by_sql(arel, &block).freezeを入れて、@recordsをreturnしています。

このときklassにはUserクラスが入っています。

0> klass
=> User(id: integer, email: string, encrypted_password: string, reset_password_token: string, reset_password_sent_at: datetime, remember_created_at: datetime, confirmation_token: string, confirmed_at: datetime, confirmation_sent_at: datetime, unconfirmed_email: string, created_at: datetime, updated_at: datetime, url_name: string)

find_by_sqlメソッド

def find_by_sql(sql, binds = [], preparable: nil, &block)
  result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable)
  column_types = result_set.column_types.dup
  attribute_types.each_key { |k| column_types.delete k }
  message_bus = ActiveSupport::Notifications.instrumenter

  payload = {
    record_count: result_set.length,
    class_name: name
  }

  message_bus.instrument("instantiation.active_record", payload) do
    result_set.map { |record| instantiate(record, column_types, &block) }
  end
end

rails/querying.rb#L40-L54 · rails/railsより

result_setはActiveRecord::Resultクラスのオブジェクトで、mapのブロック引数の値はこうなっています。

0> result_set
=> #<ActiveRecord::Result:0x00007fc08d695230 @columns=["id", "post_id"], @rows=[[1, 30], [1, 29]], @hash_rows=[{"id"=>1, "post_id"=>30}, {"id"=>1, "post_id"=>29}], @column_types={"id"=>#<ActiveModel::Type::Integer:0x00007fc08f0c59c0 @precision=nil, @scale=nil, @limit=8, @range=-9223372036854775808...9223372036854775808>, "post_id"=>#<ActiveModel::Type::Integer:0x00007fc08f0c59c0 @precision=nil, @scale=nil, @limit=8, @range=-9223372036854775808...9223372036854775808>}>
0> result_set[0]
=> {"id"=>1, "post_id"=>30}

column_typesにはpost_idがどのような型なのかを定義する情報がはいっています。

0> column_types
=> {"post_id"=>#<ActiveModel::Type::Integer:0x00007fcd66236ff0 @precision=nil, @scale=nil, @limit=8, @range=-9223372036854775808...9223372036854775808>}

ActiveSupport::Notifications.instrumenter.instrumentはブロックで渡された内容の実行時間を計測します。

result_set.map do |record| instantiate(record, column_types, &block)をActiveRecord::RelationオブジェクトでラップしたものがUser.joins(:posts).select('users.id, posts.id as post_id').limit(2)とイコールになります。

0> result_set.map {|record| instantiate(record, column_types, &block)}
=> [#<User id: 1, post_id: 30>, #<User id: 1, post_id: 29>]

0> User.joins(:posts).select('users.id, posts.id as post_id').limit(2)
  CACHE User Load (0.0ms)  SELECT  users.id, posts.id as post_id FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" LIMIT $1  [["LIMIT", 2]]
  ↳ /Users/shin1rok/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/ruby-debug-ide-0.7.0/lib/ruby-debug-ide/command.rb:138
=> #<ActiveRecord::Relation [#<User id: 1, post_id: 30>, #<User id: 1, post_id: 29>]>

instantiateメソッド

def instantiate(attributes, column_types = {}, &block)
  klass = discriminate_class_for_record(attributes)
  attributes = klass.attributes_builder.build_from_database(attributes, column_types)
  klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block)
end

rails/persistence.rb#L68-L72 · rails/railsより

klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block)がActiveRecord::Relationオブジェクトの各要素です。

0> klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block)
=> #<User id: 1, post_id: 30>

0> klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block).post_id
=> 30

ここまででpost_idをUserモデルのインスタンスに割り当てている処理が特定できたので今回の探索はここまでにします。

続きは君の目で確かめてくれ!

余談

user_idpost_idのペアが欲しかったのでselectを使ったのですが、

id_pairs = User.joins(:posts).select('users.id, posts.id as post_id')
id_pairs.group_by(&:id).each do |user_id, pairs|
  p user_id
  p pairs
end

ActiveRecord::Relationオブジェクトを作らない分速くなるので、こちらの方が良かったですね。
pluckに関連先のカラムを書くという発想がありませんでした😇

id_pairs = User.joins(:posts).pluck('users.id, posts.id')
id_pairs.group_by(&:first).each do |user_id, pairs|
  p user_id
  p pairs
end

週刊Railsウォッチ(20190826)6-0-stableの更新を見てみる、『Morning Cup of Coding』ニュースレター、Rails TutorialがRails 6対応に動き出すほか

$
0
0

こんにちは、hachi8833です。数列の1, 2, 4, 8,...みたいな表記を見ると、1, 2, 4, 8, 1, 2, 4, 8, 1, 2, 4, 8,...みたいな可能性もありそうな気がして不安になります。


つっつきボイス:「ruby-jp Slackのどこかで、Rubyの..だったか...を魔改造するみたいな話をちらっと見かけたんですけど、そっちの数学的表記の方が気になっちゃって😅」「どう解釈するか問題ね☺」「数学方面だと無限数列の...の略記ってコンベンションが頼りというか割と自明でなかったりした覚えが😆

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

今回のウォッチは分割なしです。

⚓週刊Railsウォッチ「公開つっつき会」第14回のお知らせ(無料)

第14回目公開つっつき会は、9月5日(木)19:30〜にBPS会議スペースにて開催されます。皆さまのお気軽なご参加をお待ちしております🙇

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

Rails 6がリリースされたので6-0-stableの更新をチェックしましたが、masterのdiffも一部含まれています🙇


つっつきボイス:「6-0-stableがまだ動いているというより、当面は6-0-stableがメインで更新されるということなのかも?」「それもそうでした😅」「6-0-stableが動いているのは確か?」「ですね」「6.1を出すときになったら初めて6-1なんちゃらみたいなタグなりブランチを作るのかも」「後で調べておきます↓」

現時点では6-1などに相当するブランチやタグはありませんでした。

参考: Ruby on Rails のメンテナンスポリシー - Rails ガイド

  • バージョン命名はSemantic Versioningに従う
  • 新機能はmasterブランチにのみ追加される

なおマイルストーンには6.0.1などがあります↓。

参考: Milestones - rails/rails

⚓(master)最適化: CurrentAttributes.instanceのオブジェクトアロケーションをメモ化

これは純粋なパフォーマンス最適化。
問題: stackprofオブジェクトのアロケーションプロファイルをたまたま見ていて、CurrentAttributes.instanceからのアロケーションでnameの呼び出しに気づいた。Class#nameは呼び出しのたびに新しいstringをアロケートするが、この手の不必要なオブジェクトアロケーションは自分としては避けたい。以下のスクリプトでオブジェクトアロケーションをカウントすると、このPR修正の前では1が返る。

# 同PRより
require 'stackprof'
require 'active_support/all'

class Current < ActiveSupport::CurrentAttributes
end

Current.instance # memoize
p StackProf.run(mode: :object) { Current.instance }[:samples]

解決: current_instances_keyをメモ化することでオブジェクトアロケーションをなくし、シンボルに変えることでハッシュ探索の効率が向上する。
同PRより大意


つっつきボイス:「これは確実にシングルトンですしね😋」「もしシングルトンじゃなかったらえらいこっちゃ😆」「やってることはごく普通のメモ化📋

# activesupport/lib/active_support/current_attributes.rb#L93
      def instance
-       current_instances[name] ||= new
+       current_instances[current_instances_key] ||= new
      end
...
+       def current_instances_key
+         @current_instances_key ||= name.to_sym
+       end

⚓(6-0-stable)ARオブジェクトのtakeのメモ化をクリアするようにした

あるリレーションからtakeされたARオブジェクトはメモ化されて、同じリレーションからwherefind_byが呼ばれた時にもメモ化されたものが返る。
このオブジェクトのメモ化されたステートはクリアする必要がある。
同PRより大意

これは6-0-stableに入りました。

# activerecord/lib/active_record/relation.rb#L638
    def reset
      @delegate_to_klass = false
      @_deprecated_scope_source = nil
      @to_sql = @arel = @loaded = @should_eager_load = nil
      @records = [].freeze
      @offsets = {}
+     @take = nil
      self
    end

期待する動作: Active Recordリレーションでfind_bywhereが呼ばれるたびにDBにクエリをかける。
実際の動作: リレーションでfind_bywhereが使われる前にtakeが呼び出されていると、Active Recordリレーションのクエリが返すActive Recordオブジェクトがtakeについてメモ化されてしまう。
同issueより大意


つっつきボイス:「たしかにこれはおかしくなる」「修正されたのはresetメソッドか」「takeって何件か取ってくるメソッドでしたね」「takeってあんまし使わないけど😆」「wherefind_byみたいにリレーションの形が変わるときはresetしないといけなくて、でも形が変わってたのにメモ化変数のうち@takeが同じリレーションに残って影響しちゃってたのでそれを修正したと」「メモ化するとこういうこと起きる可能性ありますね😅

# activerecord/test/cases/relations_test.rb#L2053
  def test_where_with_take_memoization
    5.times do |idx|
      Post.create!(title: idx.to_s, body: idx.to_s)
    end

    posts = Post.all
    first_post = posts.take
    third_post = posts.where(title: "3").take

    assert_equal "3", third_post.title
    assert_not_equal first_post.object_id, third_post.object_id
  end

⚓(6-0-stable)コネクションプールのreaperスレッドがforkでコケる問題を修正

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L649
      def reap
        stale_connections = synchronize do
+         return unless @connections
          @connections.select do |conn|
            conn.in_use? && !conn.owner.alive?
          end.each do |conn|
            conn.steal!
          end
        end
        stale_connections.each do |conn|
          if conn.active?
            conn.reset!
            checkin conn
          else
            remove conn
          end
        end
      end
...
      def flush(minimum_idle = @idle_timeout)
        return if minimum_idle.nil?

        idle_connections = synchronize do
+         return unless @connections
          @connections.select do |conn|
            !conn.in_use? && conn.seconds_idle >= minimum_idle
          end.each do |conn|
            conn.lease
            @available.delete conn
            @connections.delete conn
          end
        end
        idle_connections.each do |conn|
          conn.disconnect!
        end
      end

@jhawthornへ: 最新版だと以下のスクリプトがforkしたプロセス内でreaperスレッドがクラッシュすることからして、まだ何か足りないものがある気がする。このスレッドはクラッシュ後にコネクションを再確立しないと再度spawnされず、自分としては理想的とは言えない。
同issueより大意


つっつきボイス:「reaperは例の稲刈り機的にコネクションを刈り取るヤツでしたっけ」「forkするとダメになってたあたり、マルチスレッド系で起きそうなバグ🐛」「synchronizeとかやってるし、マルチスレッドのタイミング問題っぽい」

「なお#36998にこんなコメントが付いていたそうです↓」「ちょwこれ🤣」「明らかに狙ってますし🤣」「日本人しかわかんないし🤣」「なごんだ〜☺

⚓(master)pumaを4.1にバージョンアップ

# railties/lib/rails/generators/app_base.rb#L191
      def web_server_gemfile_entry # :doc:
        return [] if options[:skip_puma]
        comment = "Use Puma as the app server"
-       GemfileEntry.new("puma", "~> 3.11", comment)
+       GemfileEntry.new("puma", "~> 4.1", comment)
      end

つっつきボイス:「pumaのバージョン上げる忘れてたのかなと(以下参照)😆」「puma本家がつい最近4にバージョンアップしてましたしね☺

↑つっつき時点では6-0-stableへのコミットだと思っていたので、実際はmasterブランチの更新です🙇


puma.ioより

⚓番外: Rails Conductorは特に動きなし


つっつきボイス:「TechRachoの過去記事を集計中に例のRails Conductorの見出しを見かけて、どうなってるかなと思ったら特に何も変わってませんでした😆」「前のウォッチ↓で話題になったときのままという感じですか😆」「Rails Conductorって、ブラウザ画面でRailsを開発できるようにするみたいな感じの構想だったかな」「しばらくは動きなさそう」

週刊Railsウォッチ(20190311-1/2前編)「Rails Conductor」14年ぶり復活なるか?、RubyGemsに複数の脆弱性、2009年のRailsエコシステムほか

# conductor/app/controllers/rails/conductor/source/notes_controller.rb
# frozen_string_literal: true

class Rails::Conductor::Source::NotesController < Rails::Conductor::CommandController
  def show
    @notes = extract_notes
  end

  private
    def extract_notes
      capture_stdout { Rails::SourceAnnotationExtractor.enumerate tags_param }
    end

    def tags_param
      params[:tag].presence || %w[ OPTIMIZE FIXME TODO ].join("|")
    end
end

「Rails Conductor、そこそこ実装はあるっぽいですけど↑」「これを熱心にメンテしたい人はいなさそう🤣」「強い人はあんまり使わない感じですかね」「productionに入る機能ではないことを考えると、メンテするモチベが😆」「Railsコンソール的なものに落ち着くのか、それ以上のものになるのかで変わってきそうな予感はしますね☺」「サードパーティがRailsエンジンで作る方が回りやすかったりして」「公式として入れたい何らかのモチベがあるのかも?」

⚓Rails

⚓Railsのモデルクラス名と名前空間の衝突


つっつきボイス:「こういう問題があるって知りませんでした😅」「結局2.5のおかげで問題が起きなくなったと🎉

# 元記事より
# app/controllers/blogs_controller.rb
class BlogsController < ApplicationController
  puts "BlogsController loaded"

  before_action :set_blog, only: [:show, :edit, :update, :destroy]

  def index
    @blogs = Blog.without_draft.all
  end

  def show
  end

  private
    def set_blog
      @blog = Blog.without_draft.find(params[:id])
    end
end
# 元記事より
# app/controllers/staff/blogs_controller.rb
class Staff::BlogsController < ApplicationController
  puts "Staff::BlogsController loaded"

  before_action :set_blog, only: [:show]

  def index
    @blogs = Blog.all
  end

  def show
  end

  private
    def set_blog
      @blog = Blog.find(params[:id])
    end
end

「元記事のコード↑にclass BlogsController < ApplicationControllerclass Staff::BlogsController < ApplicationControllerというのがあると、Staff::BlogsControllerを期待しているときにBlogsControllerを取ってしまうという話ですね☺」「Ruby 2.4までは、Staff::BlogsControllerと書いて、かつStaff配下にBlogsController定数がないと、トップレベルのObject配下にそれが存在する場合にそっちを取ってきてたらしいんですよね: 全然知らなかったけど😳」「割と罠的な動き😇」「こういう直感的でない動きするんだなって😅」「2.4まではその条件でトップレベルの定数が取れた場合にwarningが出ていたのが、2.5では失敗するようになったと」「大規模なコードを書くには必要な機能だから、このあたりはちゃんとしてて欲しいっすね🧐

「今までrequire_dependencyでやってたときの問題については、6.0のZeitwerkでやれるようになったから今後は大丈夫と😋」「そういえばZeitwerkではrequire_dependencyをまったく使わなくなるってガイドにありますね↓」

参考: 2.6.3 require_dependencyについて — Rails アップグレードガイド - Rails ガイド

「この辺の問題は、踏んだときにとっても謎な死に方をするのが厄介でしたよね😭」「発生するかどうかがタイミング次第だったり😇

「ちなみにこの問題は、以前のつっつきに出てきた名前空間地獄の問題↓とは別なんでしょうか?」「少なくともwarningレベルでは別の話ですね🧐: ただ元記事の後半ではconst_missingがらみで名前空間地獄に近いところも話されてますけど」

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

⚓「Rails Tutorial」がRails 6対応に向けて動き出す

The Ruby on Rails web framework continues to be an outstanding choice for developing dynamic web applications—relied on by companies as varied as Disney, GitHub, and Airbnb.
And it keeps getting better: Ruby on Rails creator David Heinemeier Hansson has just announced the release of Rails 6.0!
With this new major release, I’m happy to say that the Ruby on Rails Tutorial book, videos, and online course will soon be updated accordingly.
Experience shows that it’s a good idea to let things shake out for a couple of weeks after any big release like this, so I’m going to spend that time going back through the whole tutorial using Rails 6 and fixing anything that needs to be updated.
Rails TutorialのNewsletter(2019/08/17)より


railstutorial.orgより


つっつきボイス:「Rails Tutorial作者のMichael Heartlさんが、Rails 6に合わせて必要な部分に更新をかけるそうで、でもしばらく自分がいじってからになると思うとのことです: Railsチュートリアルの方もその後で対応したいです☺

「RailsチュートリアルはRails 6で影響される部分ってそんなになさそうですけどね: 業務に寄ったマルチDBとかTwitter的アプリでは要らなさそうだし😆」「たしかに〜」「Action TextぐらいはRailsチュートリアルにあってもよさそうですけどね😋」「それもたしかに〜」「ちょっとリッチなテキストボックス!」「そこにドラッグアンドドロップするだけでファイルをアップロードできる!とか☺

「Railsチュートリアルのボリューム、ますます単調増加したりして😆」「さすがにMichaleさんもサポートを軽減するために、HTMLの基本とかGitの基本みたいな部分を別教材にして販売しています(Learn Enoughシリーズ↓)」「そういう基本的な部分って面倒見きれないですよね😆

参考: Learn Enough to Be Dangerous


learnenough.comより

⚓Morning Cup of Coding: 高品質の技術情報ニュースレターサイト


つっつきボイス:「『Morning Cup of Coding』は@st0012さんイチオシの技術情報リンク集ニュースレターで、評判も上がってファンディングを取り付けたことで紙の雑誌も出すらしいと聞きました」「朝に読む一杯のコーヒー的な☕」「メールが飛んでくる的な」「実は@st0012さんにずっと前に教えてもらっててサブスクライブしてたんですが、すっかり忘れててGmailの肥やしになってました😅

「これは毎日出るのかしら?」「週イチぐらいかなと思ってたら…ほぼほぼ毎日出てますね!😳」「見た感じRubyやRailsに限らないっぽい」「ですです、JavaScriptでもRustでも何でもありで」「ノリとしてははてブTech的な雰囲気かな?」「あるいは日刊Railsウォッチというか😆、このペースでウォッチ出してたら死ねるかも」「目についたものをTumblr的に流してる感も😆」「自分もまだちゃんと読み込んでませんが、キュレーションはされてると思います」

  • バックナンバー: Issues

⚓apparition gemでpoltergeistを除霊しよう(Ruby Weeklyより)

ポルターガイストだけにapparition(霊現象)とかexorcise(除霊)というネーミングになるでしょうね。


つっつきボイス:「タイトルに惹かれて拾っちゃいました😆」「poltergeistとかもう終了してどんだけ経ってるかと🤣」「poltergeistは、Capybaraで使えるPhantomJSドライバですね👻

「でこのapparitionって何をするgem?」「リポジトリにはCapybara向けChromeドライバとありますね」「これをpoltergeistと差し替えるとシームレスに動いてくれるとかそういう感じかな?」「『poltergeist APIとできるだけ互換性を取るようにしている』みたいなことも書いてますね😆」「poltergeistで書いたものを今更書き直すのがつらすぎるから、ミニマムに置き換えられるアダプタを作った的な🤣

# 同記事より
    require 'capybara/apparition'
    Capybara.javascript_driver = :apparition

「このapparition gemでめでたく成仏できそうでしょうか?」「どうだろう?さすがにもうみんなpoltergeistは置き換えてるんじゃないかと😆」「わかんないすよ〜、bundle installすら通らないようなレベルの古代のプロジェクトが発掘されることもありますし😇」「gemがなくなってるレベルとか😆」「ファラオの呪い💀

参考: Rails E2EテストでpoltergeistからHeadless Chromeへ乗り換える - Hack Your Design!
参考: ポルターガイスト現象 - Wikipedia


コンピュータプログラミングにおいて、ほかのオブジェクトに情報を渡すだけのオブジェクトを表す。アンチパターンの一つ。
ja.wikipedia.orgより

「上はWikipediaにぴろっと書いてあったのを貼ってみました↑」「Poltergeistっていうアンチパターンは知らなかったなぁ〜😳」「これだけ見たらData Access Objectも含まれそうな気がするけど」

英語版WikipediaにはOOPのアンチパターンとしてありました

参考: Poltergeist (computer programming) - Wikipedia
参考: 【ソフト開発 アンチパターン】Poltergeists - 緑茶思考ブログ

⚓その他Rails


つっつきボイス:「esaはGoogleアカウントで認証できるのがいいなと思って」「今のesaはオンラインエディティングとかも含めて全然いいと思いますね❤」「記事によると改修も素早いみたいですし」


esa.ioより

参考: esa.io の作り方 — Rails 4.2.5頃から使い始めているそうです

↓3年前の記事でだいぶ状況変わってますが一応。

Markdownで書けるドキュメントコラボレーションサービスを比較する

⚓Ruby

⚓32,000行の腐ったCSVを32,000ステップ以下で修正する(Ruby Weeklyより)


つっつきボイス:「元CSVの行数が32,000あってもタイトルみたいに32,000ステップもかからないと思いますけど😆」「CSVのどこが壊れてるかなっと」「artist列がクォートされてるのとされてないのとあったり😇」「でフィールドがずれる😇」「よくあるダメCSV😆



`
「こんなふうに↓想定カラム数となぜか食い違っちゃうとかあるある〜😆」「実データを見てきっとここがずれてるみたいに当りをつけて対応するとか😆」「ある程度開発経験があれば一度は出くわすヤツ」「いい経験にはなりますけど😅

[1, 2, 3, 4, 5] -> [x, x, x, x] =>
[[1, 2], 3, 4, 5]
[1, [2, 3], 4, 5]
[1, 2, [3, 4], 5]
[1, 2, 3, [4, 5]]

「そういえばこれ↓はRuby Weeklyの別のエントリにあったgemですが、上の記事と作者が同じですね」「CSVをちょっといい感じに整形してくれるのかな?」「同じ人だ」「なるほど、『こうじゃね?』ってパースの候補を出してくれるっぽい↓😋」「パースを推測してくれるのか!」「gemを作ったついでに記事を書いたかその逆みたいなノリかも☺」「数値に入ってるカンマで区切られるとか悲しい😭

Which one of these is correct?

(1)  artist    : 10
     title     : 000 Maniacs and Michael Stipe
     albumtitle: To Sir with Love
     label     : "Campfire Songs,Rhino"

(2)  artist    : 10
     title     : 000 Maniacs and Michael Stipe
     albumtitle: "To Sir with Love,Campfire Songs"
     label     : Rhino

(3)  artist    : 10
     title     : "000 Maniacs and Michael Stipe,To Sir with Love"
     albumtitle: Campfire Songs
     label     : Rhino

(4)  artist    : "10,000 Maniacs and Michael Stipe"
     title     : To Sir with Love
     albumtitle: Campfire Songs
     label     : Rhino

「こんなデータを作った人を問い詰めたい😆」「できちゃったものはしょうがないですし☺

「そういえばCSVと格闘してた人が社内にもいましたね↓」「CSV以外にもメールから文章を取り出すとかありますし☺

Ruby: CSVでヘッダとボディを同時に定義するやり方

⚓アルファベットのアクセント記号を除去する方法いろいろ(Ruby Weeklyより)


つっつきボイス:「なるほど、こういう置き換えね↓」「こういう便利メソッドってRubyになかったっけ?」「Martin先生がアクセント付きの大文字小文字変換をやってた覚えはあるんですけど😅」「まあ自分ら使わないけど😆」「ヨーロッパの人には切実なヤツですね」「文字が変わっちゃうと大変ですし☺

# 同記事より
UNACCENT = {
  'Ä'=>'A',  'ä'=>'a',
  'Á'=>'A',  'á'=>'a',
  'Æ'=>'AE', 'æ'=>'ae',
  'É'=>'E',  'é'=>'e',
  'Í'=>'I',  'í'=>'i',
             'ï'=>'i',
  'Ñ'=>'N',  'ñ'=>'n',
  'Ö'=>'O',  'ö'=>'o',
  'Ó'=>'O',  'ó'=>'o',
  'Œ'=>'OE', 'œ'=>'oe', 
             'ß'=>'ss',
  'Ü'=>'U',  'ü'=>'u',
  'Ú'=>'U',  'ú'=>'u',
}

つっつきボイス:「書いた人はどちらかというと変換のパフォーマンスをチェックしてるみたいですね↓」

# 同記事より
text=>AÄÁaäá EÉeé IÍiíï...<, n=20000:
                               user     system      total        real
each_char                  4.953000   0.000000   4.953000 (  4.958531)
each_char_v2               4.265000   0.000000   4.265000 (  4.265561)
each_char_v2_7bit          4.266000   0.000000   4.266000 (  4.268788)
each_char_v2_7bit_faster   3.375000   0.000000   3.375000 (  3.379573)
each_char_reduce           5.641000   0.000000   5.641000 (  5.638152)
each_char_reduce_v2        5.125000   0.000000   5.125000 (  5.113069)
gsub                       6.078000   0.000000   6.078000 (  6.079772)
gsub_v2                    5.500000   0.000000   5.500000 (  5.492944)
gsub_v3a                   4.203000   0.000000   4.203000 (  4.248036)
gsub_v3b                   5.094000   0.000000   5.094000 (  5.098041)
scan                      10.062000   0.000000  10.062000 ( 10.050102)

text=>Aa Ee Ii...<, n=20000:
                               user     system      total        real
each_char                  1.313000   0.000000   1.313000 (  1.304258)
each_char_v2               1.328000   0.000000   1.328000 (  1.327656)
each_char_v2_7bit          0.984000   0.000000   0.984000 (  0.981193)
each_char_v2_7bit_faster   0.984000   0.000000   0.984000 (  0.977593)
each_char_reduce           1.594000   0.000000   1.594000 (  1.583752)
each_char_reduce_v2        1.641000   0.000000   1.641000 (  1.650390)
gsub                       0.140000   0.000000   0.140000 (  0.127486)
gsub_v2                    0.125000   0.000000   0.125000 (  0.127581)
gsub_v3a                   0.125000   0.000000   0.125000 (  0.124071)
gsub_v3b                   0.047000   0.000000   0.047000 (  0.056792)
scan                       3.141000   0.000000   3.141000 (  3.129562)

記事には「もっといい方法があったら教えて欲しい」とありました。

⚓その他Ruby


つっつきボイス:「この成果報告会は毎年やってるっぽいですね」「金曜午後という時間帯のせいか、まだ空きがかなりあります」「まあちょっと行きづらい時間帯ではあるけど😅、当日までにはもっと増えるんじゃないかなと」「親睦会以外は無料ですし、Rubyコミッターとがっつり話す機会が多そうですね😍」「場所も品川でアクセスいいですし」

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

⚓『A Heavily Commented Linux Kernel Source Code』

PDFの日付は2019/01/24ですが、カーネルのバージョンは0.12となっています。なお自分のUbuntu 18.04 VMのカーネルバージョンは以下です。

$ uname -r
5.0.0-25-generic

つっつきボイス:「このPDF、Intelアーキテクチャのとっても基本的なところを最初の方でしっかり押さえてるのがいいですね❤: 割り込みピンとかバスの図とかもちゃんとあるし、これ1冊でコンピュータアーキテクチャの授業数本分ぐらいのボリュームが集約されてる感」(以下Linux話が続く)


同PDFより

「このPDFが15年ぐらい前に出ていたら最高の教科書だったろうなって😆」「😆」「ボリュームめちゃ多いですし😅」「1000ページ超え!」「もし読み通すことができたら相当力付くかも💪


なお以前のウォッチでもご紹介した『Linux Inside』↓のカーネルバージョンは、明言はされていませんが4.0.0-rc2という文字がありました。

「こっちのLinux Insideは新しいのがいいところですね😋」「Linux 5ではないみたいでした」「cgroupsとかも扱ってるあたりが現代のLinux!」

参考: cgroups - Wikipedia

なおLinux 5の公式ドキュメントは以下です。

⚓JavaScript

⚓clean-code-typescript


つっつきボイス:「見出しを眺めた感じではTypeScriptに限らない一般性の高そうな感じではありますね」「こういう話でTS独自のものってあんまり出てこなさそう😆」「言語固有の話と一般の話を分けてくれたらうれしい🙏

「『Promiseよりasync/awaitを使え』とか、たしかに」「このあたりは言語固有っぽいですね😋」「Promiseって慣れないと読みにくい気が😅

参考: async function - JavaScript | MDN

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

⚓8/27 12:00までのセール

⚓言語・ツール

⚓あまりに自然な英文を自動生成?

参考: 「危険すぎる」と言われたAIの自動文章作成ツール「GPT-2」のモデルが新たに公開へ - GIGAZINE

ちょっと試しに動かしてみました。

  • 入力(Goby READMEより: 実際はもっと英文ニュース的な文が望ましい)

Goby is an object-oriented interpreter language deeply inspired by Ruby as well as its core implementation by 100% pure Go. Moreover, it has standard libraries to provide several features such as the Plugin system. Note that we do not intend to reproduce whole of the honorable works of Ruby syntax/implementation/libraries.

  • 出力

======================================== SAMPLE 1 ========================================

In many cases, the project can use Java for various reasons. For example, we are interested in the Java and Java Virtual Machine. Another example was to work on a project based in Go. But we don’t have that. The project is also looking at Open Source languages such as Go which can make development smoother.

Our goal is to create and maintain an open source program which provides a complete representation of the language without limitations and it enables programmers and engineers to work with Java and Java Virtual Machine (JVM) in a highly productive time. So, there is no way that the project can change its design.

In order to achieve this goal we are developing a Go application which consists of two files which you can read directly in the IDE. These files contain all the information you need to write a program. So, as you have read that code, you can understand how it works and make your own choices.

These two files are the src folder and src/classes folder.

If you are interested in learning them as well as working on them as an an IDE, you may see more information at our articles. The same code and documentation will be available at our websites.

We will also try to produce the most current version of the app from Go and we expect that you do not have an Java or Go IDE and you will only experience Go features when developing the project.<|endoftext|>”This is a very sad episode for him,” said the head of one of the largest bitcoin companies in the world, Jihan Wu. “The CEO is already a very hard-working and loyal businessman. His staff is always helping out and there’s a sense of selflessness in this guy that makes him very loyal to the people in this company.”<|endoftext|>A recent report by the American Institute for Fiscal Studies says that the Trump administration is making a good case for the federal government to expand healthcare to poor people — a position that it’s taken in past legislative successes.

The administration has pushed to tighten up its health care requirements by requiring all Americans to have coverage before receiving an insurance plan or paying a penalty. This has helped expand Medicaid, which under Obamacare has run out of money.

But the president has indicated that he wants Congress to push for a similar expansion because of his belief that more people would be able to become members of the health-care system.

Read the analysis in the September 23, 2017 issue of The Daily Axios.

This is exactly what the White House
@article{radford2019language,
title={Language Models are Unsupervised Multitask Learners},
author={Radford, Alec and Wu, Jeff and Child, Rewon and Luan, David and Amodei, Dario and Sutskever, Ilya},
year={2019}
}


つっつきボイス:「CUDAとかTensorFlowとかが続々インストールされましたけど何とか動きました: たしかにかなりそれっぽい英文が生成されました」「この種の研究はかなり前から行われてますね☺」「要約とかではない?」「入力の語群を元に記事を生成してるようです」「この英文が自然かどうか自分にはわかりそうにないです😅

「そういえば論文の世界だとこういうので査読ハックしたりする人いたりしますね: 生成された文章を投稿してカンファレンスの審査を通したり🤣」「なんと🤣」「リポジトリに『生成した文にはcitation付けろ』とあるのもたぶんそれを念頭に置いてるんでしょうね😆」「そうやって生成した意味のありそうでない文章をcitationしてさらに…と繰り返すと謎のディープラーニング世界ができそう🤣」「日本語でできるようになったらすごいかも」「日本語自動生成の研究してる人いた気がするけど、日本語はなかなか自然な文章にならないみたいです🤔」「日本語は構造とか文体とかでいろいろ難しいかも😅

⚓その他

⚓OCB2共通鍵暗号の脆弱性を事前に発見

参考: OCB mode - Wikipedia

⚓その他のその他


つっつきボイス:「ケーキの3等分🎂」「最優秀賞の六芒星スゴい😳」「全部直線なのにできてるっぽいし」「画期的」「ツイートの続きもめちゃ盛り上がってます」「4等分し続けるの好き😆

⚓番外

⚓夢の図書館@青梅


つっつきボイス:「ものすごい量の技術雑誌や実機をコレクションしてるらしくて、そのうち行ってみたいと思ってたので貼ってみました」「『CQハムラジオ』とかあるし😆」「青梅ちょと遠いかな😆


今回は以上です。

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

週刊Railsウォッチ(20190821-2/2後編)11のgemにバックドア、ruby-jp Slackがとてもアツい、Fullstaq Rubyでチューンアップ、HTTPサービス監視chaoほか

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

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

Rails公式ニュース

Ruby Weekly


週刊Railsウォッチ(20190902)Ruby 2.6.4セキュリティ修正リリース、スライド「All About Ruby in 2019」、Shrine gem 3.0に入る新機能ほか

$
0
0

こんにちは、hachi8833です。ついさっきruby-jp Slackのワークスペースアイコン↓が見ている目の前で突然変わってびっくりしました😳。おめでとうございます!🎉

その後ちょっぴりリサイズしたようです↓。

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

※今回のウォッチは分割していません
※今回のつっつきはSlackベースで行いました

⚓週刊Railsウォッチ「公開つっつき会」第14回のお知らせ(無料)

第14回目公開つっつき会は、9月5日(木)19:30〜にBPS会議スペースにて開催されます。皆さまのお気軽なご参加をお待ちしております🙇

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

公式の更新情報のほとんどを先週の「先週の改修」で先取りしていました😋。今回も6-0-stableの更新を中心に見てみました。

他に、6.0.1マイルストーンのclosedからも見繕いました。

⚓(6-0-stable、master)fork後にConnectionPool::Reaperが親のコネクションを刈り取る問題を修正

先週#36999#36998に関連していそうです。また「attr_reader :poolsに依存しないようにした」ともあります。

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L332
          private
            def spawn_thread(frequency)
              Thread.new(frequency) do |t|
                running = true
                while running
                  sleep t
                  @mutex.synchronize do
-                   @pools[frequency].select!(&:weakref_alive?)
+                   @pools[frequency].select! do |pool|
+                     pool.weakref_alive? && !pool.discarded?
+                   end
+
                    @pools[frequency].each do |p|
                      p.reap
                      p.flush
                    rescue WeakRef::RefError
                    end
                    if @pools[frequency].empty?
                      @pools.delete(frequency)
                      @threads.delete(frequency)
                      running = false
                    end
                  end
                end
              end
            end
...
      def discard! # :nodoc:
        synchronize do
-         return if @connections.nil? # already discarded
+         return if self.discarded?
          @connections.each do |conn|
            conn.discard!
          end
          @connections = @available = @thread_cached_conns = nil
        end
      end

+     def discarded? # :nodoc:
+       @connections.nil?
+     end
...

つっつきボイス:「この辺はGC系とかも絡んでいるようでデバッグが大変そうなところだなあ」

⚓content_type=の追加部分が落ちないよう修正

# actionpack/lib/action_dispatch/http/response.rb#L420
 private
-   ContentTypeHeader = Struct.new :mime_type, :extra, :charset
-   NullContentTypeHeader = ContentTypeHeader.new nil, nil, nil
+   ContentTypeHeader = Struct.new :mime_type, :charset
+   NullContentTypeHeader = ContentTypeHeader.new nil, nil
+
+   CONTENT_TYPE_PARSER = /
+     \A
+     (?<mime_type>[^;\s]+\s*(?:;\s*(?:(?!charset)[^;\s])+)*)?
+     (?:;\s*charset=(?<quote>"?)(?<charset>[^;\s]+)\k<quote>)?
+   /x # :nodoc:

    def parse_content_type(content_type)
      if content_type && match = CONTENT_TYPE_PARSER.match(content_type)
-       ContentTypeHeader.new(match[:type], match[:extra], match[:charset])
+       ContentTypeHeader.new(match[:mime_type], match[:charset])
      else
-       ContentTypeHeader.new(content_type, nil)
+       NullContentTypeHeader
      end
    end

つっつきボイス:「この間からcontent_type周りが繰り返し修正されているようですが、ちょっとややこしいことになってそう?」「これは@kamipoさんが貼ってくれてるこのPR(#35549)↓が発端みたいすね: この辺は使ってる人たちがテストしないとなかなか踏まなそうなところ」「通常の使い方をしている分には問題はないんでしょうか?」「いや、そもそも既存のコードで使ってた人たちに問題が出たというものだと思います: ただ、今回のIssueを踏むような使い方をしている人たちがそれほど多くなくて発見が遅くなったみたいな話だと思う」

Content-Typeヘッダーに、charset以外のオプションパラメータが含まれる可能性がある。
このヘッダー行の有無を示す例として、text/csv typeに以下のようなheaderパラメータがあるとする。

Content-Type: text/csv; charset=utf-16; header=present

この種のオプションパラメータを#charsetから除外するため、このPRで#parse_content_typeの実装を変更した。
#35549より大意

@kamipoさんのレスには「#33549でdeprecation期間を置かずに変更したのは急ぎすぎだった」とあります。

⚓(6.0.1)attachables.nil?条件を除去

# activestorage/lib/active_storage/attached/model.rb#L91
      def has_many_attached(name, dependent: :purge_later)
        generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
          def #{name}
            @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self)
          end
          def #{name}=(attachables)
            if ActiveStorage.replace_on_assign_to_many
              attachment_changes["#{name}"] =
-               if attachables.nil? || Array(attachables).none?
+               if Array(attachables).none?
                  ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
                else
                  ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
                end
            else
-             if !attachables.nil? || Array(attachables).any?
+             if Array(attachables).any?
                attachment_changes["#{name}"] =
                  ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables)
              end
            end
          end
        CODE

つっつきボイス:「リファクタリングでしょうか?」「これはリファクタリング&読みやすさに倒したみたいな感じみたいですね: PR読めばそんなに難しいこと書いてない感じ」

⚓(6.0、6.0.1)connected_towhile_preventing_writesを呼ぶようになった

  • while_preventing_writesconnected_toから直接呼び出すようにした

アプリケーションの作者がデータベース切り替えミドルウェアを使ってconnected_toを明示的に呼び出したい場合がある。アプリは書き込みをオフにできるが、connected_to(role: :writing)を呼び出すまではオンに戻らない。
アプリはこの変更によって、(明示的に書き込みをオフに場合を除いて)書き込みを許可したいロールが書き込みを行っているかどうかを仮定することで修正できる。
CHANGELOGより

# activerecord/lib/active_record/connection_handling.rb#L116
-   # When using the database key a new connection will be established every time.
-   def connected_to(database: nil, role: nil, &blk)
+   # When using the database key a new connection will be established every time. It is not
+   # recommended to use this outside of one-off scripts.
+   def connected_to(database: nil, role: nil, prevent_writes: false, &blk)
      if database && role
        raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments."
      elsif database
        if database.is_a?(Hash)
          role, database = database.first
          role = role.to_sym
        end
        config_hash = resolve_config_for_connection(database)
        handler = lookup_connection_handler(role)
        handler.establish_connection(config_hash)

        with_handler(role, &blk)
      elsif role
-       with_handler(role.to_sym, &blk)
+       if role == writing_role
+         with_handler(role.to_sym) do
+           connection_handler.while_preventing_writes(prevent_writes, &blk)
+         end
+       else
+         with_handler(role.to_sym, &blk)
+       end
      else
        raise ArgumentError, "must provide a `database` or a `role`."
      end
    end

つっつきボイス:「connected_torole: :writer + prevent_writesをしようと思ったときにrace conditionしちゃう的な話なのか↓」「確信はないけどこの辺のやつで外側のAR::Base.connected_toと内側のAR::Base.connection_handlerで別のDBコネクション取っちゃうみたいなケースがあったのかなあ?」

# activerecord/lib/active_record/middleware/database_selector/resolver.rb#L46
        private
          def read_from_primary(&blk)
-           ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
-             ActiveRecord::Base.connection_handler.while_preventing_writes(true) do
-                instrumenter.instrument("database_selector.active_record.read_from_primary") do
-                 yield
-               end
+           ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: true) do
+              instrumenter.instrument("database_selector.active_record.read_from_primary") do
+               yield
              end
            end
          end

⚓番外: メンテナンスポリシーの記載を明確にした

# guides/source/maintenance_policy.md#L67
For severe security issues all releases in the current major series, and also the
- last major release series will receive patches and new versions. The
+ last release in the previous major series will receive patches and new versions. The
classification of the security issue is judged by the core team.

つっつきボイス:「これはまあ書いてあるとおりということで☺

参考: Ruby on Rails のメンテナンスポリシー - Rails ガイド

上の修正に基づくと、Railsガイドの該当箇所は以下のようになります。後ほどRailsガイドを更新します(#883)。

重大なセキュリティ問題については、最新のメジャーなシリーズにおけるすべてのリリースと、前回のメジャーなシリーズの最後のリリースに対してセキュリティパッチと新バージョンを提供します。セキュリティ問題の重大性の分類はコアチームによって行われます。

つまり今回の場合、Rails 6がリリースされたことで、Rails 5.1.z以前についてはセキュリティパッチや新バージョンは提供されなくなったということになります。

⚓Rails

⚓Jetsの実験的Rails互換「アフターバーナーモード」機能

まだexperimentalですが、RackをLambda実行コンテキストで起動することで、Railsアプリを無改造でデプロイできるようにする試みだそうです。

参考: AWS Lambda 実行コンテキスト - AWS Lambda

注意:

  • RailsプロジェクトのGemfileにjetsを追加しないこと
  • Railsアプリを必ずしもそのままサーバーレス化できるとは限らない
    • たとえばファイルや画像をファイルシステムにアップロードするアプリはそのままでは無理
  • Lambdaレイヤのサイズは250MBに制限されている
    • Railsの基本gemだけで146MBぐらいにはなる
  • 複雑なRailsアプリについてはJetsのメガモードも検討
  • アフターバーナーモードよりはJetsで直接アプリにする方が一般におすすめ

参考: AWS Lambda の制限 - AWS Lambda


つっつきボイス:「Jetsのafterburner modeはかなり初期からexperimentalとして公式ドキュメントにはありましたね: ただ、どれくらいまともに使われてるのかが謎すぎるので全然触ってない」「あ、そんなに前からあったんですね😅」「そもそもRailsをそのまま変換して動くケースの方が極レアだと思うので、そこまで魅力は感じなかったなあ(誰か人柱している人がいればレポートは見てみたいけど)」

「まあ特に魅力に感じなかったので「夢の試みだなあ」くらいにしか見てなかった😆」「アフターバーナー、見るからに重そう…」「Railsに必要なRack Middlewareを毎回ロードするということを考えるとちょっと厳しすぎるだろう感はありますねえ」

⚓Shrine gem 3.0で予定されている新機能(RubyFlowより)


  • ミュータブルなstructからの切り離し(Repositoryパターンに対応するため)
  • derivativesプラグイン
  • backgroundingプラグイン
  • 一時ストレージをスキップする機能
  • ファイル取り出しの高速化
  • 永続化APIを複数のプラグインで標準化

つっつきボイス:「Shrine、Active Storageに押されて更新やめちゃうとかも心配してたけど、開発続行してくれる感じなんですね: 既存システムのアップグレードパスとしてもありがたいし、Active Storageでできない機能とかもちょいちょいありそうだしありがたい話だ🙏」「記事には『現行のShrineはActive Recordとかにはよく合う設計だが、Hanamiとかに合わないのでstructを切り離した』とありました」「あーなるほど。確かにRails専用ってわけじゃないですもんね、それは分かる😋」「今後のメンテも期待できそうですね❤


shrinerb.comより

⚓Rails 6のWebpackerを理解する(Awesome Rubyより)


  • デフォルトでapp/javascriptに置かれる
  • app/javascript/packsにはapplication packが置かれる
  • bin/webpack-dev-serverでライブリロードできる
  • production環境ではassets:precompileタスクにwebpacker:compileが追加される
  • javascript_pack_tagヘルパーメソッドはアセットパイプラインのjavascript_link_tagと同等

つっつきボイス:「記事の行間が開きすぎてて読みにくい💦」「😆」「Understandingとあるけど割と簡単なことしか書いてない気もするので、いわゆる「完全に理解した」的な記事かな😇」「Webpackerの「Rails 6での変更点」を理解してみた的な?」「あ、たしかにRails6になって変わった部分もピックアップされてますね: 5.2からのmigration path的にはありがたい話なのかも」「Railsガイドにはありそうですが、変更点までは網羅されてないかもですね(後で見ます)」

意外にもRailsガイドにはWebpackerのまとまった記述がありませんでした😳。アップグレードガイドでWebpackerについてごく簡単に触れられている程度です↓。

参考: 2.1 Webpackerの利用について — Rails アップグレードガイド - Rails ガイド

⚓Ruby

⚓Ruby 2.6.4がリリース: セキュリティ修正(Ruby公式ニュースより)


つっつきボイス:「このツイート見るとRDocの再生成も必要…😅: RDoc生成を抑制していれば大丈夫?」「Doc生成してしかもそれをPublic公開してない限りは問題ないやつですね: 使ってる人が少ない機能とかだとなかなか発見が遅れるやつ」

主にgemの作者にとって影響がありそうですね。

参考: library rdoc (Ruby 2.6.0)

なお、現在は--no-ri--no-rdocではなくno-documentで抑制すると今頃知りました↓。

参考: ruby - How to make --no-ri --no-rdoc the default for gem install? - Stack Overflow
参考: gemrcの--no-riと--no-rdoc、deprecatedなoptionなのでみなおしたほうがいいかもですよ - Qiita

⚓Bundler 2.1.0pre.1リリース


つっつきボイス:「かなり内部をアップデートしたそうです」「マイナーバージョンアップでpreを踏むの、慎重でありがたいすね: Bundlerの挙動がおかしくなると世の中のRailsエンジニアのあちこちから悲鳴あがるし、じっくり検証&フィードバック期間があるのはありがたい🙏

後でリリースノートを見ると機能とバグフィックスがたくさん載っていますが、機能のみざっと見てみました。

  • configコマンドをサブコマンドで再実装
  • bundle plugin listを追加
  • bundle lock --gemfileフラグを追加
  • ローカルgitリポジトリソースを指定する--local_gitオプションを追加(プラグインインストール用)
  • quietフラグをインラインbundlerに追加
  • prefer_patch設定を導入(bundle updateの振る舞いをbundle update --patchのようにする)
  • Bundler.original_systemBundler.original_execを導入
  • bundle info GEMを追加(旧bundle show GEMに相当)
  • Gemfileのgemグループのリストを表示するbundle listを導入(従来はbundle showのエイリアスだった)
  • bundle outdated --filter-strictを導入(bundle outdated --strictのエイリアス)
  • bundle add:gitオプションと:branchオプションを追加
  • ruby_26:platform(s) DSLに有効な値として追加
  • bundle packageで提供されているすべての機能をbundle cacheに含めた
  • 新しいgemテンプレートを改善
  • bundle gem--[no-]gitオプションを追加(ソース管理しないgemを生成、単一リポジトリなどで有用)

⚓スライド「All About Ruby in 2019」


event.shoeisha.jp/devsumiより


つっつきボイス:「Ruby 2.3からの差分の歴史とかもさらっとまとまっててありがたいな: これは良い👍」「スライドで情報が完結していてありがたいです🙏

「RubyのDockerイメージ、Docker Community版しか知らなかったけどRubyコアチーム版もあるのか↑: 確かにDocker Community版のコンテナは実は色々盛りだくさんに入っているので、pureなRubyを動かすだけならもっと軽いイメージの選択肢はあるなと思っていた(その代わり、gem install railsがすぐにできるように色々入ってる利点もある): まだExperimentalって書いてあるからいまいち詳しいことはわからないけど」

「コアチーム版のメンテナーはmrknさんなのね↓」


「DockerHub版のDockerfile↑はなんかちょいちょいHackぽいこともやってるように見えるのでrubylang/ruby の方が整理されている感じはある、と思ったら環境変数が違うとかちゃんとスライドに書いてあった↓」

スライドには、他にRubyのPlaygroundサイトなども紹介されています。要チェックだと思います😋


ruby.github.io/TryRubyより


なお、このスライドは以下のツイートで知りました。

スライドによると、発表後の2つの変更点は以下でした。

  • numbered parameterの記法が@1から_1

  • パイプライン演算子|>をいったんご破算(記法の変更も含め将来に再検討するかも)

⚓その他Ruby

つっつきボイス:「RubyのソースでANYARGSとかいうのが使われすぎてたのを修正したそうです」「ANYARGSだけじゃなくてvoid*とかも駆逐されたりしていて相当でかい修正だな。しゅごい😳」「RubyのCのソース、表記スタイルが長年にわたってばらつきまくっているという話が以前ありましたけど、今からだと修正量が莫大になっちゃいそうだからなかなか行われないのかな…」「ソースのスタイルに大きく手を入れると開発者脳内にある関数名とかマクロ名とかのポインタがロストしてしまうので、一部のメンバーが中心になってコード書いてるようなプロジェクトでは変化させないほうが全体効率が良い、とかはあるかもですね☺

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

⚓Google App Engineスタンダード環境でRuby 2.5をサポート(ベータ)


つっつきボイス:「GAEは正直あんまり詳しくないけど、AWSのLambda Layerみたいな自由度あるのかなあ🤔」「記事のリンクによるとApp Engineには「スタンダード環境」と「フレキシブル環境(Ruby対応済み)」というのがあるそうです↓」

参考: App Engine 環境の選択  |  App Engine ドキュメント  |  Google Cloud

「GAEはstarndard / flexibleで全然環境自体が違うんですね: Lambdaは実行環境自体はAWS提供の環境でもユーザーの作ったLambda Layerでも基本的に同じはず(Firecrackerだっけかな)」「GAEのflexible は完全にDockerみたいなので、LambdaとかGAE standardと比べるには不利すぎるかな🤔

参考: Firecracker – サーバーレスコンピューティングのための軽量な仮想化機能 | Amazon Web Services ブログ

追記(2019/09/04)

Quora: (3) とうとうGoogle App EngineでRubyが標準サポートになりましたが、心境のほどはいかがでしょうか?に対するYukihiro Matsumotoさんの回答 - Quora

⚓JavaScript

⚓知っておきたいJavaScriptの新機能7つ


つっつきボイス:「はてブで見つけました: privateの記法がキモがられてるっぽいです」「optional chaining(?.)はなんかで見ましたね: Rubyで言う&.(ぼっち演算子)的な」「クライアントはブラウザ依存あるから、これらの機能が使えるようになるまでまだ時間かかりそうだけど、サーバーサイドJSで使う分には問題ないから知っておいてもよさそう😋

// 同記事より
// privateフィールドは'#'で始まらなければならない
// classブロックの外部からはアクセスできない

class Counter {
  #x = 0;

  #increment() {
    this.#x++;
  }

  onClick() {
    this.#increment();
  }

}

const c = new Counter();
c.onClick(); // 動く
c.#increment(); // エラーになる
// 同記事より: optional chaining
const stop = please?.make?.it?.stop;

Rubyのぼっち演算子はRailsの`Object#try`より高速(翻訳)

⚓その他

⚓やはり名前が大事


つっつきボイス:「以前のウォッチでも、コーディングで一番時間がかかるのが「名前付け」という記事を散りあげた覚えがあります」「命名はコーディングというよりは『設計』なんですよね: 名は体を表すの通りなので、適切な命名がされればあとは純粋なロジックを書くのに集中できるという」

⚓番外

⚓iPS細胞で視力を取り戻す

参考: 人工多能性幹細胞 - Wikipedia


つっつきボイス:「20年もしたら普通になってきそうですね」「この辺はもう治験に入ってるんだなあ: 未来感ある🚀」「度胸さえあれば、白内障になったときにこれ↓試してみたい気も😆

参考: 3Dプリンターで完璧に機能する目「バイオニックアイ」を作り出す研究 - GIGAZINE


今回は以上です。

おたより発掘

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

週刊Railsウォッチ(20190826)6-0-stableの更新を見てみる、『Morning Cup of Coding』ニュースレター、Rails TutorialがRails 6対応に動き出すほか

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

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

Ruby 公式ニュース

Rails公式ニュース

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

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

$
0
0

概要

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

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

原注: 本記事はRails 6.0.0.rc2を元にしています。

正直に申し上げると、Action Textコンポーネントに対してこれといった興味はありませんでした。Action Textを使うことは業務でもサブプロジェクトでもまずありそうになかったからです。しかし#36177を解決するために多少時間を取ってAction Textを調べました。

Action Textはある種の「沼」なのですが、現時点ではどうもこれといったオンライン資料が見当たらないので、自分が調べてわかった範囲で本記事にまとめました。本記事がAction Textに取り組み始めた人にもお役に立てばと願っています。

本記事のテーマはAction Textのファイルアップロード機能なので、Active Storageコンポーネント(主にダイレクトアップロード機能)についても多少言及します。

全体の手順は以下の3つに分かれています。

  1. アップロード
  2. パース、変換、アタッチ
  3. レンダリング

(Action Textを調べているうちに、あっと驚くものを見つけましたので、本記事の最後に書いておきます😉

1. アップロード

本セクションはAction Textとはあまり関連していませんが、ブラウザのテキスト入力エリアに画像をドラッグアンドドロップする仕組みもめちゃめちゃ面白いと思います(さてどうでしょう?)😄

1. RailsのJavaScriptライブラリは、最初に以下のようなファイルメタデータを持つPOSTリクエストを/rails/active_storage/direct_uploadsに送信します(このエンドポイントは設定変更可能です)。

2. RailsはActiveStorage::Blobレコードを1件作成し、ダイレクトアップロード用のURLとそのblobの署名済みidを返します。
3. JSライブラリは、ダイレクトアップロードのURLを受け取るとそのファイルアップロードを実行します。
4. それと並行してTrixエディタのテキストエリアに画像が挿入され、画像がfigure要素でラップされます。アップロードされたファイルのメタデータは、figure要素のdata-trix-attachment属性の中に配置されます。

この時点までに、アップロードした画像はページで表示され、アップロードしたファイルはストレージに、画像のblobレコードはデータベースにそれぞれ保存されます🎉

しかしRailsは、次の手順でテキストコンテンツを保存するまでは、blobレコードをアタッチする対象をまだ認識していません。

パース、変換、アタッチ

テキストコンテンツを無事に受け取っても、まだデータベースには保存できません。以下のような作業がまだ残っています。

  • その画像のblobレコードは現時点では孤立しているので、ActionText::RichTextレコードなどにアタッチする方法を見つける必要があります。
  • テキストコンテンツを必ずしもTrixエディタに表示するとは限らないので、Trixの要素を直接保存するのはよい考えとは言えません。つまり、Action Textで何らかの変換をかけておく必要があります。

以上を実行するために、まずコンテンツをパースする必要があります。

1. パース

ActionText::RichTextオブジェクトが初期化されるときに、テキストコンテンツはActionText::Contentオブジェクトの初期化に使われます(このオブジェクトはこの後のセクションでcontentオブジェクトと呼ぶことにします)。

このcontentオブジェクトは、コンテンツをツリー構造で保存するので、内部の要素を検索/置換するのは簡単です。

2. 変換

  1. Action Textはcontentのツリーを探索して、[data-trix-attachment]セレクタにマッチする要素があるかどうかを調べます。
  2. 該当のノードが見つかると、data-trix-attachmentの値をパースして一連の属性に変換します。
  3. Action Textはこれらの属性を用いてaction-text-attachmentノードを初期化します。このノードをシリアライズしてhtmlにすると以下のような感じになります。

3. アタッチ

次はリッチテキストレコードに画像blobをアタッチします。しかしすべてのアタッチメントをどのように検索すればよいのでしょうか?ここで、contentオブジェクトにはattachablesというメソッド呼び出しがあることに気が付きました。このメソッドは、(ツリーを探索してコレクションすることで)その中に含まれるaction-text-attachment要素をすべて返します。

Action Textの添付ファイル(attachment)は、実際には以下の3種類です。

  1. ActiveStorage経由でアップロードしたアセット
  2. Trixパーシャル
  3. リモート画像

ActionTextはこれらの種類のattachableをすべて検索し、ActiveStorage::BlobをgrepしてActionText::RichTextレコードにアタッチしてから、レコードを保存します。

署名済みグローバルid

ここまでで何か重要な手順が1つ抜けていることに皆さんお気づきでしょうか?「attachableはいつの間にblobレコードになったのか?」と首をかしげる方もいるでしょう。

この点を説明するには、先ほどのaction-text-attachmentノード内に表示されるsgidという属性をまず理解しておく必要があります。

sgidは「署名済みグローバルid(Signed Global ID)」のことです。何のこっちゃとお思いでしょうが、意味は次のとおりです。

  1. sgidはアプリ全体のグローバル識別子である。
  2. sgidはblobオブジェクトから直接生成される。
  3. sgidには、このblobオブジェクトを検索するのに必要な必要なものがすべて含まれている(モデル名やレコードidも)
  4. sgidは他人がこのデータを書き換えられないように署名されている(署名済みグローバルidは以下のようにコンソールで調べられます)。

グローバルidや署名済みグローバルidについて詳しく知りたいのであれば、globalid gemをチェックすべきです。

ActionTextはこれのおかげで、idもモデル名も持たない添付ファイルblobを見つけられるのです。

レンダリング

テキストコンテンツの処理方法と保存方法を理解してしまえば、ActionTextでコンテンツをレンダリングする方法を推測するのは難しくありません。

  1. コンテンツをパースしてツリーにする
  2. すべての添付ファイルノードを検索する
  3. 添付ファイルのsgidで画像blobをグローバルに検索する

さてそれでは、本記事の冒頭で予告した「あっと驚くもの」で締めくくりましょう。n+1クエリを見つけてしまったのです!!!

ActionTextは、コンテンツやblobをeager loadするために、以下のようなスコープをいくつか提供しています。

にもかかわらず、テキストコンテンツをレンダリングするときに、アプリケーションでn+1クエリが生み出されていることがわかります。一体どうなっているのでしょうか?

原因は、blobレコードをsgidで検索しているからです!

上は本質的に以下のようなものです。

この動作が添付ファイルの要素ごとに行われるので、eager loadingのスコープの恩恵をまったく得られません。実際、場合によってはこれらのスコープを使うと、利用できないデータをeager loadするクエリが余分に作成されるため、むしろ悪化することすらあります(#36177で報告されているissueがそれです)。

訳注: #36177は記事公開時点で引き続きopenになっています。

まとめ

Action Textをあれこれ調べたり遊んだりしたことで、Action Textというコンポーネントの設計が実によくできていて、リッチテキストエディタが必要になった場合のベストチョイスだと思うようになりました。ただし、Action Textにはまだ改良の余地が残されています。前述のn+1クエリなどがそうですね。

最後になりますが、既にAction Textを使い始めている方から本記事にフィードバックをいただけるとうれしく思います。ぜひみんなでこの素晴らしいAction Textを使いやすいコンポーネントとして成熟させましょう😄

おたより発掘

↓DHHが以下をリツイートしました🎉

関連記事

週刊Railsウォッチ(20181203)Railsのglobalidとは、AWS LambdaがRubyに対応、JSはPromiseを最初に学べほか

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

$
0
0

概要

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

訳注: Ruby on Whalesは、Ruby on Railsの他に、もしかすると「Boy on a Dolphin」にかけているのかもしれないと思いました。Whales(クジラ)はもちろんDockerのシンボルです。
参考: 映画「島の女」(1957)(テーマ曲:いるかに乗った少年) ( 映画レビュー ) - fpdの「映画スクラップ帖」 (名作に進路を取れ!) - Yahoo!ブログ

まえがき

本記事は、私がRailsConf 2019で話した「Terraforming legacy Rails applications」↑の、いわばB面に相当します。この記事を読んで、皆さんがアプリケーション開発をDockerに乗り換えるとまでは考えていません(皆さんが以下の動画で若干言及しているのをご覧になっていたとしても)。本記事の狙いは、私が現在のRailsプロジェクトで用いている設定を皆さんと共有することです。それらのRailsプロジェクトは、Evil Martiansのproduction development環境で生まれたものです。どうぞご自由にお使いください。

クジラに乗ったRuby: Evil Martians流Docker+Ruby/Rails開発環境構築(翻訳)

私がdevelopment環境でDockerを使い始めたのは、かれこれ3年ほど前の話です(それまで使っていたVagrantは4GB RAMのノートパソコンではあまりに重たかったのでした)。もちろん、最初からバラ色のDocker人生だったわけではありません。自分のみならず、チームにとっても「十分にふさわしい」Docker設定を見つけるまでに2年という月日を費やしました。

私の設定を、(ほぼほぼ)すべての行に解説を付けてご覧に入れたいと思います。「Dockerをわかっている」前提のわかりにくいチュートリアルはもうたくさんですよね。

本記事のソースコードは、GitHubのevilmartians/terraforming-railsでご覧いただけます。

本記事の例では以下を用います。

  • Ruby 2.6.3
  • PostgreSQL 11
  • NodeJS 11 & Yarn(Webpackerベースのアセットコンパイル用)

Evil Martians流Dockerfile

Railsアプリケーションの「環境」は、Dockerfileで定義します。サーバーの実行、コンソール(rails c)、テスト、rakeタスク、開発者としてコードとのインタラクティブなやりとりは、ここで行います。

ARG RUBY_VERSION
# 後述
FROM ruby:$RUBY_VERSION

ARG PG_MAJOR
ARG NODE_MAJOR
ARG BUNDLER_VERSION
ARG YARN_VERSION

# ソースリストにPostgreSQLを追加
RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -\
  && echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list

# ソースリストにNodeJSを追加
RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash -

# ソースリストにYarnを追加
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -\
  && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

# 依存関係をインストール
# 外部のAptfileでやってる(後ほどお楽しみに!)
COPY .dockerdev/Aptfile /tmp/Aptfile
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade &&\
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends\
    build-essential\
    postgresql-client-$PG_MAJOR\
    nodejs\
    yarn=$YARN_VERSION-1\
    $(cat /tmp/Aptfile | xargs) &&\
    apt-get clean &&\
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &&\
    truncate -s 0 /var/log/*log

# bundlerとPATHを設定
ENV LANG=C.UTF-8\
  GEM_HOME=/bundle\
  BUNDLE_JOBS=4\
  BUNDLE_RETRY=3
ENV BUNDLE_PATH $GEM_HOME
ENV BUNDLE_APP_CONFIG=$BUNDLE_PATH\
  BUNDLE_BIN=$BUNDLE_PATH/bin
ENV PATH /app/bin:$BUNDLE_BIN:$PATH

# RubyGemsをアップグレードして必要なバージョンのbundlerをインストール
RUN gem update --system &&\
    gem install bundler:$BUNDLER_VERSION

# appコードを置くディレクトリを作成
RUN mkdir -p /app

WORKDIR /app

このDockerfileの設定は必要不可欠なもののみを含んでいますので、これを出発点にできます。このDockerfileで何が行われるかを解説します。

最初の2行に少々妙なことが書かれています。

ARG RUBY_VERSION
FROM ruby:$RUBY_VERSION

FROM ruby:2.6.3みたいに適当な安定版Rubyのバージョンを書いておけばよさそうなものですよね。ここではDockerfileを一種のテンプレートとして用い、環境を外部から設定可能にしたいのです。

  • ランタイム依存の厳密なバージョンは、docker-compose.ymlの方で指定することにします(後述)。
  • aptコマンドでインストール可能な依存のリストは、これも別ファイルに保存することにします(これも後述)。

上に続く4行は、PostgreSQL、NodeJS、Yarn、Bundlerのバージョンを定義します。

ARG PG_MAJOR
ARG NODE_MAJOR
ARG BUNDLER_VERSION
ARG YARN_VERSION

今どきDockerfileをDocker Composeなしで使う人などいないという前提なので、Dockerfileではデフォルト値を指定しないことにします。

PostgreSQL、NodeJS、Yarnをaptコマンドでインストールするために、それらのdebパッケージのリポジトリをソースリストに追加する必要があります。

RUN curl -sSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -\
  && echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main' $PG_MAJOR > /etc/apt/sources.list.d/pgdg.list
RUN curl -sL https://deb.nodesource.com/setup_$NODE_MAJOR.x | bash -
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -\
  && echo 'deb http://dl.yarnpkg.com/debian/ stable main' > /etc/apt/sources.list.d/yarn.list

今度は依存関係のインストールです(apt-get installの実行など)。

COPY .dockerdev/Aptfile /tmp/Aptfile
RUN apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade &&\
  DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends\
    build-essential\
    postgresql-client-$PG_MAJOR\
    nodejs\
    yarn\
    $(cat /tmp/Aptfile | xargs) &&\
    apt-get clean &&\
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* &&\
    truncate -s 0 /var/log/*log

まずAptfileという裏技について解説します。

COPY .dockerdev/Aptfile /tmp/Aptfile
RUN apt-get install\
    $(cat /tmp/Aptfile | xargs)

Aptfileというアイデアはheroku-buildpack-aptから拝借しました。heroku-buildpack-aptは、Herokuに追加パッケージをインストールできます。このbuildpackを使っていれば、同じAptfileをローカルでもproduction環境でも再利用できます(buildpackのAptfileの方が多くの機能を提供していますが)。

私たちのデフォルトAptfileに含まれているのはたったひとつのパッケージです(私たちはRailsのcredentialの編集にVimを使っています)。

vim

私が携わっていた直前のプロジェクトでは、LaTeXやTexLiveを用いてPDFを生成しました。そのときのAptfileは、さしずめ以下のような感じにできたでしょう(当時私はこの技を使っていませんでしたが)。

vim
texlive
texlive-latex-recommended
texlive-fonts-recommended
texlive-lang-cyrillic

このようにすることで、タスク固有の依存関係を別ファイルに切り出し、Dockerfileの普遍性を高めています。

DEBIAN_FRONTEND=noninteractiveという行については、「answer on Ask Ubuntu」という記事をご覧になることをおすすめします。

--no-install-recommendsスイッチを指定すると、推奨パッケージのインストールを行わなくなるので容量を節約でき、ひいてはイメージをもっとスリムにできます。詳しくは「Xubuntu Geek: Save disk space with apt-get option “no-install-recommends” in Xubuntu」をご覧ください。

RUNの最後の部分(apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && truncate -s 0 /var/log/*log)も目的は同じです。取得したパッケージファイルのローカルリポジトリ(ここまでで必要なものはすべてインストールできているので、これらはもはや不要です)や、インストール中に作成されたすべての一時ファイルやログをクリーンアップします。 特定のDockerレイヤにごみを残さないようにするため、このクリーンアップ作業は同じRUNステートメントの中で行う必要があります。

最後の部分は、もっぱらBundlerのためのものです。

ENV LANG=C.UTF-8\
  GEM_HOME=/bundle\
  BUNDLE_JOBS=4\
  BUNDLE_RETRY=3
ENV BUNDLE_PATH $GEM_HOME
ENV BUNDLE_APP_CONFIG=$BUNDLE_PATH\
  BUNDLE_BIN=$BUNDLE_PATH/bin
ENV PATH /app/bin:$BUNDLE_BIN:$PATH

# RubyGemsをアップグレードして必要なバージョンのBundlerをインストールする
RUN gem update --system &&\
    gem install bundler:$BUNDLER_VERSION

LANG=C.UTF-8は、デフォルトロケールをUTF-8に設定します。これを行わないとRubyが文字列でUS-ASCIIを使ってしまうので、かわいいかわいい絵文字たちとおさらば👋になってしまいます。

gemインストールのパスはGEM_HOME=/bundleで設定します。この/bundleが何だかおわかりでしょうか?このパスは、依存関係を(開発環境などの)ホストシステムで永続化するために、後でボリュームとしてマウントすることになります。詳しくは後述のdocker-compose.ymlをご覧ください。

BUNDLE_PATH変数とBUNDLE_BIN変数は、gemやRuby実行ファイルを探索する場所をBundlerに伝えます。

最後に、Rubyとアプリケーションバイナリをグローバルに公開します。

ENV PATH /app/bin:$BUNDLE_BIN:$PATH

これで、いちいちbundle execを冒頭に付けなくてもrailsrakerspecといった「binstub化されたコマンド」を実行できるようになります。

Evil Martians流docker-compose.yml

Docker Composeは、コンテナ化された環境をオーケストレーションするツールで、これを用いてコンテナ同士を接続し、永続化ボリュームやサービスを定義できます。

以下は、データベースとしてPostgreSQLを、バックグラウンドジョブの処理にSidekiqを用いた、Railsアプリケーションの典型的な開発環境のためのdocker-compose.ymlです。

version: '3.4'

services:
  app: &app
    build:
      context: .
      dockerfile: ./.dockerdev/Dockerfile
      args:
        RUBY_VERSION: '2.6.3'
        PG_MAJOR: '11'
        NODE_MAJOR: '11'
        YARN_VERSION: '1.13.0'
        BUNDLER_VERSION: '2.0.2'
    image: example-dev:1.0.0
    tmpfs:
      - /tmp

  backend: &backend
    <<: *app
    stdin_open: true
    tty: true
    volumes:
      - .:/app:cached
      - rails_cache:/app/tmp/cache
      - bundle:/bundle
      - node_modules:/app/node_modules
      - packs:/app/public/packs
      - .dockerdev/.psqlrc:/root/.psqlrc:ro
    environment:
      - NODE_ENV=development
      - RAILS_ENV=${RAILS_ENV:-development}
      - REDIS_URL=redis://redis:6379/
      - DATABASE_URL=postgres://postgres:postgres@postgres:5432
      - BOOTSNAP_CACHE_DIR=/bundle/bootsnap
      - WEBPACKER_DEV_SERVER_HOST=webpacker
      - WEB_CONCURRENCY=1
      - HISTFILE=/app/log/.bash_history
      - PSQL_HISTFILE=/app/log/.psql_history
      - EDITOR=vi
    depends_on:
      - postgres
      - redis

  runner:
    <<: *backend
    command: /bin/bash
    ports:
      - '3000:3000'
      - '3002:3002'

  rails:
    <<: *backend
    command: bundle exec rails server -b 0.0.0.0
    ports:
      - '3000:3000'

  sidekiq:
    <<: *backend
    command: bundle exec sidekiq -C config/sidekiq.yml

  postgres:
    image: postgres:11.1
    volumes:
      - .psqlrc:/root/.psqlrc:ro
      - postgres:/var/lib/postgresql/data
      - ./log:/root/log:cached
    environment:
      - PSQL_HISTFILE=/root/log/.psql_history
    ports:
      - 5432

  redis:
    image: redis:3.2-alpine
    volumes:
      - redis:/data
    ports:
      - 6379

  webpacker:
    <<: *app
    command: ./bin/webpack-dev-server
    ports:
      - '3035:3035'
    volumes:
      - .:/app:cached
      - bundle:/bundle
      - node_modules:/app/node_modules
      - packs:/app/public/packs
    environment:
      - NODE_ENV=${NODE_ENV:-development}
      - RAILS_ENV=${RAILS_ENV:-development}
      - WEBPACKER_DEV_SERVER_HOST=0.0.0.0

volumes:
  postgres:
  redis:
  bundle:
  node_modules:
  rails_cache:
  packs:

このdocker-compose.ymlでは8つのサービスを定義しています。サービスを8つも定義しているのを不思議に思うかもしれませんが、その一部は単に他と共有する設定を定義しているだけで(appbackendといった抽象サービス)、残りはアプリケーションコンテナを用いる特定のコマンド(runnerなど)のためのものです。

このアプローチでは、アプリケーションをdocker-compose upで実行するのではなく、docker-compose up railsのように、実行したいサービスを常にピンポイントで指定するようにしています。development環境ではWebpackerやSidekiqなどを全部立ち上げる必要はめったにないので、合理的です。

それでは各サービスを詳しく見ていくことにしましょう。

app

appサービスの主な目的は、(上のDockerfileで定義した)アプリケーションコンテナの構築に必要な情報をすべて提供することです。

build:
  context: .
  dockerfile: ./.dockerdev/Dockerfile
  args:
    RUBY_VERSION: '2.6.3'
    PG_MAJOR: '11'
    NODE_MAJOR: '11'
    YARN_VERSION: '1.13.0'
    BUNDLER_VERSION: '2.0.2'

contextディレクトリは、Dockerのbuild contextを定義します。これはビルドプロセスで用いる一種のワーキングディレクトリであり、COPYコマンドなどで用いられます。

私たちの設定ではDockerfileへのパスを明示的に指定しています。理由は、私たちはDockerファイルをプロジェクトのルートディレクトリに配置するのではなく、.dockerdevという隠しディレクトリの中に他のすべてのDocker関連ファイルと一緒に配置しているからです。

前述したように、Dockerfileではargsを用いて依存関係の正確なバージョンを指定しています。

ここでひとつ注意すべきは、イメージにタグ付けする方法です。

image: example-dev:1.0.0

Dockerを開発に用いるメリットのひとつは、設定の変更を自動的にチーム全体で同期できることです。これは、ローカルイメージ(引数や、イメージが依存するファイルでもよい)を変更するたびにローカルイメージのバージョンをアップグレードしておきさえすれば可能です。逆に最悪なのは、ビルドタグにexample-dev:latestを使うことです。

イメージのバージョンを維持しておけば、異なる2つの環境同士で余分な追加作業を一切行わずに済むようにもできます。たとえば、長期間実行するchore/upgrade-to-ruby-3ブランチで作業している最中に、いつでもmasterブランチに切り替えて古いイメージや古いRubyを利用できます。しかもリビルド不要で。

ポイント: docker-compose.yml内のイメージにlatestタグを使うのは最悪です。


他にも、コンテナ内で/tmpフォルダにDockerのtmpfsマウントを用いるように指定することでスピードアップしています。

tmpfs:
  - /tmp

backend

いよいよ本記事で一番美味しい部分にたどり着きました。

このbackendサービスは、あらゆるRubyサービスで共有する振る舞いを定義します。

まずはvolumes:を見てみましょう。

volumes:
  - .:/app:cached
  - bundle:/bundle
  - rails_cache:/app/tmp/cache
  - node_modules:/app/node_modules
  - packs:/app/public/packs
  - .dockerdev/.psqlrc:/root/.psqlrc:ro

volumes:リストの最初の項目「- .:/app:cached」では、現在のワーキングディレクトリ(つまりプロジェクトのルートディレクトリ)をコンテナ内の/appフォルダにマウントし、かつcached戦略を用いています。このcachedという修飾子は、MacOSでのDocker環境の効率を高めるうえで重要なポイントです。cachedについては別記事を書いていますので😉、本記事ではこれ以上は深堀りしません。「こちらの公式ドキュメント」をご覧ください。

その次の行では、/bundleの中身をbundleという名前のボリュームに保存するようコンテナに指示しています。私たちはこのようにして、gemのデータを永続化して複数の実行で使えるようにしています。docker-compose.ymlで定義されたすべてのボリュームは、docker-compose down --volumesを実行するまで持続します。

以下の3行も、「DockerがMacだと遅い」という呪いをお祓いするために書かれています。私たちは、生成されたファイルをすべてDockerボリュームに配置することで、ホストマシンでディスク操作が重くなるのを回避しています。

- rails_cache:/app/tmp/cache
- node_modules:/app/node_modules
- packs:/app/public/packs

ポイント: macOSでDockerを十分高速に動かすには、ソースファイルを:cachedでマウントし、かつ、生成されたコンテンツ(アセットやbundleなど)の保存にはボリュームを使うこと。


末尾の3行では、特定のpsql設定をコンテナに追加しています。私たちはほとんどの場合、コマンド履歴をアプリのlog/.psql_historyに保存することで永続化する必要があります。psqlをRubyのコンテナに追加している理由は、rails dbconsoleを実行するときに内部で使われるからです。

私たちの.psqlrcファイルには、履歴ファイルを環境変数経由で指定できるようにするために以下の仕掛けが施されています。履歴ファイルへのパスをPSQL_HISTFILE環境変数で指定できるようにし、利用できない場合は$HOME/.psql_historyにフォールバックします。

\set HISTFILE `[[ -z $PSQL_HISTFILE ]] && echo $HOME/.psql_history || echo $PSQL_HISTFILE`

環境変数について説明します。

environment:
  - NODE_ENV=${NODE_ENV:-development}
  - RAILS_ENV=${RAILS_ENV:-development}
  - REDIS_URL=redis://redis:6379/
  - DATABASE_URL=postgres://postgres:postgres@postgres:5432
  - WEBPACKER_DEV_SERVER_HOST=webpacker
  - BOOTSNAP_CACHE_DIR=/bundle/bootsnap
  - HISTFILE=/app/log/.bash_history
  - PSQL_HISTFILE=/app/log/.psql_history
  - EDITOR=vi
  - MALLOC_ARENA_MAX=2
  - WEB_CONCURRENCY=${WEB_CONCURRENCY:-1}

話せばきりがありませんが、ここでは1箇所に注目したいと思います。

まず、X=${X:-smth}についてですが、これを人間の言葉で表せば「コンテナ内のXという変数については、ホストマシンにXという環境変数の値があればそれを用い、なければ別の値を用いる」ということです。つまり、RAILS_ENV=test docker-compose up railsのようにコマンドで別の環境を指定してサービスを実行できるのです

DATABASE_URL変数、REDIS_URL変数、WEBPACKER_DEV_SERVER_HOST変数は、Rubyアプリケーションを別のサービスに接続しますDATABASE_URL変数はRailsのActive Recordで、WEBPACKER_DEV_SERVER_HOST変数はRailsのWebpackerでいつでもサポートされます。ライブラリによってはREDIS_URL変数もサポートします(Sidekiq)が、どのライブラリでもサポートされているとは限りません(たとえばAction Cableは明示的に設定しなければなりません)。

私たちはbootsnapを用いてアプリケーションの読み込みを高速化しています。bootsnapのキャッシュはBudlerのデータと同じ場所に保存しています。理由は、このキャッシュに含まれている内容のほとんどがgemのデータだからです。つまり、Rubyを別のバージョンにアップグレードするようなことがあれば、それらを一括廃棄するべきということです。

HISTFILE=/app/log/.bash_historyは、開発者のUXにとって重要な設定です。この設定によって履歴が特定の場所に保管され、永続化されるようになります。

EDITOR=viは、たとえばrails credentials:editコマンドでcredentialファイルを管理するのに用います。

末尾の2つの設定であるMALLOC_ARENA_MAXWEB_CONCURRENCYは、Railsのメモリハンドリングをチェックしやすくするためのものです。

他にbackendサービスで説明すべきは以下の行だけです。

stdin_open: true
tty: true

この設定によって、サービスをインタラクティブ(TTYを提供するなどの対話的な操作)にできます。私たちの場合、たとえばRailsコンソールやBashをコンテナ内で実行するのに必要です。

これは、-itオプションを付けてDockerコンテナを実行するのと同じです。

webpacker

webpackerで言及しておきたいのはWEBPACKER_DEV_SERVER_HOST=0.0.0.0という設定だけです。これによって、Webpack dev serverに「外部から」アクセスできるようになります(デフォルトではlocalhostで実行されます)。

runner

このrunnerサービスの目的を説明するために、私がDockerを開発に用いるときの段取りについて説明させてください。

  • 私はDockerデーモンの起動で以下のようなカスタムdocker-startスクリプトを実行します。
#!/bin/sh

if ! $(docker info > /dev/null 2>&1); then
  echo "Docker for Macを開いています..."
  open -a /Applications/Docker.app
  while ! docker system info > /dev/null 2>&1; do sleep 1; done
  echo "Docker準備OK!"
else
  echo "Dockerは実行中です"
fi
  • 次に、コンテナのシェルにログインするために、プロジェクトでdcr runnerを実行します(dcrdocker-compose runのエイリアス)。dcr runnerは以下のエイリアスになります。
$ docker-compose run --rm runner
  • 後はこのコンテナの中でほとんどの作業を行います(テストやマイグレーションやrakeタスクなど何でも構わない)。

以上でおわかりのように、私は何かタスクを1つ実行する必要が生じるたびにいちいちコンテナを1つ立ち上げたりせず、いつも同じ設定でやっています。

つまり私は、なつかしのvagrant sshと同じ感覚でdcr runnerを使っているのです。

私がこれをshellと呼ばずにrunnerと呼んでいる理由はただひとつ、コンテナの中で任意のコマンドをrunするのにも使えるからです。

メモ: このサービスをrunnerと呼ぶかどうかは好みの問題であり、(デフォルトのcommand/bin/bash)は別としても)webサービスと比べて何ひとつ目新しい点はありません。つまり、docker-compose run runnerdocker-compose run web /bin/bashと完全に同じです(ただし短い😉)。

おまけ: Evil Martians特製のdip.yml

Docker Compose式のやり方がまだ難しいとお思いの方に、Dipというツールをご紹介します。これは開発者がスムーズなエクスペリエンスを得られるようにと、Evil Martiansのあるメンバーがこしらえたものです。

dip.ymlは、複数のcomposeファイルを使い分ける場合や、プラットフォームに依存する複数の設定を使い分ける場合に特に便利です。dip.ymlはそれらをまとめて、Dockerでの開発環境を管理する一般的なインターフェイスを提供できるからです。

dip.ymlについては別記事にて詳しく説明しようと思います。どうぞご期待ください!

追伸

本記事のtipsを共有してくれたSergey PonomarevMikhail Merkushinに感謝いたします🤘

元記事のトップ画像のクレジット: © NASA/JPL-Caltech, 2009

訳注

以下のスライドも合わせて読むことで、より理解が進むと思います。

関連記事

Rails 6のB面に隠れている地味にうれしい機能たち(翻訳)

週刊Railsウォッチ(20190909-1/2前編)Rails 6のキャッシュバージョニング、Rubyのキーワード引数周りが変わる、Faker 2がリリースほか

$
0
0

こんにちは、hachi8833です。消費税アップが迫ってきましたね。


つっつきボイス:「そうそう、消費税アップ来ますね😅」「皆さんの中で消費税対応されてる方は?」「経理部とやりとりしたりしてますね」「もう終わりました?」「まだまだです🤣」「食品みたいに軽減税率対応のものを扱ってると面倒そうですね」「どちらにしろ2%アップはやってくるので、商品マスターデータを更新するかどうかとか考えないといけないかも☺


同PDFより

「今回の場合、消費税を『還元する』とか『サービスする』的な触れ込みをしてはいけないという指示とかが上のPDFにもあるんですよね↑」「ちとわかりにくいですけど😆」「他にもいろいろ注意事項あるので、消費税を扱ってるかどうかにかかわらず読んどかないとですね☺

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

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

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

今回は6.0.1マイルストーン最近マージされたPR中心に見繕ってみました。

⚓リグレッション修正: @prepared_statement_statusをスレッドローカル変数化してインスタンス固有にした

# activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L13
+ require "concurrent/atomic/thread_local_var"
...
      attr_accessor :pool
-     attr_reader :visitor, :owner, :logger, :lock, :prepared_statements
+     attr_reader :visitor, :owner, :logger, :lock
      alias :in_use? :owner
...
      def initialize(connection, logger = nil, config = {}) # :nodoc:
        super()
        @connection          = connection
        @owner               = nil
        @instrumenter        = ActiveSupport::Notifications.instrumenter
        @logger              = logger
        @config              = config
        @pool                = ActiveRecord::ConnectionAdapters::NullPool.new
        @idle_since          = Concurrent.monotonic_time
        @visitor = arel_visitor
        @statements = build_statement_pool
        @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new

        if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
-         @prepared_statements = true
+         @prepared_statement_status = Concurrent::ThreadLocalVar.new(true)
          @visitor.extend(DetermineIfPreparableVisitor)
        else
-         @prepared_statements = false
+         @prepared_statement_status = Concurrent::ThreadLocalVar.new(false)
        end

        @advisory_locks_enabled = self.class.type_cast_config_to_boolean(
          config.fetch(:advisory_locks, true)
        )
      end
...
+     def prepared_statements
+       @prepared_statement_status.value
+     end
...
      def unprepared_statement
-       old_prepared_statements, @prepared_statements = @prepared_statements, false
-       yield
-     ensure
-       @prepared_statements = old_prepared_statements
+       @prepared_statement_status.bind(false) { yield }
      end

この問題をデモする簡単なアプリ: https://github.com/careport/racy

この問題は、システムテストで単一のDBコネクションが全サーバースレッドで共有されていることから始まっている。unprepared_statementがコネクションの@prepapred_statementsインスタンス変数を改変するので、これを用いるやりとりがよくないことになっている。その改変が、コネクションを共有する他のスレッドから透けて見えるため、一部のクエリのパラメータが誤ってパラメータ化される。
この問題が853f568で顕在化しない理由はわからない。引き続き調べてみるが、Active Recordの内部に詳しい人ならすぐわかるかもしれない。
同issueより大意


つっつきボイス:「スレッド関連かな?」「システムテストで1つのデータベースコネクションを全サーバースレッドで共有しているときに問題が生じたとissueにありました↑」「Concurrent::ThreadLocalVarなんてのがある」「これはRubyのメソッドかな?」「ruby-concurrencyっていうgemみたいです」

参考: ruby-concurrency/concurrent-ruby: Modern concurrency tools including agents, futures, promises, thread pools, supervisors, and more. Inspired by Erlang, Clojure, Scala, Go, Java, JavaScript, and classic concurrency patterns.
参考: Class: Concurrent::ThreadLocalVar — Concurrent Ruby


同リポジトリより

Concurrent::ThreadLocalVarのドキュメントにJavaのThreadLocalへのリンクがあるから↓、これとパラダイムが近いということかも」「一瞬このライブラリがJavaなのかと思った😆

参考: ThreadLocal (Java Platform SE 7 )

「最近スレッド周りの修正が毎週のように出てきますね😳」「探せば探すほど出てきちゃうのかも」「修正も大変ですけど、スレッド絡みの現象を再現するのはもっと大変ですね😅: Active Recordのこの辺で問題が起きそうと見当をつけることはできても、こういう再現テストを書ける人ってマジでスゴいと思いますし🙇」「スレッドのバグって、100万回回すと数回起きるみたいなのもありますね😇

⚓WIP: rails newのプロジェクト名に丸かっこ()があるとビューテンプレートが見つからなくなる問題を修正

# actionview/lib/action_view/template/resolver.rb#L352
      def build_regex(path, details)
-       query = escape_entry(File.join(@path, path))
+       query = Regexp.escape(File.join(@path, path))
        exts = EXTENSIONS.map do |ext, prefix|
          match =
            if ext == :variants && details[ext] == :any
              ".*?"
            else
              arr = details[ext].compact
              arr.uniq!
              arr.map! { |e| Regexp.escape(e) }
              arr.join("|")
            end
          prefix = Regexp.escape(prefix)
          "(#{prefix}(?<#{ext}>#{match}))?"
        end.join
        %r{\A#{query}#{exts}\z}
      end

なお、issue #37107については以下のPRが先にあったのですが、入れ違いで上のPRが投げられていたため以下はcloseされていました。


つっつきボイス:「プロジェクト名に丸かっこ()を入れるのってちょっとどうかなと思ったので😆」「なんと🤣」「issueの方がわかりやすいかも」「これか↓」「()に限らずメタ文字が入るとビューを参照できなくなってたそうです」

# 同issueより
rails new "path-escape-bug (testing)"
cd "path-escape-bug (testing)"
rails g controller public example
bin/rails s
# open "http://localhost:3000/public/example" in your browser

「これが正常に動くと思う人っていなさそうだけど、それでも書きたい人がいるのかも😆」「なお#37119はまだマージされてませんね」「Railsプロジェクト名にダブルクォートとか入れたい人っているんだろうか🤣」「仕様上メタ文字はプロジェクト名で使えないことにする方が早い気もしますし😆

  def test_can_find_when_special_chars_in_path
    special_chars = ((" ".."~").grep(/\W/) - ["|", "/", "\\"]).join("_")
    dir = "test#{special_chars}"
    with_file "#{dir}/hello_world", "Hello funky path!"

    templates = resolver.find_all("hello_world", dir, false, locale: [:en], formats: [:html], variants: [:phone], handlers: [:erb])
    assert_equal 1, templates.size
    assert_equal "Hello funky path!", templates[0].source
    assert_equal "#{dir}/hello_world", templates[0].virtual_path
  end

「ところで、テストのこの(" ".."~")という書き方↑でメタ文字をカバーできるのかな?🤔」「ASCIIコード表見ながらIRBを動かしてみると、スペース文字から~までを取って、/\W/で英数字を除去して、["|", "/", "\\"]を差し引いて_でjoinすると…たしかにできた!」「おぉ〜」「こうやってRangeで取るのは初めて見たな〜: 脳内にASCIIコードがないと思いつかないし😆」「numbered parameterで書きたかったけどそういえばRuby 2.6にはなかった😆」「このファイル名、Windowsで通るんだろうか😆」「そのうち絵文字もありになったり?😆

special_chars = ((" ".."~").grep(/\W/) - ["|", "/", "\\"]).join("_")
#» " _!_\"_#_$_%_&_'_(_)_*_+_,_-_._:_;_<_=_>_?_@_[_]_^_`_{_}_~"

参考: ASCIIコード表

⚓Active Jobキュー名の文字列をfreezeさせた

# 同PRより
Retained String Report
-----------------------------------
  ...
217  "default"
200  /tmp/bundle/ruby/2.6.0/bundler/gems/rails-59e746d4d07b/activejob/lib/active_job/qu
# activejob/lib/active_job/queue_name.rb#L49
      def queue_name_from_part(part_name) #:nodoc:
        queue_name = part_name || default_queue_name
        name_parts = [queue_name_prefix.presence, queue_name]
-       name_parts.compact.join(queue_name_delimiter)
+       -name_parts.compact.join(queue_name_delimiter)
      end

つっつきボイス:「キュー名の"default"という文字列が重複してたそうで、大したことはないけど修正はすごく簡単だったので修正したとPRにありました」「memory profilerで見つけたんでしょうね」「シンボルで返すようにしたのかな?と思ったら、-でfreezeさせたのね」「+がdupを返すんでしたっけ」「この辺の記号、どっちがどっちだったか不安になりがち😅

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

「たしかに-だとobject_idは一致する↓」

h1 = -"hoge"
#» "hoge"
h2 = -"hoge"
#» "hoge"
h1.object_id
#» 70279179364960
h2.object_id
#» 70279179364960

h3 = "hoge"
#» "hoge"
h4 = "hoge"
#» "hoge"
h3.object_id
#» 70136481465760
h4.object_id
#» 70136481387620

⚓Style/BracesAroundHashParameters copを無効にした

-# .rubocop.yml#L37
-# Do not use braces for hash literals when they are the last argument of a
-# method call.
-Style/BracesAroundHashParameters:
-  Enabled: true
-  EnforcedStyle: context_dependent

つい最近のRubyのキーワード引数の扱い変更↓に伴う変更のようです。

参考: RuboCop | Style/BracesAroundHashParameters EnforcedStyle - Qiita


つっつきボイス:「Rubyのキーワード引数の扱いが変わることになったのを反映したとのことです」「#14183でずっと議論してたヤツね: キーワード引数とハッシュが混じるとえらいことになる、breaking change不可避の問題↓」「issue長いな〜」「closeはしたけどまだ少々動いてるっぽいですね」「ここはみんな困っている問題だから、何らかの形で修正しないといけないヤツですね」

参考: キーワード引数の現状と将来構想 - HackMD

Ruby2.5.xのパラメータの制約についてまとめてみた

# actionpack/test/dispatch/cookies_test.rb#L900
    key_generator = @request.env["action_dispatch.key_generator"]
    old_secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
-   old_value = ActiveSupport::MessageVerifier.new(old_secret).generate({bar: "baz"})
+   old_value = ActiveSupport::MessageVerifier.new(old_secret).generate({ bar: "baz" })

「このスペース追加↑って何なんでしょう?」「スペースはあっても解釈は変わらなかったと思うけど🤔」「Style/BracesAroundHashParametersのcopがコンフリクトしてたので外してオートコレクトしたらスペースが入ったということみたい」「d94263f…5665fb5にはこういう修正が入ってる↓」「あ、こっちの{}追加の方がメインだったのか😅

# activesupport/test/message_encryptor_test.rb#L127
  def test_rotating_serializer
    old_message = ActiveSupport::MessageEncryptor.new(secrets[:old], cipher: "aes-256-gcm", serializer: JSON).
-     encrypt_and_sign(ahoy: :hoy)
+     encrypt_and_sign({ahoy: :hoy})

    encryptor = ActiveSupport::MessageEncryptor.new(@secret, cipher: "aes-256-gcm", serializer: JSON)
    encryptor.rotate secrets[:old]
    assert

参考: RuboCop | Style/BracesAroundHashParameters EnforcedStyle - Qiita

「Rubyの#2395のコメント↓を見ると『昨日developer meetingで受理された』とあって、それが6日前(つっつき当日から見て)ということだから、つい1週間前だったんですね」


#2395コメントより

「話は変わりますけど、RubyとRailsはコミッターが割と近い関係にあるから、言語とフレームワークがこうやって密に連携しているのがありがたいですよね😋」「たしかに!」「PHPとかだとかなり分かれてる感ありますし☺

⚓番外: SolarisではProcess.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)を使ってはいけない


つっつきボイス:「Solaris環境ではProcess#clock_gettime(CLOCK_PROCESS_CPUTIME_ID)は使うなとchangelogに追加されてました」「ソラリス!😆」「互換性がなかったんでしょうね」「Ruby on Solarisやってる人いるんだ😆

「SolarisっていうOS、そもそも皆さんご存知です?」「いやぁ😅」「SolarisといえばあのSunの特徴的な薄いブルーというか紫色の筐体で、秋葉のジャンク屋に安く積んであったりするとつい買っちゃったりしますよね😆」「見たことならありますけど触るまでは😅

参考: Solaris - Wikipedia

「Solarisっていう名前の由来って、もしかすると旧ソ連が国力を挙げて制作した映画『惑星ソラリス』と関係あるんだろうか?ってうっすら気になってました」「由来はわかんない😆」「この映画を見た人はあまりの尺の長さに必ず寝ちゃう催眠フィルムだと一部で評判でした😆」「😆

後でSolarisという名前の由来をググると以下が出てきましたが、今のWikipediaには英語版日本語版ともにこの記述はないので、たぶん違うんでしょうね。

参考: Solaris系OSはなぜ ソラリス とゆうのですか?教えてください。 - Solaris(ソラ... - Yahoo!知恵袋

参考: 惑星ソラリス - Wikipedia

⚓Rails

⚓Rails 6のcollection_cache_versioning

以前も簡単に取り上げました(ウォッチ20190507)。

コンフィグでActiveRecord::Base.collection_cache_versioning = trueを指定しないと有効になりません。

参考: 3.7 Active Recordを設定する — Rails アプリケーションを設定する - Rails ガイド


つっつきボイス:「これってどういう機能だったっけ」「上のガイドに一応あります」「ここは英語で見ておきたいな↓」

参考: Configuring Rails Applications — Ruby on Rails Guides

config.active_record.collection_cache_versioning enables the same cache key to be reused when the object being cached of type ActiveRecord::Relation changes by moving the volatile information (max updated at and count) of the relation’s cache key into the cache version to support recycling cache key. Defaults to false.
Ruby on Rails Guidesより

「元記事のタイトルにもあるけど、キャッシュキーがrecyclableなのがポイントらしい: そうそう、キャッシュバージョンとキャッシュキーを分けられるようになったということですね↓」「おぉ?」

# 同記事より
>> ActiveRecord::Base.cache_versioning = true

>> post = Post.last

>> cache_key = post.cache_key
=> "posts/1"

>> before_update_cache_version = post.cache_version
=> "20190527080152975653"

>> Rails.cache.fetch(cache_key, version: before_update_cache_version) { post }
=> #<Post id: 1, title: "First Post", created_at: "2019-05-22 17:23:22", updated_at: "2019-05-27 08:01:52">

「以前はキーとバージョンが一体化していたので、キャッシュバージョンを更新するとキャッシュ自体が別キャッシュとして作られてたんですが、バージョニングできるようになったことでキーとバージョンを別々に更新できるようになったということだと思います」「記事の最後にこう書いてますね😋↓: cache_versionはタイムスタンプとかを含んでいてvolatileだから、キャッシュが同じでもこの部分は変わる可能性があり、cache_keyは一意に生成される」「Rails 6から挙動がこう変わるから、コンフィグでActiveRecord::Base.collection_cache_versioning = trueを指定しないと有効にならないようにしたんでしょうね」

Rails 6より前
ActiveRecord::Relationのcache_keyフォーマットは{table_name}/query-{query-hash}-{count}-{max(updated_at)}だった
Rails 6の場合
以下に分割される
・安定的なcache_key:{table_name}/query-{query-hash}
・変わりうるcache_version: {count}-{max(updated_at)}

「Redisとかにキャッシュを乗せている場合、ここが変わるとデプロイしたときに全部のキャッシュが死んでしまうでしょうね😇」「あ〜」「Railsって、rails app:updateみたいなコマンドで、互換性を保つ設定をconfig/initializers/new_framework_defaults.rbに生成するみたいな機能がありましたよね」「ありますね: で後から対応できたときに順に消していくという」「このキャッシュ機能のコンフィグもそういうときに使うんでしょうね☺」「Rails 6でrails newする分にはいいけど、アップグレードでは注意が必要そう」

参考: 1.4 アップデートタスク — Rails アップグレードガイド - Rails ガイド

⚓GoRails.comのスクリーンキャスト


gorails.com/seriesより

RailsCastsの更新が止まって無料化されて久しいですが、GoRailsを最近見てなかったので覗いてみました。今もマメに更新されているようです。

このスクリーンキャストとかがちょっと気になりました↓。


つっつきボイス:「GoRails?」「RailsCastsに代わるスクリーンキャストというと今のところここぐらいなのかなと思って見に行くと、サイトが前より心持ちシンプルになってて、かなりマメに更新してました」

「こういうオンライン講座的なものって皆さんは見たりします?自分は動画になってるとコピペとかできないんで見ない方ですけど😆」「ITとは少し違いますけどCoursera↓とかは見たりしますね」「こーせら?」「日本語化もされてるんですね😋


ja.cousera.orgより

「GoRails割と頑張ってるみたいで、ここのリポジトリ↓を見る方が新着情報もわかりやすいかなと」「どちらかというと、動画で今見えてるものをさくっとコピペしたいんですよね〜」「作り込み難しそうですね😅」「まあ今ならスクショ取ってGoogleドライブに置いてOCRする手もあるかもしれませんけど😆

「初心者の方はこういう動画の講座を好む印象がちょっとあるかな〜」「あと英語圏もスクリーンキャスト好きですよね」「ステップバイステップの操作って記事で書こうとすると量が増えてすんごく大変なんですよ😭: 動画はその点では製作しやすいから、動画でやりたい気持ちはわかるかも☺

⚓Rails 6のDNS Rebinding対策で開発中にlvh.meが効かなくなる(Awesome Rubyより)

参考: 【Rails】ローカル環境の開発でサブドメインがある場合「localhost」ではなく「lvh.me」を使う - FujiYasuの日記
参考: DNS Rebinding ~今日の用語特別版~ | 徳丸浩の日記


つっつきボイス:「おぉ、この記事にある問題、今日まさに社内のメンバーが踏んでましたよ😆: Rails 6にはDNSリバインディング対策が入ったので、デフォルトのlocalhost以外のドメインはconfig.hostsに明示的に追加しないとdevelopment環境でアクセスできなくなるという↓」「おぉ〜」

# 同記事より
# config/enviroments/development.rb
config.hosts << '.lvh.me'

⚓Trestle: Bootstrap 4ベースのレスポンシブ管理画面(Awesome Rubyより)


trestle.ioより


つっつきボイス:「Railsの管理画面は山ほどありますけど、これはどうかなと思って」「みんなこういうのいっぱい作りますよね☺」「出たなDSL↓」

Trestle.resource(:posts) do
  # Add a link to this admin in the main navigation
  menu do
    group :blog_management, priority: :first do
      item :posts, icon: "fa fa-file-text-o"
    end
  end

  # Define custom scopes for the index view
  scopes do
    scope :all, default: true
    scope :published
    scope :drafts, -> { Post.unpublished }
  end

  # Define the index view table listing
  table do
    column :title, link: true
    column :author, ->(post) { post.author.name }
    column :published, align: :center do |post|
      status_tag(icon("fa fa-check"), :success) if post.published?
    end
    column :updated_at, header: "Last Updated", align: :center
    actions
  end

  # Define the form structure for the new & edit actions
  form do
    # Organize fields into tabs and sidebars
    tab :post do
      text_field :title

      # Define custom form fields for easy re-use
      editor :body
    end

    tab :metadata do
      # Layout fields based on a 12-column grid
      row do
        col(sm: 6) { select :author, User.all }
        col(sm: 6) { tag_select :tags }
      end
    end

    sidebar do
      # Render a custom partial: app/views/admin/posts/_sidebar.html.erb
      render "sidebar"
    end
  end
end

「こういうDSLって自分で作ってるときが一番楽しいんですよね🤣」「達成感は半端ない🤣」「見た目はよさそうだけど、凝ったカスタマイズをしようとすると辛くなりそうな予感が😆」「業務のプロジェクトでこういうのが流れてきたら頭抱えそう😅: でも自分しか使わないアプリならむしろこれでいいかなって思いますし😋」「今度オレオレRailsアプリで使ってみようかな😍」「自分だけが使うなら許せる😆

「こういう管理画面とかをプログラマーがボタンぽちぽちして自動生成できたらうれしい気持ちはあるんですけど、それならRailsでやらなくてもよくね?って思ったりしますし、コードを書いて作るのはコードでないとできないことがあるからですし」「たしかに〜」

「グラフのカスタマイズとかもついやりたくなりますけど、ああいうのもGoogleデータポータル↓とかでやる方がよかったりしそうですよね😆」「データポータルは使ったことないな〜: Googleアナリティクスをこねこねする的な印象ありますけど、こういうのってなかなか思ったように作れなかったりしそうですし😅」「内部のMySQLだかPostgreSQLだかを把握するとうまく使えるようになるかなって思ったり☺」「半端にGUIになってるとやりづらかったりしますね☺」「競合はTableauあたりかな🤔

⚓Rails Attributes APIで巨大JSONをPostgreSQLに保存する(Ruby Weeklyより)


つっつきボイス:「JSONをRails側でGzip圧縮解凍、ってわざわざRails側でやる必要あるんだろうか?😆」「たぶんぽすぐれのそういう圧縮機能とかデータストアを探して使う方がよくね?😆」「実用では使わないと思いますけど、練習にはいいと思いますね☺

# 同記事より
  def value_to_hash(value)
    JSON.parse(
      ActiveSupport::Gzip.decompress(value),
      symbolize_names: true
    ) || {}
  end

  def value_to_binary(value)
    ActiveSupport::Gzip.compress(value)
  end

参考: PostgreSQL: Documentation: 9.0: Binary Data Types

⚓その他Rails



つっつきボイス:「Twitterで見かけたRubyMineの質問に@yusukeさんがすごい勢いで回答してました」「JetBrainsの日本の代理店をやってる方ですね☺」「いつも見ててくれてるらしい😍


なお、その後のRails Tutorial(英語版の方)では、進行中のRails 6対応版のうち第3章までのみが公開されています↓。

⚓Ruby

⚓k0kubunさんの「Optimizing Ruby with JIT」

Buildersconでのスライドです。


つっつきボイス:「こういうRubyのコアをきっちり解説してくれるのはうれしい😂」「しかも日本語で🇯🇵」「NESでの測定では2.5倍速くなったそうです」

⚓Faker 2がリリース(Ruby Weeklyより)


同リポジトリより

Changelogの日付が米国式風(年-日-月)になっていて、一瞬間違ってるのかと思いました😅。現在は2.2.2まで進んでいます。2.0でオプション引数がごっそりキーワード引数に置き換わっています。

# 同リポジトリより
Faker::Books::Dune.quote(character = nil)
↓
Faker::Books::Dune.quote(character: nil)

つっつきボイス:「Fakerはどこが変わったのかしら?」「2.0でオプション引数をキーワード引数に置き換えたのがbreaking changesだそうです」

Fakerだけでもロケール次第で日本語のフェイクデータを作れるんですね↓。デフォルトの日本語データが意外にささやかなサイズでした。

「Fakerは日本語拡張が使いたい部分ですね😋: こういうのとか↓」「あ、デフォルト以外にこんな大きい日本語Fakerがあるんですね😅」「名前データでかい!」「日本語住所は対応してないようですが」「バージョンアップで影響受けないといいけど(受けます: 後述)」

# ruby-faker-japaneseより
    姓:高垣        名:芳人
    姓:尾関        名:きね子
    姓:夜久野      名:利常
    姓:飯星        名:邦則
    姓:草柳        名:大造
    姓:納米        名:ナツコ
    姓:金高        名:喜久蔵
    姓:キーティング        名:秋江
    姓:島居        名:悦二郎
    姓:戸河内      名:さかえ
    姓:片里        名:朝長
    姓:上依知      名:洌
    姓:湧井        名:宗矩
    姓:隆速        名:美禅

「ついでに@willnetさんのgimeiというgem↓を今頃知りました」「gimeiというのもありますね〜: 日本語の名前専門かと思ったら、いつの間にか日本語のダミー住所も生成できるのか」

# gimeiより
address = Gimei.address
address.kanji                 # => 岡山県大島郡大和村稲木町
address.to_s                  # => 岡山県大島郡大和村稲木町
address.hiragana              # => おかやまけんおおしまぐんやまとそんいなぎちょう
address.katakana              # => オカヤマケンオオシマグンヤマトソンイナギチョウ
address.romaji                # => Okayamaken Ooshimagunyamatoson Inagicho

追記: つっつきの後にkoicさんが以下の記事を出していました。Faker 2に移行するときのbreaking changesに対応するRuboCop Fakerを作ってくださったそうです🙇

参考: Faker 2.2.2 がリリースされた - koicの日記
参考: RuboCop Faker を作った - koicの日記

⚓Rumale: Rubyで機械学習


同リポジトリより

# 同リポジトリより
require 'rumale'

# Load the training dataset.
samples, labels = Rumale::Dataset.load_libsvm_file('pendigits')

# Map training data to RBF kernel feature space.
transformer = Rumale::KernelApproximation::RBF.new(gamma: 0.0001, n_components: 1024, random_seed: 1)
transformed = transformer.fit_transform(samples)

# Train linear SVM classifier.
classifier = Rumale::LinearModel::SVC.new(reg_param: 0.0001, max_iter: 1000, batch_size: 50, random_seed: 1)
classifier.fit(transformed, labels)

# Save the model.
File.open('transformer.dat', 'wb') { |f| f.write(Marshal.dump(transformer)) }
File.open('classifier.dat', 'wb') { |f| f.write(Marshal.dump(classifier)) }

つっつきボイス:「ruby-jp Slackでちらっと見かけたRubyの機械学習gemです」「よく作ったな〜これ😳」「モチベーションが知りたい」「Pythonに依存しないんでしょうか?」「READMEにPythonのScikit-Learnに近いインターフェースになってるってありますし、Pythonを使ってるとは書いてないからおそらくそうなんでしょうね🤔」「Rubyでここまでやるのスゴい!」

Red Data Toolsプロジェクトとは別のようですね。

⚓custom_cops_generator: RuboCopのカスタムcopを生成

# 同リポジトリより
$ custom_cops_generator rubocop-foobar
Creating gem 'rubocop-foobar'...
      create  rubocop-foobar/Gemfile
      create  rubocop-foobar/lib/rubocop/foobar.rb
      create  rubocop-foobar/lib/rubocop/foobar/version.rb
      create  rubocop-foobar/rubocop-foobar.gemspec
      create  rubocop-foobar/Rakefile
      create  rubocop-foobar/README.md
      create  rubocop-foobar/bin/console
      create  rubocop-foobar/bin/setup
      create  rubocop-foobar/.gitignore
Initializing git repo in /tmp/tmp.Gu7G94wX00/rubocop-foobar
Gem 'rubocop-foobar' was successfully created. For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html
create rubocop-foobar/lib/rubocop-foobar.rb
create rubocop-foobar/lib/rubocop/foobar/inject.rb
create rubocop-foobar/lib/rubocop/cop/foobar_cops.rb
create rubocop-foobar/config/default.yml
create rubocop-foobar/spec/spec_helper.rb
create rubocop-foobar/.rspec
update lib/rubocop/foobar.rb
update lib/rubocop/foobar.rb
update lib/rubocop/foobar/version.rb
update rubocop-foobar.gemspec
update rubocop-foobar.gemspec
update Rakefile
update Gemfile

It's done! You can start developing a new extension of RuboCop in rubocop-foobar.
For the next step, you can use the cop generator.

  $ bundle exec rake 'new_cop[Foobar/SuperCoolCopName]'

つっつきボイス:「これもruby-jpでちらっと見かけましたけど、カスタムcopを作りたいことって結構あるんでしょうか?」「プロジェクト固有のコーディングルールを設定したいときとかでしょうね: 事業会社とかで1つのRailsプロジェクトに何十人も職業的メンバーがいるみたいな環境ならあるかも☺」「たしかShopifyがcop的なものを作って頑張っているみたいな話がありましたね↓: それこそ100人以上のRailsエンジニアがコードを書いている環境」

参考: RubyKaigi、shopifyのテストの話が良かった · hoshinotsuyoshi.com - 自由なブログだよ

⚓その他Ruby


前編は以上です。

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

週刊Railsウォッチ(20190902)Ruby 2.6.4セキュリティ修正リリース、スライド「All About Ruby in 2019」、Shrine gem 3.0に入る新機能ほか

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

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

Rails公式ニュース

Ruby Weekly

Awesome Ruby

週刊Railsウォッチ(20190910-2/2後編)buildersconと「20年後のソフトウェアテスト」、はてなブックマークがScalaに移行、「詳解PostgreSQL」、Go 1.13ほか

$
0
0

こんにちは、hachi8833です。

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

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

⚓DB

⚓そーだいさんの「詳解 PostgreSQL」連載

『失敗から学ぶRDBの正しい歩き方』でお馴染みのそーだいさんによるPostgreSQL連載記事です。


つっつきボイス:「PostgreSQLの10や11にも対応していて内容が新しいのがいいなと思って」「Web+DB Press誌の連載なのね😳

「今回おいでの皆さまの中でPostgreSQLが好きという人は?」「おぉ少ない😅」「PostgreSQL、使ったことまだないですね☺」「自分はぽすぐれ好きですが今日は少数派…: ちなみにRDBMSは何をお使いですか?」「MySQLです」「NoSQL系とか」「AWSのRDSとかAuroraですがMySQL系ということで😆」「経理系ですがPostgreSQLはちょっとだけ使ったりしてます💰

参考: Amazon RDS(マネージドリレーショナルデータベース)| AWS
参考: Amazon Aurora(高性能マネージドリレーショナルデータベース)| AWS

「PostgreSQLはデータベースの機能が豊富で、ある程度以上大きく育ったビジネスアプリで性能限界に突き当たったときにデータベースに頑張らせるという道が残されているのがいいところだと思っています😋」「ふむふむ」「MySQLももちろん使っていますけど、MySQLを速くしようとするとテーブル構成とかもMySQL用に作らないといけなくなったりするのが結構大変ですね😅: MySQLに最適化するとすごく速くなるんですが、その分テーブル定義の正規化が崩れたりということもあるので、自分はぽすぐれが好き❤」「😆

参考: サイボウズ版 MySQL パフォーマンスチューニングとその結果 - Cybozu Inside Out | サイボウズエンジニアのブログ

「あと、他のRDBMSだと通らないような普通じゃないシンタックスのSQL文がMySQLだと通ったりするんですよ: SELECTしてないカラムでGROUP BYできるとか色々謎が多い🤪」「😲

参考: MySQLでSELECT句とGROUP BYの非集約カラムが揃ってなくても通るケース - Qiita

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

⚓はてなブックマークがScalaに移行


つっつきボイス:「おぉ、はてブがいつの間にかScalaに移行してたんですね🎉」「自分も気づいてなかったんですが、2015年にScalaに移行する宣言して↓完了していたそうです」「はてブもちょっと前にHTTPS対応完了ましたし、頑張ってますよね☺」「Perlもちょっぴり残ってはいるみたいです」「『ここ数年の大きな課題は、ちょっと改善のために手を加えるだけでも、けっこうな開発工数がかかってしまうことでした』とインタビューで答えているのが切実でした」

参考: Scala - Wikipedia

「記事ははてなの中の人たちへのインタビュー形式になっていて、悔しいけどいつもながらいい記事です😂」「エンジニアHubは予算も付けてかなり頑張ってる感じで、いい記事多いですよね👍」「インタビュー相手のセレクションも毎回うまいなと思いますし😍

⚓その他クラウド


つっつきボイス:「自分たちはAWS派ですけど、お集まりの皆さんはAWSとかGCPとかAzureとかお使いですか?」「AWSを使い始めたところです☺」「AWSはこの間みたいな障害が起きるとつらいですけど😭」「自分たちはたまたまAZが別だったので無事でした😋」「おぉそれは運がいい!」

「ああいう障害のときって情報が小出しにされがちですよね😔」「同意ですね、たしかに間違ったことは言ってないんですけど、もう少し情報出してもらえればって思いましたし😢」「かといって自社でホスティングとか今更しませんし🤣」「🤣

参考: Amazon Elastic Block Store (Amazon EBS) - Amazon Elastic Compute Cloud

「皆さんの中で、自社内にサーバーがある方っています?」「おぉいませんね!」「BPSは今も社内にサーバーラックがありますけど、新しく買う意義はもうないでしょうね😆」「電気代もかかりますし😆


「ちなみにGCPの中ではBigQueryはよくできていて好きですね❤」「おぉ😀」「BigQueryはAWSに同等のサービスがありませんし」

参考: BigQuery - アナリティクス データ ウェアハウス  |  BigQuery  |  Google Cloud

つっつき後に見つけたツィートです↓。

⚓JavaScript

⚓スライド: TypeScriptの流儀


つっつきボイス:「TypeScriptだ☺」「お集まりの皆さんはTypeScriptって書きます?」「やってみたいんですけどなかなか携わる機会がなくて😆」「自分も少し読んでみた程度でまだ書くところまでは😅」「規模がささやかならTypeScriptまでしなくても素のJSでいいかなという気もするので悩ましいですね」

ついでにTypeScript 3.6がリリースされたそうです。

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

⚓20年後のソフトウェアのテスト

これもbuildersconのスライドです。


builderscon.ioより

Buildersconは最後の方しか見られませんでした😭。きりがありませんので、以下のスライドまとめで。

参考: builderscon tokyo 2018 資料まとめ - Qiita


つっつきボイス:「今年のbuildersconは東京電機大学(東京千住キャンパス)でしたか」「人生初の北千住でした😆」「電機大は神田キャンパスの方に通ってました🎓」「すごくピカピカのキャンパスで駅まで行かないとタバコ吸えなくて😇」「今どきはしょうがない☺」「ピカピカの北千住ってまだイメージできないかも😆」「随分変わってるらしいですよ😋

参考: 東京電機大学 - Wikipedia

「このセッションも大人気で部屋に入れずでした😭が、割と未来の話というか、今後出現するIoTデバイスをどうテストするかというテーマでした」「テクノロジーも将来になれば変わりますし☺

「お〜、BLE(Bluetooth Low Energy)のエミュレーター↓とかあるし」「電波干渉もエミュレーションできるとは😳」「電波干渉の再現は実は昔から行われていますね😎: ものすごく不安定な接続をテストするときとか」

「そうそう、こういう感じの特性になります↓」

参考: Bluetooth Low Energy - Wikipedia
参考: BluMoon: Bluetooth Low Energy Emulation System with Software-Implemented Controller - IEEE Conference Publication

「こういう世界だと、ソフトウェアであっても実質的にハードウェアと密結合するので、そういうものもテストしないといけなくなりますね」「こういう箱に端末を置くだけでいい感じに検証できると↓🤓

「今の自分たちはソフトウェアのテストしかやってないから関係ありませんけど😆、下のレイヤでどういう障害が起こりうるかというのはある程度知っておかないといけなくなるでしょうね: データを扱うだけならまだしも、何かを物理的に動かすようなものだと人命に関わる可能性もありますし」「未来のテストは大変そう😅

「自分は以前研究室でまさにこういう感じの小さいデバイスとかを扱ってたんですけど、装置によって調子の良し悪しがバラついてたりとか、フラッシュの書き込みが3回に1回は失敗するなんてことがよくありましたね😆」「😆」「ソフトウェアのせいかと思ったら実は個体差だったとか、あとボタン電池も個体差がかなり大きいんですよ🔋」「自分もいろいろ覚えあります😅」「出力されたりされなかったりとか、もうすべてを疑ってかからないと😇」(以下略)

⚓言語・ツール

⚓Go 1.13がリリース

1つ目に止まったのは、数値リテラル周りの変更の中に、1_000_0000b_1010_01103.1415_9265のように数値にアンダースコアを入れられるようになったことです。


つっつきボイス:「今度のGoはTSL 1.3がデフォルトとは進化してるな〜」

参考: TLS 1.3とは? TLS 1.2との違いと企業が取るべき戦略

「このリテラル記法ってRubyみたいですよね↓?」「このアンスコ記法、自分はあまり使ってないけど割と好きですね❤」「書きながら自分を疑うときに使ったり😆」「Ruby以外の言語にはないんでしょうか?」「他の言語でも見たことはあったと思うけどあんまりメジャーじゃないかも🤔

Digit separators: The digits of any number literal may now be separated (grouped) using underscores, such as in 1_000_000, 0b_1010_0110, or 3.1415_9265. An underscore may appear between any two digits or the literal prefix and the first digit.
Changes to the languageより

後で調べると、少なくともPythonにはありました↓。

参考: PEP 515 -- Underscores in Numeric Literals | Python.org

⚓LinkedInが「Isolation Forests」アルゴリズムでやんちゃを検出・防止(Morning Cup Of Codingより)


同記事より


つっつきボイス:「LinkedInがやってるanti abuseの記事です: Isolation Forestというアルゴリズムを初めて見たんですけど、新しすぎて日本語の用語はなさそうでした」「たぶん日本語はないでしょうね😆」「自分も英ママの方が助かります😆」「元論文が2008年だから他にも実装があるかも🤔」「アルゴリズムを理解して適切に使い分けるのが大変」

同社が公開したライブラリはScala言語で書かれています。outlier detectionが「外れ値検出」なんですね。

参考: 異常検知 - Wikipedia

以下は記事とは別に見つけたIsolation Forestのスライドです。

後で調べると、昨年軽く取り上げた異常検知ナイトでもIsolation Forestが言及されていました↓。

参考: 異常検知ナイトで講演してきました | Preferred Research

「そういえば、最近だと機械学習方面でアンサンブル学習っていうやり方があるらしくて、1個のモデルだけで学習させるんじゃなくて複数のモデルをいいとこ取り的に組み合わせるんだとか」「複数のモデル全部に同じデータを学習させて、いいスコアが出たモデルを適宜選択するみたいなヤツですね☺」「データセットのでかさにもよるでしょうけど、計算資源はもう豊富にあるという前提で😆

参考: 機械学習上級者は皆使ってる?!アンサンブル学習の仕組みと3つの種類について解説します

⚓その他言語


つっつきボイス:「お、これもどこかで話題になってたような?」「単品のツイートで見かけたので文脈がわかってませんが😅、Rubyは『楽に書けるように言語側が複雑になっている』みたいなことをMatzが以前言っていたのを思い出したので」「そうそう、Rubyは人間がシンプルに書ける分、内部のパーサーはすごいことになってますし」

⚓その他

⚓macOSの公証でてんてこ舞い


つっつきボイス:「社内アプリチームがこのニュースでざわついていたので」「最近のMacもどんどん不便になりつつある感じしますね…自分が使ってるプログラムがどんどん動かなくなりそう😇」「マカー受難😆」「大学みたいなアカデミックなところが出している古いMacアプリが動かなくなりそうで心配です😭

iOS 13における必須対応について

⚓BoseのARサングラス

参考: Boseが「聴くARメガネ」発表。音の拡張現実プラットフォームを推進 - Engadget 日本版


つっつきボイス:「このBoseのARメガネをこの夏偶然体験する機会があって、米国の親戚がドヤ顔でちょっと使わせてくれたんですが、なかなか強烈な体験でした😍」「へぇ〜?」「耳の穴には何も突っ込んでないのに、ほぼ自分にしか音楽が聞こえなくて、サウンドも高品質で♬、何やら他にもセンサー内蔵らしくて」「骨伝導とか?」「骨伝導は使ってないようでした」「そういえばOculus Quest↓なんかも自分にしか聞こえないような超指向性のサウンドだったりするらしいですね」

参考: Oculus Go、Oculus Quest、Oculus Rift S、3機種を比較!どれを買うべき? - Rentio PRESS[レンティオプレス]

「BoseのAR、かなり欲しくなっちゃったんですけど2〜3万ぐらいするらしくて😅」「HoloLensだと何十万もするぐらいだから、それなら全然安いと思いますし😋

参考: Microsoft HoloLens 2発表、3500ドル。視野角から画素密度、視線+ハンドトラッキングまで徹底改良 #MWC19 - Engadget 日本版

⚓Railsガイドのロゴがリニューアル

こちらはつっつきの後の情報です。

⚓番外

⚓小さな工夫

参考: 画像ギャラリー | 横断歩道「斜め」にしたら事故減少か 横断中の歩行者が見つけやすくなる納得の理屈 | 乗りものニュース

つっつきボイス:「上のリンク先の図が見やすいと思います」「視界を広く取らなくても歩行者が見えると」「こういう道路交通関連の分野は学問として昔からあるから、そういう研究があったりしそう」「記事にも『交通工学の専門家』ってありますし」「渋滞学を思い出しました」「こういう工夫で事故を減らせるならいいですね😋

参考: 交通工学 - Wikipedia
参考: 渋滞学の観点から、スムーズな社会の動きを生み出す〜西成活裕・東京大学教授 | Top Researchers


後編は以上です。

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

週刊Railsウォッチ(20190909-1/2前編)Rails 6のキャッシュバージョニング、Rubyのキーワード引数周りが変わる、Faker 2がリリースほか

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

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

Morning Cup Of Coding

morningcupofcoding_banner_captured

Viewing all 1380 articles
Browse latest View live