Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ(20200615前編)`rails new`に`minimal`がマージ、ARの共通集合を取る`and`、RubyMine+Dockerチュートリアル動画ほか

こんにちは、hachi8833です。オードリー・タンさんのPodcast聞いちゃいました😋。


つっつきボイス:「Rebuild昨日やってたんですね」「どちらも英語のレベルも話のレベルも高くて脱帽でした🎩」「やはりリモートで対談」「見出しの『モナドは自分の仕事で必要というわけでもない』というあたりしか覚えてませんが😆」「こういう人のジョブがどんなのか興味あるな〜😋」

参考: モナド (プログラミング) - Wikipedia

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄

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

ホストのcookieドメイン選択のマッチを厳密にした

# actionpack/lib/action_dispatch/middleware/cookies.rb#L442
        def handle_options(options)
          if options[:expires].respond_to?(:from_now)
            options[:expires] = options[:expires].from_now
          end
          options[:path]      ||= "/"
          options[:same_site] ||= request.cookies_same_site_protection
          if options[:domain] == :all || options[:domain] == "all"
            # If there is a provided tld length then we use it otherwise default domain regexp.
            domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
            # If host is not ip and matches domain regexp.
            # (ip confirms to domain regexp so we explicitly check for ip)
            options[:domain] = if !request.host.match?(/^[\d.]+$/) && (request.host =~ domain_regexp)
              ".#{$&}"
            end
          elsif options[:domain].is_a? Array
-           # ホストが、ドットが前に付いていないドメイン名のいずれかにマッチする場合
-           options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
+           # hostが、渡されたドメインのいずれかにマッチする場合
+           options[:domain] = options[:domain].find do |domain|
+             domain = domain.delete_prefix(".")
+             request.host == domain || request.host.end_with?(".#{domain}")
+           end
          end
        end

つっつきボイス:「どれどれ、以前はexample.comを指定してた場合にexample.com.aumyexample.comにまでマッチしてたのか!」「それはイカン😅」「『cookieでは元々おかしなドメイン名は無視するから互換性はそんなに心配いらないだろうけど』とコメントにありますね」「ブラウザ側の実装上は一応大丈夫ということなのかな?🤔」「いずれにしろ正しい挙動にするのがいいですね👍」「行儀が悪いのを直したと」

新機能: Active Recordでリレーション名.andをサポート

集合論で言う「共通集合」を取れるそうです。

参考: 共通部分 (数学) - Wikipedia


つっつきボイス:「@kamipoさんのプルリクでした」「おぉ、今度はandが入った🎉」「なるほど、サンプルコードのようにdavid_and_mary.and↓と書けるようになった😋」「今までだとwhere文のところにSQLを書かないとできなかったヤツですね」「なるほど!」「こういうクエリを書きたいときはあるので、Active Recordの機能でやれるようになったのはいいことですね👍」

# 同PRより
david_and_mary = Author.where(id: [david, mary])
mary_and_bob   = Author.where(id: [mary, bob]) # => [bob]

david_and_mary.merge(mary_and_bob) # => [mary, bob]

david_and_mary.and(mary_and_bob) # => [mary]
david_and_mary.or(mary_and_bob)  # => [david, mary, bob]

「これいいな〜、いつ使えるようになるんですか?😋」「マージされたから次のリリースで入るのかな〜😋」「早く欲しいよ〜😂」「😆」「😆」

委譲をやめて属性アクセスを15%高速化

# activemodel/lib/active_model/attributes.rb#L135
      def read_attribute(attr_name)
        name = attr_name.to_s
        name = self.class.attribute_aliases[name] || name

-       _read_attribute(name)
+       @attributes.fetch_value(name)
      end

つっつきボイス:「こちらも@kamipoさんでした」「read_attribute周りの修正か」「『たった1行のメソッドに委譲する意味はないので避けた』と書かれてますね」「こういう修正の見当が付くのがスゴい💪」「メソッド自動生成がらみはわけわからなくなりがちですし😭」「@kamipoさん止まらないですね」

わずか1行のメソッドに委譲するほどの価値はない。委譲を避けることでread_attributeを15%高速化できた。
同コミットより大意

Warming up --------------------------------------
read_attribute('id')   165.744k i/100ms
read_attribute('name')
                       162.229k i/100ms
