Tech Racho エンジニアの「?」を「!」に。
  • 開発

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

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

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

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

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

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

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

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

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


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

おそらくこれかなと↓。

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

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

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

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

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

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

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

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

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


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

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

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

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

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

参考: Feature Policy -- W3C

追記(2019/07/17)

feature-policyは、基本的にサーバー側からブラウザ機能やAPIを選択的に有効にしたり無効にしたりする機能であるとのことです🙇。

開発者は、セキュリティやパフォーマンス上の予防措置として、特定のブラウザ機能やAPIへのアクセスを選択的に無効にして、開発者のアプリケーションを「ロックダウン」することで、アプリケーション内の自社や第三者のコンテンツが望ましくない動作や予期しない動作を引き起こすのを防げる。
w3c.github.io/webappsec-feature-policyより大意

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

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

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

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

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

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

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

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

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

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

Rails

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

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

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

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

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

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

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

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

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

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


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

同記事見出しより:

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

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


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

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

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

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

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

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


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


12factor.netより

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

その他Rails


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

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

Ruby

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

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


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


同記事より

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

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

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

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

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

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

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

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

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

# 同記事より
def proc_without_block
  proc
end

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

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

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

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

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

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

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

Ruby: Kernelの特殊変数を$記号を使わずに書く

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

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

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

Evil Martiansの中の人です。


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

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

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


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

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

その他Ruby

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

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

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


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

Ruby Trunkより

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

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


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

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

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


前編は以上です。

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

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

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h


CONTACT

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