- Ruby / Rails関連
週刊Railsウォッチ(20200615前編)`rails new`に`minimal`がマージ、ARの共通集合を取る`and`、RubyMine+Dockerチュートリアル動画ほか
こんにちは、hachi8833です。オードリー・タンさんのPodcast聞いちゃいました😋。
台湾の IT 大臣がゲスト‼️スゴい‼️😳✨ #rebuildfm https://t.co/RhQ49a59K1
— 安川要平/Yohei Yasukawa (@yasulab) June 10, 2020
つっつきボイス:「Rebuild昨日やってたんですね」「どちらも英語のレベルも話のレベルも高くて脱帽でした🎩」「やはりリモートで対談」「見出しの『モナドは自分の仕事で必要というわけでもない』というあたりしか覚えてませんが😆」「こういう人のジョブがどんなのか興味あるな〜😋」
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内や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.au
やmyexample.com
にまでマッチしてたのか!」「それはイカン😅」「『cookieでは元々おかしなドメイン名は無視するから互換性はそんなに心配いらないだろうけど』とコメントにありますね」「ブラウザ側の実装上は一応大丈夫ということなのかな?🤔」「いずれにしろ正しい挙動にするのがいいですね👍」「行儀が悪いのを直したと」
⚓新機能: Active Recordでリレーション名.and
をサポート
- PR: Support `relation.and` for intersection as Set theory by kamipo · Pull Request #39558 · rails/rails
集合論で言う「共通集合」を取れるそうです。
つっつきボイス:「@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句でもない場合(between
やgt
やlt
など)、双方の条件は同じカラムであっても維持されていた。
これではマージの振る舞いに熟知していないと予測が難しい。
元々自分は、この振る舞いは意図したものではなく単なる実装上の問題だと推測していた。理由は、merge
より後に導入されたunscope
やrewhere
の挙動は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ぐらいしか見当たりませんでしたし、当時はいろいろしょうがないと思います😆」「時代が違うのでご勘弁を😆」
- サイト: jQuery
- サイト: 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マイグレーションの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コンパイル話っすね😆」
前編は以上です。
おたより発掘
rails new --minimalは欲しかったやつ https://t.co/HyOeUsQvqm
— lhside (@lhside) June 15, 2020
バックナンバー(2020年度第2四半期)
週刊Railsウォッチ(20200609後編)Rubyにカスタマイズ可能な軽量fiberスケジューラを実験導入、RailsとGraphQL、DBについて知って欲しいことほか
- 20200608前編 RubyKaigi 2020が開催中止に、ネステッドSTIを避けるべき理由、rails newがインタラクティブになるかもほか
- 20200602後編 JSONストリームパーサーyajl-ruby、ruby-buildとopenssl、GoogleのCloud SQL、Rubyと機械学習ほか
- 20200601前編 Active Recordに新機能「delegated typing」追加、RuboCopのデフォルト設定アンケートほか
- 20200526後編 Rubyでよくやるスレッドバグ、Kubernetesでよくあるミス10、CSS/SVG/Canvasの使い分けほか
- 20200525前編 2020年のRailsマストgem 19個、スライド『Fat Modelの倒し方』、AR mergeのrewhereオプションを変更ほか
- 20200519後編 Rails 5と6のセキュリティ修正、Ruby 3.0のGuildがRactorに名前変更、Node作者によるDeno登場ほか
- 20200518前編 スライド『令和時代のRails運用』、Ruby 3.0のキーワード引数変更リスケ、Action CableのCLIほか
- 20200512後編 RubyのPStoreライブラリ、Lambda StoreのサーバーレスRedisは有能、Amazon Linux 2のライブパッチほか
- 20200511前編 Rails 6.0.3リリース、rails newに–masterオプションが追加、system specとfeature specの違いほか
- 20200428後編 Rubyのバックトレース順序が戻る、KubernetesでRailsをスケール、セキュリティソフト入れますか?ほか
- 20200427前編 Railsで避けたい8つのミス、ridgepole導入の注意点、RDS ProxyのPostgreSQL対応ほか
- 20200421後編 Ruby 2.4サポート終了、Ruby 3の右代入演算子、GitHubコア機能無料化ほか
- 20200420前編 anyway_config gemでRails環境設定、ShopifyのLiquidテンプレートエンジン、書籍『Beyond the Twelve-Factor App』ほか
- 20200414後編: Ruby 3で”endレス”メソッド定義構文が追加、ECMAScript 2020の新機能、紛失防止デバイスほか
- 20200413前編: 最近macOSでRailsが遅い、トランザクションでのreturnやbreakなどが非推奨化、Rails監視ツールリスト2020年度版ほか
- 20200407後編: RubyのTracePointでデバッグ、Rubyとモナド、Gitノウハウ集、リモートワークほか
- 20200406前編: Ruby 2.7.1セキュリティ修正、RailsビューHTMLにテンプレート名を出力、Action Mailboxテスト用フォーム改良ほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。