Quantcast
Channel: Rails – TechRacho
Viewing all 120 articles
Browse latest View live

RailsConf 2017のパフォーマンス関連の話題(1)BootsnapやPumaなど(翻訳)

$
0
0

こんにちは、hachi8833です。
今回は、大著「The Complete Guide to Rails Performance」で知られるNate Berkopec氏の記事を何回かに分けてお送りいたします。

  • 第1回: BootsnapやPumaなど(本記事) — 開発中のRailsをBootsnapで倍速起動するなど
  • 第2回: rack-freezeやsnip_snipなど — マルチスレッドバクや未使用カラムを検出するgem
  • 第3回: 「あなたのアプリサーバーの設定は間違っている」など

概要

原著者より許諾を得て翻訳・公開いたします。

楽しい画像はすべて元記事からの引用です。

RailsConf 2017のパフォーマンス関連の話題(1)BootsnapやPumaなど(翻訳(翻訳)

概要: この記事は、RailsConf 2017に参加できなかった皆さまや、参加したものの先端的なパフォーマンス関連の話題を何か見落としてないかと気になっている皆さまにお送りいたします(2330 word、12分程度)。

RailsConfを惜しくも逃した方、当ブログへようこそ!RailsConf 2017は既に終了しましたので、RailsConf 2016のときと同様、私が会場で見かけたり話したりしたパフォーマンス関連のあらゆる話題を手短に盛り込みました。


逃したお…(´・ω・`)

1. Bootsnap

Shopifyが先ごろリリースしたBootsnapは、巨大なRubyアプリを高速に起動できるgemです。リリースはカンファレンス開催の一週間ほど前でしたが、DiscourseのSam Saffron氏はこのgemの素晴らしさについて皆に語っていました。「Gemfileにポンと書き込めばあら不思議、アプリが速くなったよ!」という便利なgemはそうそうありませんが、Bootsnapはまさにそうしたタイプのgemのようです。Discourseでは開発時の起動時間を50%削減したそうです。


50%速い…だと…?

bootscaleというgemについては既にご存じの方もいるかもしれません。Bootsnapはこのgemの進化版であり、bootscaleに取って代わることを意図しています。

Bootsnapはどのような仕組みになっているのでしょうか。よくあるパフォーマンス改善プロジェクトとは異なり、BootsnapのREADMEではそうした点について非常に詳しく述べられてるのがありがたい点です。基本的には2つの点を改善します: requireの高速化と、コンパイルされたRubyコードのキャッシュです。

requireの高速化はかなりストレートです。bootsnapでは、Rubyで発生するシステムコールの数を減らすためにキャッシュを使います。たとえばrequire 'mygem'すると、Rubyはmygem.rbという名前のファイルをLOAD_PATH上のすべてのフォルダについて開こうとします。何ということでしょう。Bootsnapはこれを先回りします。アプリのコードがキャッシュされる期間はわずか30秒なので、ファイルの変更が見逃される心配はありません。

2つめの機能は、コンパイル済みRubyコードのキャッシュです。このアイデアはいっとき取沙汰されていたことがありました。たしかEileen UchitelleAaron Pattersonもある時期こうした作業を行っていたことがありましたが、いずれも挫折または横道にそれてしまっていたと思います。基本的にBootsnapではあらゆるRubyコードのコンパイル結果を、ファイル自身が持つ拡張ファイル属性に保存します。これはなかなかうまいハックです。残念ながら、いくつかの理由によってこの仕組みはLinux上ではうまく動きません。ファイルシステムがext2またはext3の場合は拡張ファイル属性がオンにならず、たとえできたとしてもLinuxのxattrsの最大サイズは非常に限られているので、Bootsnapが生成するデータを収納しきれないかもしれません。

カンファレンスでもこれについていくつか議論になっていましたが、読み込みパスのキャッシュ機能はBundlerかRubygemsにマージできるのではないかという見解に落ち着きました。

訳注: _ko1さんのツイートがありました。

2. フロントエンドのパフォーマンス


か、会場の!WiFiが!共用…できない…

今回私は「フルスタック開発者のためのフロントエンドパフォーマンス」というタイトルでワークショップを開催しました。内容はChrome Developer Toolsで最初のページの読み込み速度の体感のプロファイリングと診断を行う方法の紹介でした。

準備万端かと思われたのですが、どっこい、会場のWiFiで突然サンプル用のWebページの大半で振る舞いが激変してしまい、演習どころではありませんでした。私も少々動転してしまいましたが、幸いなことにRichardが頑張ってCodeTriageでJavaScriptバンドルをasyncとマークし、CodeTriageの描画時間を半減させてくれました。

3. アプリケーションサーバーのパフォーマンス

私はある顧客で最近経験したことを元に、RailsConfでPumaのパフォーマンス上のいくつかの問題の診断と改善を行うというちょっとした役割を引き受けました。

その問題とは、処理するリクエストをPumaがどのようにして受け付けるかというものでした。Pumaのすべてのプロセス(ワーカー)にはそれぞれの内部に「リアクター」というものがあります。このリアクターの役割はソケットのリッスン、リクエストのバッファ、そしてリクエストを手すきのスレッドに渡すことです。


リクエストを腹いっぱいいただくPumaのリアクター

Pumaの問題というのは、デフォルトでリクエストを際限なしにめいいっぱい受け付けてしまうリアクターの振る舞いです。こうなると、Pumaのワーカープロセス間でロードバランシングがうまく機能しなくなります。この問題はリブート中が特に顕著です。

Pumaを導入したRailsアプリを再起動したとしましょう。再起動中にはソケットにリクエストが100個積み上げられ、処理待ちになります。そしてPumaではときどき、処理すべき山積みのリクエストのほんの一部しか受け付けられなくなってしまいます。こうなると、Pumaのリクエストのキュー滞留時間が異常に長くなってしまいます。

この振る舞いについてはこれまでよくわかっていませんでした。Pumaのワーカーにスレッドが5つあるとすると、どうしてリクエストを5つ以上も受け付けてしまうのか。他のワーカープロセスは完全に手すきで待ちぼうけだというのに。この待ちぼうけプロセスには仕事を割り当てるべきでしょう。

そしてEvanがこの問題を修正してくれました。もうPumaは一度に処理可能なリクエスト数を超えて受け付けることはありません。これによってシングルスレッドのPumaアプリのパフォーマンスは著しく向上したはずです。もちろん、マルチスレッドのPumaアプリもです。

私は長い間、そして今もPumaでのリクエストのロードバランシングにはまだ改良の余地があるはずだと考えています。たとえば、Pumaのワーカープロセスが5つあり、うち4つがリクエストを処理中で、1つが完全に暇だとしましょう。このとき、処理中のワーカーがたまたま新しいリクエストを受け取る可能性があります。たとえばMRI/CRubyの環境で、処理中のワーカーの1つがIOブロックしてしまったとしましょう(=データベースからの結果待ち)。このI/O待ちのワーカーが、完全に暇なワーカーの代わりにリクエストを受け付けてしまうことがあります。これは望ましくありません。しかし私の知る限り、「ソケットのリッスン」と「処理受け付け可能な全プロセス」との間のルーティングは完全にランダムです。

唯一の方法は、Pumaにもっと賢くなってもらうことでしょう。つまり、Pumaのワーカーがソケットを自分でリッスンするのではなく、Pumaのリクエストルーティングにソケットリッスン用の何らかの「マスタールーティングプロセス」を置くことです。Evanの提案の1つは、Pumaの「マスタープロセス」に単にリアクター(=新しいリクエストのバッファとリッスンを行う)を置くという方法です。処理を行う子プロセスをこのリアクターが決定するわけです。こうするとPumaのルーティングアルゴリズムの実装が複雑になるかもしれません。たとえばラウンドロビンや、Passengerで採用されている「least-busy-process-first」(一番暇なプロセスに優先的にルーティングする)アルゴリズムのように。

Passengerについても少し触れておきましょう。Phusionの創立者であるHongli氏はこのアイデアを逆手に取り、PassengerがPumaのリバースプロキシやロードバランサーとしても機能するようにしています。これは確実に動作しますし、Pumaが静的ファイルのサービスをpassengerに任せることでPumaの負荷も軽減できるなどのメリットもあります。しかし私は、Pumaに「マスターリアクター」的なマスタープロセスを導入する方がさらに効果があるのではないかと考えています。

続き: RailsConf 2017のパフォーマンス関連の話題(2)rack-freezeやsnip_snipなど(翻訳)


RailsConf 2017のパフォーマンス関連の話題(2)rack-freezeやsnip_snipなど(翻訳)

$
0
0

こんにちは、hachi8833です。引き続きNate Berkopec氏の記事をお送りいたします。rack_freezeとsnip_snipはRails開発で役に立ちそうです。

  • 第1回: BootsnapやPumaなど — 開発中のRailsをBootsnapで倍速起動するなど
  • 第2回: rack-freezeやsnip_snipなど(本記事
  • 第3回: 「あなたのアプリサーバーの設定は間違っている」など

概要

原著者より許諾を得て翻訳・公開いたします。

楽しい画像はすべて元記事からの引用です。

RailsConf 2017のパフォーマンス関連の話題(2)rack-freezeなど(翻訳)

4. rack-freeze


「うちのアプリがスレッドセーフかって?調査では(たぶん)大丈夫だったっつってんの!」いーのいーの、何でもないから

パフォーマンス関連ではよく「自分のRubyアプリがスレッドセーフかどうかをどうやって確認すればよいのでしょうか?」という疑問を耳にします。私が普段用意している答えは、テストをマルチスレッドで実行するというものです。

しかしこのアドバイスには2つの問題があります。1つはRSpecはマルチスレッドで実行できないという点です。したがってこの方法はminiTestに限られます。2つ目は、この方法は単体テストやアプリケーション単位でのスレッドバグを見つける場合にしか通用しないことです。依存関係についてはほとんどカバーできません。

Rackミドルウェアはスレッドバグの発生源の1つです。この問題は基本的に以下のような感じになります。

class NonThreadSafeMiddleware
  def initialize(app)
    @app = app
    @state = 0
  end

  def call(env)
    @state += 1

    return @app.call(env)
  end
end

この問題は、すべてのRackミドルウェアであらゆるものをfreezeするという興味深い方法であぶり出すことができます。

上のコード例で言うと、@state +=行ではマルチスレッドアプリに誤った方法で暗黙に値を追加する代わりに、そこでコケてRuntimeErrorを返すようになります。rack-freeze gemは上のコードに対してまさにこのように動作します。作者である@schneemsのためにもrack-freezeを広めましょう。

5. snip_snip

何の話題だったか思い出せませんが、会場の廊下でKevin Deiszと立ち話したときにsnip_snipというgemがあることを知りました。開発中などにbulletを試してみた人はたくさんいると思います(bulletはアプリのN+1問題の検出に役立つgemです)。

snip_snipbulletと少し似ていますが、こちらはSELECTしたのに使われていないデータベースカラム検出用のgemです。

class MyModel < ActiveRecord::Base
  # 属性は :foo, :bar, :baz, :qux
end

class SomeController < ApplicationController
  def my_action
    @my_model_instance = MyModel.first
  end
end

上に続いて以下を行うとします。

# my_action.html.erbの他の部分

@my_model_instance.bar
@my_model_instance.foo

するとsnip_snipが「せっかくSELECTした:baz属性と:qux属性が使われてないゾ!」と知らせてくれます。それではコントローラのアクションを次のように書き直してみましょう。

class SomeController < ApplicationController
  def my_action
    @my_model_instance = MyModel.select(:bar, :foo).first
  end
end

全属性のSELECT(デフォルトの動作)をやめて属性を絞り込むと、大量のActiveRecordオブジェクト(通常100個超え)を一括作成するときや、多数の属性を抱えているオブジェクト(Userなど)を取得するときのスピードがよい感じに向上します。

6. Rubyのインライン展開

廊下でNoah Gibbsと立ち話したときのことです。Noahによると、Rubyのコンパイル時にコンパイラのinline threshold値を増やしたところ、少し速くなったそうです。

inline thresholdは基本的に、コードのセクションをコピペし、個別の関数を呼び出す代わりに1つの関数にインライン展開する作業をどの程度まで行ってよいかをコンパイラに指定します。コンパイル時にインライン展開するとプログラムで別の場所にジャンプするよりも速くなるのが普通ですが、もしプログラム全体を単純にインライン展開してしまうとRubyのバイナリサイズが1GBぐらいに膨れ上がってしまうかもしれません。

Noahによれば、inline threshold値を少し増やすとRubyのバイナリサイズが最大で3MBほど増加する代わりにoptcarrot benchmarkで5%から10%ほど高速化できたとのことです。これは多くの開発者にとってかなり魅力的なトレードオフになります。

以下は私が試した結果です。Mac環境のデフォルトのコンパイラであるClangを使う場合、CFLAGS環境変数でコンパイラにいくつかのオプションを渡せます。

CFLAGS="-O3 -inline-threshold=5000"

Example with ruby-install
ruby-install ruby 2.4.0 -- --enable-jemalloc CFLAGS="-O3 -inline-threshold=5000"

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

CFLAGS="-O3 -finline-limit=5000"

今のところ、これをproduction環境で使う気にはなりません。というのも、私のローカル環境ではたまにsegmentation faultになるからです。もちろん、development環境で遊んでみる価値は十分あると思います。

続き: RailsConf 2017のパフォーマンス関連の話題(3)「あなたのアプリサーバーの設定は間違っている」など(翻訳)

関連記事

週刊Railsウォッチ(20170616)railsdiff.orgはアップグレードに便利、RubyのDSLとかっこの省略、TerraformをRubyで制御ほか

$
0
0

こんにちは、hachi8833です。先週から歯痛で転げ回ってましたが、レントゲンにもCTにもそれっぽいものが映りませんでした。謎です。

6月第2週のRailsウォッチ、いってみましょう。

Rails 5.0.4.rc1リリース

バグ修正用です。リグレッションが見つからなければ早くも来週月曜にリリースされるそうです。

変更点はきわめてわずかで、ActiveRecordActiveModelの修正だけでした。Rails 5.0.x系をお使いの方はアップデートをおすすめします。

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

新機能: mattr_accessorでもデフォルト指定オプションを追加

DHHが先週class_attributeにデフォルト値を指定できるオプションを追加したからmattr_accessorでも追加しようよ、という流れです。今回も以下のような感じでガシガシ追加されています。

# actionmailer/lib/action_mailer/preview.rb
-      mattr_accessor :preview_interceptors, instance_writer: false
-      self.preview_interceptors = [ActionMailer::InlinePreviewInterceptor]
+      mattr_accessor :preview_interceptors, instance_writer: false, default: [ActionMailer::InlinePreviewInterceptor]

ついでにcattr_accessorでもガシガシやっています。

# actioncable/lib/action_cable/server/base.rb
-      cattr_accessor(:config, instance_accessor: true) { ActionCable::Server::Configuration.new }
+      cattr_accessor :config, instance_accessor: true, default: ActionCable::Server::Configuration.new

つっつきボイス: 「ところでcattr_accessormattr_accessorって何が違うんでしょうか」「なんだか動作同じなんですけど」

morimorihogeさんがRubyMineで追っかけてみたところ、以下のオチでした。一同「何じゃそりゃーw」

# activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
alias :cattr_accessor :mattr_accessor

新機能: 多数のキャッシュエントリを一括で書き込めるwrite_multi 💪

Rails.cache.write_multi foo: 'bar', baz: 'qux'
#write_multi_entriesを実装するストアに、より高速な#fetch_multiを追加。
見つからないキーは(個別ではなく)一括でキャッシュストアに書き込まれる

デフォルトの実装では単にエントリーごとに#write_entryを呼んでいる。
Redis MSETのように一括書き込み可能なストアはオーバーライドを行える。

# activesupport/lib/active_support/cache.rb
         options = names.extract_options!
         options = merged_options(options)
-        results = read_multi(*names, options)

-        names.each_with_object({}) do |name, memo|
-          memo[name] = results.fetch(name) do
-            value = yield name
-            write(name, value, options)
-            value
+        read_multi(*names, options).tap do |results|
+          writes = {}
+
+          (names - results.keys).each do |name|
+            results[name] = writes[name] = yield(name)
           end
+
+          write_multi writes, options
         end
       end

つっつきボイス: 「CacheバックエンドにRedisを使ってればRedisのMSET命令で一括書き込みできるということかな」

修正: チェックボックスやラジオボタンのラベルをクリックしても選択できるようになった

「チェックボックスやラジオボタンのラベルもクリッカブルにする」は最近のWeb UIエクスペリエンス周りではよく行われていますが、今回から対応しました。

# actionview/lib/action_view/helpers/tags/collection_check_boxes.rbの場合
           def check_box(extra_html_options = {})
             html_options = extra_html_options.merge(@input_html_options)
             html_options[:multiple] = true
+            html_options[:skip_default_ids] = false
              @template_object.check_box(@object_name, @method_name, html_options, @value, nil)
           end
         end

つっつきボイス: 「これ仕様だと思ってたけどついに直るのか」「自分でラベルを処理しようとするとすごく面倒くさい」

そういえばラベルをクリッカブルにするChrome拡張機能を以前使ったことがあるのですが、今探したところうまく見つけられませんでした。

修正: パーシャルのキャッシュログで誤ったパーシャルから属性を取得していた問題

# actionview/lib/action_view/helpers/cache_helper.rb
       def fragment_for(name = {}, options = nil, &block)
+        # Some tests might using this helper without initialize actionview object
+        @cache_hit ||= {}
         if content = read_fragment_for(name, options)
-          @cache_hit = true
+          @cache_hit[@virtual_path] = true
           content
         else
-          @cache_hit = false
+          @cache_hit[@virtual_path] = false
           write_fragment_for(name, options, &block)
         end
       end
# actionview/lib/action_view/renderer/partial_renderer.rb
           content = layout.render(view, locals) { content } if layout
-          payload[:cache_hit] = view.cache_hit
+          payload[:cache_hit] = !!view.cache_hit[@template.virtual_path]
           content
         end
       end

つっつきボイス: 「あー、パーシャルがネストしたときとかが問題だったのか」

余談: たった今気づいたのですが、これをコミットしたStan Lo氏は、この間Railsウォッチで紹介したGobyのメインメンテナでもあります。

RailsでAPMサービスをスクラッチ開発(RubyFlowより)

私が最初元記事を「AMP」と空目してしまい、ひととおりスマホのAMP談義があったあとで「あれ、これAPMじゃね?」と気づきました。失礼しました。


traceb.inより

つっつきボイス: 「なーんだ、Application Performance Monitoringのことか」「Advanced Power Managementではない(キリッ」「NewRelicみたいなことを自分たちでやろうとしてるのかー」「またしても車輪の再発明w?」

そこから、NewRelicはやっぱり凄いという話題になりました。morimorihogeさんがその場でNewRelicにログインしてみると、大昔に作ったトライアルのエントリでもしっかりチャートが生成されました。

つっつきボイス: 「NewRelic、やっぱりいろいろよくできてる」「ライブラリやメソッドの呼び出しレベルまでチェックしてくれるし」「高いけどねw」「トライアルだとどこまでできるんだったかな?」「NewRelicってアプリの相当深いところまで食い込むのか」「たしかそのためのエージェントをgemでインストールしたはず」


newrelic.comより

静的型付け言語だからバグが減るわけではない?

某所で見かけました。これは2016年に書かれた元記事が別サイトに再掲載されたものですが、忘れた頃にバズったようです。

原文でも断言は避けていますが「静的型付け言語ならバグが減る、とは言い切れないのでは」「むしろ言語仕様のシンプルさの方がバグ減らしに貢献しているのでは」という見解です。

グラフのx軸はバグの密度です。グラフ上ではRubyが健闘しています。


labs.ig.comより

つっつきボイス: 「GitHubのbugタグでかき集めたのか」「うーん、これは信頼に足る情報なんだろうか」「とりあえず集計方法は疑問だなー」「すごく巨大なC++プロジェクトとかあったらbug多いに決まってるんだから不利じゃん」「極端なデータは捨てないといけないのかな」「それもデータの種類や目的とかによりますね: 極端なデータに意味があることもあるし」

DockerとクラウドでHeroku的なデプロイソリューションを構築(Ruby Weeklyより)

タイトル通りです。長いです。


semaphoreci.comより

つっつきボイス:Google Container Engineとか使えばいいのに」「意識高めの車輪の再発明という感じ」

stdgems.org: Rubyバージョンごとのデフォルトgemとバンドルgemのリスト

サイト: stdgems.org

Rubyのバージョンごとにどんな標準のgemがあるかを一覧できます。標準のgemはデフォルトgem(取り外せない)またはバンドルgem(外部メンテ、取り外し可)に区別されており、Ruby 2.2以降が対象です。


stdgems.orgより

Rubyの標準ライブラリはgem化が進められており、Ruby 2.5の「たぶんこうなる」も見ることができます。

つっつきボイス: 「これ悪くないかも」「知ってれば見るかなー」「普通にrbenvでインストールして確認しちゃうかも」

同時実行制御を深掘り: イベントループの巻(Ruby Weeklyより)

シリーズものです。


http://blog.appsignal.com/より

つっつきボイス: 「このあたりを掘っていくとOSの話は避けられないね」「OSは一度みっちりやっておくのが大事」

そこから、concurrentとparallelの違いなどについて話題になりました。なお、上の記事にはparallelという言葉は一度も使われていません。

つっつきボイス: 「concurrentとparallelはITの世界でははっきり違う」「concurrentは「異なるタスクの手分け」、parallelは「同じ種類のタスクの手分け」」「concurrentをsimultaneousという言葉で形容することはない: simultaneousは「開始が同時」というだけ」

Rubyの#each_consは他の言語でどう書くの?(Ruby Weeklyより)

Rubyだと以下のように書けるコードを他の言語で書くとどうなるか、という記事です。F#、C#、Kotlin、Idrisのコード例などが集められています。

([0] + arr).each_cons(2).count {|x,y| x == 0 && y == 1 }

Rubyリファレンスマニュアル: each_consでは以下のようになっています。

要素を重複ありで n 要素ずつに区切り、 ブロックに渡して繰り返します。
ブロックを省略した場合は重複ありで n 要素ずつ繰り返す Enumerator を返します。

C#ではそれ用のコードをこしらえています↓。


[realfiction.netより

つっつきボイス: 「ちょC#スクショだしww」「しかも斜めってる」「#each_consって要素が重なるのか」「重ならないのが#each_slice、その名のとおり」

インタビュー: Aaron Patterson(Ruby Weeklyより)


blog.rubyroidlabs.comより

tenderloveの名前でおなじみのAaron Patterson氏インタビューで、Railsconf 2017でのインタビュー音声つきです。HTTP/2と「Rack 2」計画との関連など、技術面でも非常に読み応えのある良記事です。

ベーコン作りが趣味で、Rubyのハックに必要な日本語記事を読むために11年前から日本語を勉強し、今では日本語ブログも問題なく読めるようになったそうです。凄い!

インタビュー中で、kazuho氏の作ったH2OというWebサーバーを「HTTP/2サーバーとしては最高峰だ」と激賞しています。

つっつきボイス: 「これは日本語になってたら読みたい」「長いから英語で読むのはだるい」「H2O、数年前に話題になってました」「当時HTTP/2をまともに使えるWebサーバーがこれしかなかったんだったかな」「H2Oのh2o.examp1e.netってドメイン名、クラックサイトっぽいwww」「お、lが1になってるw」

参考: 東京 Ruby 会議 11 直前特集号 Aaron Patterson さんインタビュー

開発用ローカルWebサーバーをNginxからTræfikに置き換える(Ruby Weeklyより)

Træfikは文字化けではありません。Goで書かれたリバースプロキシ兼用Webサーバーで、GUIで設定できるのがウリのようです。


traefik.ioより

つっつきボイス: 「GUI(゚⊿゚)イラネww」「CLIでちゃんと設定できない人がGUIでできると思えない」「両方で設定を繰り返すと泣くことになりそうってkazzさんも言ってた、そういえば」

Railsでcookieをテストする(Ruby Weeklyより)


blog.arkency.comより

capybaraでfeature specを書くのは避けたかったらしく、rack-test gemの#get-cookieを使っています。

# http://blog.arkency.com/2017/06/testing-cookies-in-rails/ より
describe do
  specify do
    get "/"

    Timecop.travel(35.minutes.from_now) do
      get "/"

      cookie = get_cookie(cookies, "foo")
      expect(cookie.value).to eq("some value!")
      expect(cookie.expires).to be_present
    end
  end

  # rack-test > 0.6.3 に組み込まれる予定
  def get_cookie(cookies, name)
    cookies.send(:hash_for, nil).fetch(name, nil)
  end
end

つっつきボイス: 「cookieでここまでテストしないといけないのか、大変だなー」

たった2分でRubyプリミティブをカスタムドメインオブジェクトに書き換える方法(Ruby Weeklyより)

ここではNet::HTTPが返すStringのステータスコード'200'を、#success?でチェックするようリファクタリングしています。テストコードも一緒に書いてあるので助かります。

つっつきボイス: 「こういうの、たまにやるかなー」

GeoEngineering: TerraformのDSLをRubyで書けるgem(Ruby Weeklyより)

インフラをコードで管理するソフトウェアであるTerraformをRubyで制御するラッパーのようです。なおTerraformはGo言語で書かれています。


www.terraform.ioより

つっつきボイス: 「Terraformは超有名で実績も多数」「Terraformのリポジトリ、★8,600超えだ」「TerraformのDSLは割りと独特なので、それをRubyで書きたいってことか」「GeoEngineeringの方は★200個程度というのがちと不安」

# https://www.terraform.io/ のサンプルDSL
resource "aws_elb" "frontend" {
  name = "frontend-load-balancer"
  listener {
    instance_port     = 8000
    instance_protocol = "http"
    lb_port           = 80
    lb_protocol       = "http"
  }

  instances = ["${aws_instance.app.*.id}"]
}

resource "aws_instance" "app" {
  count = 5

  ami           = "ami-408c7f28"
  instance_type = "t1.micro"
}
# https://github.com/coinbase/geoengineer のサンプルコードより
class GeoEngineer::Resources::AwsSecurityGroup < GeoEngineer::Resource
  # ...
  def all_egress_everywhere
    egress {
        from_port        0
        to_port          0
        protocol         "-1"
        cidr_blocks      ["0.0.0.0/0"]
    }
  end
  # ...
end

project.resource('aws_security_group', 'all_egress') {
  all_egress_everywhere # use the method to add egress
}

ところで、GeoEngineeringという名前は明らかにTerraformを意識してますね。暁星記という未完のマンガを思い出しました。

n_plus_one_control: RSpec/miniTestの両方で使えるN+1問題検出マッチャー(Ruby Weeklyより)


github.com/palkan/n_plus_one_controlより

expect { subject }.to query(2).timesのように具体的な回数を書かずにN+1問題をテストできるそうです。クエリの数がO(N)ではなくO(1)として振る舞うかどうかをテストします。

bulletでもテスト書けるんだけど?」については、「bulletは銀の弾丸(silver bullet)じゃないので、偽陽性や偽陰性の可能性が残る」だそうです。誰がうまいこと言えと。

つっつきボイス: 「N+1が起きてないことをテストする?」「N+1警察はN+1でないことを確認するテストを書くんだろうなーw」「★まだ80個台…」

Rubyのコードを読む: DSL(RubyFlowより)

連載記事の4回目です。RubyのROMのDSLをとことん追求している濃厚な記事です。

# https://blog.mikecordell.com より
def schema(dataset = nil, infer: false, &block)
  if defined?(@schema)
    @schema
  elsif block || infer
    self.dataset(dataset) if dataset
    self.register_as(self.dataset) unless register_as

    name = Name[register_as, self.dataset]
    inferrer = infer ? schema_inferrer : nil
    dsl = schema_dsl.new(name, inferrer, &block)

    @schema = dsl.call
  end
end

つっつきボイス: 「ROMはRuby Object Mapperのことか」「RubyでDSL書きたい人にはよさそう」


rom-rb.orgより

morimorihoge「RubyのDSL入門ならCodeSchoolのRuby Bits part 2がシンプルでおすすめですよー

その後、Rubyの引数のかっこを省略できる仕様の理由を私は今頃になって初めて知りました。

つっつきボイス: 「Rubyでかっこを省略できるのは、RubyでDSLを書きやすくするためという明確な目的がある」「そうだったのか!」「たしかmatzはRubyの初期からそのつもりでやっているはず」

ネットの記事で「Rubyではかっこを省略できるので、DSLを簡単に書けます」という記述をときどき見かけていました。そこから「たまたまかっこを省略できるのでそれを利用して…」かなと思っていましたが、違いました。Rubyは「言語を書くための言語」を最初から志向していたんですね。

RailsでUIコントローラ(RubyFlowより)

ネストしたフォームを使う場合など、ビューが複雑になった場合にコントローラをどう扱うかを検討し、ここでは以下の3番目の方法を使っています。

  • 同じコントローラに新しいメソッド(アクション)を追加
  • 別のコントローラを書く
  • UIコントローラをネストする(Railsの密かな第3のオプション)

ここではコントローラでささやかなDSLを書く方法を使っています。さらに、ネストを重ねたり、ネストしたコントローラでもパーシャルを参照できるようにするなどし、ルーティング用のモジュールも書いています。

つっつきボイス: 「この方法、悪くないかも」「『RESTfulを崩さないかぎりコントローラは増やしてもいい』がDHH流ですよね」「ネストするフォームってつらくなりがち」「この落書きwww↓」「1分間に何回WTF(何やこれは!)って言われるかでコードの良し悪しがわかるw」


labs.kollegorna.seより

Railsアプリのアップグレードに役立つリソース集(RubyFlowより)

「アップグレードのときはこれ見ましょう」リンク集です。

つっつきボイス: 「Rails 4どまりかー」「世の中にはRails3っていうものもあるんだじぇー」

⭐ railsdiff.org ⭐

むしろ、その中で紹介されていたrailsdiff.orgの方が一同の目を引きました。

RailsのバージョンアップでRailsアプリのどこを変更しなければいけないかをざっと表示するもので、内部コードのdiffではありません。当然ながらバージョンが離れるほどどんどん量が増えます。こちらは何とRails 2.3.6以降に対応しており、Rails 4.xとRails 3.xといった設定の差も表示できます。


railsdiff.orgより

つっつきボイス: 「これ、いいんじゃん!?」


railsdiff.orgより

⭐ を進呈いたします。おめでとうございます。

Rubyで機械学習: 線形回帰を実装(RubyFlowより)

x_data = []
y_data = []
# CSVから配列に読み込む - 自由変数 X と従属変数 Y
# 各行には以下のようにプロパティと生活圏の広さ(平方フィート)を含む
# [ SQ FEET PROPERTY, SQ FEET HOUSE ]
CSV.foreach("./data/staten-island-single-family-home-sales-2015.csv", :headers => true) do |row|
x_data.push( [row[0].to_i, row[1].to_i] )
y_data.push( row[2].to_i )
end

つっつきボイス: 「Pythonでやってくれーw」

Quora: 有名サイトでRubyが使われてないのはどして?

最近のQuoraから届く更新メールはこれ系の記事が多く、こちらのクリックがトラック・分析されていることをひしひしと実感します。

回答は「そりゃRailsが登場する前からあるサイトばかりだからなんじゃ?」「Rails登場以前はRubyで大規模Webサイトを作るとかありえなかったし」などなど。

つっつきボイス: 「(トラック・分析は)記事表示の最適化がQuoraの売りだから」「煽ってくるなー、このタイトル」「その割にスレが伸びてないなw」「みんな不感症になっちゃった?」「初期のTwitterとかもRailsで構築されていたんだけどな」「表にもTwitterあるし」


今週は以上です。

関連記事

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

RailsConf 2017のパフォーマンス関連の話題(3)「あなたのアプリサーバーの設定は間違っている」など(翻訳)

$
0
0

「RailsConf 2017のパフォーマンス関連の話題」は今回で完結です。

概要

原著者より許諾を得て翻訳・公開いたします。

楽しい画像はすべて元記事からの引用です。

RailsConf 2017のパフォーマンス関連の話題(3)「あなたのアプリサーバーの設定は間違っている」など(翻訳)

7. あなたのアプリサーバーの設定は間違っている

私は今回のカンファレンスで「あなたのアプリサーバーの設定は間違っている」(スポンサー: Heroku)というタイトルで講演いたしました。現時点ではまだ動画はアップされていませんが、私のTwitterアカウントをフォローいただければアップされたときにお知らせいたします。

私がこれまでコンサルティングしてきたアプリで見つかった問題の第1位は「アプリサーバーの設定ミス」(Puma、Unicorn、Passengerなど)でした。企業のアプリサーバーに設定ミスがあると月額コストが数千ドルにも達し、アプリのパフォーマンスも30%から40%ほど低下することがあります。これはひどい事態です。ぜひ私の講演をご視聴ください。

8. パフォーマンスパネル

カンファレンスのラス日に、Richard、Eileen、そして私でパネルディスカッションが行われました。司会はSam Saffronです。そのときの動画がこちらです。

出席者のSavannahが、マインドマップっぽいクールなものを作ってくれました。

9. パフォーマンス関連のその他の話題

RailsConfでは、この他にもRubyのパフォーマンスに関心のある方なら押さえておきたいプレゼンが他にもいくつかありました。

訳注: いずれも35分程度の動画です。

プレゼンはShopifyのSimon Eskildsenです。Simonの話し方はいつも部外者にわかりやすくてとても素晴らしいですね。世界トップ100入りするサイトをRailsで構築したい方は必見です。

GitHubのBryana Knightによるこのプレゼンでは、SQLクエリを最大限に高速化する方法について解説しています。プレゼンの大半はインデックスとインデックスの利用状況の調査方法にあてられています。

Braulio Carrenoによる、もうひとつのパフォーマンス戦記です。

10. 秘密のプロジェクト

ここには詳しく書きませんが、ある人物から実にクールなJavaScriptプロジェクトを見せていただきました。そのプロジェクトは「シングルページアプリ(SPA)を持たない人のためのJavaScriptフレームワーク」とでも言うべきもので、Turbolinksアプリや、JavaScriptコードを大量に導入しながら他のフレームワークを使っていないアプリで非常に高い効果がありそうに見えました。外見上は「控えめな(unobtrusive)JavaScript」フレームワークと似た感じです。これについては、プロジェクトがpublic releaseにこぎつけたときにお知らせいたします。


いいかぼうず、$(document).readyを追加し始めたら一巻の終わりだ、そこから先は…

私の個人的な意見ですが、Turbolinksの問題のひとつは「Turbolinksを使える複雑なアプリをビルドする学習用リソースや教育用理論が少なすぎる」ことだと思っています。TurbolinksではアプリのJavaScriptに対してさまざまなアプローチを必要とします。たとえばBackbone.jsやAngular.jsなどのSPAフレームワークを使いたい、以前キッチンシンクからturbolinks:load hooksにダンプしたときと同じようにJavaScriptを書きたいだけなのに、そこから悪夢が始まります。このフレームワークは、ページの振る舞いに一種のゴールデンパス(golden path)を追加することでこうした問題を解決できそうに思えました。

11. HTTP/2

HTTP/2についてはAaronのキーノートスピーチでも少し触れられていましたが、会場の廊下でAaronやEvanと立ち話したときにRackでのHTTP/2サポートへの道筋について議論になりました。

私はアプリの前にHTTP/2対応のCDNを配置して済ませるという従来の方法を擁護してきましたが、Aaronと私はこの点でかなり意見が合いました。Aaronは、RackのenvハッシュにHTTP/2固有のキーを追加することで、リクエストがHTTP/2対応であるかどうかがRackから知らされたときにHTTP/2の機能をアプリで自由に使えるコールバックを作りたいと考えています。しかし私は、Server PushがCDNリバースプロキシでおおよそ実装できることから、その用法はかなり限定されるのではと考えています。

12. RPRG/Chatについての続報

RubyConf 2016の続報で、私は次のように書きました。

最後に、「パフォーマンス野郎たちの集い」ではいくつか素晴らしい議論が繰り広げられ、2つの大きな成果がありました。Ruby Performance Research Group(RPRG)と「Ruby Performance」コミュニティグループです。

私はこれらのプロジェクトに今でも参加し続けています。近々Research Groupに何かアップされると思います(私は高マルチスレッドのRubyアプリにおけるメモり断片化についてのネタがあります)。コミュニティグループの方にもその後でときどきアップされるでしょう。

13. カラオケ!


こちらがJon McCartieでございます、皆さま

RailsConf 2017についてはだいたい以上です。来年のさらなるRubyパフォーマンスの話題とカラオケを楽しみにしています。

(終わり)

関連記事

Rails深読み: ActiveRecord::PendingMigrationErrorの発生条件からMigrationの挙動を追う

$
0
0

morimorihoge です。最近ゲームらしいゲームをやれてない。

おかげさまで社内有志で週間Railsウォッチを始めてぼちぼち1年が経とうとしています。普段は利用者としてRailsを使っているとRailsの内部挙動まで追いかけないといけないケースは少ないのですが、最新のRails commit履歴やバグフィックス履歴を見ていくことで色々と「あーこの使い方でバグるのか」とか「次のバージョンからこれできるようになるのか」といった発見があります。

僕がRailsをまともに使い始めたのは3.0.0rcの頃でしたので、GitHubのリリース履歴を見ると少なくともその時代からももう7年が経ち、歴史あるソフトウェアになってきたなあというのを感じます。それに合わせてコードベースも大きくなり、新たに学習しようとした場合のハードルはどうしても上がっていっている気はしますね。

数年前社内有志でCrafting Rails 4 Applications読み会を行いましたが、あれもRailsの全体を網羅する内容というよりは、一つ一つ絞った内容を深掘りするような内容でした。

参考: Amazon: Crafting Rails 4 Applications: Expert Practices for Everyday Rails Development (The Facets of Ruby)

Railsの内部実装は日々更新されていっている中、既存から大きく実装が差し替わる場合にはChangeLogや解説記事が出たりしますが、昔から変わらない部分については古い記事を信用していいのかよく分からないところも多いので、結局自分で読んでいくしかない感もあるということで、気が向いたときに少しずつ気になった部分の挙動を読んでいったメモを記事にしていこうと思います。

※本記事執筆時点の 5.1-stable ブランチを参照しています df776aabc45b17dff2cf8edbdd3b1367a1c21167

ActiveRecord::PendingMigrationError とは?

ActiveRecord::PendingMigrationError はRails開発をそこそこ続けていれば一度くらいは見たことのあるエラーかと思います。RailsのMigrationの仕組みの中で 未実行のMigrationがある場合にraiseされるエラー になります。
開発現場では、db/migrateの下にあるmigrationファイル(YYYYMMDD#{timestamp}_#{migration_name}.rb)が追加されたのに rails db:migrate されていない状態でRails serverにアクセスすると発生します(注: Rails5からはrailsコマンドでもrakeタスクが実行可能)。

とりあえずソースから見ていきましょう。何も考えず PendingMigrationError で全文検索すると、activerecord/lib/active_record/migration.rbのL127あたり に定義がありました。

  class PendingMigrationError < MigrationError#:nodoc:
    def initialize(message = nil)
      if !message && defined?(Rails.env)
        super("Migrations are pending. To resolve this issue, run:\n\n        bin/rails db:migrate RAILS_ENV=#{::Rails.env}")
      elsif !message
        super("Migrations are pending. To resolve this issue, run:\n\n        bin/rails db:migrate")
      else
        super
      end
    end
  end

見覚えのある感じのメッセージですね。

ソースを追ってみる

では実際にPendingMigrationErrorをraiseしている部分はというと、同じく全文検索結果からactiverecord/lib/active_record/migration.rbのL574あたり が出てきます。
少し関係ない部分を整形してクラス階層構造を抜き出すとこんな感じになっています。 ActiveRecord::Migration.check_pending! ですね。なおここ以外にPendingMigrationErrorをraiseしている部分はありませんでした。

module ActiveRecord
  class Migration
    class << self
      # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending.
      def check_pending!(connection = Base.connection)
        raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection)
      end
    end
  end
end

上記の通り、ActiveRecord::Migration.check_pending!ではActiveRecord::Migrator.needs_migration?(connection)をチェックしているので次はそちらを見てみます。
同じくactiverecord/lib/active_record/migration.rbのL1042あたりです。

      def needs_migration?(connection = Base.connection)
        (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0
      end

ソースから挙動を予想する

さて、ここでざっくり挙動を予想してみましょう。実装の雰囲気からmigrations(migrations_paths)は各migrationを抽象化したオブジェクトのcollectionが入っており、そこからversionメソッドで取り出したバージョン番号の配列を取り出しています(.collect(&:version))。
※このメソッド引数に& + シンボルを渡す書き方は超一般的なidiomなので、もし知らない人がいたら覚えておきましょう。 .collect{|obj| obj.version}と同じ意味になります。

参考: Rubyで “&” を使うと幸せになれるらしいよ (*´Д`)ノ

また、migrations(migrations_paths)はパスを引数に取るため、こちらは恐らくソースコード定義からmigrationに対応するオブジェクトを作成しているのではないかと予想できます。

次に、get_all_versions(connection)の方はどうでしょうか?migrationsメソッドの方は引数にmigrations_pathsが入っているのに対してこちらにはconnectionが引数になっているので、恐らくDBから取り出した値になるのではないかと予想できます。

予想を検証する

では、予想を確かめてみます。#migrationsこの辺#migrations_pathsこの辺です。抜粋すると

      def migrations_paths
        @migrations_paths ||= ["db/migrate"]
        # just to not break things if someone uses: migrations_path = some_string
        Array(@migrations_paths)
      end

      def migrations(paths)
        paths = Array(paths)

        migrations = migration_files(paths).map do |file|
          version, name, scope = parse_migration_filename(file)
          raise IllegalMigrationNameError.new(file) unless version
          version = version.to_i
          name = name.camelize

          MigrationProxy.new(name, version, file, scope)
        end

        migrations.sort_by(&:version)
      end

#parse_migration_filename周辺は

    MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc:
      def parse_migration_filename(filename) # :nodoc:
        File.basename(filename).scan(Migration::MigrationFilenameRegexp).first
      end

となっています。MigrationFilenameRegexpの1つめの([0-9]+)にマッチする部分が文字列で返却されそうな感じですね。
ここから分かるのは、 Migrationファイルのファイル名は意味を持つので勝手に変えてはいけない ということです。

次に、#get_all_versionsの方を見ていきます。activerecord/lib/active_record/migration.rbのL1030あたりです。

      def get_all_versions(connection = Base.connection)
        if SchemaMigration.table_exists?
          SchemaMigration.all_versions.map(&:to_i)
        else
          []
        end
      end

さてここで出てくる SchemaMigrationですが、これはactiverecord/lib/active_record/schema_migration.rb で、
※以下は一部抜粋

module ActiveRecord
  class SchemaMigration < ActiveRecord::Base # :nodoc:
    class << self
      def primary_key
        "version"
      end

      def all_versions
        order(:version).pluck(:version)
      end
    end
  end
end

というversionカラムをprimary keyに持つ至って普通のActiveRecordオブジェクトです。
Railsの標準Migration機能を使った場合、DBにschema_migrationsというテーブルが自動的にできますが、それに対応するModelクラスですね。

振り返って整理する

ここまで追いかけた上で、改めてneeds_migration?の実装を見てみましょう。

      def needs_migration?(connection = Base.connection)
        (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0
      end

migrations(migrations_paths).collect(&:version)でファイル名から抽出したバージョン番号文字列配列、get_all_versions(connection)schema_migrationsテーブルから取り出したバージョン番号配列を取り出し、Array#- して差分が無ければOKということになります。
そしてここでもう一つ発見があります。Array#-で評価しているということは、左辺であるmigrationファイルから取得したリストにあるものは右辺のschema_migrationsにないといけませんが、逆は成り立たなくても良いのです。
すなわち、 migrationファイルに対応するschema_migrationのversion定義は必須だが、schema_migrationsにmigrationsファイルに対応しないversionレコードがあっても問題ない ということになります。

例えば、migrationファイルが大量になりすぎてうざいから昔のmigrationをまとめちゃいたいよー、と思ったときにA -> B -> CのmigrationをCにまとめてしまったとしても問題ない(はず)ということですね(ABのファイルを消してもPendingMigrationErrorにはならない)。
※この手のことをやるGemを捜すとSquasher が見つかりますが、僕は古いMigration履歴も開発の時系列履歴として残しておきたい派なので使ったことはないです

まとめ

そんなわけで、今回はざざっとActiveRecord::PendingMigrationErrorを追いかけてみました。
普段あまり意識しないで使えていますが、こうやって内部実装を追いかけることで、Rails的に何が許されていて何が許されていないのかという境目を見ることができます。Migrationファイルのリネームなんかは何も知らないとやってしまう人がいそうな部分ではありますし、Migrationファイルとschema_migrationsテーブルの相互関係についても「多分こうかな〜」というのはあっても確信を得るにはソースコードを読むのが一番ですね。

またそのうち気が向いたら書いていこうと思います。ではでは。

週刊Railsウォッチ(20171124)GitHubにセキュリティアラート追加、RailsでVue.jsを使う、Railsテスト本2種、node-pruneで瞬間クリーンアップほか

$
0
0

こんにちは、hachi8833です。

Rails: 今週の改修

今週もcommit差分から見繕いました。

klass.all高速化のため不要なspawnを抑制

# activerecord/lib/active_record/scoping/default.rb
              # The user has defined their own default scope method, so call that
               evaluate_default_scope do
                 if scope = default_scope
-                  (base_rel ||= relation).merge(scope)
+                  (base_rel ||= relation).merge!(scope)
                 end
               end
             elsif default_scopes.any?
               base_rel ||= relation
               evaluate_default_scope do
                 default_scopes.inject(base_rel) do |default_scope, scope|
                   scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call)
-                  default_scope.merge(base_rel.instance_exec(&scope))
+                  default_scope.merge!(base_rel.instance_exec(&scope))
                 end
               end
             end

つっつきボイス:mergemerge!に変わったのか」

ActiveRecord::SpawnMethodsを見るとmergespawnがありました。

# actionpack/lib/action_controller/metal/strong_parameters.rb#L718
    def merge(other)
      if other.is_a?(Array)
        records & other
      elsif other
        spawn.merge!(other)
      else
        raise ArgumentError, "invalid argument: #{other.inspect}."
      end
    end

    def merge!(other) # :nodoc:
      if other.is_a?(Hash)
        Relation::HashMerger.new(self, other).merge
      elsif other.is_a?(Relation)
        Relation::Merger.new(self, other).merge
      elsif other.respond_to?(:to_proc)
        instance_exec(&other)
      else
        raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation"
      end
    end

after_bundleコールバックを非推奨化

fbd1e98からRailsのプラグインで生成時にbundle installが実行されなくなったため、
after_bundleコールバックはbundle後に実行されなくなった。
このコールバック名と実際の動作が合わなくなったので削除すべきと考える。
#60c550より

# railties/lib/rails/generators/rails/plugin/plugin_generator.rb
       def run_after_bundle_callbacks
+        unless @after_bundle_callbacks.empty?
+          ActiveSupport::Deprecation.warn("`after_bundle` is deprecated and will be removed in the next version of Rails. ")
+        end
+
         @after_bundle_callbacks.each do |callback|
           callback.call
         end

つっつきボイス: 「Rails 5のプラグインって何だろうと思ったら、Railsガイドに書いてあった」

参考: Gem、Railtieプラグイン、Engine(full/mountable)の違いとそれぞれの基礎情報

ActiveStorageルーティングの一部でオプションが無視されていたのを修正

# activestorage/config/routes.rb
Rails.application.routes.draw do
   get "/rails/active_storage/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob, internal: true

-  direct :rails_blob do |blob|
-    route_for(:rails_service_blob, blob.signed_id, blob.filename)
+  direct :rails_blob do |blob, options|
+    route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
   end

-  resolve("ActiveStorage::Blob")       { |blob| route_for(:rails_blob, blob) }
-  resolve("ActiveStorage::Attachment") { |attachment| route_for(:rails_blob, attachment.blob) }
+  resolve("ActiveStorage::Blob")       { |blob, options| route_for(:rails_blob, blob) }
+  resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
...

つっつきボイス: 「おー、随分抜けてたなー」「テストが見当たらないけど後から足すのかな?」

Rails

書籍『Everyday Rails Testing with RSpec』2017年版の第2章が更新

leanpub.com/everydayrailsrspecより

見逃していましたが、同書は6月にSpec 3.6とRails 5.1向けにメジャーアップデートしていて、それがさらに更新されたということです。英語版を購入した方は無料で更新版をダウンロードできます。

書籍『Rails 5 Test Prescriptions』(ベータ版)


pragprog.comより

Rails 5.1+Webpackに対応しているそうです。おおよその目次は以下です。

  • TDDの寓話
  • TDDの基本
  • RailsのTDD
  • テストを良くする要素
  • モデルのテスト(書籍サンプルPDF
  • テストで日時を扱う
  • ダブル(double)をモックやスタブとして使う
  • CapybaraとCucumberを使った結合テスト
  • JavaScriptの結合テスト
  • JavaScriptの単体テスト
  • Railsの表示要素のテスト
  • MiniTest
  • セキュリティのテスト
  • トラブルシューティング/デバッグ
  • テストの高速化
  • レガシーコードのテスト

つっつきボイス: 「prescription: 処方箋ですね」「このあたりの本、輪読会に向いてそう」

RSpecからController specを消し去る(RubyFlowより)


everydayrails.comより


つっつきボイス: 「コントローラを薄くしてコントローラのテストも薄くするという考え、とても同意できる: コントローラに業務ロジックとか書かなければ、コントローラのテストはアクセスチェックで十分なはず」「昔コントローラのテストをどこまで書くべきか悩んでました」

[Rails] RSpecをやる前に知っておきたかったこと

RailsアプリをAWS Elastic Beanstalkにデプロイする


syndicode.coより


つっつきボイス: 「Elastic Beanstalkってどんなサービスだったかしら」「論理VMレベルのDockerに近いかも: Vagrantに近いと言えばイメージ近いかな」「この記事ではRailsのSECRET_KEY_BASEも設定してますね」「最近の作って捨てるポリシのインフラ界隈では、configはハードコードせずに環境変数としてinjectionせよ、というのがベストプラクティスになりつつあるので、実はRailsのsecret.key.enc方式はそうした流れに逆行しているかな」

参考: よくある質問: AWS Elastic Beanstalk

FastRuby.io: 古いRailsのアップグレード相談サイト

以前のウォッチでもご紹介したhttps://www.upgraderails.com/とちょっと似た感じです。upgraderails.comはアップグレード請負が全面に出ていますが、fastruby.ioは「まずはご相談」という雰囲気で、無料ガイド(PDF)も配布しています。

www.upgraderails.comより

JRubyだとTime.iso8601Time.parseの14倍速かった

早すぎる最適化もたまにはいいことがあるという主旨です。

# 同記事より
# JRuby 9.0.4.0
Warming up --------------------------------------
           No format     1.111k i/100ms
          ISO format    18.031k i/100ms
Calculating -------------------------------------
           No format     16.364k (± 3.3%) i/s -     82.214k
          ISO format    237.077k (± 4.2%) i/s -      1.190M

Comparison:
          ISO format:   237076.7 i/s
           No format:    16364.4 i/s - 14.49x slower

つっつきボイス: 「CRubyだとTime.iso8601に変えても3%しか速くならなかったそうです」「へー、JavaのDateライブラリはとても使いにくくて何度も変わってたのに」「Rubyのパースは柔軟だけどその分遅いのかな」

参考: wikipedia-ja ISO8601

RailsでVue.jsを使う


classandobjects.comより

RailsにVue.jsを導入する手順の解説です。

# 同記事より
app/javascript/
├── components
│   ├── App.vue
│   └── shared
│       └── csrf.vue
├── packs
│   ├── devise
│   │   └── registrations
│   │       └── new.js
└── views
    ├── devise
        └── registrations
            └── new.vue

つっつきボイス:rails new myapp --webpack=vueでVue.jsインストールできるのか↓」「5.1からこれ使ってました」「=jqueryはないというかなしさ」

# Intalling vue
# Rails 5.1+ new application
rails new myapp --webpack=vue

Vue.jsサンプルコード(01〜03)Hello World・簡単な導入方法・デバッグ・結果の表示とメモ化

DHHインタビュー(Ruby Weeklyより)


lifehacker.comより

子供の頃プログラミングを学ぼうとして何度も挫折し、もっぱらゲームに勤しんでたそうです。


つっつきボイス: 「いつもの写真と違いますね」「これも最近の写真じゃないだろうなきっと」「今もTextMateが好きらしいです」「TextMateはPHP時代にかなり長い間使ってたけど、日本語サポートが残念すぎて半角日本語フォントを自作ビルドして入れるとかしないとまともに表示できなかったり、TextMate3がいまいちだったりした辺りで乗り換えたな」

Service Object支援gem 2本立て

active_interaction: ビジネスロジックをCommandパターンで書くgem

# AaronLasseigne/active_interactionより
class BooleanInteraction < ActiveInteraction::Base
  boolean :kool_aid

  def execute
    'Oh yeah!' if kool_aid
  end
end

BooleanInteraction.run!(kool_aid: 1)
# ActiveInteraction::InvalidInteractionError: Kool aid is not a valid boolean
BooleanInteraction.run!(kool_aid: true)
# => "Oh yeah!"

waterfall: チェインを意識した関数型的Service Object gem(Ruby Weeklyより)

apneadiving/waterfallより

# apneadiving/waterfallより
class FetchUser
  include Waterfall

  def initialize(user_id)
    @user_id = user_id
  end

  def call
    chain { @response = HTTParty.get("https://jsonplaceholder.typicode.com/users/#{@user_id}") }
    when_falsy { @response.success? }
      .dam { "Error status #{@response.code}" }
    chain(:user) { @response.body }
  end
end

Flow.new
    .chain(user1: :user) { FetchUser.new(1) }
    .chain(user2: :user) { FetchUser.new(2) }
    .chain  {|outflow| puts(outflow.user1, outflow.user2)  } # report success
    .on_dam {|error|   puts(error)      }                    # report error


apneadiving/waterfallより


つっつきボイス: 「この間のRails開発のコツ記事にもありましたが、みんなService Objectをどうにかしたいんだなと感じました」「このWaterfallというgemの名前は機能に即してて好き: 図もわかりやすいし↑」「ワークフローっぽいですね」「JSのPromiseをちょっと連想しました」
「ところでService ObjectのServiceという言葉、意味が広すぎてあまり好きじゃないです: Domain Objectと呼んで欲しかった」「Domainも相当意味が広い気がしますね」

参考: 混乱しがちなサービスという概念について

Ruby trunkより

リクエスト: Kernel#ppをデフォルトで有効にして欲しい

賛成が集まりつつあります。


つっつきボイス:#ppって使ったことないけど何の略?」「Kernel#pp(pretty print)は普通によく使いますね: printfデバッグ的なことをするときとか」「確かにデフォルトでrequireされるようになったらありがたい」

Ruby

RubyのChain of ResponsibilityパターンとProxyパターン(RubyFlowより)

rubyblog.proより

Rubyデザインパターンの記事2本です。ProxyパターンではVirtual proxy/Protection proxy/Remote proxy/Smart referenceの4つの応用例が示されています。

Rubyでワーカープールを実装する(Ruby Weeklyより)

# 同記事より
worker_1 got #<Proc:0x007fc35a132d18@worker_pool_2.rb:40 (lambda)>
worker_0 got #<Proc:0x007fc35a130a40@worker_pool_2.rb:89 (lambda)>
worker_3 got #<Proc:0x007fc35a1309a0@worker_pool_2.rb:89 (lambda)>
worker_5 got #<Proc:0x007fc35a130950@worker_pool_2.rb:89 (lambda)>
worker_7 got #<Proc:0x007fc35a1308b0@worker_pool_2.rb:89 (lambda)>
worker_9 got #<Proc:0x007fc35a130810@worker_pool_2.rb:89 (lambda)>
worker_5 got #<Proc:0x007fc35a1305b8@worker_pool_2.rb:89 (lambda)>
# reduced output lines...
worker_4 got #<Proc:0x007fc35a130428@worker_pool_2.rb:89 (lambda)>
worker_6 got #<Proc:0x007fc35a130900@worker_pool_2.rb:89 (lambda)>
worker_2 got #<Proc:0x007fc35a130478@worker_pool_2.rb:89 (lambda)>
worker_1 got #<Proc:0x007fc35a1307c0@worker_pool_2.rb:89 (lambda)>
worker_8 got #<Proc:0x007fc35a130018@worker_pool_2.rb:89 (lambda)>
worker_4 got #<Proc:0x007fc35a1304f0@worker_pool_2.rb:89 (lambda)>
worker_0 got #<Proc:0x007fc35a1306f8@worker_pool_2.rb:89 (lambda)>

つっつきボイス: 「ワーカーというとUnicornとかPumaとか」「そういうソースを追うときに役立ちそうですね」

google_translate_diff: Google翻訳APIで巨大な文の差分だけ翻訳するgem(RubyFlowRuby Weeklyより)

# 同記事より
s = "There are 6 pcs <b>Neumann Gefell</b> tube mics MV 101 with MK 102 capsule. It is working with much difference capsules from neumann / gefell.\nAdditionally…"

GoogleTranslateDiff.translate(s, from: :en, to: :es)

=> # Tokenize

["There are 6 pcs ", :text],
 ["<b>", :markup],
 ["Neumann Gefell", :text],
 ["</b>", :markup],
 [" tube mics MV 101 with MK 102 capsule.", :text],
 ["It is working ... / gefell.\n", :text],     # NOTE: Separate sentence
 ["Additionally…", :text]]                     # NOTE: Also, separate sentence

=> # Load from cache and translate missing pieces

["Ci sono 6 pezzi ", :text],                   # <== cache
 ["<b>", :markup],
 ["Neumann Gefell", :text],                    # <== Google ==> cache
 ["</b>", :markup],
 [" Tubi MV 101 con ... ", :text],             # <== Google ==> cache
 ["Sta lavorando cn ... / gefell.\n", :text],  # <== cache
 ["Inoltre…", :text]]                          # <== cache