fast_read_attribute('id')
                       192.543k i/100ms
fast_read_attribute('name')
                       191.209k i/100ms
Calculating -------------------------------------
read_attribute('id')      1.648M (± 1.7%) i/s -      8.287M in   5.030170s
read_attribute('name')
                          1.636M (± 3.9%) i/s -      8.274M in   5.065356s
fast_read_attribute('id')
                          1.918M (± 1.8%) i/s -      9.627M in   5.021271s
fast_read_attribute('name')
                          1.928M (± 0.9%) i/s -      9.752M in   5.058820s

同じカラムをmergeしたときの条件の扱いをRails 6.2で統一

従来の振る舞いは非推奨になるとのことです。

同じカラムでのmergeで両方の条件を維持しないようになり、Rails 6.2では常に後者の条件で置き換えられるようになる。
Rails 6.2の振る舞いに移行するにはrelation.merge(other, rewhere: true)を使うこと。
Changelogより

# Changelogより
# Rails 6.1 (IN句はマージする側の等価条件で置き換えられる)
Author.where(id: [david.id, mary.id]).merge(Author.where(id: bob)) # => [bob]

# Rails 6.1 (両者の条件に矛盾が存在する: 非推奨化済み)
Author.where(id: david.id..mary.id).merge(Author.where(id: bob)) # => []

# Rails 6.1 で`rewhere`を用いることでRails 6.2の振る舞いに移行する
Author.where(id: david.id..mary.id).merge(Author.where(id: bob), rewhere: true) # => [bob]

# Rails 6.2 (IN句と同じ振る舞い、マージされる側の条件は常に置き換えられる)
Author.where(id: [david.id, mary.id]).merge(Author.where(id: bob)) # => [bob]
Author.where(id: david.id..mary.id).merge(Author.where(id: bob)) # => [bob]

つっつきボイス:「この間から@kamipoさんがmerge周りに手を加えてましたね」「この間のrewhere: trueあたりとか(ウォッチ20200525)」「今後振る舞いが少し変わるのか🤔」「今回はdeprecation warningを出すようにしたんですね😋」

関連PR: #39250と#39236
今回の変更の目的は、マージの振る舞いがこれまで一貫していなかったのを統一すること。
現在は、マージされる側(mergee)の条件がマージする側(merger)によって置き換えられるのは、双方のarelノードが等価条件またはIN句の場合のみだった。
言い換えると、マージされる側の条件が等価条件でもIN句でもない場合(betweengtltなど)、双方の条件は同じカラムであっても維持されていた。
これではマージの振る舞いに熟知していないと予測が難しい。
元々自分は、この振る舞いは意図したものではなく単なる実装上の問題だと推測していた。理由は、mergeより後に導入されたunscoperewhereの挙動はmergeよりも一貫性が高くなっているため。
等価条件やIN句は条件のほとんどを占めるのが普通なので、この問題を踏んだことのある人はほとんどいないだろうと推測しているが、一貫性に欠ける現在の振る舞いを非推奨化し、将来のUXを改善するために今後完全に統一したいと思う。
同PRより大意


「ところでRails 6.2っていつ頃出るんでしょう?🤔」「どうでしょう、新し目の機能もだいぶたまってきた感ありますし、そんなに先の話じゃないのかな?」「さっきのmerge周りとかは挙動が変わるので、今のうちにテスト書いておきたいですね☺️」「今作ってる機能が月末リリースなので早いとこRailsのバージョン上げたいです〜😅」「今月だと絶妙に微妙そう😆」

後でRailsのマイルストーンを見てみました。

  • マイルストーン: 6.1.0 Milestone -- 現時点で92件中残り21件
  • マイルストーン: 6.2 Milestone -- 現時点で3件中残り2件

「そういえばmergeの変更はコンフィグで現状維持できるんでしたっけ?🤔」「コンフィグあったかな...😅」「もしかすると今後コンフィグを追加するのかも?」「リリースまではまだ多少時間もありますし、そうするかもしれませんね☺️」「breaking changeならコンフィグ欲しいかも」

続報: rails new--minimalオプション機能がマージ(Ruby Weeklyより)