=> # Join back

"Ci sono 6 pezzi <b>Neumann Gefell</b> Tubi MV 101 con capsula MK 102. Sta lavorando con molte capsule di differenza da neumann / gefell.\nInoltre"

やはりというか、差分翻訳のコンテキストは失われてしまうそうです。


つっつきボイス: 「こういうオレオレ翻訳支援ツールって車輪の再発明がものすごく多い世界: ローカライズ業界では訳文の再利用に翻訳メモリというものをよく使ってるんですが、サイズが大きくなって低品質の翻訳が混じるとどんどん残念になってしまう」

Google Translator Toolkitと翻訳メモリ(ノーカット版) : RubyWorld Conference 2013より

sniffer: 外向きHTTPリクエストのアナライザgem


aderyabin/snifferより

1月足らずで★250超えです。これも含め、最近evilmartians.comがスポンサーになっているgemをときどき見かけます。

# aderyabin/snifferより
require 'http'
require 'sniffer'

Sniffer.enable!

HTTP.get('http://example.com/?lang=ruby&author=matz')
Sniffer.data[0].to_h
# => {:request=>
#   {:host=>"example.com",
#    :query=>"/?lang=ruby&author=matz",
#    :port=>80,
#    :headers=>{"Accept-Encoding"=>"gzip;q=1.0,deflate;q=0.6,identity;q=0.3", "Connection"=>"close"},
#    :body=>"",
#    :method=>:get},
#  :response=>
#   {:status=>200,
#    :headers=>
#     {"Content-Encoding"=>"gzip",
#      "Cache-Control"=>"max-age=604800",
#      "Content-Type"=>"text/html",
#      "Date"=>"Thu, 26 Oct 2017 13:47:00 GMT",
#      "Etag"=>"\"359670651+gzip\"",
#      "Expires"=>"Thu, 02 Nov 2017 13:47:00 GMT",
#      "Last-Modified"=>"Fri, 09 Aug 2013 23:54:35 GMT",
#      "Server"=>"ECS (lga/1372)",
#      "Vary"=>"Accept-Encoding",
#      "X-Cache"=>"HIT",
#      "Content-Length"=>"606",
#      "Connection"=>"close"},
#    :body=> "OK",
#    :timing=>0.23753299983218312}}