つっつきボイス:「この間の#39444--interactiveが入るかと思ったらこちらが先でした」「minimumじゃなくてminimalとは😆」「ほぼスッピンの形でrails newやれるようになるそうです」

# railties/lib/rails/generators/rails/app/app_generator.rb#300
+       if options[:minimal]
+         self.options = options.merge(
+           skip_action_cable: true,
+           skip_action_mailer: true,
+           skip_action_mailbox: true,
+           skip_action_text: true,
+           skip_active_job: true,
+           skip_active_storage: true,
+           skip_bootsnap: true,
+           skip_dev_gems: true,
+           skip_javascript: true,
+           skip_jbuilder: true,
+           skip_spring: true,
+           skip_system_test: true,
+           skip_webpack_install: true,
+           skip_turbolinks: true).tap do |option|
+             if option[:webpack]
+               option[:skip_webpack_install] = false
+               option[:skip_javascript] = false
+             end
+           end.freeze
+       end

「ミニマルにするとAction Mailerもスキップされるのか😆」「Active Strage、最初はなくてもいいかな😋」「JBuilderは今となってはなくてもいいでしょう😆」「Webpackも飛ばせるとは、まさしくミニマル😳」「すっぽんぽん🤣」「普通のAPIモードよりさらに禁欲的というか🤣」「システムテストも飛ばしてる🤣」「スースーしそう🤣」

参考: Rails による API 専用アプリケーション - Railsガイド

「『いいね👍』付けてる人がめちゃ多い」「嬉しい人は嬉しいでしょうね😋」「Railsに慣れた人がうんとささやかなAPIサーバーを作りたいときなんかはミニマルだとありがたいかも☺️」「JavaScriptも切るあたりがAPIを想定してるっぽいかも😋」

「インタラクティブなrails newはそれはそれでやるのかな?」「Railsの場合、機能同士の依存関係なんかもあるので、何を外すべきかを考えるのって割と難しいところはありますね😅」「あ、そうですね😳」「ミニマルで作っておいて、機能が必要になったら後から足す方が理にかなってそうですし🧐」

「そもそもどの機能を外したらいいのかが自分に見当が付かないという😆」「デフォルトだと機能が山盛りだからなおさら😆」「これとこれは競合するとか、これはこれに依存するとか」「機能を外したつもりなのに外れてなかったこともあった気がします😅」「Action TextがActive Storageに依存してるんでしたっけ、だとするとActive Storageを外したつもりでもAction Textを使うとまた入ってきたり😆」

Rails

RailsアプリをJS抜きで同じに作り直してみた


つっつきボイス:「この記事こないだ読んだんですけど意図がよくわからなかったかも😅」「前に作ったRailsアプリを、機能を変えずにJS抜きで作り直したのかな?🤔」「サーバーサイドだけのアプリケーションにしてみたという感じかも」

# 同記事より
# app/views/todos/_todo.html.erb

<div id="<%= dom_id(todo) %>" class="ToDoItem">
  <p class="ToDoItem-Text"><%= todo.name %></p>
  <%= button_to "-", todo_path(todo.id),
      method: :delete,
      remote: true,
      class: "ToDoItem-Delete"
     %>
</div>

「こんなふうに↑前はJSでDOM制御していた画面を、あえてサーバーサイドスクリプトを通して実行するようにAction Cableで書く、みたいな😆」「JavaScriptがキライなのが伝わってきそう😆」「断捨離というか😆」

「以下のhtml: render_to_stringのあたりなんかはHTMLでレンダリングしてますね: しかも書き換えは[:html]を指定してHTML置換してますし☺️」「男らしい💪」「カッコイイ✨」「フロントエンドの人たちから何か言われそうですけど😆」「想像できます😆」

# 同記事より
# app/controllers/todos_controller.rb

def create
  todo = Todo.new(todo_params)

  if todo.save
    cable_ready[TODOS_CHANNEL].insert_adjacent_html(
      selector: "#todo-list",
      position: "afterbegin",
      html: render_to_string(partial: "todos/todo", locals: {todo: todo}, formats: [:html])
    )
    cable_ready[TODOS_CHANNEL].set_value(
      selector: "#todo_name",
      value: ""
    )
    cable_ready[TODOS_CHANNEL].remove(
      selector: ".error"
    )
    cable_ready.broadcast

    return render(plain: "", status: :created)
  end

  cable_ready[TODOS_CHANNEL].insert_adjacent_html(
    selector: "#todo_name",
    position: "afterend",
    html: "<p class='error'>#{todo.errors[:name].first}</p>"
  )
  cable_ready.broadcast

  render json: {errors: todo.errors.to_h}, status: :unprocessable_entity