つっつきボイス: 「おーハッシュで取れる: アプリの動作確認とかでときどき欲しくなるヤツかも」「pryの中で動かせるのがいいですね」

bundlerでcombinationが原因のバグ


depfu.comより

# 同記事より
[1,2,3,4].combination(2).to_a
 => [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]

([1] * 26).combination(13).size
 => 10400600

つっつきボイス:#combinationって数学の順列組み合わせの組み合わせでしたっけ」「ですです」「そういえば#productはRSpecでよく使ってます」「組み合わせが爆発してbundlerがめちゃくちゃメモリ食ったのか」「記事の最後で投げているissue #6114がわかりやすそう」「組み合わせ爆発怖い…」

SQL

PostgreSQLのAdvisory lockとその使い方


shiroyasha.ioより

-- 同記事より
SELECT pg_advisory_unlock(23);
SELECT pg_advisory_unlock(112, 345);

SELECT mode, classid, objid FROM pg_locks WHERE locktype = 'advisory';

 mode | classid | objid
------+---------+-------
(0 rows)

つっつきボイス: 「Advisory lockの訳語って勧告的ロックなのね」「自分でunlockしないといけないあたり、何だかRubyのFiberを思い出しました」「強力すぎて怖いなー」「自己責任迫られるヤツ」

Advisory: 顧問

JavaScript

JavaScriptのコスト(JavaScript Weeklyより)


medium.com/dev-channelより

JavaScriptのどこで時間やリソースを食っているかという調査です。


medium.com/dev-channelより

忙しいJS開発者のためのES6いいとこ取り(JavaScript Weeklyより)


thenewstack.ioより

// 同記事より
var id = `Your name is ${firstName} ${lastName}.`
var url = `http://localhost:8080/api/messages/${id}`
let chicken = {
     name: 'Pidgey',
     jobs:['scratch for worms', 'lay eggs', 'roost'],
     showJobs() {
        this.jobs.forEach((job) => {
        console.log(`${this.name} wants to ${job}`);
       });
    }
};
chicken.showJobs();
//Pidgey wants to scratch for worms
//Pidgey wants to lay eggs
//Pidgey wants to roost

つっつきボイス:
「ES6、バッククォートで式展開できるようになってるじゃないの!」「webpackとbabel使うなら普通に使ってよい機能なんで、最近使い始めた」
「式展開を最初に知ったのはRubyだったんですが、他の言語は?」「PHPで使ったことあった」「Perlにもあったかな」「式展開使ってる言語いろいろありますよ: メジャーなLL系言語ならほぼ持ってるんじゃないかな」

Rubyでの文字列出力に「#+」ではなく式展開「#{}」を使うべき理由

「fat arrow => はクロージャなのか」「そういえばCoffeeScriptのアローって->=>の2種類あるんですが、たまに使い分け間違えたりしてその後嫌いになったりすることあった」

参考: CoffeeScript -> と => の違い

AngularとReactとVueのどれがいいの?


objectpartners.comより

// 同記事より: Reactの例
export class Counter extends React.Component {
  render() {
    return <div>
      <h2>Current value is { this.state.value }</h2>
      <button>Increment</button>
    </div>
  }

  increment() {
    this.setState({
      value: this.state.value + 1
    });
  }
}

つっつきボイス: 「みんな悩みますよね」「Reactはrendererがあるのかー: 趣味に合わない」「大きなプロジェクトだとAngularなのかな: TypeScriptだし」

「依存性の注入(DI)」なんか知らなくてもいい(JavaScript Liveより)

面接で「DIとは何かと」いう質問があったのがきっかけで書いた記事だそうです。

// 同記事より
class Knight extends React.Component {
  static propTypes = {
    weapon: PropTypes.any.isRequired
  };
  render() {
    return `🐴 ${this.props.weapon}`;
  }
}

つっつきボイス: 「DIといえばもうJavaでしょう: Rubyだと特に必要を感じないなー」

参考: 猿でも分かる! Dependency Injection: 依存性の注入

⭐node-prune: nodeの不要なファイルを一瞬で除去(GitHub Trendingより)⭐

公開後5日しか経過していないのに★2200超えです。Go言語で書かれています。


github.com/tj/node-pruneより


つっつきボイス: 「これみんな欲しかったヤツでしょうね」「npm使ってるとファイルじゃんじゃん増やされるし」「↑図がすべてを表してるw」「node_moduleはブラックホールより重い、と」

試しに動かしてみると、本当に一瞬で完了しました。

今週の⭐を進呈いたします。おめでとうございます。

CSS/HTML/フロントエンド

CSS Writing Modes Level 3がRecommendation間近?

11/23にCRが更新されていました。


w3.orgより


grid項目のアスペクト比


css-tricks.comより

gridのアスペクト比でお悩みの方向けです。

See the Pen Aspect Ratio Boxes Filling by Chris Coyier (@chriscoyier) on CodePen.

その他

GitHubにセキュリティアラート機能が追加

GitHubのPublicなリポジトリでInsights > Code Dependencyを表示するとセキュリティアラートが表示されるようになりました。現時点ではJavaScriptとRubyが対象です。


つっつきボイス: 「これいいなー」「何年も前にGitHubに放置していた自分のリポジトリで見てみたらどっと出てた…」「gem使う前にInsights > Code Dependencyチェック、が合言葉」

開発者にとって重要な5つの問題解決スキル

dev.to記事です。


dev.toより

  1. 大きくて複雑な目標をシンプルな目標に分割できる
  2. 並列を考えられる
  3. 抽象化できる(やりすぎないこと)
  4. 既存のソリューションを再利用できる
  5. データフローに即して考えられる

つっつきボイス: 「いい感じかつ実用的かも」

tmuxinator: tmuxセッションを簡単に作れる(GitHub Trendingより)


github.com/tmuxinator/tmuxinatorより

★7200超えです。


つっつきボイス: 「BPS社内は確かtmux派とbyobu派がいましたね」「(GNU) screen派もいたはず」「自分は使ってないなー」

qt: Go言語とQtバインディングでマルチプラットフォームアプリ


github.com/therecipe/qt/wiki/Galleryより

★3200超えです。WidgetとQMLのどちらでも動きます。
これが本当なら、同一ソースからWin/Mac/iOS/Androidなど向けアプリを一気にビルドできますね。Qtの商用ライセンス料と、Store登録の面倒臭さが何とかなるといいのですが。


つっつきボイス: 「Qtってキューティーじゃなくてキュートって発音するんじゃなかったでしたっけ?」「あー、そうでした(今初めて発音した…)」

ちょっと手元で動かしてみようと思ったのですがまだサンプルをセットアップできていません。QtのSDK削除するんじゃなかった…

primitive: 画像を幾何学図形の集まりで再現する(GitHub Trendingより)


github.com/fogleman/primitiveより

★8000近くあります。TechRachoの画像加工でも早速使っています。

shapecatcher.com: 手書きした文字に似ているUnicode文字をリストアップ

絵文字を絵で検索することもできます。

番外

仕事でしかコード書かない開発者ってどうよ?

記事というよりツイート並の短さですね。怒涛のようにレスが付いています。

どうしてこうなったんだっけ

プラセボは腰痛にも効く


つっつきボイス: 「そういえばデュアルディスプレイやめたら肩こり治ったんですよ」「マジで?!」「どうも首を左右に動かしていたのが肩によくなかったのかも」

おめでとうございます!


今週は以上です。

バックナンバー(2017年度)

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

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

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Rails: :before_validationコールバックの逸脱した用法を改善する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

なお、原文のbefore_validateは訳文でbefore_validationに修正しました。

Rails: :before_validationコールバックの逸脱した用法を改善する(翻訳)

ActiveRecordのコールバックが多くのプロジェクトで乱用され、もっとよい方法で簡単に回避できるユースケースであっても誤った理由で使われているのは今に始まったことではありません。実行される処理と関係のない、かなり逸脱した理由で多用される、特殊なコールバックが1つあります。それがbefore_validationコールバックです。

データ整形

データ整形、特に文字列のストリップは、アプリの主要な部分を占めることが多い処理です。たとえば、スペースで問題が生じないように URLをストリップすることを考えてみましょう。どのようなアプローチが考えられるでしょうか。

1つの方法は、before_validationを使うことです。特にデータ形式のバリデーションを行っている場合です。

# app/models/my_model.rb
class MyModel
  before_validation :strip_url

  private

  def strip_url
    self.url = url.to_s.strip
  end
end

これで処理は完了します。しかしテストはどうすればよいでしょうか。そのモデルでvalid?メソッドを呼び、URLがストリップされたかどうかをチェックする必要があるのでしょうか?これはいかにも変ですし、次のspecを見れば違和感がもっとよくわかるでしょう。

# spec/models/my_model_spec.rb
require "rails_helper"

RSpec.describe MyModel, type: :model do
  it "バリデーション前にURLをストリップする" do
    model = MyModel.new(url: "  http://rubyonrails.org")

    model.valid?

    expect(model.url).to eq "http://rubyonrails.org"
  end
end

このコードがTDDの結果であるとはちょっと考えられません。では他に方法はあるでしょうか。