end

「まあ昔はこういう書き方が結構使われてましたね: Railsでも今はなきRJSとかで、JSを含むパーシャルとHTMLを含むパーシャルをサーバーサイドでいい感じに返して、それを使ってDOMを書き換えるようなコードは自分も書いてましたし🧐」「自分も当時そういうコードいっぱい書いてたら後の時代にフロントエンドの人に怒られました😅」「まあ治安の悪さを考えれば、この書き方がなくなったのもワカル😆」

参考: RubyOnRails を使ってみる 【第 7 回】 RJS を使ってみる

「でもあの当時はjQueryが一般的な時代でしたし」「そうですよね」「あの時代はJSフレームワークと言うと他にBackbone.jsぐらいしか見当たりませんでしたし、当時はいろいろしょうがないと思います😆」「時代が違うのでご勘弁を😆」


記事見出しより:

  • Action Cableをセットアップする
  • JavaScriptコードを消し去る
  • JavaScript抜きで機能を再実装する
  • 締めくくり

ActiveModel::AttributeAssignmentとは


つっつきボイス:「今日のWebチーム内発表で話題に出た機能です」「そうそう☺️」

「Railsのフォームに標準で入ってくるDate/Timeセレクタって、そのままだと『年』『月』『日』『時』『分』『秒』という6つのセレクトボックスができるんですけど、それをそのままPOSTすると当然ながら6つのパラメータに分解されてから送信されるので、それを組み立てる仕事をやってるのがこれだそうです」「へぇ〜😳」「hashアトリビュートになっている値を渡すとよしなにやってくれるらしいです😆」

# 同APIより
class Cat
  include ActiveModel::AttributeAssignment
  attr_accessor :name, :status
end

cat = Cat.new
cat.assign_attributes(name: "Gorby", status: "yawning")
cat.name # => 'Gorby'
cat.status # => 'yawning'
cat.assign_attributes(status: "sleeping")
cat.name # => 'Gorby'
cat.status # => 'sleeping'

「勉強会のお題ではActive Recordを使わないでActive Modelだけでやってたので、この機能を明示的に使う必要があったんだそうです」「へ〜、assign_attributes()ってここに実装されてるのね😳」「好きな人が多い機能でしたっけ」「assign_attributes()は普通によく使うヤツですね☺️」

後で調べると、assign_attributes()はRails 3.1のときにActive Recordに入ってたんですね。APIdockを見た感じでは、5.0のときにActive Modelに引っ越したようです。

参考: Rails 3.1: assign_attributesメソッド - Rails 雑感 - Ruby on Rails with OIAX
参考: assign_attributes (ActiveModel::AttributeAssignment) - APIdock

「そうそう、Qiitaの記事↓でいうとPOSTされたときはこんな感じデータになってたのを、このメソッドでいい感じにRailsのTimeWithZoneに変換してやってくれたということで」「Rails標準だとこういう形になるよねという話」

参考: 【Tips】Rails の assign_attributes は分割されたパラメータを飲み込む - Qiita

# Qiita記事より
params
=> {
    "name"=>"ダミー名前",
    "reserved_at(1i)"=>"2020",
    "reserved_at(2i)"=>"3",
    "reserved_at(3i)"=>"2",
    "reserved_at(4i)"=>"00",
    "reserved_at(5i)"=>"00"
  }

「記事はこの動作に興味を持って追ったんですね」「実際、POSTでやってくるこの謎パラメーターに一度は首を傾げますし😆」「1iとか2iとか😆」

geared_pagenation: 速度可変のページネーション(Ruby Weeklyより)


つっつきボイス:「Basecampが直々に出してきたgemのようです」「gearedというと回るギアの?⚙️」「自動車の変速装置というかトランスミッションみたいな動作をイメージしてるのかな🤔」

# 同リポジトリより
class MessagesController < ApplicationController
  def index
    set_page_and_extract_portion_from Message.order(created_at: :desc)
  end