次のように、単に専用の属性ライターメソッド(#url=)を書く方法ならどうでしょう。

# app/models/my_model.rb
class MyModel
  def url=(val)
    super(val.to_s.strip)
  end
end

この機能に対応するspecとして次が考えられます。

# spec/models/my_model_spec.rb
require "rails_helper"

RSpec.describe MyModel, type: :model do
  it "strips URL" do
    model = MyModel.new(url: "  http://rubyonrails.org")

    expect(model.url).to eq "http://rubyonrails.org"
  end
end

この実装とspecならどちらもずっとシンプルになりますし、ずっと自然です。データ整形はバリデーションと何の関係もないので、このようなユースケースを扱うためにバリデーションがらみのコールバックを使う必要はありません。

属性やリレーションシップを代入する

もうひとつのよくあるシナリオは、属性やリレーションシップの代入です。たとえば、contentを1つ持つコメントと、current_userになる著者(author)を作成し、かつパフォーマンス上の理由から何らかのdenormalization(非正規化)を行って、current_userが属するコメントにgroupを直接代入したいとします。以下はbefore_validationコールバックを少し使った例です。

Comment.create!(
  content: content,
  author: current_user,
)
# app/models/my_model.rb
class MyModel
  before_validation :assign_group

  private

  def assign_group
    self.group = author.group if author
  end
end

これも先のデータ整形のユースケースとかなり似ています。この機能のテストを書くためにvalid?を呼ぶ必要があるのでしょうか?バリデーションは、属性やリレーションシップの代入とは何の関係もないのですから、必要性はさほど感じられません。これは、次のようにもっとシンプルで明示的な方法で扱えます。

Comment.create!(
  content: content,
  author: current_user,
  group: current_user.group,
)

マジックを使わない単なる代入なので、テストも理解も簡単です。

まとめ

before_validationコールバックが考えうる限り最善の選択になることがもしかするとあるかもしれません(私はそのような状況に出会ったことがありませんが)。しかし私は、データ整形や属性/関連付けの代入は、before_validationコールバックに適していないとある程度確信しています。

関連記事

Railsの`Object#try`がダメな理由と効果的な代替手段(翻訳)

3年以上かけて培ったRails開発のコツ集大成(翻訳)

年末特集: TechRachoの2017年度人気記事リスト

$
0
0

こんにちは、hachi8833です。
TechRachoをお読みいただきありがとうございます。

2017年の年末特集として、2017年に公開した記事の中から人気の高かったものをリストアップしました。

2017年度の人気記事一覧(総合)

1位

Linux CUI初心者に早く知っておいて欲しいコマンド操作

BPS Webチームリーダーmorimorihogeさんが多忙な業務の合間を縫って書いた記事です。ここぞというときの爆発力はピカイチです。私も来年は超えるぞっと。

2位

米国から見た日本のRuby事情(翻訳)

RubyKaigi 2017の直前というタイミングも相まって2位にランクインしました。

記事中の「Rubyコアコントリビューターの多くが10分から15分もあればお互いに行き来できるほどの近所に固まって住んでおり」の場所について、この間初めて参加したRails勉強会@東京 第93回でこっそり尋ねてみたところ、私の予想とどんぴしゃりでした。

3位

3年以上かけて培ったRails開発のコツ集大成(翻訳)

特に「2. コントローラにload_resourceを書く」は社内外でも議論になりました。

4位

週刊Railsウォッチ(20170707)Railsの新機能ActiveStorage、高速Rubyフォーマッタrufo gemが超便利、Railscasts全コンテンツが無料公開ほか

ActiveStorageを報じた回がRailsウォッチのトップになりました。

5位

[インタビュー] Aaron Patterson(前編): GitHubとRails、日本語学習、バーベキュー(翻訳)

Aaron Patterson氏インタビュー前後編は、技術的なやりとりのレベルも高く読み応えのある内容でした。RubyKaigi 2017でご本人に直接お礼を申し上げたのもなつかしい思い出です。「ここは日本語で話しかけるべき」と逆の意味で緊張しました。

2017年度の人気記事一覧(はてなブックマーク)

総合にない記事から選びました。

1位

PostgreSQLの機能と便利技トップ10(2016年版)(翻訳)

PostgreSQL記事も多くの方にお読みいただきましたが、その中でも最多でした。今年はBPS社内も新規案件はたいていPostgreSQLでした。Railsウォッチで追いかけてきたRailsの改修もPostgreSQL寄りのものが多く、バージョン10のリリースも相まってPostgreSQL機運が高まっているのを感じました。

2位

Rails開発者のためのPostgreSQLの便利技(翻訳)

こちらもPostgreSQL記事です。ウォッチつっつき会でもPostgreSQLのexplainの使いやすさ・見やすさが何度となく話題になりました。

3位

RailsのCSRF保護を詳しく調べてみた(翻訳)

CSRFの設定そのものは簡単で、Railsガイドでもある程度解説されていますが、詳しいしくみについての関心の高さが伺えました。

2017年度の人気記事一覧(Twitter)

上に含まれない記事から選びました。Twitterの傾向ははてブとはまた違っています。

1位

Ruby: 「マジック」と呼ぶのをやめよう(翻訳)

オピニオン記事だけに反応は大きいものでした。
特に根拠はありませんが、おそらく聖書の影響で、英語圏での「magic」という言葉の奥底にはどこか禍々しいイメージが付きまとっている気がします。日本語だと「コックリさん」とか「犬神憑き」とか「恐山」のような。

2位

Goby: Rubyライクな言語(1)Gobyを動かしてみる

Matzにツイートいただいたおかげもあって、Rubyライクな言語であるGobyちゃんの記事がTwitterで予想外に伸びました。

今年は他にもMatzからTechRacho記事をツイートいただきました。ありがとうございます!

3位

TestProf: Ruby/Railsの遅いテストを診断するgem(翻訳)

CIで待たされてばかりだと開発のリズムが乱れてしまうので、テストの高速化は常に課題です。関心の高さが伺えました。

2017年度の人気記事一覧(Facebook)

上に含まれない記事から選びました。

1位

【漫画翻訳実績】吉田貴司さま「やれたかも委員会」を日本語から英語に翻訳しました(テキスト翻訳のみ)

Facebookの1位はげんきだまCEOのgenkiさんの記事でした。
Facebookの傾向ははてブともTwitterとも大きく違っているのが面白い点です。

2位

[Rails 5] rails newで常に使いたい厳選・定番gemリスト(2017年版)

「これだけは入れておきたいgem」は自分も欲しかったので書いた記事です。

3位

クリスマスだしcanvasで雪を降らせるJS書いてみた

BPSアドベントカレンダー2017でクリスマスイブの夕方に公開した記事です。多忙な中で素敵な記事を書いてくださったスギヤマさん、ありがとうございました!

2017年の終わりに

2016年8月より平日欠かさず記事を公開し続けてはや1年半近くが経ち、徐々にペースが掴めてきたように思います。
元々は翻訳する記事を見繕うつもりで始めた週刊Railsウォッチでしたが、ほどなく毎週公開前にBPS社内メンバーで記事をつっつく通称「つっつき会」に発展し、Railsコミットの読み方を実地で目にできる貴重な機会となっています。今ではappear.inを使って「つっつき会」を社内で共有するようになりました。

皆さまからのTechRacho記事へのご意見やご感想をお待ちしております。こういった皆さまからのフィードバック↓が何よりも励みになります。Twitterの@techrachoまたは@hachi8833、あるいは末尾のフォームまでどうぞ。

お読みいただいている皆さまに改めてお礼を申し上げます。来年もどうぞよろしくお願いいたします。
それでは良いお年をお迎えください!

フォーム


Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)

2005年にRailsのActiveRecordを初めて見たときに、稲妻のような天啓を感じたのを未だに覚えています。当時はPHPアプリで生SQLクエリを書いていましたが、それまで面倒で退屈でたまらなかったデータベースの扱いが、そのときを境に突如として簡単で楽しいものに変身したのです。そう、楽しくなったのです。

…やがて、ActiveRecordのパフォーマンスの問題に気づくようになりました。

ActiveRecordそのものが遅いわけではありませんでした。ちょうどその頃には実際に実行されるクエリに注意を払わなくなっていたのです。やがて、Rails CRUDアプリで用いられる最も定番のデータベースクエリの中に、データセットが巨大化したときのデフォルトのパフォーマンスがかなり見劣りするものがあることがわかってきました。

本記事では、パフォーマンスを損なう3つの主要な犯人について解説します。しかし最初に、データベースクエリが正常にスケールしているかどうかを調べる方法について説明しましょう。

パフォーマンスの測定

データセットが十分小さければ、どんなデータベースクエリでも十分パフォーマンスを発揮できます。したがって、本当のパフォーマンスを実感するには本番のサイズでデータベースのベンチマークを取る必要があります。ここでは22,000レコードを持つfaultsというテーブルを用いることにします。

データベースはPostgreSQLです。PostgreSQLでパフォーマンスを測定するには次のようにexplainを使います。

# explain (analyze) select * from faults where id = 1;
                                     QUERY PLAN
--------------------------------------------------------------------------------------------------
 Index Scan using faults_pkey on faults  (cost=0.29..8.30 rows=1 width=1855) (actual time=0.556..0.556 rows=0 loops=1)
   Index Cond: (id = 1)
 Total runtime: 0.626 ms

クエリ実行の見積もりコスト(cost=0.29..8.30 rows=1 width=1855)と、実際の実行にかかった時間(actual time=0.556..0.556 rows=0 loops=1)の両方が表示されます。

もう少し読みやすくしたいのであれば、次のようにYAML形式で出力することもできます。

# explain (analyze, format yaml) select * from faults where id = 1;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Index Scan"         +
     Scan Direction: "Forward"       +
     Index Name: "faults_pkey"       +
     Relation Name: "faults"         +
     Alias: "faults"                 +
     Startup Cost: 0.29              +
     Total Cost: 8.30                +
     Plan Rows: 1                    +
     Plan Width: 1855                +
     Actual Startup Time: 0.008      +
     Actual Total Time: 0.008        +
     Actual Rows: 0                  +
     Actual Loops: 1                 +
     Index Cond: "(id = 1)"          +
     Rows Removed by Index Recheck: 0+
   Triggers:                         +
   Total Runtime: 0.036
(1 row)

本記事では、「Plain Rows」と「Actual Rows」の2つだけに注目することにします。

  • Plan Rows: クエリに応答するときに、最悪DBがループを何行回すかという予測を示します
  • Actual Rows: クエリの実行時にDBが実際にループを何行回したかを示します

上のように「Plain Rows」が1の場合、このクエリは正常にスケールすると見込まれます。「Plain Rows」がデータベースの行数と等しい場合、クエリが「フルテーブルスキャン」を行っていることが示されます。この場合クエリはうまくスケールできないでしょう。

クエリパフォーマンスの測定方法の説明が終わりましたので、Railsのいくつかの定番コードでどんな問題が起きているかを見てみましょう。

犯人1: count

以下のコードはRailsビューで非常によく見かけます。

Total Faults <%= Fault.count %>

このコードから生成されるSQLは次のような感じになります。

select count(*) from faults;

explainで調べてみましょう。

# explain (analyze, format yaml) select count(*) from faults;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Aggregate"          +
     Strategy: "Plain"               +
     Startup Cost: 1840.31           +
     Total Cost: 1840.32             +
     Plan Rows: 1                    +
     Plan Width: 0                   +
     Actual Startup Time: 24.477     +
     Actual Total Time: 24.477       +
     Actual Rows: 1                  +
     Actual Loops: 1                 +
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Parent Relationship: "Outer"+
         Relation Name: "faults"     +
         Alias: "faults"             +
         Startup Cost: 0.00          +
         Total Cost: 1784.65         +
         Plan Rows: 22265            +
         Plan Width: 0               +
         Actual Startup Time: 0.311  +
         Actual Total Time: 22.839   +
         Actual Rows: 22265          +
         Actual Loops: 1             +
   Triggers:                         +
   Total Runtime: 24.555
(1 row)

これは…シンプルなcountクエリが22,265回もループしているではありませんか。これはテーブルの全行数です。PostgreSQLでは、countは常に全レコードセットをループします。

このレコードセットのサイズを減らすには、クエリにwhere条件を追加します。要件によっては、パフォーマンスが十分受け入れられる程度にサイズを減らすことができるでしょう。

この問題を回避する他の方法として、唯一、count値をキャッシュする方法があります。Railsにはそのための仕組みがあるので、以下のように設定できます。

belongs_to :project, :counter_cache => true

クエリが何らかのレコードを返すかどうかのチェックにも別の方法があります。Users.count > 0をやめて、代わりにUsers.exists?をお試しください。こちらの方がずっとパフォーマンスは上です(情報提供いただいたGerry Shawに感謝いたします)。

訳注: より高度なcounter_culture gemを使う方法もあります。

Rails向け高機能カウンタキャッシュ gem ‘counter_culture’ README(翻訳)

犯人2: ソート

indexページは、ほぼどんなアプリにも1つや2つあるでしょう。indexページでは、データベースから最新の20レコードを取り出して表示します。これをどうやってもっとシンプルにできるのでしょうか?

レコード読み出し部分はおおよそ以下のような感じになっていると思います。

@faults = Fault.order(created_at: :desc)

このときのSQLは次のような感じになります。

select * from faults order by created_at desc;

分析してみましょう。

# explain (analyze, format yaml) select * from faults order by created_at desc;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Sort"               +
     Startup Cost: 39162.46          +
     Total Cost: 39218.12            +
     Plan Rows: 22265                +
     Plan Width: 1855                +
     Actual Startup Time: 75.928     +
     Actual Total Time: 86.460       +
     Actual Rows: 22265              +
     Actual Loops: 1                 +
     Sort Key:                       +
       - "created_at"                +
     Sort Method: "external merge"   +
     Sort Space Used: 10752          +
     Sort Space Type: "Disk"         +
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Parent Relationship: "Outer"+
         Relation Name: "faults"     +
         Alias: "faults"             +
         Startup Cost: 0.00          +
         Total Cost: 1784.65         +
         Plan Rows: 22265            +
         Plan Width: 1855            +
         Actual Startup Time: 0.004  +
         Actual Total Time: 4.653    +
         Actual Rows: 22265          +
         Actual Loops: 1             +
   Triggers:                         +
   Total Runtime: 102.288
(1 row)

このクエリを実行するたびに、データベースが22,265行をソートしていることがわかります。これはあかんやつです。

SQLはデフォルトで、ORDER BY句のたびにレコードセットをその場でソートします。これにはキャッシュも効きませんし、うまいマジックもありません。

解決法は、インデックスを用いることです。この例のようにシンプルであれば、created_atカラムにソート済みインデックスを追加するだけでクエリはかなり高速になります。

Railsのマイグレーションに以下を追加します。

class AddIndexToFaultCreatedAt < ActiveRecord::Migration
  def change
    add_index(:faults, :created_at)
  end
end

このマイグレーションで、以下のSQLが実行されます。

CREATE INDEX index_faults_on_created_at ON faults USING btree (created_at);

末尾の(created_at)はソート順を指定しています。デフォルトは昇順です。

これでソートのクエリを再度実行してみると、ソートが行われなくなることがわかります。インデックスからソート済みのデータを読み出すだけで済むようになりました。

# explain (analyze, format yaml) select * from faults order by created_at desc;
                  QUERY PLAN
----------------------------------------------
 - Plan:                                     +
     Node Type: "Index Scan"                 +
     Scan Direction: "Backward"              +
     Index Name: "index_faults_on_created_at"+
     Relation Name: "faults"                 +
     Alias: "faults"                         +
     Startup Cost: 0.29                      +
     Total Cost: 5288.04                     +
     Plan Rows: 22265                        +
     Plan Width: 1855                        +
     Actual Startup Time: 0.023              +
     Actual Total Time: 8.778                +
     Actual Rows: 22265                      +
     Actual Loops: 1                         +
   Triggers:                                 +
   Total Runtime: 10.080
(1 row)

複数のカラムでソートする場合は、複数カラムでソートしたインデックスを作成する必要があります。Railsマイグレーションでは以下のような感じで記述します。

add_index(:faults, [:priority, :created_at], order: {priority: :asc, created_at: :desc)

より複雑なクエリに対処する場合は、explainで確認するとよいでしょう。なるべく早いうちに頻繁に行うのがポイントです。クエリによっては、わずかな変更をかけただけで、PostgreSQLでソートのインデックスが効かなくなることに気づくかもしれません。

犯人3: limitoffset

データベースの全項目をindexページに表示することはめったにありません。普通はページネーションを使って、一度に10件から30件、50件程度を表示します。このときに最もよく使われるのがlimitoffsetの組み合わせです。Railsでは次のような感じになります。

Fault.limit(10).offset(100)

このときのSQLは次のような感じになります。

select * from faults limit 10 offset 100;

ここでexplainを実行してみると奇妙なことに気づきます。スキャンされた行数は110件で、ちょうどlimitoffsetを足したのと同じです。

# explain (analyze, format yaml) select * from faults limit 10 offset 100;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Limit"              +
     ...
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Actual Rows: 110            +
         ...

ここでoffsetを10,000に増やしてみると、スキャンされた行数も一気に10010件に増加し、クエリの実行時間も64倍に増えます。

# explain (analyze, format yaml) select * from faults limit 10 offset 10000;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Limit"              +
     ...
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Actual Rows: 10010          +
         ...

ここから残念な結論が得られます。ページネーションでは後のページになるほど速度が低下します。上の例では1ページあたり100件を表示するので、100ページ目になると1ページ目より13倍も時間がかかってしまいます。

どうしたらよいでしょうか?

正直に申し上げると、これについて完全な解決法をまだ見つけられていません。私なら、ページ数が100ページや1000ページにならないよう、まずはデータセットのサイズを減らせないか検討するかもしれません。

レコードセットのサイズを減らすのが無理であれば、offsetlimitwhereに置き換える方法が一番有望でしょう。

# データの範囲を指定
Fault.where("created_at > ? and created_at < ?", 100.days.ago, 101.days.ago)

# またはidの範囲を指定
Fault.where("id > ? and id < ?", 100, 200)

まとめ

本記事が、PostgreSQLのexplain関数を利用してデータベースクエリに潜むパフォーマンスの問題を検出するのにお役に立てば幸いです。どんなシンプルなクエリであってもパフォーマンス上の大きな問題の原因となる可能性があるのですから、チェックする値打ちは十分あると思います :)

関連記事

Rails向け高機能カウンタキャッシュ gem ‘counter_culture’ README(翻訳)

[Rails] RubyistのためのPostgreSQL EXPLAINガイド(翻訳)

Rails開発者のためのPostgreSQLの便利技(翻訳)