end
# app/views/messages/index.html.erb

Showing page <%= @page.number %> of <%= @page.recordset.page_count %> (<%= @page.recordset.records_count %> total messages):

<%= render @page.records %>

<% if @page.last? %>
  No more pages!
<% else %>
  <%= link_to "Next page", messages_path(page: @page.next_param) %>
<% end %>

「なるほど、人間の性格として、ページネーションのあるページをオートスクロールするときなんかだと、2ページ目ではそんなにたくさん表示しなくてもいいけど、2ページ目まで開いた人なら3ページ目はどうせ開くだろうし、もっとたくさん表示して欲しいと思うでしょうから」「ふむふむ」「READMEにも書いてますけど、たとえばページのelementsは1ページ目なら15個でいいけど、2ページ目なら30個、3ページ目なら50個、4ページ目なら100個...みたいにだんだん増やしていく、というのをgeared pagenationと呼んでるんでしょうね😋」「な〜るほど!」

「実際ページのスレッドを追いかけて次々にページをめくっていると、どうせなら先に進むに連れて多めに読み込んで欲しいって思うことありますし☺️」「ときにはページネーションなしで一気に全ページ出して欲しいと思うときもありますけど😆」「それもわかる😆」「マウスホイールをゆっくり回したときと勢いよく回したときでスクロールの距離が違うみたいな🐭」「そんな感じ」

「なるほど〜という感じのgemですけど、これをサーバーサイドでやるのかという気持ちはちょっとありますね😆」「😆」

Railsでメモ化しない方がいい場合(RubyFlowより)

# 同記事より
def slow_method
  @result ||= perform_slow_method
end

つっつきボイス:「メモ化を使わないとき」「記事にもありますけど、いつも言ってる『それはメモ化してもセーフなのか?』というヤツ😆」「あ、なるほど」「クラス変数やクラスインスタンス変数をメモ化すると競合が発生する可能性がありますし、スレッドで使ったときもそうですし🧐」

「メモ化ってそんなに好きというほどじゃないかも😆」「もちろん何でもメモ化するのはよくありませんけど、Active Recordで毎回pluckで取ってきたりすると遅くなるデータもあるので、自分は状況に応じてメモ化を使いますね☺️」

Rails: pluckでメモリを大幅に節約する(翻訳)

Railsマイグレーションのup_only


つっつきボイス:「めちゃめちゃ短い記事なんですけど、こんなのあるって知らなかったので😳」「up_onlyですって😳」「で探してみたらkoicさんの少し前の記事が見つかりました↓」「up_onlyが当初RuboCopでアラート出たからCopに書き足してくれたのね😋」

up_onlyというとdownはやらないということでしょうか?」「でしょうね、データを更新するマイグレーションなんかだとdownしたくないこともありますし☺️」

その他Rails


つっつきボイス:「JetBrainsの動画記事です」「RubyMineは前からDocker Composeをサポートしてるけど新しい機能でも増えたのかなと思ったらチュートリアル動画ね☺️」「コメント欄でjnchitoさんが『これは素晴らしい!』と激賞していますね」「RubyMineとDocker Composeか〜😋」

JetBrains IDEのDocker Composeインテグレーション

「ところでRubyMineというかJetBrains IDEのDocker Composeインテグレーションは、右クリックでexecできたりしますし、なかなかよくできてますよ❤️」「おぉ😍」

「記事からリンクされてるこれ↑もいい機能ですし🥰」「リモートインタプリタ?」「つまりローカル環境にRubyがなくても、Docker Composeの中で動かすDockerコンテナの上にあるRubyをリモートインタプリタとして指定できます👍」「そしたらローカルにわざわざいろんなバージョンのRuby入れんでもええよねと😋」「そうそう、OpenSSH 1.0.いくつを使わないとコンパイルできないような古〜いRubyでもDockerコンテナに乗ってれば作業できますし😆」「この間の古いRubyコンパイル話っすね😆」


前編は以上です。

おたより発掘

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

週刊Railsウォッチ(20200609後編)Rubyにカスタマイズ可能な軽量fiberスケジューラを実験導入、RailsとGraphQL、DBについて知って欲しいことほか

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h


CONTACT

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