Rails tips: RSpecの「スパイ(spy)」の解説(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: RSpecの「スパイ(spy)」の解説(翻訳)

RSpecの「スパイ(spy)」は、モックとスタブの組み合わせです。モックやスタブがよくわからない方は、前回記事「Rails tips: RSpecでシンプルなスタブを使う(翻訳)」をご覧ください。

spyは以下の3つの手順の流れにわけられます。

1. セットアップallowでクラスをスタブして、欲しいレスポンスを取得する。
2. エクササイズ: テストされたメソッドを実行する
3. 検証: expecthave_receivedを用いて、コードがexpectationを満たしているかどうかをテストする

サンプルのクラスで実装を見てみることにしましょう。

class UserService
  def initialize(user:, name:)
    @user = user
    @name = name
  end

  def save_name
    name_service = NameService.new(name: name)
    user.update_attribute(:name, name_service.get_name(format: :short))
  end

  private
  attr_reader :user, :name
end
require 'spec_helper'

describe UserService do
  describe "#save_name" do
    # セットアップ
    name_service = instance_double(NameService, get_name: double)
    user = instance_double(User, update_attribute: double)
    short_name = 'Nick'
    name = 'Nick Martin'
    allow(NameService).to receive(:new).with(name: name).and_return(name_service)
    allow(name_service).to receive(:get_name).with(format: :short).and_return(short_name)
    allow(user).to receive(:update_attribute).with(:name, short_name)

    # エクササイズ
    user_service = UserService.new(user: user, name: name)
    user_service.save_name

    # 検証
    expect(name_service).to have_received(:get_name).with(format: :short)
    expect(user).to receive(:update_attribute).with(:name, short_name)
  end
end

訳注: specにitブロックがありませんが、省略されているようです。

もちろんもっと速い書き方はありますが、特に複雑なテストケースではspyを使うと明快になるので、このテスト全体をいくつかのセクションに分割しました。

Railsでお困りの方にお知らせ

知りたいことがありましたら、twitter または連絡用フォームにてお気軽にお問い合わせください。

関連記事

Rails tips: RSpecでシンプルなスタブを使う(翻訳)

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

[Rails] RSpecのモックとスタブの使い方

週刊Railsウォッチ(20180316)Rails 5.2のドキュメント更新中、Value Objectの使い方、RubyがTIOBEトップテン復活、Rails「雪だるま」エンコーディングほか

$
0
0

こんにちは、hachi8833です。先週終点で車両の座席に置き忘れたiPhone 7が粉々になって戻ってきて風景がぐらりとかしいだ気がしましたが、補償が効いて本体交換できてケロッと立ち直りました。春ですねぇ。

春たけなわのウォッチ、いってみましょう。

Rails: 今週の改修

5.2はまだ出ていませんが、ドキュメント更新が増えていて、収束に近づいていることを感じさせます。今週も5.2-stableと6.0向けmasterの両方から見繕いました。いずれも変更の可能性がありますので。

Rails 5.1->5.2アップグレードドキュメント

まずは5.2-stableから。CSPとドキュメント周りの改修が目立ちます。

一応現時点のアップグレードドキュメントです↓。今のところ作業量は少なくて済みそう。

Rails 5.1からRails 5.2へのアップグレード
* Bootsnap

#29313でRails 5.2からBootsnap gemが含まれるようになりました。app:updateタスクはboot.rbで設定されます。使いたい場合はGemfileにこのgemを追加し、使わない場合はBootsnapを使わないようにboot.rbを変更してください。

  • cookie値に署名済みまたは暗号化cookieの有効期限が設定されるようになった

セキュリティ向上のため、署名済みまたは暗号化cookieの値に有効期限の情報が埋め込まれるようになりました。これによって、5.2より前のRailsとcookieバージョンの互換性が失われます。5.1以前のcookieが必要な場合や、5.2デプロイを検証中でロールバックの道を残しておきたい場合は、Rails.application.config.action_dispatch.use_authenticated_cookie_encryptionfalseに設定してください。

同コミットより大意


つっつきボイス:bootsnapはShopifyのgemがRailsで標準採用になったやつですね」「Shopfyはカナダのオタワですって」「運用中のサーバーでcookieの互換性が失われると、挙動としてはたとえば強制ログアウトが発生したりとか」「ソシャゲみたいにユーザーがめちゃ多いサービスでcookieが一斉に切れるとヤバイ: ユーザーが再ログインしようとして一気に押しかけて、ログインサーバーに負荷が集中してお亡くなりになったりとか」

その後ロードバランサーなどの話題になりました。

Railsエンジンを場所を変えてマウントできるようになった

# actionpack/lib/action_dispatch/routing/mapper.rb#L652
           def define_generate_prefix(app, name)
             _route = @set.named_routes.get name
             _routes = @set
-            app.routes.define_mounted_helper(name)
+
+            script_namer = ->(options) do
+              prefix_options = options.slice(*_route.segment_keys)
+              prefix_options[:relative_url_root] = "".freeze
+              # We must actually delete prefix segment keys to avoid passing them to next url_for.
+              _route.segment_keys.each { |k| options.delete(k) }
+              _routes.url_helpers.send("#{name}_path", prefix_options)
+            end
+
+            app.routes.define_mounted_helper(name, script_namer)
+
             app.routes.extend Module.new {
               def optimize_routes_generation?; false; end
+
               define_method :find_script_name do |options|
                 if options.key? :script_name
                   super(options)
                 else
-                  prefix_options = options.slice(*_route.segment_keys)
-                  prefix_options[:relative_url_root] = "".freeze
-                  # We must actually delete prefix segment keys to avoid passing them to next url_for.
-                  _route.segment_keys.each { |k| options.delete(k) }
-                  _routes.url_helpers.send("#{name}_path", prefix_options)
+                  script_namer.call(options)
                 end
               end
             }

これは実は昨年のコミットですが、#793c11dのコミットメッセージで目に止まったので。


つっつきボイス: 「同じマウンタブルエンジンを別名でマウントできるようになったと」「マウンタブルエンジン使うのって、Sidekiqの管理画面をマウントするときぐらいだけどなっ」「あとletter_opener導入するとエンジン入ってブラウザで見られますね」

CSPがWelcomeページやmailerプレビュー表示を邪魔しないよう修正

# railties/lib/rails/application_controller.rb#L7
+  before_action :disable_content_security_policy_nonce!
+
+  content_security_policy do |policy|
+    if policy
+      policy.script_src :unsafe_inline
+      policy.style_src :unsafe_inline
+    end
+  end
...
+    def disable_content_security_policy_nonce!
+      request.content_security_policy_nonce_generator = nil
+    end

つっつきボイス: 「こうやって追いかけているとわかりますが、最近のRailsではこういうCSP周りがちょくちょくアップデートされてますね」

CSPをコントローラからオフにできる機能を追加

# actionpack/lib/action_controller/metal/content_security_policy.rb#L16
     module ClassMethods
-      def content_security_policy(**options, &block)
+      def content_security_policy(enabled = true, **options, &block)
         before_action(options) do
           if block_given?
             policy = request.content_security_policy.clone
             yield policy
             request.content_security_policy = policy
           end
+
+          unless enabled
+            request.content_security_policy = nil
+          end
         end
       end

つっつきボイス: 「オンにできるならオフにできないとね」「たしかに」

CSPポリシーインスタンスを常にyieldするように変更

# actionpack/lib/action_controller/metal/content_security_policy.rb#L17
       def content_security_policy(enabled = true, **options, &block)
         before_action(options) do
           if block_given?
-            policy = request.content_security_policy.clone
+            policy = current_content_security_policy
             yield policy
             request.content_security_policy = policy
           end
...
+
+      def current_content_security_policy
+        request.content_security_policy.try(:clone) || ActionDispatch::ContentSecurityPolicy.new
+      end

つっつきボイス:cloneやめて常に同一のCSP設定を参照できるようにしたと: でないと挙動を追ったり変更したりできないですからね」

i18nドキュメント更新


  • 存在しなくなったGlobalize::Backend::Staticへの参照を削除
  • Google Groupsへの参照を削除
  • Globalize3への参照を削除(紛らわしいので)
  • 保存したコンテンツの翻訳方法についてのセクションを追加

本ガイドに記述されているI18n APIは、主にUI文字列の翻訳への利用を意図しています。モデルのコンテンツの翻訳手法をお探しの場合は、UI文字列とは別のソリューションが必要です。
モデルコンテンツの翻訳で役立ついろいろなgemがあります。

  • Globalize: 翻訳用の別テーブルに訳文を保存できます。1つのテーブルが1つの翻訳済みモデルになります。
  • Mobility: 訳文用テーブルやJSON columns(PostgreSQL)などさまざまな形式で訳文を保存できます。
  • Traco: Rails 3や4向けの翻訳可能なカラムを使えるようにします。カラムは元のテーブル自身に保存します。
    ガイド更新箇所より大意(強調はTechRacho編集部)

i18nのサードパーティgemがいくつか公式ガイドに載ったのが目に止まりました。


つっつきボイス: 「お、i18n gemを紹介してくれるようになるのか!」「公式が推してくれるのはうれしいっすね」「今のところGlobalizeがメジャーらしいです」「↑上にも書いてますがUI翻訳のyamlはRailsでサポートするけどコンテンツの方はRailsがサポートすることは今後もないだろうから、こういう形にしたのかも」
「モデルのi18nは自力で実装するとつらいよw: 昔やったけど当時はこういうgemなかったんで」「あ、例のMangaRebornですね」「たとえばサイトにデフォルト言語を設定したりとか、フォールバックする言語を指定したりとか必要になってくるので」「台湾語がなければ中国語、みたいな」
「Mobilityは例のshioyamaさんです↓: 名字のSalzbergをそのままもじってますね」

RubyのModule Builderパターン #1 モジュールはどのように使われてきたか(翻訳)

「こういうi18n gemは、コードを追うまではしないとしても、どういうインターフェイスを用意しているかという部分に注目して比較してみると結構勉強になりますよ: みんなそれぞれ個性があって」

ルーティングガイド更新

Railsのルーティング設定

アプリやエンジンのルーティングはconfig/routes.rbに保存されます。以下は典型的な外観です。

Rails.application.routes.draw do
  resources :brands, only: [:index, :show]
    resources :products, only: [:index, :show]
  end

  resource :basket, only: [:show, :update, :destroy]

  resolve("Basket") { route_for(:basket) }
end

これは普通のRubyソースファイルなので、Rubyのあらゆる機能を用いてルーティングを定義できますが、変数名がルーターのDSLと衝突しないようにご注意ください。
メモ: ルーティング定義を囲むRails.application.routes.draw do ... endブロックは、ルーターのDSLがスコープを確立するために必要なので絶対に削除しないでください。
ガイド更新箇所より大意(強調はTechRacho編集部)


つっつきボイス: 「Railsのルーティングの包括的というか完全なドキュメントが欲しいっすねマジで: 機能はやたらめったらあるけど、知らないと使いようのない機能の多さではRails内ではトップかも」
「今頃変数名にはご注意…だと?」「asとか使うとヘルパーが自動生成されたりとかゴロゴロありますからねー」「まRailsに慣れてくると『この語はキケン』みたいなのをだんだん身体で思い知るけど」「(´・ω・`)」

Railsのルーティングを極める(前編)

「そうそう、sheepみたいに単数形複数形が同じ語を使うと、生成されるヘルパー名が通常と違ってくることあります」「え~~!」

resources :penguins

# 通常は以下が生成される
penguin_path(@penguin)
penguins_path
resources :sheep

# 
sheep_path(@sheep)
sheep_index_path   # 区別のため「_index」が付く

「ActiveSupportにそういう活用形をチェックするメソッドがある↓」「それは知ってたけど…くぅ」「活用形といえばdataは複数形で、単数形はdatum: みんなもう知ってるよね!」

[Rails5] Active Support::Inflectorの便利な活用形メソッド群

ラテン語由来の英単語はたいてい不規則活用になりますね。symposionとsymposiumとか。ちょっと話はそれますが、indexの複数形はindicesが正式とされていますが、近年急速にすたれつつある印象です。

ActiveSupport::Cache::Entryをメモ化してマーシャリングの負荷を軽減

これは5.2-stableとmasterの両方に入っていました。ここからはmasterです。

# activesupport/lib/active_support/cache.rb#L806
+        def marshaled_value
+          @marshaled_value ||= Marshal.dump(@value)
+        end

メモ化といえば、おなじみ「縦縦イコール」ですね。


つっつきボイス: 「kazzさんが以前『たてたてイコール』って呼んでたのが可愛かったのでw」「本当は何て言うんだっけ?」「『オアイコール』?」
「ちなみに以前も話したことあるけど、Marshal.dumpはRubyのバージョンが変わると互換性が失われることがあるので、データベースにそのまま保存すると後で痛い目に遭うかもよ」「怖!」

起動メッセージの無意味な「Exiting」を除去

# railties/lib/rails/commands/server/server_command.rb#L158
           if server.serveable?
             print_boot_information(server.server, server.served_url)
-            server.start do
-              say "Exiting" unless options[:daemon]
-            end
+            after_stop_callback = -> { say "Exiting" unless options[:daemon] }
+            server.start(after_stop_callback)
           else
             say rack_server_suggestion(using)
           end

rails routes --expandedの横線をきれいにした

$ rails routes --expanded
--[ Route 1 ]------------------------------------------------------------
-------
(snip)
--[ Route 42 ]-----------------------------------------------------------
--------
(snip)
--[ Route 333 ]----------------------------------------------------------
---------
(snip)
$ rails routes --expanded
--[ Route 1 ]------------------------------------------------------------
(snip)
--[ Route 42 ]-----------------------------------------------------------
(snip)
--[ Route 333 ]----------------------------------------------------------
(snip)

つっつきボイス: 「前は横棒固定か」「IO.console.winsizeって初めて知った: これならターミナルに合わせて調整できるし」「地味だけどありがたい修正!」

+        previous_console_winsize = IO.console.winsize
+        IO.console.winsize = [0, 23]

参考: Rubyリファレンスマニュアル IO.console

rails routes -gで結果が空の場合のメッセージを修正

  • ActionDispatch::Routingのドキュメント更新
    • -gの記述を追加
    • rails routes--expandedオプションの説明を追加
  • ActionDispatch::Routing::ConsoleFormatter::Baseの導入
    • Baseを作ってSheetExpandedで継承し、コード重複を防止
      • Expandedのコンポーネントで末尾の”\n”を削除
      • Expanded#headerの戻り値を@bufferからnilに変更
    • -gのときのno_routesメッセージがよくなかったので修正
      • -cの場合のメッセージは「Display No routes were found for this controller」
      • -gの場合のメッセージは「No routes were found for this grep pattern」

PRメッセージより大意

# actionpack/lib/action_dispatch/routing.rb#L85
         def normalize_filter(filter)
-          if filter.is_a?(Hash) && filter[:controller]
+          if filter[:controller]
             { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ }
-          elsif filter
-            { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ }
+          elsif filter[:grep_pattern]
+            {
+              controller: /#{filter[:grep_pattern]}/,
+              action: /#{filter[:grep_pattern]}/,
+              verb: /#{filter[:grep_pattern]}/,
+              name: /#{filter[:grep_pattern]}/,
+              path: /#{filter[:grep_pattern]}/
+            }
           end
         end

つっつきボイス: 「最近自分はブラウザで/rails/info/routesで見ちゃうこと多いかなー」「このパスが割りと覚えにくいという」「/aとか打ってルーティングエラー出す方が早いっすね」「たしかに」

Rails

Railsビューをin_groups_ofでリファクタリング(RubyFlowより)

// 同記事より
%table.sponsors{width: "100%;"}
  - sponsors_by_level.levels.each do |level|
    - level.sponsors.in_groups_of(level.sponsors_per_line, false) do |group|
      %tr
        - group.each do |sponsor|
          %td{colspan: 12 / group.size, style: "text-align: center !important;"}
            = link_to sponsor.path do
              = image_tag(sponsor.logo_url, alt: sponsor.name, title: sponsor.name, style: "display: inline; float: none;")
    %tr
      %td{colspan: 12}
        %hr

つっつきボイス: 「ほっほー、in_groups_ofとな」「内部でeach_slice使ってるからこれを直接使う方が早かったかも、だそうです」「改修前のhaml、見たくないやつ…」

参考: in_groups_of
参考: Rubyリファレンス・マニュアル each_slice

RailsのシステムテストでJSエラーをキャッチする方法(RubyFlowより)

WARN: javascript warning
http://127.0.0.1:60979/assets/application.js 9457 Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user’s experience.
Got 3 failures and 1 other error from failure aggregation block “javascript errrors”:
1) http://127.0.0.1:60481/sso 10:18 Uncaught SyntaxError: Unexpected token ;
同記事より


つっつきボイス: 「テーブル使うのはどうかと思うけどまあそれはおいといて」「どうやらきれいにキャッチする方法がないからコンソールに出力してそっちで見れ、ってことみたい」「たしかに原理的に難しそう」

Railsのメール設定でdeliverdeliver_nowは使うな(Hacklinesより)

deliver_laterにしとけ、だそうです。

# 同記事より
class User
  after_update :send_email
  def send_email
    ReportMailer.update_mail(id).deliver_later
  end
end

つっつきボイス:deliver_nowは同期的なのか: じゃあ使いたくないやつですね」「deliver_laterで非同期になると、それはそれでテストで考慮しないといけない点が増えて大変になるけど: キューに入ったりメールサーバーが応答したりしてもそれだけでよしとできないとか」「キューに入ってコケたかどうか、とか」

参考: Rails API deliver_later

RailsでReduxのフォームを使うには(Awesome Rubyより)


redux-form.comより

# 同記事より
    def create
        authorize resource_plan, :create?
        command = GlobalContainer['plan.services.create_plan_command'] #Plan::CreatePlan.new
        respond_to do |format|
          format.json {
            command.call(resource_plan, params[:plan]) do |m|
              m.success do |plan|
                flash[:notice] = t('messages.created', resource_name: Plan.model_name.human)
                render json: { id: plan.id}, status: :ok, location: settings_plan_path(plan)
              end
              m.failure do |form|
                render json: {
                  status: :failure,
                  payload: { errors: form.react_errors_hash }
                }, status: 422
              end
            end
          }
        end
      end

Redux-Formでうまく書けたそうです。Redux-Formは完成途上らしいので、末尾でFinal Formというフレームワーク非依存JSフォームライブラリも紹介しています。


github.com/final-form/final-formより


つっつきボイス: 「コードの中でRepresenterというのを置いてますね」

HABTMをhas_many throughに置き換える(RubyFlowより)

# 同記事より
class PostTag < ApplicationRecord
  belongs_to :post
  belongs_to :tag
end
class Post < ApplicationRecord
  has_many :post_tags, -> { order(rank: :asc) }
  has_many :tags, through: :post_tags
end
class Tag < ApplicationRecord
  has_many :post_tags
  has_many :posts, through: :post_tags
end

つっつきボイス: 「絵に描いたようなHABTMリファクタリングですが、基本ということで」

参考: 仕事のねた: rails3でHABTMが非推奨になってる

Railsで巨大データをdedupする(Hacklinesより)

# 同記事より
# log.rb
class Log < ActiveRecord::Base
  has_many :user_logs

  def store(data)
    key = Digest::MD5.hexdigest(data)
    log = Log.find_by_checksum(key)
    if log.nil?
      log = Log.new(data: data, checksum: key)
      Log.transaction(requires_new: true) do
        begin
          log.save!
        rescue ActiveRecord::RecordNotUnique => e
          raise ActiveRecord::Rollback
        end
      end
    end
    log
  end
end

ActiveRecordリレーションをyield_selfでコンポジション可能にする(Hacklinesより)

# 同記事より
def call
  base_relation.
    joins(:care_periods).
    yield_self(&method(:care_provider_clause)).
    yield_self(&method(:hospital_clause)).
    yield_self(&method(:discharge_period_clause))
end

private

def care_provider_clause(relation)
  if params.care_provider_id.present?
    relation.where(care_periods: { care_provider_id: params.care_provider_id })
  else
    relation
  end
end
...

つっつきボイス:yield_selfってどっかで見たゾ」「あーこれだ↓」「そうそう、tapしないで書けるというのはちょっといいかも」「tapだとビックリマーク付きのwhere!になりますね: 破壊的にならないからいいだろ?っていう趣旨なのかな」「別に破壊的でもいい気はするけど」「メモリ効率とかの話を別にすれば、イミュータブルな方が望ましくはあるし、where!は基本使いたくはないので気持ちはわかる」

Ruby 2.5の`yield_self`が想像以上に何だかスゴい件について(翻訳)

RailsアプリをHerokuからAWSに移して年8万ドル以上節約した件について(Awesome Rubyより)


つっつきボイス: 「1000万近くって以前どれだけザルだったのかとw」「ちゃんと読んでないけど、HerokuとAWSの違いというより設定が大きかったんじゃ?」

search_flip: ElasticSearchクエリをチェインするクライアント(RubyFlowより)

★はまだ少ないです。


つっつきボイス: 「類似のgemがあるんではないかと思って」「ははあ、searchkickと違ってハッシュ使わずに書けるぞ↓ドヤアってことかな」

# 同リポジトリより
# elasticsearch-ruby
Comment.search(
  query: {
    query_string: {
      query: "hello world",
      default_operator: "AND"
    }
  }
)

# searchkick
Comment.search("hello world",
               where: { available: true },
               order: { id: "desc" },
               aggs: [:username])

# search_flip
CommentIndex.where(available: true)
            .search("hello world")
            .sort(id: "desc")
            .aggregate(:username)

RabbitMQはSidekiqの単なる置き換え以上のものだ(RubyFlowより)


つっつきボイス: 「熱烈にRabbitMQ推してますね: 永続性の保証とかで違ってくるみたい」「Sidekiqだって用途に合ってればとってもいいヨって最後に書いてますね」


rabbitmq.comより


sidekiq.orgより

Railsデプロイ前にこれだけはチェックしたい5項目(RubyFlowより)


  • public/404.htmlとか設定したか
  • HTTPSにしたか
  • URLでデータベース内容がお漏らししないようにしたか
  • 監視設定やったか
  • デプロイを自動化したか
# 同記事より
class User < ApplicationRecord
  has_secure_token :uuid # DBでUNIQUE indexにしておけばベスト

  def to_param
    self.uuid
  end
end

つっつきボイス: 「年バレネタですが『ウルトラ5つの誓い』を思い出しちゃって」

参考: ウルトラ5つの誓いとは (ウルトライツツノチカイとは) [単語記事] - ニコニコ大百科

RubyのValue Objectはこう使おう(RubyFlowより)

# 同リポジトリより
# Good
BigDecimal('100').to_i     # => 数値を変えずに精度だけ下げる
# Bad
Quantity.new(10, 'm').to_i # => コンテキストが失われる: Quantity#amountとする方がずっといい

# Acceptable
Dates::Period.to_activercord # => コンテキストによってはあり
# Questionable
Dates::Period.to_regexp      # => #regexpでいいんじゃね?

つっつきボイス: 「けっこうがっつり書いてあってよさそうです」「なぜGitHubリポジトリなのかはおいといて」「あのzverokさんだ↓」

Ruby: 「マジック」と呼ぶのをやめよう(翻訳)

QuickType.io: JSONを貼るとRubyやSwiftやJSのコードに変換するサイト(RubyFlowより)


同サイトより

Rubyの場合、例のdry-rbを使ってくれます。

  • 変換元


同サイトより

  • 変換先


同サイトより


つっつきボイス: 「お、これ便利かも」「Pythonが入ってないのが何となく男らしい」「逆変換もできたらいいな♡」

悪いのはRailsじゃない、Active Recordだっ(Hacklinesより)

# 同記事より
data = [
  { name: "Owner", email: "owner@example.com" },
  { name: "Employee", email: "employee@example.com" },
  ...
]

# Raw SQL
INSERT INTO users (name, email) 
VALUES ("Owner", "owner@example.com"), ("Employee", "employee@example.com")

# Sequel
  db[:users].multi_insert(data)

# ActiveRecord by #import
  User.import(data.first.keys, data.map(&:values))

# Arel
  table = Table.new(:users)
  manager = Arel::InsertManager.new
  manger.into(table)

  manager.columns = [table[:name], table[:email]]
  manager.values = manager.create_values_list(data.map(&values))
  • 結局SQL構文知らないと使えない
  • RubyによるSQL構文チェックがない
  • オブジェクト指向じゃない
  • メンテがつらい
  • モデルにビジネスロジックだのエンティティ構造だの追加アクションだのしょぼいロジック定義が山盛りになる

つっつきボイス: 「PV狙いのタイトルっぽい」「悪いとしたらビュー周りかなと思った」「この人『Arelは悪くない、Sequelいいヤツ』って言ってますけど、Arelについてはちょっとどうかなー」「うーむ」「私は最終的には生SQLが最強だとこっそり信じてますけど」「生SQLは覇者」「ORMでやってても結局SQLチェックしますしね」「文中のRecursive Common Table Expressionって何だったかな(↓)」

参考: COMMON_TABLE_EXPRESSION (TRANSACT-SQL) | Microsoft Docs

私がRailsよりHanamiが好きな理由(Ruby Weeklyより)


hanamirb.orgより

  • Repositoryパターンなところ
  • アクションがクラスであるところ
  • ビュークラスがあるところ

著者はあのRyan Biggさんです↓。

Railsの`CurrentAttributes`は有害である(翻訳)

SOLIDの原則その2: オープン/クローズの原則

# 同記事より
class UserCreateService
  def initialize(params, validator: UserValidator)
    @params = params
    @validator = validator
  end

  def call
    return false unless validator.new(params).validate
    process_user_data
  end

  attr_reader :params, :validator

  def process_user_data
    ...
  end
end

RubyMineで速攻殺している自動チェック項目3つ(Hacklinesより)

とても短い記事です。


つっつきボイス: 「Cucumberとかスペルチェックはともかく、”Double quoted string”って式展開がない場合はシングルクォートにしろっていうアレですかね?」「RuboCopちゃんに怒られるから基本シングルクォートにする癖がついてる」「実はオフにする方法を知らないけどなっ: 玉突きで変更しないといけないし」

Rubyスタイルガイドを読む: 数値、文字列、日時(日付・時刻・時間)

海外のRuby/Railsカンファレンス

Ruby/Rails関連おすすめ情報源(RubyFlowより)

書籍やサイトやチュートリアルがずらっと並んでいます。

あるRails開発会社の会社概要(RubyFlowより)

開発ツールや進め方が割りと事細かに書かれています。全部字ばっかりなのが逆に珍しいかも。

ビューやヘルパーは別世界か


つっつきボイス: 「ちょうど最近この辺の話をよくしてたので」「そうそう、#lとか#tみたいにビュー全体で使うようなのをヘルパーに置くのはまだわかるんだけど、『これ汚いからビューから逃したい』みたいなのをグローバルなヘルパーに置くのはどうかな~っていつも思ってる」「さすがヘルパー嫌いマン」「Railsのヘルパーは、WordPressで言うfunctions.phpみたいなものって説明してもらって腑に落ちたことあります」
「a_matsudaさんといえばactive_decoratorの作者ですよね: モデル名と紐付いているデコレータのモジュールをビューに行くまでにこっそりインクルードするみたいな: まさにそういう話ですよね↑」「俺それ正解だと思うよマジで」

「ヘルパーに置いてグローバルになるくらいなら、いっそコントローラに書いちゃいますね: helper_methodっていうメソッド↓があって、これを使うとコントローラにヘルパーを書けちゃうんですよ」「へー!」「知らなかった」「しょっちゅうは使わないけど、ここぞというときに控えめに使う感じで: 内部の挙動はまだよく知らないし本当にいいものかどうかはちょっと微妙なんですが」

参考: Rails API helper_method

class ApplicationController < ActionController::Base
  helper_method :current_user, :logged_in?

  def current_user
    @current_user ||= User.find_by(id: session[:user])
  end

  def logged_in?
    current_user != nil
  end
end

Railsの「雪だるまエンコーディング」問題が修正☃️

先ほど流れてきたので。

その他小粒記事

Ruby trunkより

特殊変数を排除してPathnameを高速化(継続)

Regexp#=~だと$&などの特殊変数を更新する分オーバーヘッドが生じるので、更新しないRegexp#match?に置き換えたとのことです。ベンチマークの書式がびしっと整ってます。

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


つっつきボイス: 「ちょっと話それるけど、名前がFileなのにパスっぽいものも渡されたりするとどうかと思うことがある」「それは確かによくないかも」

提案: キーがない場合にraiseする#dig!(継続)

hash = {
    :name => {
        :first => "Ariel",
        :last => "Caplan"
    }
}

hash.dig!(:name, :first) # => Ariel
hash.dig!(:name, :middle) # => nil   ●これをraiseしたい
hash.dig!(:name, :first, :foo) # raises TypeError (String does not have #dig method)

「キーワード引数でできるのでは?」「deep_fetch gemでできる」という回答です。


つっつきボイス: 「ビックリマーク付きの#dig!か」「!はこの場合いいんだろうか?」

Ruby:「プリマドンナメソッド」の臭いの警告を私が受け入れるまで(翻訳)

提案: begin(またはdo)-elseendrescueが抜けてたらsyntax errorにしたい(受理)

begin
  p :foo
else
  p :bar
end

# => :foo
# => :bar

joker10071002さんです。特にdoで始まるときにrescueを置き忘れやすいので、syntax errorにしたいとのことです。


つっつきボイス: 「自分的にはwarningのままの方がいいかなーという気がするけど」「そういえばエラー処理のelseensureってどう違うんでしたっけ?」「elseは上のどれでもなかった場合で、eusureは結果にかかわらず必ず実行するやつだったかと」

なおその後acceptされました。

参考: Rubyリファレンスマニュアル begin

Ruby

RubyにもGolangのdeferが欲しいので作ってみた話


つっつきボイス: 「ちょうど上の話にも通じてる」「Goのdeferはブロックの外にも置けてRubyのensureより強力な印象ですね: まだ使ったことないけど」「JavaScriptのPromiseもDeferredって呼ばれてた」

参考: Golang の defer 文と panic/recover 機構について - CUBE SUGAR CONTAINER
参考: 非同期処理とPromise(Deferred)を背景から理解しよう - hifive

licensed: GitHub自ら提供する依存関係のライセンス照合/キャッシュgem(Ruby Weeklyより)

# 同リポジトリより
$ bundle exec licensed status
Checking licenses for 3 dependencies

Warnings:

.licenses/rubygem/bundler.txt:
  - license needs reviewed: mit.

.licenses/rubygem/licensee.txt:
  - cached license data missing

.licenses/bower/jquery.txt:
  - license needs reviewed: mit.
  - cached license data out of date

3 dependencies checked, 3 warnings found.

まだ1か月経ってない新しいgemです。類似のgemを取り上げたことがありました。

rakeタスクをきれいに書くコツ(Hacklinesより)

DHHのYouTubeチャンネルでヒントを得たそうです。

asciidoctor: AsciiDoc形式テキストプロセッサのRuby版(Awesome Rubyより)

= Hello, AsciiDoc!
Doc Writer <doc@example.com>

An introduction to http://asciidoc.org[AsciiDoc].

== First Section

* item 1
* item 2

[source,ruby]
puts "Hello, World!"

AsciiDocはこんな感じ↑で書けるようです。単なるMarkdownの置き換えではないと言ってます。

参考: What is AsciiDoc? Why do we need it? | Asciidoctor
参考: 脱Word、脱Markdown、asciidocでドキュメント作成する際のアレコレ

ヒアドキュメントで式展開#{}を展開させない方法(Hacklinesより)

# 同記事より
venue = "world"
str = <<-'EOF'
I'm the master of the #{venue} !
No you're dead bro..\n
EOF

2018年のRuby GUI開発事情(RubyFlowより)


saveriomiroddi.github.ioより

なお著者はGobyのcontributorであることを今思い出しました。

RubyのリゾルバでSSRFフィルタをバイパスされる脆弱性(Hacklinesより)

# 同記事より
irb(main):008:0> Resolv.getaddresses("127.0.0.1")
=> ["127.0.0.1"]
irb(main):009:0> Resolv.getaddresses("localhost")
=> ["127.0.0.1"]
irb(main):010:0> Resolv.getaddresses("127.000.000.1")
=> [] # 😱

参考: Rubyリファレンスマニュアル Resolv

SCSSコンパイラを自力で書いてみたお(RubyFlowより)


同記事より

RubyがTIOBEのトップ10言語に返り咲く(Hacklinesより)


tiobe.comより


つっつきボイス: 「記事のグラフ見ると、むしろC言語がいったん下がってからガッと上がっているのが気になる」「JavaとC以外はまだ混戦かなー」

Aaron Pattersonさんから

#kind_ofが悪手になる場合

どこかで「#is_a?は今は非推奨」とmatzがツイートしていた気がしましたが、そのエイリアスである#kind_of?もどうやらあまり使って欲しくない様子です。


つっつきボイス: 「Matzが#kind_of?警察やってる」「#is_a?が非推奨になったのは、確か名前がよくなかったからだったような」「『純粋なオブジェクト指向ならメッセージベースでやろうぜ』『クラスなんてものはオブジェクト間の通信には本来不要である』という趣旨なんでしょうね」
「質問者の方もそうだけど、Javaから来ると型チェックしたくなる気持ちはわかる」「Javaにはインターフェイスがあるから」「Rubyにはrespond_to?がある」

私もつい型チェック的思考に傾きかけてたかも。反省。

参考: RubyリファレンスマニュアルObject#respond_to?


「ところでRailsには?なしのrespondo_toというのがあってですね」「紛らわし!」「Railsを先にやると、Rubyのrespond_to?の方でむしろ首を傾げたりとか」「RSpecのrespond_toマッチャーも同じ過ぎるし」「名前がこれだけ似てて意味がまるで違うという」

参考: Rails API respond_to

Ruby生誕25周年記念: コミットのビジュアル表示

SQL

データベースのモデル化アンチパターン3種(Postgres Weeklyより)


  1. Entity Attribute Values
  2. Multiple Values per Column
  3. UUID

つっつきボイス: 「EAVはSQLアンチパターンにも載っている定番中の定番っすね: 一度はやりたくなってしまうやつ」「略語になってるんですね」
「むかーしエンタープライズ系のJavaの本で『EAVはベストプラクティスのひとつである』みたいな記述があったんですが」「マジかーw」「いや、たぶんこれはメモリ構造に乗せてEAVする分にはよかったはず: RDBMSでやるもんじゃないですよもちろん」「後で検索が必要になったときに死ねるやつ」
「2.は今のRDBMSなら普通にできたりしますね: PostgreSQLのArrayとか」

PostgreSQLの全文検索でVACUUMを使うときにやるべきこと(Postgres Weeklyより)


つっつきボイス: 「出たーVACUUM ANALYZE: めちゃめちゃ重い」

PostgreSQLの新機能「シーケンス」のメリットと落とし穴(Postgres Weeklyより)

動画: データベースの隠し技紹介(Postgres Weeklyより)

オーストラリアでこの3月に行われたRubyカンファレンスであるRubyConf Auでの発表です。

JavaScript

prettier: JS界のRuboCop


同リポジトリよりより

★めちゃ多いです。

JavaScriptのエレガントな「ROROパターン」(Frontend Weeklyより)

割りと長い記事です。ROROは「Receive an object, return an object」だそうです。RubyのPOROとは違いました。

スーパー速い「Radi.js」フレームワークを作ったお(JavaScript Weeklyより)

Virtual DOMを使わないことで速くしたそうです。

Glimmer.jsとPreact.jsのパフォーマンス比較(JSer.infoより)

Linkedinの技術ブログです。

CSS/HTML/フロントエンド

Tumult Hype: フロントのアニメーション表示を徹底制御(Hacklinesより)

<a https://tumult.com/hype/”>
同記事より

HoudiniプロジェクトのCSS Paint API(Frontend Focusより)

Chrome 65以降でないと動かないので、brew cuで速攻Chromeをアップグレードしました。以下で「Fail」が出るブラウザではできないそうです。

See the Pen CSS Paint API Detection by Will Boyd (@lonekorean) on CodePen.

動画: Chromeの新機能「Local Overrides」でパフォーマンス上の仮説をテストする(Frontend Focusより)

「King’s Pawn Game」に学ぶUIデザイン(Frontend Weeklyより)


同記事より

UIデザイナー向けの記事です。King’s Pawn Gameは、チェスの序盤の定石のようです。

参考: Wikipedia-en King’s Pawn Game

CSSで四隅を切り欠くには(Frontend Focusより)

See the Pen Notched Boxes by Chris Coyier (@chriscoyier) on CodePen.

World Wide Webが29歳の誕生日(Frontend Focusより)

その他

Stackoverflowのアンケート結果

かなり長いです。

MacのiTerm2で出力を任意のエディタに送り込む(Hacklinesより)

AppleScriptの小ネタです。

若手開発者サバイバルガイド: コードが動かないときにうまく先輩に伝えるには(Hacklinesより)


つっつきボイス: 「これも翻訳打診してみますね」

HomebrewとPythonバージョンの混乱

Mathpix snipping tool: 数式を撮影するとLaTeXに変換するスマホアプリ

よく見たら昨年からiPhoneにインストールしてました。どっちかというとソルバーです。

番外

「いいこと聞いた」と思うかどうかが分かれ目?

ソイレントといえば

私の年だとソイレント・グリーンですが、ゼノギアスの方が有名っぽいですね。

たけのこ

巨星墜つ

モンティ・パイソンに出演したホーキング博士も素敵でした。


今週は以上です。

バックナンバー(2018年度)

週刊Railsウォッチ(20180309)RubyGems.orgのTLS 1.0/1.1接続非推奨、2年に1度のRailsアンケート、DockerのMoby Project、Ruby拡張をRustで書けるruruほか

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

Awesome Ruby

RubyFlow

160928_1638_XvIP4h

Hacklines

Hacklines

Postgres Weekly

postgres_weekly_banner

Frontend Weekly

frontendweekly_banner_captured

Frontend Focus

frontendfocus_banner_captured

JavaScript Weekly

javascriptweekly_logo_captured

JSer.info

jser.info_logo_captured

Railsのフラグメントキャッシュを分解調査する(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

なお、本記事はRubyWeekly #405でも紹介されています。

Railsのフラグメントキャッシュを分解調査する(翻訳)

本記事は「Rails分解調査」シリーズの第一弾です。このシリーズのねらいは、Railsのコンポーネントを使うときにそれぞれの機能(今回のフラグメントキャッシュなど)が互いにどう絡み合うかという点について一般的な概念を示すことです。何ぶんこの種の記事を書くのは初めてですので、何かお気づきの点がありましたらぜひ元記事までコメントをどうぞ。


Railsのフラグメントキャッシュはほとんどの方がご存知か使ったことがあると思いますが、そのしくみについてはあまり知られていないのではないでしょうか。そこで本記事では、Railsの背後のフラグメントキャッシュのしくみについて簡単にご紹介したいと思います。

まず、フラグメントキャッシュの実行に関連する以下のコンポーネント(クラスやモジュール)を見ていきましょう。

もちろんここでは一般的な概念を示すにとどめますので、すべてのクラスやモジュールまではリストアップしていません。

ユーザーインターフェイス

フラグメントキャッシュの使い方はいたってシンプルです。次のようにcacheを呼ぶだけで完了します。

<% cache project do %> 
  <b>All the topics on this project</b>
  <%= render project.topics %>
<% end %>

このcacheメソッドがあるのはActionView::Helpers::CacheHelperというビューヘルパーです。

# File actionview/lib/action_view/helpers/cache_helper.rb, line 165
module ActionView
  module Helpers 
    module CacheHelper
      def cache(name = {}, options = {}, &block)
        if controller.respond_to?(:perform_caching) && controller.perform_caching
          name_options = options.slice(:skip_digest, :virtual_path)
          safe_concat(fragment_for(cache_fragment_name(name, name_options), options, &block))
        else
          yield
        end

        nil
      end
    end
  end
end

ここでは主に次の2つを行っています。

  • cache_fragment_nameでキャッシュのキーを生成する
  • fragment_forでこのブロックをキャッシュする

fragment_forの動作を見てみましょう(訳注: これはprivateメソッドです)。

# actionview/lib/action_view/helpers/cache_helper.rb
def fragment_for(name = {}, options = nil, &block)
  if content = read_fragment_for(name, options)
    ......
    content
  else
    ......
    write_fragment_for(name, options, &block)
  end
end

この動作も非常にシンプルで、キャッシュが存在する場合は内容を読み出し、なければコンテンツを書き込んでいるだけです。キャッシュの内容の読み書き以外に、以下のようにテンプレートのレンダラーにキャッシュのヒットの有無も通知します。

# actionview/lib/action_view/helpers/cache_helper.rb
def fragment_for(name = {}, options = nil, &block)
  if content = read_fragment_for(name, options)
    @view_renderer.cache_hits[@virtual_path] = :hit if defined?(@view_renderer)
    ......
  else
    @view_renderer.cache_hits[@virtual_path] = :miss if defined?(@view_renderer)
  ......
  end
end

Railsはキャッシュヒットの有無をこのようにして認識します。そしてfragment_forメソッドはキャッシュの内容の読み書きのためにread_fragment_forwrite_fragment_forを使っています(訳注: これらもprivateメソッドです)。

# actionview/lib/action_view/helpers/cache_helper.rb
def read_fragment_for(name, options)
  controller.read_fragment(name, options)
end

def write_fragment_for(name, options)
  pos = output_buffer.length
  yield
  ......
  fragment = output_buffer.slice!(pos..-1)
  ......
  controller.write_fragment(name, fragment, options)
end

ご覧のように、最終的にcontrollerという名前の変数に対してread_fragmentwrite_fragmentが呼び出されています。ご推察のとおり、このcontrollerとは現在のリクエストを処理しているRailsのコントローラなのです。つまりここから主体がビューからコントローラに移ることになります。controller.write_fragmentのソースを追ってみると、AbstractController::Caching::Fragmentsにあるのがわかります。

メインのロジック:  AbstractController::Caching::Fragments

このモジュールが提供する本質的なキャッシュ操作はread_fragmentwrite_fragmentexpire_fragmentの3つです。

  # actionpack/lib/abstract_controller/caching/fragments.rb
  # (instrumentation関連のコードは省略)
  def write_fragment(key, content, options = nil)

    ......

    content = content.to_str
    cache_store.write(key, content, options) # <-ここでキャッシュの内容をキャッシュストアに書き込む
    content
  end

  def read_fragment(key, options = nil)
    ......
    result = cache_store.read(key, options) # <- ここでキャッシュの内容をキャッシュストアから読み出す
    result.respond_to?(:html_safe) ? result.html_safe : result
  end

  def expire_fragment(key, options = nil)
    ......
    if key.is_a?(Regexp)
      cache_store.delete_matched(key, options) # <- ここでキャッシュストアから複数のキャッシュコンテンツを削除する
    else
      cache_store.delete(key, options) # <- ここでキャッシュストアからキャッシュコンテンツを削除
    end 
  end

このモジュールは、ユーザーインターフェイス(cacheメソッド)とキャッシュストレージの中間に位置する抽象化レイヤと見なすこともできます。このモジュールがActionController::Baseによってincludeされることで、コントローラ内でこれらのメソッドをすべて呼び出せるようになります。

根幹の部分: ActiveSupport::Cache::Store

次はActiveSupport::Cache::Storeというコンポーネントです。ここでは、redismemcachefileなどさまざまなキャッシュストレージの汎用的なインターフェイスを定義しています。以下を実行してみるとわかります。

# Rails 5.2.0
ActiveSupport::Cache::Store.descendants
#=> [ActiveSupport::Cache::Strategy::LocalCache::LocalStore,
# ActiveSupport::Cache::FileStore,
# ActiveSupport::Cache::MemoryStore,
# ActiveSupport::Cache::RedisStore]

なお一部のクラスについては、対応するgemがインストールされていないとRailsで読み込めないため表示されません(例: ActiveSupport::Cache::MemCacheStoreの読み込みにはdalli gemのインストールが必要)。

readwritefetchdeleteといったインターフェイスがよく使われますが、すべてを解説すると煩雑なので、ここではreadメソッドのみを例示するにとどめます。

  # activesupport/lib/active_support/cache.rb
  # (instrumentation関連のコードは省略)
  def read(name, options = nil)
    options = merged_options(options)
    key     = normalize_key(name, options)
    version = normalize_version(name, options)

      entry = read_entry(key, options)

      if entry
        if entry.expired?
          delete_entry(key, options)
          nil
        elsif entry.mismatched?(version)
          nil
        else
          entry.value
        end
      else
        nil
      end
    end
  end

上のread_entrydelete_entry(他にwrite_entry)メソッドは、各キャッシュストレージクラスでの実装に必要なインターフェイスです。

# activesupport/lib/active_support/cache.rb
def read_entry(key, options)
  raise NotImplementedError.new
end

def write_entry(key, entry, options)
  raise NotImplementedError.new
end

まとめ

本記事でコードを追ったことで、Railsを支えるフラグメントキャッシュの基本的な理解にお役に立てばと思います。お気づきの点やご意見がありましたらぜひ原文にコメントをお寄せください。このような記事をもっとお読みになりたい方もぜひお知らせください。がんばって書きますので。

関連記事

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

Rails: RedisキャッシュとRackミドルウェアでパフォーマンスを改善(翻訳)

Rails: Rack::Attackで対PHPボットを防ぐ(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

Rails: Rack::Attackで対PHPボットを防ぐ(翻訳)

長く運営しているサイトや人気の出始めたサイトで、大量の404エラーがログ出力されることがあります。

そうしたページエラーが/wp_login.phpなどのPHPファイルから発生していることがあります。この種のエラーが出力されていたら、自動化ボットがWordPressベースのサイトのセキュリティ脆弱性がないかインターネットをスキャンしているかもしれません。

こうしたボットは標的を無差別に探しているので、RailsアプリにPHPファイルがあるかどうかまで探そうとします。

次の事態を避けること

ログが引っ掻き回され、自動化ボットによってアプリの速度が潜在的に落ちる。

次のようにすること

ボットからのリクエストをRack::Attackではじき、攻撃元のIPアドレスを遮断する。

Gemfile

gem 'rack-attack'

config/application.rb

module YourAppName
  class Application < Rails::Application
    config.middleware.use Rack::Attack
  end
end

config/initializers/rack_attack.rb

class Rack::Attack
  Rack::Attack.blocklist('bad-robots') do |req|
    req.ip if /\S+\.php/.match?(req.path)
  end
end if Rail.env.production?

そうすべき理由

世界中にあるWordPressを長年に渡って使っているWebサイトの割合は膨大です。しかしWordPressによる長年の一極支配は、自動化攻撃の格好の標的となります。セキュリティの既知の弱点を含んだままアップデートされていないWordPressインストールが多数あります。

アプリがやられる前にそうしたリクエストをはじいてボットのIPアドレスを無効にすることで、パフォーマンスの低下を防ぎ、ログのノイズを削減して例外の発生を抑えます。

Railsの場合、Rack::Attackはリクエストやブロックリストの情報の保存にデフォルトでRails.cacheを用います。デフォルトではメモリストアが使われますが、production環境ではRedisなどで弾力性の高いキャッシュを使いたくなるでしょう。

この方法はRack::Attackが提供するアクセス制御のユースケースの1つに過ぎません。Rack::Attackはアプリを保護する頼もしいツールです。

そうすべきでない理由があるとすれば

Rack::Attackを追加してキャッシュへの依存性が1つ増えると、アプリやインフラの複雑さがその分増加します。

ボットによるエラーが発生しないうちからこの保護を予防的に追加する意味は必ずしもあるとは限りません。

理論上は正当なユーザーをブロックすればこの問題は解決しますが、こんなことをする人はまずいないでしょう。

はてブより

Rails: Rack::Attackで対PHPボットを防ぐ(翻訳)

なるほど、確かにwordpress狙っていろいろしてくるね

2018/07/15 11:57

関連記事

[Rails] rack-attack gemでRack middlewareレベルのアクセス制限を実施する

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

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします(パブリックドメイン)。

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

今回は「Rails分解調査シリーズ」第2弾です。前回の記事「Railsのフラグメントキャッシュを分解調査する」をお読み頂いてない方でも今回の記事を読むのに差し支えはありませんが、それでもご一読をおすすめいたします。

Railsのテンプレートレンダリングの背後では実に多くの処理が行われているので、2回に分けて詳しくご紹介いたします。その第1回目として、表示したいテンプレートをRailsのrenderメソッドがどのように探索しているかを説明いたします。続く第2回では、テンプレートオブジェクトがレスポンスに使えるHTMLに変換される過程を説明します。それでは始めましょう!

注目すべきファイル

ソースコードを自分でも見てみたい方向けに(私からも推奨します)、今回のテーマで注目すべきファイルをリストアップします。

訳注: Rails 5.2-stableのソースにリンクしました。

ユーザーインターフェイス

Railsはさまざまな方法でテンプレートをレンダリングします。その1つが#renderメソッドを手動で実行することです。そこでまず以下から始めることにします。

render template: "comments/index", formats: :json

薄々お気づきのように、このrenderメソッドはActionView::Helpers::RenderingHelperというヘルパーにあります。

# actionview/lib/action_view/helpers/rendering_helper.rb
def render(options = {}, locals = {}, &block)
  case options
  when Hash
    if block_given?
      view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
    else
      view_renderer.render(self, options)
    end
  else
    view_renderer.render_partial(self, partial: options, locals: locals, &block)
  end
end

この#renderメソッドを見てみると、テンプレートのレンダリングを担当するview_rendererがあることがおよそ見て取れます。このview_rendererは実際にはActionView::Rendererオブジェクトなので、そちらの#renderメソッドを見てみましょう。

# actionview/lib/action_view/renderer/renderer.rb
module ActionView
  class Renderer
    def render(context, options)
      if options.key?(:partial)
        render_partial(context, options)
      else
        render_template(context, options)
      end
    end

    def render_template(context, options) #:nodoc:
      TemplateRenderer.new(@lookup_context).render(context, options)
    end

    def render_partial(context, options, &block)
      PartialRenderer.new(@lookup_context).render(context, options, block)
    end
  end
end

こちらを見ると、2つの異なるクラスがそれぞれtemplateのレンダリングとpartialのレンダリングを担当していることがわかります。partialのレンダリングはもう少し複雑なので、今回はtemplateのレンダリングのみをご紹介します。

ここで注目すべきは、Renderer@lookup_contextインスタンス変数です。探索するテンプレートに関する必要な情報はすべてここにあります。

#<ActionView::LookupContext:0x00007fa8d5c7f670
 @cache=true,
 @details=
  {:locale=>[:en],
   :formats=>
    [:html,
     :text,
     :js,
     :css,
       ......
     ],
   :variants=>[],
   :handlers=>[:raw, :erb, :html, :builder, :ruby]},
 @details_key=nil,
 @prefixes=[],
 @rendered_format=nil,
 @view_paths=
  #<ActionView::PathSet:0x00007fa8d5c7eba8
   @paths=
    [#<ActionView::FileSystemResolver:0x00007fa8d5c9c7c0
      @cache=#<ActionView::Resolver::Cache:0x7fa8d5c9c680 keys=0 queries=0>,
      @path="/Users/st0012/projects/rails/actionview/test/fixtures",
      @pattern=":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">]>>

ActionView::LookupContextについて

私見では、このActionView::LookupContextこそがテンプレートのレンダリングにおける最も重要度の高いコンポーネントです。このコンポーネントの属性、特に@details@view_pathsを見てみましょう。

@detailsはハッシュで、localeformatsvariantshandlersが含まれています。@detailsの情報の使いみちは次の2とおりです。

1. 見つかったテンプレートのキャッシュに使われるキャッシュキーの一部となる(コード

# actionview/lib/action_view/template/resolver.rb
def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
  cached(key, [name, prefix, partial], details, locals) do
    find_templates(name, prefix, partial, details)
  end
end

2. Railsはこの情報を元にテンプレートのファイル拡張子をフィルタする(コード

# actionview/lib/action_view/template/resolver.rb
module ActionView
  class PathResolver < Resolver #:nodoc:
    EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
    DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
  .....
  end
end

次の@view_pathsは、ActionView::PathSetのインスタンスです。PathSetはテンプレートの探索対象となるパスのセットで、各パスはResolverというオブジェクトで次のようにガードされます。

#<ActionView::FileSystemResolver:0x00007fa8d5c9c7c0
  # これは見つかったテンプレートのキャッシュに用いる
  @cache=#<ActionView::Resolver::Cache:0x7fa8d5c9c680 keys=0 queries=0>,
  # これはテンプレートを探索すべき対象
  @path="/Users/st0012/projects/sample/app/views",
  # このパターンは、テンプレートクエリの組み立てに用いられ
  # 多くの場合違いはほとんどない
  @pattern=":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">

通常の場合、アプリのRAILS_PROJECT/app/viewsはビューテンプレートの置き場所の1つなので、この場所をガードするリゾルバがこの場所でのテンプレート探索を補助します。一部のRailsエンジンkaminarideviseなど)を使っている場合、kaminari/app/viewsdevise/app/viewもリゾルバによってガードされ、これらのgemのテンプレート探索を補助します。

ご覧いただいたように、LookupContextはテンプレートの探索場所や探索すべきテンプレートの種類をRailsに伝える役割を果たします。ここがテンプレートのレンダリングで最も重要な箇所であると申し上げた理由はこれです。

Railsがテンプレートを探索するまでの道のり

ここでActionView::TemplateRenderer#renderに戻りましょう。

# actionview/lib/action_view/renderer/template_renderer.rb
module ActionView
  class TemplateRenderer < AbstractRenderer
    def render(context, options)
      ......
      template = determine_template(options)
      ......
        render_template(template, options[:layout], options[:locals])
    end

    def determine_template(options)
      ......
      if ......
      elsif options.key?(:template)
        ......
        find_template(options[:template], options[:prefixes], false, keys, @details)
        ......
      end
    end
  end
end

テンプレートをレンダリングするには、まずテンプレートオブジェクトの取得が必要なので、これより説明します。ここからの数ステップはメソッド委譲が連続しているだけなので、説明を少し簡略化します。ステップは次のようになります。

1. TemplateRenderer#find_template@lookup_contextに委譲)
2. LookupContext#find_template#findのエイリアス)
3. LookupContext#find@view_pathsに委譲)
4. PathSet#find#find_allを呼び出し、その#find_all#find_allを呼び出す
5. PathSet#_find_allpath (resolver)eachで回して#find_allを呼び出す

# actionview/lib/action_view/path_set.rb
def _find_all(path, prefixes, args, outside_app)
  prefixes = [prefixes] if String === prefixes
  prefixes.each do |prefix|
    paths.each do |resolver|
      ......
      templates = resolver.find_all(path, prefix, *args)
      ......
      return templates unless templates.empty?
    end
  end
  []
end

6.  Resolver#find_allからPathResolver#find_templateを呼び出す

これでやっと、実際のテンプレート探索ロジックにたどり着きました(コード)。

# actionview/lib/action_view/template/resolver.rb

def find_templates(name, prefix, partial, details, outside_app_allowed = false)
  path = Path.build(name, prefix, partial)
  query(path, details, details[:formats], outside_app_allowed)
end

def query(path, details, formats, outside_app_allowed)
  query = build_query(path, details)

  template_paths = find_template_paths(query)
  template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed

  template_paths.map do |template|
    handler, format, variant = extract_handler_and_format_and_variant(template)
    contents = File.binread(template)

    Template.new(contents, File.expand_path(template), handler,
      virtual_path: path.virtual,
      format: format,
      variant: variant,
      updated_at: mtime(template)
    )
  end
end

実際のテンプレート探索の3つのステップ

実際のテンプレート探索は大きく3つのステップからなります。

1. テンプレートクエリのビルド

# actionview/lib/action_view/template/resolver.rb

def find_templates(name, prefix, partial, details, outside_app_allowed = false)
  path = Path.build(name, prefix, partial)
  query(path, details, details[:formats], outside_app_allowed)
end

def query(path, details, formats, outside_app_allowed)
  query = build_query(path, details)
    ......
end

できあがったクエリは次のような感じになります。

"/Users/stanlow/projects/sample/app/views/posts/index{.en,}{.html,}{}{.raw,.erb,.html,.builder,.ruby,.coffee,.jbuilder,}"

2. テンプレートのクエリをかける

# actionview/lib/action_view/template/resolver.rb

def query(path, details, formats, outside_app_allowed)
  query = build_query(path, details)

  template_paths = find_template_paths(query)
  ......
end

def find_template_paths(query)
  Dir[query].uniq.reject do |filename|
    File.directory?(filename) ||
      !File.fnmatch(query, filename, File::FNM_EXTGLOB)
  end
end

3. 見つかったテンプレートでAcrtionView::Templateを初期化

# actionview/lib/action_view/template/resolver.rb

def query(path, details, formats, outside_app_allowed)
  query = build_query(path, details)

  template_paths = find_template_paths(query)
  ......
  template_paths.map do |template|
    ......
    contents = File.binread(template)

    Template.new(contents, File.expand_path(template), handler,
      virtual_path: path.virtual,
      format: format,
      variant: variant,
      updated_at: mtime(template)
    )
  end
end

まとめ

テンプレート探索の道のりは非常に長く、個人的にも必要以上に長いと思います。テンプレート探索の実際のロジックは非常に素直かつシンプルですが、メソッド委譲が折り重なっていることで覆い隠されています。これに比べれば、本編であるテンプレートレンダリング(テンプレートオブジェクトから出力を得る)の方がずっと興味深く、かつテンプレートレンダリングのしくみはかなりよくできていると思います。そこで次回は、いよいよerbテンプレートがHTMLドキュメントに変わるまでを追いかけます。どうぞお見逃しなく😄

関連記事

Railsのフラグメントキャッシュを分解調査する(翻訳)

ベトナム開発拠点のご紹介

$
0
0

こんにちは。BPSの渡辺です。BPSは開発会社として高い品質と付加価値あるスキルと安心を中くらいの価格帯で提供する企業です。コスパには自信と実績があります。でもすこしずつ、創業当初とくらべて、予算が限られているプロジェクトを支援できなくなってきてます。その時の新たな選択肢のために、ベトナム開発拠点があります。以下、概要です。ホームページでの正式な情報は現在制作中です。乞うご期待。ページができるまえに利用してみたいという方はお手数ですがBPSホームページの総合問い合わせフォームからご連絡をお願いいたします。

事業内容

Webアプリケーション開発

  • 開発実績:KPI分析システム,支払管理システム,占い師マッチングサイト,商品レビューサイト, etc.
  • 開発言語:PHP, JAVA, Ruby, Golang, Python, Nodejs, etc.

Mobileアプリケーション開発(Android/iOS/Windows Phone)

  • 開発手法:様々なOSで、WEBベース、ネイティブ、ハイブリッドなアプリ開発に対応
  • 開発実績:就職支援アプリ,フリマアプリ,商品レビューアプリ,コミュニケーションアプリ, etc.
  • 開発言語:Swift, Android Java, Objective-C, React-native, HTML5, JavaScript, etc.
  • 開発環境:Xcode, Eclipse, Unity, Cocos2d-x, etc.

ゲーム開発&運用

  • 開発実績:タワーディフェンスゲーム開発, ソーシャルパズルゲーム運用, etc.
  • 開発言語:Unity, Cocos2d-x, Html5, etc.

強みや特徴

ベトナム国内最高学歴人材で構成

拠点全体の半数が入学難易度が東大より高いといわれているハノイ工科大学の出身者であり、残り半数もベトナム国内の一流大学卒業生を採用しています。特にエンジニアは、そのほとんどがハノイ工科大学出身者になります。研究論文の下調べやそれらを応用した試験的な開発だけでなく、消費者向けの一般的なアプリケーションの開発から運用まで、幅広く対応いたします。

日本のソフトウェア開発経験豊富

BPSのベトナム拠点は日本のソフトウェア開発に特化した部隊です。担当させていただくプロジェクトは立ち上げたばかりのスタートアップのフレキシブルなアジャイル開発もあれば、大手SIer経由の仕様書にそったフォーマルなシステムソフトウェア製造もあったりと、多岐にわたります。ハノイ工科大学出身のエンジニアは国費留学で日本にきてソフトウェアの開発を学生時から若手社会人になるまで経験している割合が高く、言葉だけでなく現場を知っています。

取引先がすべて日本企業ということもあり、日本での業務を経験したメンバだけでなく、全員の仕事に対する姿勢や品質に対する価値観は日本企業の常識をもっています。前職で優秀な成果を残している中途であっても、新卒同様に、戦力になる保証ができるようになってからのみ、お客様に開発体制の一部として提案させていただいております。また、ベトナム国内基準で即戦力という評価だからといって、すぐにお客様に向けた開発体制の提案に組み込むわけではありません。要件の一つとして日本語の学習のほかに、日本のシステムソフトウェア設計開発手法や品質、運用時の価値観への理解が含まれています。プログラムを量産することに長けているかもしれないが仕事では使えない、といったメンバが開発体制に含まれないよう細心の注意と日々注力を行っております。

エンジニア含む全員が日本語を話す

全社的に日本語の学習を支援および義務付けております。そのためエンジニア全員が日々日本語の勉強を社内外で続けており、通訳を通さず直接日本語で会話できます。プロジェクトでは開発効率をあげるためにブリッジエンジニアを体制に加えますがお客様の言葉をそのまま理解できるレベルの日本語力が全員にあることで、開発方針だけでなくプロジェクトやプロダクト、ひいては事業の方向性にあった行動や提案ができるものと考えております。普段お客様側で使われているチャットツール(Slack等)に参加させていただき無駄のないコミュニケーションと開発を実現させていただきます。

今後も、取引先は日本を中心に拡大していくため、頂戴した費用は技術だけでなく、言語や文化の学習にも重点的に投資していきます。

自社開発と同等の開発品質

日本国内で採用できる人材の単価よりも弊社でオフショア開発した場合の単価が低いため、納品後も継続的に開発させていただくケースがとても多いです。そのため、引き続き追加開発するに適した管理体制およびソースコード品質を保つようにしています。具体的には、開発者向けドキュメントの整備および、ソースコードの定期的なレビューや状態管理があります。

ドキュメントも通訳係がシステム納品を済ませるためだけにまとめたものではなく、日本語の語学勉強を継続中の担当エンジニアが今後情報共有と保守がしやすいように開発者向けドキュメントをまとめます。それが結果的に、納品後お客様側で保守運用および追加開発を行うときに、スムーズに引き継ぎ可能になります。作り散らかされたものではなく、愛情をもって長期的に面倒をみるつもりで開発したものをそのまま引き継いでいただけます。

最先端技術学習への継続投資

一般的にWebアプリケーションやAndroid/iOS開発知識と経験のほかに、機械学習(AI)と画像認識の領域への技術力向上に注力しています。日本国内拠点と同様に、ベトナム拠点も技術者主体で構成されているため、自然と最新技術の類への学習に投資が集中します。そのなかでも、機械学習(AI)や画像認識技術を選択しています。

日本でAIという言葉が一般的に使われるようになり、システムを用いた業務効率化への意識が高まったのはよいのですが、本格的に開発しようとするとどうしても時間も費用もかさみます。枯れた技術ではないため技術者の費用もとても高く、AIを用いた研究知見のない企業が実施するには相当な金額と覚悟が必要な事業投資になってしまいます。

ベトナムと日本の人件費の格差を利用し、日本国内のお客様から頂戴した利益を技術者の学習にあて、リーズナブルな価格帯で専門知識をもった技術者の提供を行うことで日本市場に貢献したいと考えております。

機械学習(AI)や画像解析面での豊富な実績

日本国内ではいまや需要過多により集めにくい分野の研究者や技術者を数多く採用し育成し続けています。特に機械学習と画像解析の分野においては、自社の採用のためにベトナム国内だけでなく、業務提携かねて日本で講師としてセミナーで登壇させていただいております。自社のチームへの合流するケース、自社に知見がない場合試験的に活用いただくケース、両方に対応しています。

住所・アクセス

ベトナム本社

R. 401 LK 4B-(8) Mo Lao Urban Resettlement Area, Mo Lao Ward, Ha Dong District, Ha Noi, Vietnam

ベトナム支社

7th Floor, Hoang Ngoc Building, Lot C2C, Lane 92, Tran Thai Tong Street, Cau Giay District, Ha Noi, Vietnam

ベトナム オフショア 契約方法

準委任開発契約

一般的な開発準委任契約に対応しています。

業務委託開発契約

一般的な開発業務委託契約に対応しています。

Time and Material(T&M)契約

ラボ型とも呼ばれることが多い、予め時単価と最大稼働時間を決めておき、稼働した時間に対して費用を頂戴する契約です。毎週または隔週で定例MTGで稼働予定と作業報告を行い、月末に当月分を請求させていただきます。

専用ページができるまでのお問い合わせについて

まだ正式な紹介ページが弊社ホームページにないため、お問い合わせやご依頼はこちらのBPSホームページの総合問い合わせフォームからご連絡をお願いいたします。


Rails: macOSをMojaveにアップグレード後`bundle install`がエラーになった場合の対応方法

$
0
0

問題

macOSをHigh Sierra(10.13)からMojave(10.14)にアップグレードした後、Railsアプリを新規作成するためにbundle installすると以下のエラーが発生しました。

なお、私のMacBook ProにはXcodeは入れておらず(サイズがでかすぎるので)、CommandLineTools(Command_Line_Tools_macOS_10.13_for_Xcode_10.dmg)をインストールしていましたが、Mojaveにアップグレードした機会にhttps://developer.apple.com/download/more/から現時点で最新のCommand_Line_Tools_macOS_10.14_for_Xcode_10.1_Beta_2.dmgをダウンロードして上書きインストールした状態でした。

  • bundle initを実行し、生成されたGemfileの#gem "rails"のコメントを外す。
  • bundle installを実行(bundler抜きでのrails newでもおそらく同じ結果になります)
$ bundle install
Fetching gem metadata from https://rubygems.org/..........
Fetching gem metadata from https://rubygems.org/.
Resolving dependencies...
Fetching concurrent-ruby 1.0.5
Fetching rake 12.3.1
Fetching minitest 5.11.3
Installing minitest 5.11.3
Installing rake 12.3.1
Installing concurrent-ruby 1.0.5
Fetching thread_safe 0.3.6
Installing thread_safe 0.3.6
Fetching builder 3.2.3
Installing builder 3.2.3
Fetching erubi 1.7.1
Fetching mini_portile2 2.3.0
Installing erubi 1.7.1
Fetching crass 1.0.4
Installing mini_portile2 2.3.0
Fetching rack 2.0.5
Installing crass 1.0.4
Installing rack 2.0.5
Fetching nio4r 2.3.1
Installing nio4r 2.3.1 with native extensions
Fetching websocket-extensions 0.1.3
Installing websocket-extensions 0.1.3
Fetching mini_mime 1.0.1
Installing mini_mime 1.0.1
Fetching arel 9.0.0
Installing arel 9.0.0
Fetching mimemagic 0.3.2
Using bundler 1.16.2
Fetching method_source 0.9.0
Installing method_source 0.9.0
Fetching thor 0.20.0
Installing mimemagic 0.3.2
Installing thor 0.20.0
Fetching tzinfo 1.2.5
Fetching i18n 1.1.0
Installing tzinfo 1.2.5
Installing i18n 1.1.0
Fetching nokogiri 1.8.5
Fetching websocket-driver 0.7.0
Installing websocket-driver 0.7.0 with native extensions
Installing nokogiri 1.8.5 with native extensions
Fetching rack-test 1.1.0
Fetching mail 2.7.0
Installing rack-test 1.1.0
Installing mail 2.7.0
Fetching sprockets 3.7.2
Installing sprockets 3.7.2
Fetching marcel 0.3.3
Installing marcel 0.3.3
Fetching activesupport 5.2.1
Installing activesupport 5.2.1
Fetching activemodel 5.2.1
Fetching globalid 0.4.1
Installing globalid 0.4.1
Installing activemodel 5.2.1
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

current directory:
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/gems/nokogiri-1.8.5/ext/nokogiri
/Users/hachi8833/.rbenv/versions/2.5.1/bin/ruby -r ./siteconf20181010-6601-9lqb3q.rb
extconf.rb
checking if the C compiler accepts ... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include
    --without-opt-include=${opt-dir}/include
    --with-opt-lib
    --without-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=/Users/hachi8833/.rbenv/versions/2.5.1/bin/$(RUBY_BASE_NAME)
    --help
    --clean
/Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:456:in `try_do': The
compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:574:in `block in
try_compile'
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:521:in
`with_werror'
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:574:in
`try_compile'
    from extconf.rb:138:in `nokogiri_try_compile'
    from extconf.rb:162:in `block in add_cflags'
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:632:in
`with_cflags'
    from extconf.rb:161:in `add_cflags'
    from extconf.rb:410:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can be
found here:

/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/extensions/x86_64-darwin-17/2.5.0-static/nokogiri-1.8.5/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/gems/nokogiri-1.8.5
for inspection.
Results logged to
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/extensions/x86_64-darwin-17/2.5.0-static/nokogiri-1.8.5/gem_make.out

An error occurred while installing nokogiri (1.8.5), and Bundler cannot
continue.
Make sure that `gem install nokogiri -v '1.8.5' --source 'https://rubygems.org/'`
succeeds before bundling.

In Gemfile:
  rails was resolved to 5.2.1, which depends on
    actioncable was resolved to 5.2.1, which depends on
      actionpack was resolved to 5.2.1, which depends on
        actionview was resolved to 5.2.1, which depends on
          rails-dom-testing was resolved to 2.0.3, which depends on
            nokogiri


Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

current directory:
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/gems/nio4r-2.3.1/ext/nio4r
/Users/hachi8833/.rbenv/versions/2.5.1/bin/ruby -r
./siteconf20181010-6601-1octr2h.rb extconf.rb
checking for unistd.h... *** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

Provided configuration options:
    --with-opt-dir
    --without-opt-dir
    --with-opt-include
    --without-opt-include=${opt-dir}/include
    --with-opt-lib
    --without-opt-lib=${opt-dir}/lib
    --with-make-prog
    --without-make-prog
    --srcdir=.
    --curdir
    --ruby=/Users/hachi8833/.rbenv/versions/2.5.1/bin/$(RUBY_BASE_NAME)
/Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:456:in `try_do': The
compiler failed to generate an executable file. (RuntimeError)
You have to install development tools first.
    from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:590:in `try_cpp'
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:1097:in `block
in have_header'
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:947:in `block in
checking_for'
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:350:in `block (2
levels) in postpone'
    from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:320:in `open'
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:350:in `block in
postpone'
    from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:320:in `open'
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:346:in
`postpone'
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:946:in
`checking_for'
from /Users/hachi8833/.rbenv/versions/2.5.1/lib/ruby/2.5.0/mkmf.rb:1096:in
`have_header'
    from extconf.rb:14:in `<main>'

To see why this extension failed to compile, please check the mkmf.log which can be
found here:

/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/extensions/x86_64-darwin-17/2.5.0-static/nio4r-2.3.1/mkmf.log

extconf failed, exit code 1

Gem files will remain installed in
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/gems/nio4r-2.3.1 for
inspection.
Results logged to
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/extensions/x86_64-darwin-17/2.5.0-static/nio4r-2.3.1/gem_make.out

An error occurred while installing nio4r (2.3.1), and Bundler cannot
continue.
Make sure that `gem install nio4r -v '2.3.1' --source 'https://rubygems.org/'`
succeeds before bundling.

In Gemfile:
  rails was resolved to 5.2.1, which depends on
    actioncable was resolved to 5.2.1, which depends on
      nio4r


Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

current directory:
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/gems/websocket-driver-0.7.0/ext/websocket-driver
/Users/hachi8833/.rbenv/versions/2.5.1/bin/ruby -r
./siteconf20181010-6601-1svzpns.rb extconf.rb
creating Makefile

current directory:
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/gems/websocket-driver-0.7.0/ext/websocket-driver
make "DESTDIR=" clean

current directory:
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/gems/websocket-driver-0.7.0/ext/websocket-driver
make "DESTDIR="
compiling websocket_mask.c
In file included from websocket_mask.c:1:
In file included from
/Users/hachi8833/.rbenv/versions/2.5.1/include/ruby-2.5.0/ruby.h:33:
In file included from
/Users/hachi8833/.rbenv/versions/2.5.1/include/ruby-2.5.0/ruby/ruby.h:29:
/Users/hachi8833/.rbenv/versions/2.5.1/include/ruby-2.5.0/ruby/defines.h:112:10:
fatal error: 'stdio.h' file not found
#include <stdio.h>
         ^~~~~~~~~
1 error generated.
make: *** [websocket_mask.o] Error 1

make failed, exit code 2

Gem files will remain installed in
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/gems/websocket-driver-0.7.0
for inspection.
Results logged to
/Users/hachi8833/deve/rails/rails5_2_1/vendor/bundle/ruby/2.5.0/extensions/x86_64-darwin-17/2.5.0-static/websocket-driver-0.7.0/gem_make.out

An error occurred while installing websocket-driver (0.7.0), and Bundler
cannot continue.
Make sure that `gem install websocket-driver -v '0.7.0' --source
'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  rails was resolved to 5.2.1, which depends on
    actioncable was resolved to 5.2.1, which depends on
      websocket-driver

nokogiriやwebsocket-driverなどのnative extensionのコンパイルでstdio.hなどのヘッダファイルが見つからないということのようです。

参考: Go言語のテストでもエラー

このとき、go testでも同じくcgoでヘッダファイルが見つからないというエラーが発生していました。

go test -ldflags "-s -w" ./...
# runtime/cgo
_cgo_export.c:3:10: fatal error: 'stdlib.h' file not found

試してはいませんが、この他のC言語のコンパイル関連もおそらく軒並み同じような結果になるでしょう。

解決方法

最終的に以下のツイートのおかげで解決できました。ありがとうございます🙇

具体的にはコマンドラインで以下を実行します(pkgをダブルクリックしても同じです)。インストーラが起動し、ヘッダファイルがインストールされます。

sudo open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg

これでbundle installが正常に動作するようになりました😊

おそらくですが、Mojave用のCommandLineToolsがベータ版のため、インストーラからmacOS_SDK_headers_for_macOS_10.14.pkgが呼び出されていなかったのだと思います。通常はインストールすればヘッダファイルもインストールされます。

関連記事

[Rails 5] rbenvでRubyをインストールして新規Rails開発環境を準備する

macOSのアップデート失敗後にダウンロード前の状態に戻す

ベトナムオフショア開発拠点のご紹介

$
0
0

皆様こんにちは。もう2018年も終わりですか。契約しているビルの同じフロアに、60坪ほどの空きがでてたけれど気づくの遅くて契約できず、凹んでいます。2019年から3月頃に合流し日本に常駐するベトナムメンバの何名かのために確保したかったのに。

ベトナムは2018年10月時点での開発者数は100名弱。国内の各開発拠点(東京西新宿、福岡、北海道)と同様に営業と開発体制を持ち、今後は今までと同様に基本独立した動きをしつつ連携を少しずつ増やします。ということもあり、ベトナム拠点の事業紹介ページを公開いたしました。気になる方はぜひご連絡ください。:)

ベトナム拠点(株式会社HBLAB)

オフショア開発拠点1

R. 401 LK 4B-(8) Mo Lao Urban Resettlement Area,
Mo Lao Ward, Ha Dong District, Ha Noi, Vietnam

オフショア開発拠点2

7th Floor, Hoang Ngoc Building, Lot C2C, Lane 92,
Tran Thai Tong Street, Cau Giay District, Ha Noi, Vietnam

写真等

その他拠点

東京新宿本社(BPS株式会社)

〒160-0023
東京都新宿区西新宿6-20-7
コンシェリア西新宿TOWER’S WEST 2F

福岡拠点(株式会社ウイングドア)

〒810-0013
福岡市中央区大宮 2-6-11
第14泰平ビル 5F

北海道札幌拠点(株式会社キロル)

〒001-0010
札幌市北区北10条西4丁目1-19
楠本第10ビル 8F

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

$
0
0

morimorihogeです。しばらくぶりですが、この度銀座Rails#10 @リンクアンドモチベーションにて発表させていただきましたので、その内容をまとめたいと思います。
※当日は時間が足りなくて端折ってしまう部分もあるかと思うので、その補遺としての意味合いもあります

注1:本記事では分かりやすさのためにTABLEやVIEWなどのSQL予約語は大文字で記載していきます。
注2:Rails 5.2.3、PostgreSQL 11環境で検証しています

おさらい:VIEWについて

本記事におけるVIEWはRDBMSにおけるVIEWの話で、ActionViewではありません
VIEWについて使ったことがない人もいるかなと思うので、最初に軽く解説します。

VIEWは一言で言ってしまえばSELECT文の実行結果に名前を付けて、TABLEと同じようにアクセスできるものです。
例えば、以下のようなproductsテーブルとデータがあったとします。

CREATE TABLE products
(
    id             SERIAL       NOT NULL PRIMARY KEY,
    name           VARCHAR(255) NOT NULL,
    price          INTEGER      NOT NULL,
    purchase_price INTEGER      NOT NULL
);

INSERT INTO products(name, price, purchase_price)
VALUES ('安いビール', 150, 140),
       ('普通のビール', 200, 180),
       ('お高いビール', 1500, 1300);

このproductsテーブルに対して、idカラムを除いたデータを参照するview_productsを作るにはCREATE VIEW [view_name] AS [query]構文を使います。

CREATE VIEW view_products AS SELECT name, price FROM products;

こうして作られたVIEWは、通常のTABLEと同じようにSELECTすることができます

view_demo_development=# select * from products;
 id |     name     | price | purchase_price
----+--------------+-------+----------------
  1 | 安いビール   |   150 |            140
  2 | 普通のビール |   200 |            180
  3 | お高いビール |  1500 |           1300
(3 rows)

view_demo_development=# select * from view_products;
     name     | price
--------------+-------
 安いビール   |   150
 普通のビール |   200
 お高いビール |  1500
(3 rows)

VIEWの機能は基本的にはこれだけです。シンプルですね。

また、VIEWはSQL標準機能としてSQL89から存在しており、一般的なRDBMSであれば使えます
PostgreSQLであれば\dでTABLEやSEQUENCEと一緒に一覧できます。

view_demo_development=# \d
                  List of relations
 Schema |         Name         |   Type   |  Owner
--------+----------------------+----------+----------
 public | products             | table    | postgres
 public | products_id_seq      | sequence | postgres
 public | view_products        | view     | postgres
(3 rows)

RailsのActiveRecordとVIEW

RalisのORM(Object-Relational Mapper)であるActiveRecordでは、実は接続しているDBに既に作られたVIEWであれば何も考えずに参照することができます

$ bundle exec rails console
Running via Spring preloader in process 23674
Loading development environment (Rails 5.2.3)
irb(main):001:0> class ViewProduct < ApplicationRecord;end
=> nil
irb(main):002:0> PP.pp ViewProduct.all
  ViewProduct Load (0.5ms)  SELECT "view_products".* FROM "view_products"
[#<ViewProduct:0x000055717a1b21c8 name: "安いビール", price: 150>,
 #<ViewProduct:0x000055717a1b10e8 name: "普通のビール", price: 200>,
 #<ViewProduct:0x000055717a1b0fa8 name: "お高いビール", price: 1500>]
=> #<IO:/dev/pts/5>

※後述しますが、migration systemで管理する場合はGemの導入が必要です

#save系メソッドなどの更新系については可能なVIEWと不可能なVIEWがあり、更新可能なカラムもVIEWの定義によって変わります。更新可能なVIEWの条件についてはPostgreSQLのCRAETE VIEWのドキュメントが詳しいです。
※他のRDBMSでも基本的な条件は近いと思いますが、実装依存もあると思うので実際に使う場合は利用しているRDBMSのドキュメントを参照してください。

ここでは、VIEWはSELECT文を実行する観点においてTABLEと同一視することができるということを抑えてくれればOKです。

AciveRecordでVIEWをいい感じに使う

では、実際に具体的な事例を用いてVIEWを使った事例を紹介していきます。
説明のためにusersテーブルを定義します。一般的なメールアドレス+パスワードでログインするようなECサイトのようなものを想像してください。

class User < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :email, null: false, unique: true, comment: 'メールアドレス'
      t.string :name, null: false, comment: '公開ユーザー名'
      t.text :description, null: false, default: '', comment: '公開用紹介文'
      t.string :password, null: false, comment: 'パスワードハッシュ'
      t.datetime :confirmed_at, comment: 'メール認証日時'

      t.timestamps
    end
  end
end

要件1:有効なユーザーだけを取得したい

users.confirmed_atでメール認証を行うことが読み取れますが、逆に言えばメール認証されていないユーザーはまだ有効なユーザーとして扱いたくないわけです。
具体的にはusers.confirmed_at IS NOT NULLなユーザーだけを取得したいとします。

一般的なRails Wayに従うのであれば、scopeを利用するのが妥当でしょう。

# app/models/user.rb
class User < ApplicationRecord
  scope :confirmed, -> { where.not(confirmed_at: nil) }
end

ここではconfimedscopeを定義しました。あとはController等から呼び出すときにscoped chainを使って実装すれば良いわけですね。

# 全件取得
@users = User.all

# confirmedなものだけ取得
@users = User.confirmed

一般的にはこれで問題ないかと思うのですが、scoped chainはアプリケーションが複雑になっていくと以下のような問題が発生しがちです。

  • scoped chainが増えてくると付け忘れや使い分けのミスが発生しやすい
    • 今はメールアドレス確認だけだが、アカウントBANフラグ、退会済みフラグ、論理削除などの要件が増えてくるとscoped chainが複雑になる
    • 特に、ある程度サービスが育った後に全体に影響するscopeを追加すると、影響範囲が広く検証が大変になる
  • 「ほとんどいつも使う」scopeの取り扱い問題
    • いつもscopeを書くのは冗長で面倒、かつ付け忘れてしまう事故の温床になる
    • default_scopeを使うという手もあるが、初期化時の副作用やunscopedしたときに想定外のものまでunscopedされてしまったりといった事故につながる可能性があり、やりたくない

では、VIEWを使った実装を見てみます。

まずはconfirmed_usersというconfirmedなユーザーだけを取得するVIEWを作成します。

CREATE VIEW confirmed_users AS
  SELECT * FROM users WHERE confirmed_at IS NOT NULL;

その上で、confirmed_usersを参照するModelを作成します

# app/models/confirmed_user.rb
class ConfirmedUser < ApplicationRecord
end

あとは、データを取り出すときにUserモデルではなくConfirmedUserモデルから取得すればOKです。

# 全件取得
@users = User.all

# confimedなものだけ取得
@confirmed_users = ConfirmedUser.all

このように、scope代わりのfilterとしてVIEWを使うのは、以下のような特徴があります。

  • 利点
    • 複雑化したscoped chain hellから脱却できる
    • 将来的に「有効なユーザー」の定義が変わっても、VIEWのmigrationだけで対応できる(unscopedするコードがあってもVIEWに定義されていないデータはアクセスできないので問題ない)
    • default_scopeが嫌いな人達も😊
  • 欠点
    • 引数付きscopeなど、複雑なアプリケーションロジックが必要なscopeは置き換えが難しい

次行きましょう。

要件2:公開情報用のカラムを追加し、ユーザーごとにどの情報を公開できるか設定できるようにしたい

SNS系のサービスではよくあるやつですね。ここでは、以下の3つのカラムを定義し、それぞれis_public_#{FIELD_NAME}というboolean値を持ち、それがtrueであれば公開OKとします。

  • description: 自己紹介文
  • twitter_id: TwitterのID
  • facebook_id: FacebookのID

追加のmigrationは以下のようになりました。

class AddIsPublic < ActiveRecord::Migration[5.2]
  def change
    # descriptionは既に作成済み
    add_column :users, :is_public_description, :boolean, null: false, default: false

    add_column :users, :twitter_id, :string
    add_column :users, :is_public_twitter_id, :boolean, null: false, default: false
    add_column :users, :facebook_id, :string
    add_column :users, :is_public_facebook_id, :boolean, null: false, default: false

    # scenicによるVIEW更新用の設定(後半で解説します)
    update_view :confirmed_users, version: 2, revert_to_version: 1
  end
end

# 公開用データはテーブル分けた方がいいんじゃないかとか、正規化してもいいんじゃない?とかそういう話もあると思いますが、ここでは一旦置いておきます。

では、この状態でView(こっちはActionView)で表示する際の出し分けをどうするか、Rails Wayに乗るのであれば、以下のようなケースが考えられます。

  • 愚直にViewの中で <%= @user.description if @user.is_public_description %> などと書いていく
    • とても事故りやすいのでオススメしません。とりあえず品質は無視して作り捨てたいプロトタイピングなど以外ではやらない方が良いでしょう。
  • ModelをViewに渡すときにModel本体ではなく、Decorator(またはPresenter)系のGemを利用してラップされたものを渡す
    • DraperActiveDecoratorを使う想定になります。
    • 既にプロジェクト全体でこうしたGemが利用されていれば「Modelを直接Viewに渡さない」というルールが徹底されているので問題ないと思いますが、使われていないプロジェクトで新たに導入したり、Decoratorを使ったことのないメンバーが加入したりするとDecoratorを通し忘れて生のModelを渡してしまうといったミスが発生する危険性があります。
  • そもそもActiveRecordは指定しない限りはSELECT *で全カラムを取り出してしまうので、本来そのリクエストで取得する必要のない情報もDBから取り出してしまうという問題がある
    • #inspect などをすると見えてしまうため、重要な個人情報などはアプリケーションログやエラーメッセージも含めて注意しないと、思わぬところから非公開のつもりだったカラムが見えてしまう、という可能性はあります。

では、VIEWを使って解決してみます。
VIEWでやる場合、今度はconfirmed_users VIEWから公開情報だけを取り出したpublic_users VIEWを作成します。

CREATE VIEW public_users AS
SELECT id,
       email,
       name,
       CASE is_public_description
           WHEN true THEN description
           ELSE NULL
           END AS description,
       CASE is_public_twitter_id
           WHEN true THEN twitter_id
           ELSE NULL
           END AS twitter_id,
       CASE is_public_facebook_id
           WHEN true THEN facebook_id
           ELSE NULL
           END AS facebook_id
FROM confirmed_users;

突然CASE文が出てきましたが、これによって各カラムが「is_public_#{FIELD_NAME}がtrueなら#{FIELD_NAME}の内容を表示、falseならNULLを返す」ようにしています。
こうすることで、DBからデータを取ってきた時点で既にマスクされたデータを作ることができます。

データを取り出すには、先ほどのConfirmedUserと同じようにPublicUser Modelを作成すればOKです。

# app/models/public_user.rb
class PublicUser < ApplicationRecord
end

取り出す際は何も考えず出せば良いですね。

# Controller
@user = PublicUser.find(params[:id])
<dl>
  <dt>自己紹介</dt><dd><%= @user.description %></dd>
  <dt>Twitter ID</dt><dd><%= @user.twitter_id %></dd>
  <dt>Facebook ID</dt><dd><%= @user.facebook_id %></dd>
</dl>

View側は実際には設定されてなければ非表示にしたりなどするかと思いますが、少なくとも表示・非表示のロジックをViewにもControllerにも書かずにマスクできています。

このように、特定用途向けにデータを加工したVIEWを使う方法には、以下の特徴があります。

  • 利点
    • 加工前の生データはRails側のメモリにすら乗らないので、アプリケーションコードのミスで漏れるということが(参照するModelを間違えない限りは)ない。
      • 大規模CMSなどでデータ管理用のRailsアプリとフロント用のRailsアプリが分離している場合などに特にうまく働きます(フロントアプリには表示して良いVIEWしか見せないことができる)。
      • 他社にDBの一部を参照させたい場合にも、VIEWを使って見せてよい部分だけ公開・加工することでデータの公開・閲覧範囲を絞ることができる
    • SQL関数をうまく使うことで、一部のデータ変換や文字列操作をRDBに任せることができる
      • Railsアプリケーション側のメモリが節約できる(RDB側の負荷は上がる可能性がある)
    • 非RailsシステムのDBを参照する場合でも、Rails Wayに則った名前に変換して利用することができる
      • 堅い業務システムなどではD01SHIMEIのようなとても分かりにくい名前がついていることがありますが、ASでリネームしてやることで、Railsの世界ではまともな名前を使うことができる。ただし、同様のことがしたければalias_attributeでもできるので、DB(SQL)レベルで名前を隠蔽したいかどうかはプロジェクトによって検討の余地がある。
  • 欠点
    • 機能ごとにVIEWを作り続けていると、VIEWの数が増え続けてVIEW沼に陥る可能性がある
    • 複雑なSQLは読み書きが難しくなってくるため、プロジェクトチームメンバーのSQL習熟度によってはチーム内の一部のSQLに強いメンバに管理が俗人化していまう(その人がDBA(DataBase Administrator)化する)

さて、最後にかなり強力なもの(だと僕が思っている)を紹介します。

要件3:関連を含めた複雑なテーブル構造の中で、様々なテーブルの項目を横断して検索したい

今度はusersテーブルの外に目を向けます。
実はこのサイトはECサイトで、以下のようなテーブルを持っているとします。

  • users: 会員ユーザー
  • tags: 会員種別タグ(例: WEB会員、店舗会員など)
  • orders: 注文情報(レシート1枚に相当)
  • order_details: 注文明細(レシート内の1行1行に相当)
  • products: 商品情報
  • categories: 商品カテゴリ(例:高額商品、安価商品など)

さらにそれらの中間テーブルを含めて以下のような図になっているとします。

こうしたテーブル構造の中で「ユーザーごと、商品ごとに何をどれだけ買ったのか集計したい」「高額商品を買ってくれているユーザーを一覧したい」「ユーザー・カテゴリごとの商品販売数を一覧したい」などを要望されたとしましょう。

Rails Wayでは複数テーブルをまたいだ条件を記述するのには#joins#mergeを駆使して集計していくことになるのではないかと思います。
乱暴に一旦丸っとメモリに展開して#each#selectで頑張るということはできないわけではありませんが、とても重いですしかなり早い段階で破綻することが多いです。
1-3テーブル程度であれば問題ないのですが、OUTER JOINが入ったり関連の遠いテーブルも持ってきたりしていくと、Arelのコードだと合っているのかどうかが不安になり、#to_sqlして確かめているうちに「生SQL書いた方が早くね?」と思ってQuery Objectを作り始めたり集計用のクラスを作ったりし始めるのは割とあるあるなのではないでしょうか。

参考: Qiita: Rails における内部結合、外部結合まとめ

他にも、検索・ソート・絞り込みではransackなんかが使えるとお手軽なのですが、複雑なJOINやJOIN先のテーブルで絞り込みを利用するにはransackable_scopesなどを使ってゴリゴリカスタマイズしないといけなくなったりします。お手軽ではなくなりとても辛い。

ここでVIEWの出番です。ここのキモはActiveRecordで単純に集計しやすいVIEWを作ることです。
以下のようなVIEWを作ります。

CREATE VIEW order_summaries AS
SELECT users.id                           AS user_id,
       users.name                         AS user_name,
       ARRAY(SELECT t.name
             FROM tags t
                      INNER JOIN tags_users tu ON t.id = tu.tag_id
             WHERE tu.user_id = users.id) AS user_tag_names,
       products.id                        AS product_id,
       products.name                      AS product_name,
       ARRAY(SELECT c.name
             FROM categories c
                      INNER JOIN categories_products cp ON c.id = cp.category_id
                      INNER JOIN products p ON p.id = cp.product_id
             WHERE p.id = products.id
           )                              AS category_names,
       SUM(order_details.total_price)     AS order_total_price,
       SUM(order_details.amount)          AS order_amount
FROM users
         LEFT JOIN orders ON users.id = orders.user_id
         INNER JOIN order_details ON orders.id = order_details.order_id
         INNER JOIN products ON products.id = order_details.product_id
GROUP BY users.id, products.id;

VIEW定義だとわかりにくいので、適当なデータを突っ込んでSELECTしたものが以下です。

# SELECT * FROM order_summaries ORDER BY user_id, product_id;
 user_id |      user_name      |   user_tag_names   | product_id | product_name | category_names | order_total_price | order_amount
---------+---------------------+--------------------+------------+--------------+----------------+-------------------+--------------
       5 | テストユーザーその1 | {WEB会員,無料会員} |          1 | 高いビール   | {高い商品}     |             22500 |           15
       5 | テストユーザーその1 | {WEB会員,無料会員} |          3 | 高い日本酒   | {高い商品}     |             15000 |            3
       6 | テストユーザーその2 | {店舗会員}         |          2 | 安いビール   | {安い商品}     |               400 |            2
       6 | テストユーザーその2 | {店舗会員}         |          4 | 安い日本酒   | {安い商品}     |              4000 |            2
       8 | テストユーザーその4 | {店舗会員}         |          1 | 高いビール   | {高い商品}     |              1500 |            1
       8 | テストユーザーその4 | {店舗会員}         |          2 | 安いビール   | {安い商品}     |               200 |            1
       8 | テストユーザーその4 | {店舗会員}         |          3 | 高い日本酒   | {高い商品}     |              5000 |            1
       8 | テストユーザーその4 | {店舗会員}         |          4 | 安い日本酒   | {安い商品}     |              2000 |            1
(8 rows)

ユーザーごと、商品ごとに販売数・総額を出し、タグやカテゴリはARRAY型の中に入れたデータになっています({}で囲まれた部分)。
ではここでこのorder_summaries VIEWから「高い商品」カテゴリに属する商品の売上をユーザー別に集計して昇順に取り出すには、以下のようにします。
※OrderSummary Modelの作成は省略します

OrderSummary.select('user_id, user_name, SUM(order_total_price) AS total')
    .where("'高い商品' = ANY(category_names)")
    .group(:user_id, :user_name)
    .order('total DESC')

どうでしょうか?集計を行ったりPostgreSQLのARRAY型を使っている関係で多少は煩雑ですが、この処理自体はorder_summaries VIEWにしかアクセスしないので、wheregroupなどがとてもシンプルになっています。
というわけで、多数のテーブルをまとめた検索用VIEWを使うことには以下の特徴があります。

  • 利点
    • VIEWさえできていれば、内部は複雑なクエリでもRailsエンジニアは通常のテーブルと同じように検索・参照できる
      • つぎはぎ開発で複雑怪奇なテーブル構造になってしまったようなケースでは特に有効
      • 1テーブルを前提として作られているGemはそのまま使えるので、Railsのエコシステムを使うことができる
      • 純粋にデータアクセス部分の開発難易度が下がるので、開発経験が浅めのエンジニアでも開発を進められるようになる
    • 取得したオブジェクトはAR::Relationなので、ActiveRecord拡張のGemがそのまま使える
      • AR::Base.executeなどの生SQLから取得したデータと違い、普通のActiveRecordの書き方で利用できる
  • 欠点
    • VIEWを作るためにそれなりにSQLに長けたエンジニアが必要になる
    • VIEWの定義によっては重いクエリになるので、DBサーバーの負荷が想定外に増えることがある

以上、3ケースに分けて代表的と思われるユースケースを紹介してみました。

VIEWを使うときの注意点

実際にVIEWを使った実装を進めていく中で気を付けないといけない点をまとめます。

migration管理にはscenic gemが必要

定義済みのVIEWを参照する分には追加Gemは不要ですが、VIEWの作成や更新をmigration管理したい場合には標準ではできません。
Scenicを使うことでVIEWもmigration管理できるようになるため、実質必須になると思います。
Scenicの扱い自体はそれほど難しくないので、公式READMEを読めばわかると思います。

また、SELECT *で記述されたVIEWはVIEWを作成した瞬間のカラムリストに展開されるので、元テーブルのカラムが変更された場合はVIEWも更新する必要があります

VIEW作成文は生SQLなので、Railsのデータ変換層を通らない

具体的にはVIEW定義内で時刻系ロジックを記述する場合にTimezoneの扱いに注意が必要です。Railsの世界ではTimeオブジェクトを渡すように作る限り問題ありませんが、VIEW定義するときだけはRailsの世界から離れてRDBMSの目線で考える必要があります。

アプリケーションロジックがVIEWとRailsに分散する

これが一番問題になると思います。
Rails Wayならscopeで実装するようなものもVIEW定義で書くことができるため、乱用するとVIEWとアプリケーション側にロジックが分散します
プロジェクトの開発責任者は何をVIEWに出して何をRails側のロジックコードとして書くべきなのかを考えながらバランスを取っていく必要があるでしょう。

参照専用のVIEWであればreadonlyにすべき

ScenicのREADMEにも記載がありますが、以下のようにreadonly?メソッドがtrueを返すように定義してやると、誤った#saveを防止できます。

class OrderSummary < ApplicationRecord
  def readonly?
    true
  end
end

発表スライド

まとめ

ここに書ききれなかったユースケースもたくさんありますが、VIEWを知らない人に知ってもらうにはこれくらいのボリュームが良いかなと思うので、ここまでにしたいと思います。
本記事の反響があれば続きを書こうと思いますので、Twitterやはてブ等でコメントいただければ幸いです。
※Twitterは「TechRacho」という文字列を含めてもらえれば追いかけます

それでは皆様、良いWeb開発ライフをお送りください :)

Rails+Google Cloud Vision APIで文字認識を実装してみた

$
0
0

こんにちは、BPSの福岡拠点として一緒にお仕事させて頂いてます、株式会社ウイングドアの坂本です。

日頃よくお客様から「OCR(光学的文字認識)をつかって画像から文字を抽出して〜(うんぬんかんぬん)」
という要望を頂きます。
以前は少しハードルが高く感じていましたが最近はいくつかのサービスでOCRのAPIを提供されており、
大分手を出しやすくなったのではないでしょうか?

先日、RailsでそのGoogle CloudのVision APIを利用した検証用デモを作成しました。
意外とRubyの記事は少なかったため少しでも役に立てばと実装例と使ってみた所感をご紹介したいと思います。

前提

  • デモ作成の目的はOCRで以下の要件が満たせそうか検証すること。
    そのため以下の要件を意識しながらデモを作成しています。
    • 日本語を含む手書きの表を読み取れること(今回はシフト表を想定)
    • 表の中身の項目は複数ある文字パターンの1つが入る
    • 文字パターンは表毎に変動
    • 文字数は今回考慮しない
    • 表形式に出力できること
  • あくまで「検証用」ということでレイアウトはほとんど調整出来ていません。
  • この検証は2019年5月の実装時点のものです。パラメータなどの詳細は公式ドキュメントを参照下さい。

作ったもの

1. 入力画面

2. 画像アップロード後

アップロードし、APIの結果が取得できると上から以下の項目が表示されます

3. アップロードの結果

  1. 結果を表形式に成形したもの
    ※上記の画像は読み取り用の記号がないため「No Data」と表示されています
  2. APIから帰ってきた文字とその左上の座標(x, y)
  3. 結果のオブジェクトをベタで表示したもの
    to_s の内容をそのまま表示しています

実装

1. アカウントの設定

GCPを利用できるようにGoogleや環境変数などの設定を行います。

gem追加

Google Cloud Vision APIのgemを追加します。

# Gemfile
gem 'google-cloud-vision'

3. フロントを作成

今回はNuxt.jsでお手軽に画面を作成。 詳細は割愛します。

4. API作成

画像がアップロードされた時のAPIを作成します。
アップロードされた画像を取得し、Cloud Visionの「ドキュメント テキスト検出」を呼び出しています。

# app/controllers/google_api/vision_controller.rb

module GoogleApi
  class VisionController < ApplicationController
    # Imports the Google Cloud client library
    require 'google/cloud/vision'

    def upload
      images = []
      upload_file = params[:file]
      if upload_file != nil
        images << upload_file.path
      end

      responses = send_images(images)
      results = format_result(responses)

      render json: {results: results, responses: responses.to_s}
    rescue => e
      render status: 500, json: {status: 'ERROR', result: e.to_s}
    end
    def send_images(images)
      image_annotator_client = Google::Cloud::Vision::ImageAnnotator.new
      image_annotator_client.document_text_detection images: images, image_context: {language_hints: [:ja, :en]}
    end

    def format_result
      # 以下略 
    end
end

アップロードされたファイルをメソッドsend_imagesに渡して、メソッドformat_resultでテーブルで表示できるように成形してjson形式で返すようにしています。

responsesを渡してるのはデバッグ用のおまけです。

送信の処理はこれだけ。

    def send_images(images)
      image_annotator_client = Google::Cloud::Vision::ImageAnnotator.new
      image_annotator_client.document_text_detection images: images, image_context: {language_hints: [:ja, :en]}
    end

image_annotator_client.document_text_detection images: images だけドキュメント テキスト検出APIの呼び出しが可能ですが、 language_hintsで日本語を指定しました。

language_hintsなどのオプションの指定は、Rubyのドキュメントが見つからず他の言語のドキュメントやソースコードを見比べたりして辿りつきました。

document_text_detectiontext_detectionに入れ替えれば「テキスト検出」を呼び出すようにもできます。

検証結果

入力した画像と、表部分の出力結果は以下のような形になりました

1. アルファベット 1文字

真ん中あたりが空白。 大分文字を取りこぼしているようです。

入力画像

出力結果(表)

2. アルファベット 2文字

なぜかアルファベット1文字の時よりずれた結果になりました。

入力画像

出力結果(表)

3. 漢字 1文字

1行消えてる上、複数の文字が連結されています。

入力画像

出力結果(表)

4. 日本語 2文字

1行消えていますが、表の中身きちんと取り込めています。
1文字の時のように隣接するマスとの連結もありません。

入力画像

出力結果(表)

所感

ほとんどの結果で中身が空白の行のラベルを取りこぼしていたり、余分な文字がついていたりしました。
こちらは表出力のロジックも含めて修正が必要そうです。

他にもテストを繰り返して(正確に数えていないのですが検証パターン数が20 ~ 50程度だったと思います)
以下のような知見を得ました。

  • 文字数が多いほうが正しい値が検出されやすい
  • language_hinten(英語)を優先度を高くすると罫線がT などとして検出される場合がある
  • アルファベット
    検出されやすい?: ABD、XYZ、H
    検出されにくい: C、E
  • 記号
    • ○: 0Oと混ざる。隣接するマスとまとめられる
    • ●: ほぼ取得不可。 「」 として検出されることも
    • □■▲△: 取得不可
    • ☆、★: 日本語と隣接しているとほぼ取れる
    • 時刻の形式「12:00」などよくあるパターンだと正しく取り込まれる
  • 日本語
    • 1文字より2文字以上の場合が読み取りやすい

まとめ

英数字や記号の方が精度が高いと思っていましたが、他の文字と認識されることが多く精度が確保できないのが意外でした。

特に表形式ですと罫線が認識されてしまい、除外したりするのがとても難しかったです。逆に、難しいだろうと思っていた日本語の方が精度が高く、2文字以上になるとほとんど読み取ってくれたのには驚きました。

機械学習でカバーしてくれるんでしょうか?
表だともう少し工夫が必要そうですが、文章だったらますます精度が高くなって使いやすそうです。

他にも顔検出やラベル検出など興味深い項目がたくさんありますので、
これからも機会を見つけてぜひ触ってみたいと思います!


株式会社ウイングドアでは、Ruby on RailsやPHPを活用したwebサービス、webサイト制作を中心に、
スマホアプリや業務系システムなど様々なシステム開発を承っています。

Rails 6: UUIDで`first`や`last`を使う(翻訳)

$
0
0

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

Rails 6: UUIDでfirstlastを使う(翻訳)

UUIDを主キーに使うとさまざまなメリットを得られますが、Railsの「暗黙の順序」で問題が生じます。

2018年の記事ではfirstlastは名前付きスコープで使うことをおすすめしましたが、現在はもっと簡単にActive Recordのデフォルトの振る舞いを再び有効にできるようになりました。

以下のように書くよりも

Active Recordモデル上のシーケンスでないidに対して#first#lastを使うのを避ける。あるいはそれ用の特別な名前付きスコープを追加して使う。

以下のように書こう

モデルのimplicit_order_columnで、自動生成されたcreated_atカラムを指定する。

class Coffee < ApplicationRecord
  self.implicit_order_column = "created_at"
end

そうする理由

UUIDをデータベースで用いることで、「一意性」「代入可能性」「セキュリティ」という大きなメリットを得られます。

さらにそこに「self.implicit_order_column = "created_at"」を1行足すことで、Railsのfirstlastヘルパーメソッドも利用できるようになります。

そうしない理由があるとすれば

implicit_order_columnはRails 6の機能です。それより前のRailsでは、以前の記事で説明したように明示的に順序指定を実装する必要があります。

明示的な順序指定のアプローチの方が、意図が明確になるので好ましいと思う人もいるでしょう。

データモデルでUUIDを使わないのであれば、この方法を使う理由はほぼありません。

ひとつ注意があります。2つのレコードの作成時刻が完全に一致すると、その2つのレコードの並び順は主キーの順序に従います。

もうひとつ注意すべきは、created_atは(id主キーと異なり)デフォルトではデータベースインデックスを持たないという点です。インデックスなしでこのクエリが走ると、データセットが巨大な場合に時間がかかる可能性があります。その場合はcreated_atフィールドにデータベースインデックスを追加すべきです。

スペシャルサンクス

implicit_order_columnは、友人のTekinが思いついたアイデアがRailsに実装されたものです(#34480)。

Benjamin Alexanderからは、created_atにデータベースインデックスが必要なことを指摘いただきました。

編集部追記(2020/10/15)

現在ならulidを検討してみてはどうかという意見も社内でありました。

参考: ソート可能なUUID互換のulidが便利そう - Qiita

ulid/spec - GitHub

関連記事

RailsのモデルIDにUUIDを使う(翻訳)

Viewing all 120 articles
Browse latest View live