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

Rails: ビューでstrftimeを直書きするのはたぶんよくない(翻訳)

$
0
0

概要

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

Rails: ビューでstrftimeを使うとよくない場合がある(翻訳)

strftimeメソッドはさまざまな言語にありますが、いずれもC言語が由来です。書式を指定する引数の(ほとんど暗号のような)構文は長年変わっておらず、驚くほど多くの言語で一貫しています。

Rubyのstrftimeの書式はTimeクラスで網羅されています。

ただし、strftimeをビューで使うと動作が一貫せず混乱する可能性が高まります。

以下のように書くのではなく

ビューで日付や時刻の書式をstrftimeで設定する。

<%= @user.last_signed_in_at.strftime("%m-%e-%y %H:%M") %>

以下のように書く

日付や時刻の書式はRailsの組み込みメソッドで設定する。

つまり私がやったようにDateTimeで独自の:stampフォーマットを作成する。

  • config/initializers/time_formats.rb
Date::DATE_FORMATS[:stamp] = "%Y%m%d"       # YYYYMMDD
Time::DATE_FORMATS[:stamp] = "%Y%m%d%H%M%S" # YYYYMMDDHHMMSS

続いてビューで以下のように書く。

<%= @user.last_signed_in_at.to_s(:stamp) %>

そうする理由

引数がややこしくて間違えそうになるメソッドをビューに直書きすると、一貫性が落ちる原因になります。

日付や時刻の書式をアプリケーショングローバルに定義する主なメリットは、後から参加する開発者(未来の自分も含む)が助かることです。これによって、現在時刻の表現方法を事前に少数の方法にまとめてアプリケーション全体で一貫させることができます。今後参加する開発者も独自のフォーマットを構築できます。

表記が一貫することで、アプリケーションのユーザーもメリットを得られます。日付や時刻が常に同じ方法で表現されれば、ユーザーが日付や時刻を読み解く手間も削減されます。「11 Jun」ではなく常に「Jun 11」で統一するのは一見些細なことではありますが、手間をかける価値があります。

自分の欲しい日付フォーマットをずばり見つけられる便利なサイトもいろいろありますので、ここでいくつかご紹介します。

strftimeのフォーマット文字列をビジュアル表示で気持ちよく参照できます。アプリケーション全体で使える実際のデータ形式を生成できます。

実際の文字列で表現された日付や時刻をコピペすると、その文字列の書式を設定できるstrftime形式の日付文字列を得られます。

DateTimeがデフォルトで提供するフォーマットは私には到底覚えられませんし、そもそもドキュメントがめったに見当たりません。そこで、RailsのDateTimeフォーマットをスペルアウトする以下のサイトを自分でこしらえました。

そうしない理由があるとすれば

ビューでstrftimeを呼び出しても実際のパフォーマンスは低下しません。この記事の方法は一貫性と整理のためのものです。

訳注

以下の記事ではDATE_FORMATS[:default]は改変しない方がよいと指摘されています。なお上の記事は改変ではなく独自の:stampを追加しています。

また同記事では、I18nを使う方法も紹介されています。

関連記事

[Ruby/Rails] strftimeのよく使うテンプレート


週刊Railsウォッチ(20191125)Ruby 3.0は2020年12月にリリース決定、Rails 5.2.4rc2とRuby 2.7.0-preview3がリリースほか

$
0
0

こんにちは、hachi8833です。先週発熱してしまいました💊。今週のウォッチはいつもより短くなっています🙇

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

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

第16回目公開つっつき会は、来週12月05日(木)19:30〜にBPS会議スペースにて開催されます。

週刊Railsウォッチの記事やここだけの話にいち早く触れられるチャンス!発言・質問も自由です。引き続き皆さまのお気軽なご参加をお待ちしております🙇

⚓Rails

⚓Rails 5.2.4rc2がリリース(Ruby公式ニュースより)

Changelogを見た限りでは、修正この間の6.0.1よりも少なめで、2017年などやや古い修正も含まれています。セキュリティ関連の修正は見当たりません。

PR: Make ActiveSupport::Logger Fiber-safe by cmrd-senya · Pull Request #36753 · rails/rails

Rails 6.0.1がリリース!修正を追ってみました

⚓Rails 6のActive Record新機能10(Ruby Weeklyより)

目次より:

  1. rails db:prepare
  2. rails db:seed:replant
  3. データベース自動切り替え
  4. Enumのnot_*スコープ
  5. #extract_associated
  6. #annotate
  7. #touch_all
  8. #destroy_by#delete_by
  9. #whereでのエンドレスレンジ..
  10. implicit_order_column

⚓Ruby

⚓Ruby 2.7.0-preview3がリリース

preview2からの変更点はそれほどありませんので、違いのみピックアップします。

キーワード引数について

  • preview3リリースの主な目的は、キーワード引数の互換性確認用。
    • キーワード引数関連のwarningが冗長という指摘があり、「deprecation warningをデフォルトで無効にする(#16345)」か「deprecation warningを減らす(#16289)」の2つのソリューションを議論中。まだ決定は下されていないが、最終リリースでは修正の予定。

つまりpreview3で互換性を開発者にどしどし確認して欲しいということですね。

取り消された機能

  • experimentalのメソッド参照演算子.:Feature #12125, Feature #13581, Feature #16275)が取り消し
  • Symbol#to_sModule#nametrue.to_sfalse.to_snil.to_sが常にfrozenを返す変更(Feature 16150)が取り消しに
  • regexp#match?にnilを渡した場合に(StringやSymbolと同様に)TypeErrorを返す変更(Feature #13083)が取り消し

デフォルトgem

上はすべて11/06から公開されています。

  • 以下のデフォルトgemはruby-coreに昇格のみ、rubygems.orgでは未公開
    • monitor
    • observer
    • timeout
    • tracer
    • uri
    • yaml

その他

  • MonitorクラスとMonitorMixinモジュールのパフォーマンス改善(Feature #16255

⚓RubyConf 2019に参加した

先週ナッシュビルで開催されたRubyConf 2019のNoah Gibbsさんによる記事です。

抜粋:

  • Ruby 3.0の来年12月リリースが決定したことが公式にアナウンスされた
  • Fiberを用いたasyncなgemがRubyコアに多数入りそう
  • 個人的にArtichokeが面白そう
  • rubyfmtは発展途上だがアツい(以下の記事参照)

Matzのキーノート動画(54m52s)を覗いてみたところ「よほどのことがない限り来年12月にRuby 3.0をリリースする」「期限切ったことがとても重要」「もしかしたら機能をいくつか落とすかもしれないけど」「頑張って締め切り守る」とのことでした。

RuboCop作者がRubyコードフォーマッタを比較してみた: 前編(翻訳)



rubyconf.orgより

⚓その他Ruby


今週は以上です。

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

週刊Railsウォッチ(20191119後編)メソッド参照演算子が廃止、GitHub新機能続々、平成Ruby会議、GitHub OAuthバイパスほか

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

銀座Rails#15で「出張Railsウォッチ」発表させていただきました

$
0
0

morimorihogeです。涼しいなあと思ってたらもう冬並の寒さじゃねーか!という昨今ですね。

2019/11/15(金)に開催された銀座Rails#15 @リンクアンドモチベーションでTechRachoの週刊Railsウォッチの出張版ということで参加させていただきました。スライドは以下の通りです。

Rails6新機能シリーズは一息ついたので、今回は普段Rails開発をしていてコミュニケーションミスになってるんじゃないかなーと思っていたproduction / staging / developmentという言葉の定義や使われ方についてを話しました。
この辺の話ってあんまり話題になっているのを聞いた記憶はないのですが、最近はチャットベースのコミュニケーションが増えたり、エンジニアは人の出入りが激しくなっているので認識齟齬が起きやすくなっている界隈なのではないかと勝手に思っています。

他にも、受託開発だととてもたくさんのRailsプロジェクトを渡り歩くことが多いのですが、Staging環境の取り扱いなんかもかなりプロジェクトによる個性が強いところだと思います。
SIer方面の経験がある方なんかは比較的本番にしっかり近い環境を整える傾向が強いですが、Web系で1人開発なんかをメインでやってきた人なんかはそもそもStagingと開発環境の区別すらなかったりなど、一言でStagingといってもかなりのギャップがありますね。

その後の懇親会などでも話を聞いていた所、このネタは思いの外好評だったので、近いうちに一本記事にまとめてみようと思います。

次回は次回の銀座Rails#16 @リンクアンドモチベーションは12/13(金)19:00~となります。
ピックアップトピックはまた開発・運用系から何か考えようと思っていますが、もし何か取り上げてほしいネタなどありましたらTwitterにて @morimorihoge までご意見お寄せ下さい。

週刊Railsウォッチ 公開つっつき会のお知らせ

来週12/5(木)19:30より、弊社会議スペースにて毎月恒例の週刊Railsウォッチ 公開つっつき会を開催します。ぼちぼち忘年会シーズンでお忙しいかと思いますが、もしよろしければご参加検討下さい。

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。画像はすべて元記事からの引用です。
原文の目次は省略しました。原文の乱れは訳文で修正してあります。

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

👋皆さんこんにちは。本記事では私がRubyやRuby on Railsを学んだ一環として、ドキュメントに書かれていないことを全部盛りしました。RubyやRailsのエコシステムは初めてなので、皆さまからのフィードバックを心よりお待ちしております。お気づきの点がありましたら元記事にコメントをどうぞ。

Ruby on Railsのためのチュートリアルの大半はAPIの利用方法にフォーカスしていますが、これは理にかなっています(例: Rails をはじめよう - Rails ガイド)。

しかしrails new blogしてコントローラを編集し、git push herokuを何度かやってみるまではいいとして、そこから先はどうすればいいのでしょうか?私はその先に進みたかったので、以下についてのベストプラクティスが欲しかったのです。

  • 開発用セットアップ
  • RubyやRuby on Railsのコードエディタの設定
  • アセットの設定やオートリロード(Webpacker)
  • 再現可能なビルド、安全な環境
  • もう少し学べる書籍をいくつか

自分はこれらの点について相当じたばたしました。というのもオンライン上での議論がほとんど見当たらないからです。つまりこれらの点については「実際に体当たりで使って学ぶのが普通」ということです。

私はRailsアプリケーションを学んでデプロイするときに、自分がハマった小さなワナや、上述の課題をすべて解決するために追加しなければならなかったコード片を、以下のようにすべてREADMEファイルに盛り込むところから始めました。

ある時点からこのリストはどんどん膨れ上がってきたので、次にRailsアプリケーションを始めなければならなくなったときに「自分が使える資料にいつでも手が届く」状態にすべく書き直す必要を感じました。

このメモ書きを公開することで私の知見を共有し、(願わくば)皆さまのお役に立てられればと思います。

エディタとVS Codeのセットアップ

私はJavaScript開発者だったので、それもあってVisual Studio Codeを使っています。JetBrainsの「Ruby 2019 State of the developer ecosystem」↓を見ると、RubyMineのようにもっとよさげなエディタがあるようですね。お使いの方はご感想を私の記事のコメントまでお知らせください。

エディタの設定

シンプルな割には忘れがちですが、VS CodeはEditorConfigをサポートしています。プロジェクトのルートディレクトリに.editorconfigファイルを作成しておけば、VS Codeを使っているどのユーザーでもインデント設定や改行コードをマスター設定にできます(訳注: VS Codeの場合はEditorConfig拡張のインストールが必要です)。

最小限の.editorconfigファイルを作成します。

# editorconfig.org
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

これでプロジェクトの他のコントリビューターもEditorConfigを使えるようになります。

VS Codeの拡張

VS CodeでのRailsやRubyを気持ちよく編集するために、以下の拡張を使っています。

これらの拡張を楽にインストールできるよう以下のフォルダとファイルを作成します。これでRailsプロジェクトのメンバー全員に拡張がおすすめされ、インストールをいちいち依頼しなくてもよくなります。

  • .vscode/extensions.json:
{
  "recommendations": [
    "rebornix.Ruby",
    "kaiwood.endwise",
    "vortizhe.simple-ruby-erb",
    "bung87.rails",
    "ninoseki.vscode-gem-lens"
  ]
}

これで、プロジェクトをVS Codeで開けばこれらのチーム用拡張をインストールするようヒントが表示されます。

VS Codeの設定

さらに、RubyやRailsプロジェクトの開発効率を最大限に高めるため、以下のプロジェクト設定を使っています。

  • .vscode/settings.json
{
  "ruby.useLanguageServer": true,
  "ruby.useBundler": true,
  "ruby.intellisense": "rubyLocate",
  "ruby.format": "rufo",
  "files.associations": {
    "*.html.erb": "html"
  },
  "[html]": {
    "editor.defaultFormatter": "vscode.html-language-features"
  },
  "editor.formatOnSave": true
}

これで、.rbファイルや.erb.htmlをエディタで保存するとRubyやRailsのコードが自動的にフォーマットされるようになります。

これは現時点で私が見出したベストのセットアップです。最終的にはPrettierとそれ用のRubyプラグインがベストなオプションになるかと思いますが、記事執筆時点では.erb.htmlのフォーマットがサポートされていません(#371)。

  • フォーマットがよく効くように以下をGemfileに追加しておきます。
group :development do
  # [...]
  gem 'rufo', '~> 0.7.0', require: false
end

これで、.rbファイルや.erb.htmlファイルを保存するたびに自動フォーマットされるようになります。

Webpackerの使い方

このセクションでは[S]CSSについて説明します。この説明はすべてのSCSSファイルとCSSファイルに適用できます。Webpackerは複数のフォーマットをサポートしています(webpacker/css.md at master · rails/webpacker)。

当初、Webpackerについて以下の理由で混乱しました。

Rails 6からRailsアプリケーションにWebpackが同梱されて組み込めるようになりました。これはWebpacker経由で行われます。Webpackerは、事前に設定されたWebpackにビューヘルパーを加えたものを提供することで、生成されたアセット(JavaScriptや[S]CSSファイルなど)を簡単に対応付けられるようにします。

2019年11月の時点では、デフォルトのRailsアプリケーションのerbテンプレートファイルには以下が含まれています。

  • app/views/layout/application.erb.html
  <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track": "reload" %>
  <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

2行目は、app/javascript/packs/application.jsというエントリポイントから生成されたコンパイル済みJavaScriptファイルパスをWebpackerから取得します。

しかし1行目は、現在のアセットからのファイル生成を片っ端からアセットパイプラインに指示します。

このようにシステムが二重になっていることで混乱しました。何か理由はあるのかもしれませんが、ここではWebpackerのみを用いてアセットを生成するよう修正しましょう。

[S]CSSファイルをWebpackerで扱う

[S]CSSファイルをWebpackerで扱う(Rails 6でBootstrap 4を使うなど)には、CSSをインクルードする行を以下のように変更する必要があります。

  • app/views/layout/application.erb.html
  <%= stylesheet_pack_tag "application", media: "all", "data-turbolinks-track": "reload" %>

これにより、「application」という名前のパックについてWebpackerがスタイルシートアセットの正しいURLを提供するようになります。現在のメインのパックのファイル名をアプリケーションが実際に参照するときの名前はapp/javascript/packs/application.jsです。

stylesheet_link_tagstylesheet_pack_tagに変更されていることにお気づきでしょうか。

次に、メインのSCSSファイルを作成します(実際のファイル名や置き場所は何でも構いません)。

  • app/javascript/src/style.scss
@import "~bootstrap/scss/bootstrap"; // Bootstrapを使う予定なら

次にJavaScriptのimport文を書きます。

  • app/javascript/packs/application.js
// [...]
import "../src/style.scss";

このスタイルシートは、実際にはこのJavaScriptファイルでは読み込まれません。この記述は、単にWebpackにコンパイルを指示して、Railsアプリケーションのテンプレートでrequireのパスを指定できるようにするためのものです。

Rails 6でBootstrap 4を使う

先ほどの最後の部分を@import "~bootstrap/scss/bootstrap";に変えて動かすには、Bootstrapをインストールする必要があります。ただしGemfileに書くのではなく、Yarnを使います。~を付けた~bootstrapという記述は、node_modules/bootstrap/...以下からファイルを探すようWebpackに指示するためのものです。それでは追加しましょう。

  • ターミナルで以下を実行します
yarn add bootstrap

BootstrapのJavaScriptを動かすには、JavaScriptの依存関係もインストールする必要があります。

  • ターミナルで以下を実行します
yarn add jquery popper.js

インストールしたファイルをJavaScriptアプリケーションでrequireします。

  • app/javascript/packs/application.js
require("jquery");
require("bootstrap");

[S]CSSやJavaScriptファイルのオートリロード

私にとってWebpackの強みのひとつといえばオートリロード機能ですが、デフォルトのRails 6では無効になっています。オートリロードでは、developmentモードですべてのページの更新が常に読み込まれます。オートリロードを有効にするにはwebpack-dev-serverを使う必要があります。ありがたいことに、webpack-dev-serverは新しいRails 6アプリケーションには既に同梱されているので、これをRailsサーバーと一緒に起動するだけで使えます。

  • ターミナルで以下を実行します
./bin/webpack-dev-server

私はdevelopment用サーバーでは1つのコマンドで実行できるようにするのが好みです。rails server./bin/webpack-dev-serverをパラレルに起動できれば理想的です。昨今ならProcfile.devhivemindで今風にやると楽にできそうです。

hivemindは基本的にforemanですが、「よくできています」。私はインターネット上のツールを信頼しているのでこれを使っています。必要な作業は以下のとおりです。

  • Procfile.devを作成します
web: rails server
webpacker:  ./bin/webpack-dev-server
  • .envを作成または更新します
HIVEMIND_PROCFILE=Procfile.dev

これで、rails serverrails sをローカルで起動する代わりに、以下のようにhivemindでやれます。

  • ターミナルで以下を実行します
brew install hivemind
hivemind

おめでとう、これでhttp://localhost:5000にアクセスすればWebサーバーが実行されます。組み込みのオートリロードも高速です。


1つ問題があったのは、WebpackerがYarn integrityチェックを頻繁に行うことでdevelopment環境で遅くなる点でした。私のYarn依存関係は最新かつ常に同じに保たれていることを確認してあるので(次の「再現可能な環境」を参照)、Yarn integrityは無効にしました。

Webpackerを使えばいろんなことができるのですが、メインのRailsドキュメントには情報がありません。Webpackerについて学ぶには以下がおすすめです。私はWebpackerについて必要なこと(特にフォルダ構造webpack-dev-server、 CSS)はすべてここで学びました。

訳注: 以下の記事も参考にどうぞ

【保存版】Rails 5 Webpacker公式ドキュメントの歩き方+追加情報

自動テストとデスクトップ通知

私はJavaScript方面から来たので、JestというJavaScript例外テストツールを使っていました。これは今やJavaScriptアプリケーションのテストでは定番です(他のものを使っても別に構いません)が、RubyやRuby on Railsのテストにはさまざまな方法があります。ほとんどの場合自分に必要なのは、テストの自動実行と、テストがパスまたは失敗した場合のデスクトップ通知です。

テストの自動実行ツールは皆さんも既にネットでいろいろ見つけていることでしょう。たとえば「Automating Minitest in Rails 6」はよい出発点です。

でも自分は以下のようなデスクトップ通知がどうしても必要欲しいのです。

これならエディタを切り替えずにテストの状態をチェックできます。

最も簡単に通知を実現する方法はterminal-notifierとterminal-notifier-guardを使うことだとわかってきました。

必要な作業は、Gemfileのdevelopmentグループとtestグループに以下を追加することだけです。

gem 'terminal-notifier', '2.0.0'
gem 'terminal-notifier-guard', '1.7.0'

これでbundleを実行すれば、guardがこれらのgemを自動検出してナイスな通知を表示してくれます。


本記事では当初Growlを推していましたが、理由はterminal-notifierがその時点でうまく動かなかったためでした。しかしコメントで再挑戦を勧められて、今度はうまくいきました。

もちろんGrowlでも問題なくやれますので、既にお使いの方はそのままどうぞ。

再現可能な環境

私はNode.jsやJavaScript方面の出身なので、「再現可能な環境」に必要な正しい習慣を身に付けてきました。

再現可能な環境(reproducible environments)についてご存知ですか?Travis CIで動かそうがHerokuで動かそうが、はたまたMacbookで動かそうが、常に同一の依存関係、プラットフォーム、言語バージョンで実行するべきです。MacbookではYarn 1.9にRuby 2.6にNode.js 12.3.4という依存関係で動かしつつ、HerokuではYarn 1.1にRuby 2.4にNode.js 12.1.0という依存関係で動かしたいことなどありえません。理想は、どのバージョンについてもインストールやメンテナンスやアップグレードを簡単に行えることです。

bundler、Rails、Ruby、Node.js、Yarnもこれに該当します。次のセクションで方法を説明します。

再現可能な環境: Ruby編

  1. rbenvでさまざまなバージョンのRubyを簡単にインストールできるようにする
  2. .ruby-versionファイルでRubyバージョンを全プラットフォームで強制的に設定する
  3. Gemfileを変更して、bundler実行時にRubyのバージョンを強制的に設定する
  4. 依存関係のバージョンを抽出する(いわゆる依存関係のピニング(pinning)

1. rbenvをインストールします

2. .ruby-versionファイルを以下の内容で作成します

2.6.5

3. GemfileのRubyバージョンを、Bundlerで使うRubyバージョンと合わせます

ruby '2.6.5'

4. GemfileのRuby依存関係で正確なバージョンを指定します

gem 'webpacker', '~> 4.0'

具体的には、上を以下のように変更します。

gem 'webpacker', '4.2.0'

ありがたいことにVS Codeにインストールした拡張のおかげで、Ruby依存関係の最新版を簡単に確認できます。Gemfileの該当行にマウスオーバーするだけで以下のように表示されます。

依存関係のピニングはデリケートな問題です。ありがたいことに、Gemfileやbundlerでバージョンを指定するときのさまざまな戦略のメリットやデメリットについてうまくまとめた良記事がありますのでご覧ください。

再現可能な環境: Node.jsとYarn編

RailsがWebpackに依存するようになったことで、Node.jsとYarnにも依存するようになりました。これは驚きですね。つまり、それぞれのバイナリで使うバージョンも正確に指定する必要があります。

以下の要領で進めます。

  1. インストールして使う.Node.jsのバージョンは.nvmrcファイルで指定する。HerokuもTravis CIも、利用するNode.jsのバージョンをこれで推測する。
  2. インストールして使うYarnのバージョンはYarnポリシーで指定する。これはYarn 1.13.0以降であればどこでも使えます。Yarn policyは誕生して1年ですが、安全です。
  3. (Webpacker経由のWebpackなどのように)Node.jsスクリプトを実行するYarnやNode.jsのバージョンは、package.jsonのenginesフィールドで検証される。
  4. 依存関係のバージョンを抽出する

1. nvmをインストールします

続いて以下の内容で.nvmrcファイルを作成します。

12.13.1

本記事をお読みの方は、Node.jsの最新バージョンをNode.jsサイトで確認してお使いください。

2. ターミナルで以下のYarnポリシーを設定します

yarn policies set-version 1.19.1

これでYarn 1.19.1がダウンロードされて.yarnrcファイルが作成され、Yarn 1.19.1を使うようYarnのバイナリに指示されます。いいですね〜❤️。繰り返しますが、本記事をお読みの方は、Yarnの最新バージョンをYarnサイトで確認してお使いください。

最新のLTS版Node.jsも利用できます。

3. package.jsonを以下のように更新します

{
  // [...],
  "engines": {
    "node": "12.13.1",
    "yarn": "1.19.1"
  }
}

4. package.jsonの依存関係をすべて正確なバージョンにピニングします

"@rails/webpacker": "^4.2.0",

具体的には、上のバージョンを以下のように変更します。

"@rails/webpacker": "4.2.0",

次のセクションでは、依存関係を実際にピニングできるツールについて説明します。

これでマスターとなるNode.jsとYarnの環境ができました!

依存関係の自動アップデート

ご覧のように、再現可能な環境づくりの一環として、私はいついかなるときでも依存関係のバージョンはピンポイントで指定しています。なにかのはずみで、環境やプラットフォームの依存関係バージョンがちょっと違ってしまうという事態になって嬉しい人などいません。そんなことになれば時間をドブに捨てるはめになります。

もちろん、Gemfile.lockやyarn.lockのようなロックファイルのおかげで大半の問題は解決できますが、それだけでは不十分です。ロックファイルを使っても失敗する可能性がある(参考)というだけでなく、私はいついかなる場合でも現在の直接的な依存関係も正確に知っておかないと気が済まないのです。今使える機能や今あるバグの両方について知りたいときに、package.jsonやGemfileの”~2.x”みたいなバージョン表記の背後で何が起きているのかを解明するのに謎のコマンドを調べまくるはめになりたくないのです。

しかし、たまには依存関係のアップグレードもやりたいのです。それも一切つらい目に遭わずに。JavaScriptやRubyの依存関係の自動アップデートについては、Renovateを一度チェックしてみることをおすすめします。

Herokuと、Yarnインストールかぶり現象

Herokuにデプロイする場合、Herokuの以下のガイドがよくできているので、これに従いましょう。

私の場合、Herokuにデプロイ中にYarnインストールが複数回トリガされるというおかしな動作に遭遇しました。ひとつはHerokuでpackage.jsonファイルが検出されたとき、もうひとつはWebpackerでアセットをバンドルしているときです。これでは困るので、これらのタスクを以下のように分割したいと思いました。

  1. Yarn依存関係をインストール
  2. Ruby依存関係をインストール
  3. アセットのコンパイル

タスク1.と2.はHerokuで自動検出されますが、自分で順序を指定することもできます。これを行うには、buildpacksとbuildpacksの順序を自分で設定し、最初にNode.jsのを、次にRubyのを使うようにしなければなりません。

  • ターミナルで以下を実行します
heroku buildpacks:clear
heroku buildpacks:add heroku/nodejs
heroku buildpacks:add heroku/ruby

今度はWebpackerによるYarnインストールをすべて無効にしなければなりません。Rakefileに以下を追加します。

Rake::Task['yarn:install'].clear
namespace :yarn do
  desc "Disabling internal yarn install from Rails"
  task :install => [:environment] do
    puts "Disabling internal yarn install from Rails"
  end
end

Rake::Task['webpacker:yarn_install'].clear
namespace :webpacker do
  desc "Disabling internal yarn install from Rails"
  task :yarn_install => [:environment] do
    puts "Disabling internal yarn install from Rails"
  end
end

この問題(#744)を調べてくれたRebecca Lynn Cremonaに感謝いたします。

RubyやRailsを学ぶのに最適なリソース一覧

これについては別記事に切り出しました。

解説は以上です。

Node.jsの経験を元にRails世界に飛び込んでみましたが、全般的に楽しめました。本記事で指摘した問題がいずれ解消しますように。そのときには本記事を更新いたします。本記事で取り上げたトピックについてもっと詳しい方がいらっしゃいましたら、ぜひ元記事にコメントいただくかvincent@codeagain.comまでお知らせください。

本記事がどこかでお役に立ちましたら、他の方にもおすすめください。

お読みいただいた皆さまに感謝いたします。

関連記事

Rails 6のDocker開発環境構築をEvil Martians流にやってみた

週刊Railsウォッチ(20191202)Rails 6のimplicit_order_columnはカスタマイズ可能、rubocop-rails 2.4.0リリース、Capistrano記事ほか

$
0
0

こんにちは、hachi8833です。git.ruby-lang.orgが先週から不調なようです。


つっつきボイス:「(DevToolsを覗いて)これは大変そう…😅裏でめっちゃ作業してそうな雰囲気」「お祈りします🙏

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

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

第16回目公開つっつき会は、今週12月05日(木)19:30〜にBPS会議スペースにて開催されます。

週刊Railsウォッチの記事やここだけの話にいち早く触れられるチャンス!発言・質問も自由です。引き続き皆さまのお気軽なご参加をお待ちしております🙇

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

先週のウォッチに載せられなかった分も含んでます🙇

⚓params.member?を追加してHashの振る舞いに近づけた

実際はdelegate:member?を追加しただけの改修です。

# actionpack/lib/action_controller/metal/strong_parameters.rb#L214
-   delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?,
+   delegate :keys, :key?, :has_key?, :member?, :values, :has_value?, :value?, :empty?, :include?,
      :as_json, :to_s, :each_key, to: :@parameters
# actionpack/test/controller/parameters/accessors_test.rb#165
  test "member? returns true if the given key is present in the params" do
    assert @params.member?(:person)
  end

  test "member? returns false if the given key is not present in the params" do
    assert_not @params.member?(:address)
  end

つっつきボイス:「paramskey?が使えるならmember?もある方がいいという感じでしょうか?」「Rubyのハッシュにmember?なんてあったっけ?」「Rubyの公式ドキュメント↓を見ると、key?member?include?has_key?も互いにエイリアスになってる😆」「あそうか😳」 「それなら確かに全部使える方がいいですよね😋」「むしろなぜmember?だけなかったのかと😆」「4つもあるとは😆」「後からRubyに足されたとかかしら?🤔」「member?使ったことないな〜😆

参考: instance method Hash#has\key? (Ruby 2.6.0)


docs.ruby-lang.orgより

⚓コレクション読み込みチェックを最初のレコードではなく全レコードでチェックするよう修正

現在、コレクションが読み込まれたかどうかは最初のレコードしかチェックしていない。特定のシナリオでafter_initializeフック経由でレコードをフェッチした場合、コレクションの最初のレコードは読み込まれていても残りのレコードが読み込まれていない場合がありうる。
データをフェッチするときの短絡を成功させるには、コレクション内の全レコードが読み込まれたことを検証する必要がある。
これにより、関連付けを参照するSTIがafter_initializeでプリロードに失敗する問題(#37730)が修正される。
同PRより大意

# activerecord/lib/active_record/associations/preloader.rb#L186
        def preloader_for(reflection, owners)
-         if owners.first.association(reflection.name).loaded?
+         if owners.all? { |o| o.association(reflection.name).loaded? }
            return AlreadyLoaded
          end
          reflection.check_preloadable!
          if reflection.options[:through]
            ThroughAssociation
          else
            Association
          end
        end

つっつきボイス:「たしかにlazy loadされているとこうなる😆」「firstだったのをall?にして、ちゃんと最後まで読み出せるかどうかを確認するように修正したと」「short circuitはとりあえず『短絡』としてみたんですが、どう表すのがいいのか迷い中です😅」「追加された以下のテストをちゃんと読めばfirstだと落ちるということがわかるんでしょうきっと😆」「踏んだことないエラー🤔」「STIでpolymorphic associationを使った場合のafter_initializeという組み合わせか〜」

# activerecord/test/cases/associations/eager_test.rb#629
  def test_preloading_with_has_one_through_an_sti_with_after_initialize
    author_a = Author.create!(name: "A")
    author_b = Author.create!(name: "B")
    post_a = StiPost.create!(author: author_a, title: "TITLE", body: "BODY")
    post_b = SpecialPost.create!(author: author_b, title: "TITLE", body: "BODY")
    comment_a = SpecialComment.create!(post: post_a, body: "TEST")
    comment_b = SpecialComment.create!(post: post_b, body: "TEST")
    reset_callbacks(StiPost, :initialize) do
      StiPost.after_initialize { author }
      comments = SpecialComment.where(id: [comment_a.id, comment_b.id]).includes(:author).to_a
      comments_with_author = comments.map { |c| [c.id, c.author.try(:id)] }
      assert_equal comments_with_author.size, comments_with_author.map(&:second).compact.size
    end
  end

#37730 issue↓を見る方が早そう」「見事歯抜けに🦷」「ステップ実行とかやってみたら原因追えるかも🤔


#37730より

⚓初期化時に訳文をeager loadingするよう修正

訳文読み込みによる初期レスポンスの速度低下を回避するため、アプリ初期化中に訳文をeager loadingする。
同PRより大意

# activesupport/lib/active_support/i18n_railtie.rb#L13
    config.i18n.load_path = []
    config.i18n.fallbacks = ActiveSupport::OrderedOptions.new

+   config.eager_load_namespaces << I18n
+

つっつきボイス:「i18nの訳文を後から読み込むと一発目の表示が遅くなるので修正したと」「たしかに〜」「最初に読み込むと今度はその分Railsサーバーの起動が遅くなりますけど😆」「どっちにするか選べるようにして欲しい人いそう」「config.eager_load_namespacesにデフォルトでi18nを加えるようになったのね」

config.eager_load_namespacesは前からあったようです↓(Rails 4.0.2以降)。

「railtieに入ったということはサーバーの起動速度にちょい影響しそうではある」「i18nはヨーロッパの人なら確実に使うでしょうし」「この間正式になったGoogleのCloud Runみたいな環境で動かすRailsの起動速度を極力速くしたい人にとっては、要らんお世話なのかもしれませんけどっ😆」「なるほど」「逆にGitHubみたいに既にサーバーが立ち上がった状態で普通にリクエストを受けるシステムなら事前にi18nを読み込んでおいてくれる方がレスポンス速度が安定してうれしいでしょうし😋」「こっちの方が本来のRailsのユースケースなので、今回の変更はごもっともという感じ☺

⚓implicit_order_columnに応じて主キーで第2ソート

# activerecord/lib/active_record/relation/finder_methods.rb#L561
      def ordered_relation
        if order_values.empty? && (implicit_order_column || primary_key)
-         order(arel_attribute(implicit_order_column || primary_key).asc)
+         if implicit_order_column && primary_key && implicit_order_column != primary_key
+           order(arel_attribute(implicit_order_column).asc, arel_attribute(primary_key).asc)
+         else
+           order(arel_attribute(implicit_order_column || primary_key).asc)
+         end
        else
          self
        end
      end

#34480のコメントにあったように、implicit_order_columnの現在の実装では、中で値が重複している場合に同じ順序でオブジェクトが返されるとは限らない。
このパッチは(implicit_order_columnとして設定されていない場合は)主キーを第2ソートとして追加することで出力の順序が一貫するようになった。
同PRより大意


つっつきボイス:「ここからは新しめの改修です」「むしろ#34480で足されていた元の機能↓の方が気になりますね: 自分は昔からこの機能があればいいのにって思ってましたし」「Rails 6で入ってたんですね😳

暗黙のorder用カラムを指定可能に
ソートのorderを明示せずにfirstlastなどのorderありfinder系メソッドを呼ぶと、Active Recordは主キーでソートする。主キーがオートインクリメントの整数値でない場合(UUIDなど)、これによって予想外の振る舞いが生じる可能性がある。今回の変更では、firstlastの結果が予測可能になるように、それらの暗黙のorderで使われるカラムをオーバーライドできるようになった。
#34480のActive Record Changelogより

class Project < ActiveRecord::Base
  self.implicit_order_column = "created_at"
end

後で調べると、6.0リリースノートでも#34480についてはごく簡単にしか触れられていませんでした↓。

⚓6.0のimplicit_order_columnカスタマイズ機能の使い所

「こういうふうに、ORDER BYを付けなかった場合にデフォルトで付けてくれる機能がある方が、世の中の人はおおむね幸せになれる😋」「なれます〜😋」「前はなかったのもびっくりですね😳

「ただしこの機能はケースバイケースでもあるんですよ: ORDER BYが付くとクエリが激重になることがあるから😇」「インデックスがらみとか?」「というより、RDBMSにとってはORDER BYが付かない方が見つけた順に結果を返せるから総じて速くなります」「そうそうっ」「ソートは重たい操作なので、ORDER BYを付けるとクエリが最初に返り始めるまでの時間が長くなりますし」「でかいテーブルが要注意なんですね😳

「デフォルトでORDER BYを付けるというのは環境によっては耐え難いほどクエリが遅くなることもあるので😎」「なるほど、だから機能を使うかどうかを選べるようになってるんですね」「新しいプロジェクトだったらあらかじめApplicationRecordで指定しちゃう手もありますし☺」「外すかどうかはデータが育ってから考えると」「そうそう、データが育っちゃった後でこの機能をいきなり使うと遅くて死ぬ可能性あります😇」「後からこの機能を使うのはコワい💀


「で元の#37626は何が修正されたんでしたっけ😆」「以下のAPIドキュメント↓を見ると、結果のソート順が主キーで確定するなら主キーでサブソートされるということみたい」「プルリクタイトルの『Additionally order by primary key if implicit_order_column is not uniq』とプルリクの内容が何だか微妙に食い違ってる気がする…😅」「Railsに限らないと思うんですけど、プルリクのタイトルってめちゃ走り書きなことがあって、これまでも先週の改修でちょくちょくダマされました😇」「🤣」「前はnon-uniqueなカラムを使うとソート順が確定しないことがあるという記述が消されてる」「created_atみたいにuniqueが保証されないカラムも指定できるようになった?」「意図がイマイチ汲み取りきれない😅

# activerecord/lib/active_record/model_schema.rb#L116
    # Sets the column to sort records by when no explicit order clause is used
    # during an ordered finder call. Useful when the primary key is not an
-   # auto-incrementing integer, for example when it's a UUID. Note that using
-   # a non-unique column can result in non-deterministic results.
+   # auto-incrementing integer, for example when it's a UUID. Records are subsorted
+   # by the primary key if it exists to ensure deterministic results.

「でもimplicit_order_columnがカスタマイズできるということがわかったのはうれしい😂」「いいこと知った😋」「新しいRailsプロジェクトなら最初から入れといてもいいぐらい😋」「付けてないことによる事故の方が多かったりしますし🚦」「特に、MySQLはオートインクリメントされたカラムを割とその順序で返してくれるけどPostgreSQLはそうでなかったりしますし」「そうそうっ😤」「でもPostgreSQLの方がSQLとして本来の姿なんですよね😆」「ORDERを指定しないところに順序なんかないっ😆

以下の記事によるとimplicit_order_columnではカラムを1つしか指定できないそうです。

参考: Rails6 のちょい足しな新機能を試す71(implict_order_column 編) - Qiita

⚓ConnectionAdapters::Resolverを削除

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L1042
      def establish_connection(config, pool_key = :default)
-       resolver = Resolver.new(Base.configurations)
-       pool_config = resolver.resolve_pool_config(config)
+       pool_config = resolve_pool_config(config)
        db_config = pool_config.db_config
# activerecord/lib/active_record/connection_handling.rb#L251
    private
      def resolve_config_for_connection(config_or_env)
        raise "Anonymous class is not allowed." unless name
        config_or_env ||= DEFAULT_ENV.call.to_sym
        pool_name = primary_class? ? "primary" : name
        self.connection_specification_name = pool_name

-       resolver = ConnectionAdapters::Resolver.new(Base.configurations)
-
-       db_config = resolver.resolve(config_or_env, pool_name)
+       db_config = Base.configurations.resolve(config_or_env, pool_name)
        db_config.configuration_hash[:name] = pool_name
        db_config
      end

つっつきボイス:「ロジックが重複していたので片方を削除したのね☺」「リファクタリング」「お引越し🚛

ConnectionAdapters::ResolverDatabaseConfiguratonsという2つのオブジェクトに同じロジックが多数実装されている。一方はconfig/database.ymlで定義された設定に用い、もう一方はestablish_connectionなどのメソッドにStringHashで生の設定を渡すのに用いる。
時間とともに2つのロジックが少し乖離してきたので、コードの複雑さを軽減して一貫性を高めるためにResolverを削除して主なメソッドをDatabaseConfigurations#resolveに置き換え、resolve_pool_configConnectionPoolに移動するなどした。
同PRより大意

「ついでにdatabase.ymlに関するAPIドキュメントがちょっと足されているみたい↓」「お〜😋」「こうやってコメントがちゃんと書かれるとありがたい🙏

# activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#L1149
        # Returns an instance of PoolConfig for a given adapter.
        # Accepts a hash one layer deep that contains all connection information.
        #
        # == Example
        #
        #   config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
        #   pool_config = Base.configurations.resolve_pool_config(:production)
        #   pool_config.db_config.configuration_hash
        #   # => { host: "localhost", database: "foo", adapter: "sqlite3" }
        #

⚓Rails

⚓rubocop-rails 2.4.0がリリース


docs.rubocop.orgより


つっつきボイス:「rubocop-railsは本家rubocopとバージョンの歩調を合わせないことにしたという話がありましたね」「そうそう、本家に構わずガンガン上げていくぜと💪

新機能:

  • Rails/ApplicationController copとRails/ApplicationMailer copを追加
  • Rails/RakeEnvironment copを追加
  • Rails/SafeNavigationWithBlankを追加

⚓Rails機能のエンジンへの切り出しにRuboCopを活用


同記事より


つっつきボイス:「Railsエンジンの切り離し?」「上の図↑はエンジンのデータを自由に読み書きできちゃまずいよねという話みたい」「ちゃんとしたインターフェイスを経由してやりとりしないと😆

isolate_namespace↓ってあったそういえば」「おぉ?」「これを使うことで名前空間が分かれる: エンジンを書くときはこうしないとRails側とかぶっちゃうとか何とかだったと思う」「きっとRailsガイドにある気がします😋」「マウンタブルエンジンについては丁寧なガイドがあるはず☺」「密結合したエンジンなんてエンジンじゃないし🤣」「🤣

参考: 2.1.1 重要なファイル — Rails エンジン入門 - Rails ガイド

Engineクラスの定義に含まれるisolate_namespaceの行を変更・削除しないことを強く推奨します。この行が変更されると、生成されたエンジン内のクラスがアプリケーションと衝突する可能性があります。
Railsガイドより

「Railsアプリが太ってきたときにどう分割するかという方法はいくつかあるんですが、そのひとつが機能のエンジン化ですね」「おぉ」「複数のRailsアプリにする方法とは別に、1つのRailsアプリの中でエンジンに切り出すという方法☺」「記事を書いた人は、その辺の作業を支援するためにcopをいくつか作ったみたいです↓」「危ないときに立ち会ってくれるcop👮」「マウンタブルエンジンとかそんなにしょっちゅうは書かないから😆、そういうのがあるとよさそう」

記事では、他の切り離し方法として「リードオンリーActive Record」や「明示的に定義された依存関係だけをテストで読み込む」「Active Recordのsaveなどにフックをかける」なども紹介されています。

⚓Bundlerを健全に使うには

# 同記事より
# 正確なバージョン指定
gem 'nokogiri', '1.0.3'
gem 'webrat', '0.3.1'

# ペシミスティックなバージョン指定
gem 'nokogiri', '~> 1.0.3'
gem 'webrat', '~> 0.3.1'

# バージョン指定なし
gem 'nokogiri'
gem 'webrat'

つっつきボイス:「2012年という古い古い記事ですが、先週出した翻訳記事↓で言及されていたので」「gemのバージョンをきっちり指定する場合とかペシミスティックに指定する場合とかについての話ね☺

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

「ところで、gemとかのバージョンをピンポイントに指定するのって、それはそれでたまにリスクがあったりするんですよ」「マジですか?!😳」「ピンポイントに指定したバージョンが何かのミスで消えちゃったりしたら当然失敗するようになるので😭: もっとも最近はそういうことはあまりありませんけど☺

「上の翻訳記事の方ではJSのモジュールのバージョンを全部ピンポイントにしないと気が済まないという感じでした」「Rubyもそうですけどね☺」「ただGemfile.lockにはピンポイントで書かれてそちらが正ですけど、Gemfileの方にはこのぐらいのバージョンなら動くよねという期待を込めてバージョンを書いたりしますし😆」「最近もGCPのgemがめちゃ古くないと動かないケースあった…😇

⚓Capistranoを使う(Ruby Weeklyより)

# 同記事より
# lib/capistrano/tasks/login.rake

desc "Login into a server based on the deployment stage"
task :login do
  on roles(:app) do |server|
    user = fetch(:user)
    path = fetch(:deploy_to)

    uri = [
      user,
      user && ‘@’,
      server.hostname,
      server.port && “:”,
      server.port
    ].compact.join
  end
end

つっつきボイス:「こっちは新しくて普通にCapistranoやった記事ですが、Capistranoの新しい記事というのが珍しそうだったので」「そうそう、Capistranoの記事って意外に少ない😆」「記事書いた人もきっと同じ思いだったに違いない🤣」「『ないなら自分で書くわい』みたいな😆」「これは翻訳したいです😋

「Capistranoのコードを書く人は、どちらかというとサーバーサイドのインフラエンジニアですよね: 書き方はいろいろだけど、いったん書き方が枯れて安定してしまえばそれで全然いいという感じですし☺」「インフラエンジニアがCapistranoコードでテンプレートを一度がっつり書いておけば、後はみんなでそれを使い回す😆」「秘伝のタレ的なconfigがあったりしますね😆」「実際Capistrano自体もそんなに変わってませんし☺


capistranorb.comより


今回は以上です。

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

週刊Railsウォッチ(20191125)Ruby 3.0は2020年12月にリリース決定、Rails 5.2.4rc2とRuby 2.7.0-preview3がリリースほか

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

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

Rails公式ニュース

Ruby Weekly

productionやdevelopment、stagingという言葉の使い分けについて

$
0
0

morimorihogeです。関東圏は雨が多い気がしますね。さむい。

さて、今年も12月ということでアドベントカレンダーの季節がやってきました。
TechRachoアドベントカレンダーでは例年弊社(BPS株式会社)で普段記事をあまり書く機会がないメンバーにも広く記事を書く機会を作ろうという企画で、エンジニアメンバーに関わらず総務や採用メンバーにも何か書いてもらうという試みをしています。
なんとなく弊社メンバーの雰囲気に触れていただければと思います。

過去のアドベントカレンダー記事は以下からどうぞ

初回は僕から、11月の銀座Railsで発表した内容を記事に再構成してみました。

Webアプリ開発初心者を惑わすproduction / development / stagingというコトバ

プログラミングに限らず専門技術界隈は色々な「その業界でしか通用しない専門用語」があります。今回取り上げるproductiondevelopmentstagingという用語もその中の一つで、主にWebアプリケーション開発や業務システムなど、サーバーがいて通信するようなシステムの世界で使われる用語になります。
なお、日本語だとそれぞれ

  • production: 本番
  • development: 開発
  • staging: 検証

と対応することが多いかなと思います(異論は認める)。
こうした用語群は、例えば弊社でも得意なRailsアプリケーション開発を前提としたならば、

  • アプリケーションの動作モードを示すもの:RailsならRAILS_ENVに相当
  • システムが動作するサーバー環境を示すもの:本番サーバー、開発サーバーなど
  • GitやSubversion、ソースコードの特定バージョンを示すもの:Gitのタグ、ブランチなど

などにこれらの用語が出てくることがあります。まずはそれぞれ簡単に解説します。

アプリケーションの動作モード

昨今のアプリケーションでは、実行時に「動作モード」を指定できることが多いです。
例えば、RailsであればRAILS_ENVという環境変数を渡すことで、デフォルトでproductiondevelopmenttestという動作モードを切り替えることができます。
# プロジェクトによっては更に別途stagingといった環境を増やしているものを見ることもあります。

ここで指定する動作モードとは、主に当該アプリケーションを実行する時単体で見たときの振る舞いを切り替えるものを指します。
例えば、Railsであればそれぞれの動作モードには以下のような特徴があります。

  • productionモード
    • 本番モードで、動作中にソースコードが編集されない前提が取れるため、クラスキャッシュなどをアグレッシブに保持して効率化ができる(config.cache_classesやconfig.eager_load)
    • 静的ファイル配信はRailsサーバーからではなくnginxなどの手前Webサーバーから配信させる前提の設定(config.public_file_server.enabled = false)がされている
  • developmentモード
    • 起動中にソースコードが編集されてもRailsサーバーの再起動不要で変更結果が反映される(config.file_watcherconfig.cache_classesの無効化など)
    • 開発者のローカル環境ではいちいちnginxなどを立ち上げなくても良いように、public以下の静的ファイルもRailsサーバーが配信する(config.public_file_server.enabled = true
  • testモード
    • developmentモードに近いが、さらにCIやターミナル前提の設定などが付く(consider_all_requests_local = :stderr

なお、RAILS_ENVの場合は基本的に日本語で「本番モード」「開発モード」などと呼称することは少ないように思います(少なくとも僕の周辺では見かけたことがないです)。
また、「prodモード」や「devモード」といった呼称も見ませんので、この辺りは見分けるのに使えるかなと思います。

サーバー環境の種別

いわゆる「本番サーバー」「検証サーバー」「開発用サーバー」といった種別になります。
イマドキ運用中システムの動作環境が本番環境しかないということは流石にありえないとは思いますが、複数環境がある場合でもいくつの環境があるか、それぞれどの様な呼称をされているかなどはプロジェクトによってかなり異なります。

例えば代表的な分類だと、以下の様な3環境が挙げられるのではないかと思います。

  • 本番(production)環境
    • サービスインしているエンドユーザーが利用する環境。git-flowならmasterブランチが対応
  • 検証(staging)環境
    • 本番リリース前の動作検証を行う場所。git-flowならdevelopブランチが対応
    • ほとんど本番に近い環境だが、サーバースペックは落としてあったり、外部API接続がテスト環境に繋がっていたりなどが異なる
  • 開発(development)環境
    • 開発中の機能を見てもらう時などに使う場所。git-flowならfeatureブランチが対応
    • stagingより更にカジュアルな環境で、簡単に作って潰すような使われ方をするためにリソースはケチって立てることが多い

ちなみに、Rails開発に慣れている人にわかりやすく伝えるのであればCapistranoのStage名がこの種別に該当します。
# イマドキk8s使ってるからcapなんて使ってねーよという人はすみません

上記3つの代表的なもの以外にも、並行開発されているプロジェクトなら各チームや開発者ごとに開発環境を持っていたり、リリース確認がQA(Quality Assurance)チームのチェックの2段階になっていたりなど、この辺りはリリースフローや開発体制によってもかなり色々な組み合わせが存在します。

そういう意味では「一つの正解」がある部分ではなく、プロジェクト規模やリソース状況に応じて一番個性が出る部分だと言えます。

ソースコードの特定バージョン

GitやSubversionでブランチやタグを使ったリリース管理をする際に使われるものになります。
例えば、僕の好きなgit-flowなんかではmasterブランチが本番環境、developブランチが開発環境といった対応になっており、前述のサーバー環境と対応させるような運用が想定されています。

ここで、前述のようにサーバー環境が複数出てくると、より細かなリリースタグを打ちたいといったニーズが出てきます。
ありがちなのはdeployフローの中にrelease/prod_201912010900といった日時付きのタグを打つことで、何月何日何時の時点でどのコードがリリースされたのかを記録するといった用途になります。
※git-flowデフォルトのmaster / developブランチでは、どんどんmaster / developのHEADが進んでいってしまうので、タグを打たないと過去いつどのバージョンがリリースされていたのかを追うことができない

バグ報告などに対応する際、どの時点のバージョンで発生していたのかがはっきりわからないと調査が大変になってしまうため、こういったタグ管理が行われているプロジェクトはそこそこ多いのではないかと思います。

こんな時にこんがらがる!

さて、ここまででproduction / development / stagingにはそれぞれの意味があることを示してきましたが、これらが混同されると開発者内で混乱が生じることがあるかもしれません。

こんなシチュエーションを考えてみます。

  • AさんはBさんのPRをレビュー中、RAILS_ENV=productionのケースでエラーが発生してしまう可能性のあるコードを見かけたので「B君のコード、productionでエラーが出てるよ~」と指摘
  • Bさんはproductionでエラーが出てるという言葉を「本番(production)サーバーでエラーが出ている」と勘違いし、慌てて本番のエラーログや障害調査を始めてしまう

これは不幸なすれ違いのケースですね。Bさんの正気度が下がりそうなケースです。

他にも

  • AさんはCさんに「production(サーバー)用のAPIキーがきたので、サーバー環境のconfigに設定してほしい」と依頼
    • ※本来ならcapistranoで言うshared_filesなどのサーバー依存置き場への設定を意図
  • Cさんは「RAILS_ENV=productionの設定に反映すれば良い」と勘違いしてconfig/settings/production.ymlにAPIキーを配置してgit pushしてしまう(大惨事)

みたいなケースはどうでしょうか?ある程度経験のある開発者メンバーであればやらないミスだとは思いますが、CさんがRails開発はあまり詳しくないエンジニアなどの場合、Railsの流儀などには詳しくないので言われたとおりに作業をしてしまう、という可能性はなきにしもあらずではないでしょうか。

後は、

  • Aさんは新規で開発に参加してきたDさんに「Dさんのコード、レビューOKだからproduction(タグを付けてGitHub)に上げておいて」と依頼
  • Dさんは本番(production)サーバーにSSHで入っておもむろにgit pullとかし始める

なども、開発やdeployルールの前提条件がきちんとドキュメント化されていないと発生しないとも限らないです。

勘違いしない・させないためにどうするのか?

実際にはある程度以上経験のあるエンジニアであれば、この手のミスは実際に手を動かす前の時点ですれ違いに気づき、認識合わせをすることで回避できると思いますが、もし仮に間違って作業してしまった場合のことを考えると悲惨な結末になるケースもあるため、コミュニケーションにおけるストレスの原因になってしまう所です。

そのため、僕がSlackや各種コミュニケーションを行うときには以下の様な点を気にしていたりします。

  • アプリケーション動作モードを指す時には明示的にRAILS_ENVであることを示したり、productionモードdevelopmentモードの用にモードという呼称を使う
  • サーバー環境にはproductionではなくproddevelopmentではなくdevなどRAILS_ENVと完全一致しない名前を付けたり、prod環境dev環境という環境という呼称を使う
  • ソースバージョンタグには production ではなくrelease/productionなどの完全名やproductionタグなどの呼称を用いる

やっている事自体はちょっと気を使っているだけなのですが、こうしたちょっとした修飾子を付けるだけで認識齟齬の可能性が一気に減らせ、お互いにストレスのない開発ライフを送ることができます。

まとめ

個人的に年に1~2回くらいは「危なっかしいな~」と思うことを記事にしてみました。

この手の「用語を正確に使うように心がけ、複数の意味を持つ用語は特に注意する」というのは初心者はないがしろにしがちですが、熟練者ほど無意識のうちに大事にやっている部分ではないかと思います。
チーム開発では自分自身がわかっているだけではダメで、チーム内のメンバーとも認識齟齬がないようにしていかないといけないので、こうした気の遣い方ができる人はありがたいと個人的には感じます。

似たようなことは他の用語でもあると思いますが、こういった認識齟齬が出た際には相手の忖度力を非難するのではなく自分の説明力が足りないことを疑うと良いでしょう。
多くの場合、多少説明を丁寧にすることによって避けられる手間は地雷を踏んで火消しする手間に比べれば大したことないですしね。
※そもそもこの手の齟齬自体気にしなくて良い用に開発フローを自動化する、というのもありだと思います。

ではでは、また12月中にもう1~2本くらいはアドベントカレンダー記事を書こうと思います。よろしくおねがいします。

週刊Railsウォッチ(20191209前編)Pumaのphased-restartとUnicornのgraceful restart、RailsのTZハックが不要になった話ほか

$
0
0

こんにちは、hachi8833です。ChainerがPyTorchに乗り換えられたそうです。


つっつきボイス:「ついさっき上のツィートが流れてきました🛶」「Chainerといえばメジャーな機械学習フレームワークだったのに」

参考: Chainer を振り返って

「中の人のブログを見るとメンテナンスモードに移行するってあるし😳」「機械学習やってる人には結構大きな変更でしょうね〜」

「Rubyで機械学習やる人は最近増えてきましたけど、RubyからPythonのライブラリを呼べるpycall.rbで有名なmrknさんがSciRubyというプロジェクト↓をやっていますね」「おぉ」「pycallはRubyからPythonライブラリ経由でPythonコードにアクセスして、Python側で作ったデータにRubyからアクセスできるようにするというものですね: 同じメモリ内でやるのでデータをメモリコピーしなくていい😋」


sciruby.comより

「mrknさんは最近だとデータ処理系のApache Arrow↓もやってたりしますね(ウォッチ20190402)」


arrow.apache.orgより


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

今回は週刊Railsウォッチ第17回公開つっつき会を元にお送りいたします。定員枠を初めて広げました。お集まりいただいた多くの皆さま、ありがとうございました!😂🙇

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

ActiveSupport::Duration#inspectの結果がおかしいのを修正

# 同PRより
(1.day / 24).inspect   #=> "0 seconds" # inspectの結果がおかしい
(1.day / 24).to_i      #=> 3600
(1.day / 24) == 1.hour #=> true
(1.hour).inspect       #=> "1 hour"
(1.hour).parts         #=> {:hours=>1}
(1.day / 24).parts     #=> {}

つっつきボイス:「Active SupportのDuration周りのバグだったようです」「ActiveSupport::Durationは、名前のとおり期間を抽象化したものですね」

    assert_equal "3600 seconds",                    (1.day / 24).inspect

「追加されたテストコード↑を見ると、1.day / 24inspectしたら3600秒になるべきだったのに0秒が返ってたのか〜」「他にもいくつか類似の修正PRがありましたので上に貼っておきました」「Durationとしては分でも秒でも同じ長さのはずなんでしょうけど、内部状態の更新あたりが微妙に失敗してたのかもしれませんね😅」「単位が絡むと面倒そう…」

# activesupport/lib/active_support/duration.rb#L377
    def inspect #:nodoc:
-     return "0 seconds" if parts.empty?
+     return "#{value} seconds" if parts.empty?

      parts.
        sort_by { |unit,  _ | PARTS.index(unit) }.
        map     { |unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" }.
        to_sentence(locale: ::I18n.default_locale)
    end

「Railsでは時刻から時刻を引くとDurationになるんでしたっけ?」「そうそう、Time同士の差を取るとActiveSupport::Durationオブジェクトになりますね☺️」

Rails5: ActiveSupport::Durationでの数値へのパッチ

redirect_to.action_controllerの通知のペイロードにActionDispatch::Requestを追加した

| Key         | Value                         |
| ----------- | ----------------------------- |
| `:status`   | HTTP response code            |
| `:location` | URL to redirect to            |
| `:request`  | The `ActionDispatch::Request` |

つっつきボイス:「これはredirect_toの引数が増えたのかと思ったらinstrumentが出てきてるからログ周りの話か」「あ、なるほど」「instrumentationはログとは少し違いますが、redirect_to.action_controllerのinstrumentationでrequestも拾えるようになったということだと思います☺️」

# #L
    def redirect_to(*)
-     ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload|
+     ActiveSupport::Notifications.instrument("redirect_to.action_controller", request: request) do |payload|
        result = super
        payload[:status]   = response.status
        payload[:location] = response.filtered_location
        result
      end
    end

RailsのInstrumentationを「バス」で理解する

「Instrumentation(測定、計測といった意味)について簡単に説明しますね: Railsではpub/sub(パブリッシャー/サブスクライバ)的な方法でInstrumentationが使えるんですけど、上のようなパブリッシャーをサブスクライバで拾うような形になります」

「pub/subってどう説明するのがいいかな…いわゆる差し込みフリータイプの電源タップ↑では電源プラグをどこにいくつ挿してもよくて、誰も使っていなくても電気が常に流れていますけど、コンピュータなんかにおける『バス』↓という概念もそれに似ていて、pub/subはそういうバスで考えるとわかりやすいと思います」「おぉ〜」

参考: バス (コンピュータ) - Wikipedia


Wikipedia-jaより

「Instrumentationではパブリッシャー(pub)がこういうバス的なところにいます: バスに誰もいなくても大勢いいてもお構いなしで」「そしてそのバス的なところにサブスクライバ(sub)が登録されていればそいつらがpubからメッセージを拾える、というような形になります」「ふむふむ」「おおむねそんな感じだったと思います😆」

「InstrumentationはRailsのログ出力にとても良く使われる仕組みですね: いろんなレベルのログを片っ端から出力すると遅くなるので、ログレベルに応じて自分の欲しいものだけをサブスクライブすることで、負荷を上げずにログを柔軟に取れるようにしているわけです」「Railsフレームワークのコードでinstrumentって出てきたらそこがログに出せるポイントになっているということです」


はみ出し:「そういえばInstrumentationっていう用語はWMI↓で初めて知ったんですけど、長ったらしくて収まりが悪いせいか未だに日本語にもカタカナにもなってなくて、Railsガイドでも英語表記で統一しました」「pub/subの方がしっくりくるな〜😆」「instrumentって楽器?」「元々は機材とか装置みたいな意味なんですけど楽器の意味もあって、musical instrumentと書けば確実に楽器ですね」

参考: Windows Management Instrumentation - Wikipedia

service_urlが非推奨化、今後はurl

Variant#service_urlPreview#service_urlを非推奨化に。
今後はBlobと一貫するurlを使う
同PRより大意


つっつきボイス:「Active Storageの改修だそうです」「VariantPreviewってのがあるのね😳」「Active Storageをまだ使ってない身としてはそうですか〜としか言えないし😆」「差分を見た感じではservice_が冗長だったってことなんでしょうね☺️」「使う人が増える前にメソッド名直しちゃえという感じなのかも☺️」「service_以外のurlがないならurlだけでええやろってことかな😆」

「Active StorageはRails 6でrails newするなら使ってもいいんじゃないかと思いますね: 前のRailsから移行してActive Storageにも移行するのは大変そうですけど😆」

URIかURLか

「めちゃ細かいこと言うとurlでいいんだろうかと🤣」「uriじゃないのかと😆」

「もともとUniform Resource Locatorの略でURLですけど、もう随分前に『本来やりたかったのはlocaltorみたいな物理的な場所ではなく識別子(identifier)だよな』ってURI(Uniform Resource Identifier)を使うべきという空気になったのに、未だにURLが広く使われているという🤣」「URL消えませんね🤣」「もう滅びない感🤣」

「ただRFCではURLもURIも一応両方あると思います☺️」「そうでしたか😳」「でURIがURLを包含していた気がする…この辺かな↓」「URIの方が意味が広いんですね」「URLは//<user>:<password>@<host>:<port>/<url-path>↓みたいなよく見かける形」

 //<user>:<password>@<host>:<port>/<url-path>

「でURIはtel:+1-816-555-1212みたいなのも含む↓」「これはURIだけどURLではないと」「気になる人はこの辺のRFCを読んでみるといいと思います☺️ 」

    The following example URIs illustrate several URI schemes and variations in their common syntax components:
      ftp://ftp.is.co.za/rfc/rfc1808.txt
      http://www.ietf.org/rfc/rfc2396.txt
      ldap://[2001:db8::7]/c=GB?objectClass?one
      mailto:John.Doe@example.com
      news:comp.infosystems.www.servers.unix
      tel:+1-816-555-1212
      telnet://192.0.2.16:80/
      urn:oasis:names:specification:docbook:dtd:xml:4.1.2

参考: Uniform Resource Identifier (URI): 一般的構文 — RFC3986 日本語訳の複製

URI は、それが位置指定子か、名前か、あるいはその両方かという点において、更に分類できる。 “Uniform Resource Locator” (URL) という用語は、リソースを識別するのに加えて、その主なアクセスメカニズム (例えば、そのネットワーク上の “位置”) を記述する事によってリソースの場所を見つける方法を提供するような、URI の部分集合を指す。 “Uniform Resource Name” (URN) という用語は、例えそのリソースが存在しなくなったり、あるいは利用不可能になっても全体において一意で永続的である事が要求される “urn” スキーム [RFC2141] の下での両方の URI、また名前の特性を持つあらゆる他の URI を参照するために歴史的に使用されている。
triple-underscore.github.ioより

HashWithIndifferentAccess#convert_valueのアロケーションを削減

productionのプロファイリングでActiveSupport::HashWithIndifferentAccess#convert_valueがかなり高いようなので調べたところ、妙なbindingアクセスを見つけた。
履歴を調べたところ、#36758@64a4301あたりが由来らしい。
元々、キーワード引数をホットスポットで用いることでアロケーションを削減するのが狙いだったが、forがキーワードなのでbinding.local_variable_getでしのいでいた。
しかしbindingは呼び出しのたびに新しくBindingをアロケートするのでかなり遅くなる。
convert_valueはprivateメソッドなので、引数をリネームするだけでこの辺りの問題を回避できると思われる。
同PRより大意

# activesupport/lib/active_support/hash_with_indifferent_access.rb#L365
    private
    ...
-     def convert_value(value, for: nil) # :doc:
-       conversion = binding.local_variable_get(:for)
-
+     def convert_value(value, conversion: nil) # :doc:
        if value.is_a? Hash
          if conversion == :to_hash
            value.to_hash
          else
            value.nested_under_indifferent_access
          end
        elsif value.is_a?(Array)
          if conversion != :assignment || value.frozen?
            value = value.dup
          end
-         value.map! { |e| convert_value(e, for: conversion) }
+         value.map! { |e| convert_value(e, conversion: conversion) }
        else
          value
        end
      end

参考: ActiveSupport::HashWithIndifferentAccess


つっつきボイス:「お、HashWithIndifferentAccess出たな😆」「😆」「便利なんですけど、とにかく名前長い😆: IDEなら補完が効くからまあいいんですけど」

ここでいったん録音が途切れました🙇

「…binding.local_variable_getはいわゆるリフレクション的なアプローチで、本来のオブジェクト指向的なアクセスではなく、今動いているRubyのプロセスからアクセス権限とか全無視で無理やりぶっこ抜けます😆」「😆」「こういうRubyインターナルなものはやってて楽しいんですけど、やりすぎるといろいろぶっ壊れます😇」

参考: リフレクション (情報工学) - Wikipedia

「改修の方は今時間なくてちゃんと見てませんが、binding.local_variable_getみたいな方法で速くしていたのが裏目に出て遅くなるのはよくあるパターンかも☺️」

ネストしたconfig_forハッシュへの非シンボルアクセス(非推奨)を削除

# railties/lib/rails/application.rb#L222
    def config_for(name, env: Rails.env)
      if name.is_a?(Pathname)
        yaml = name
      else
        yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml")
      end

      if yaml.exist?
        require "erb"
-       config = YAML.load(ERB.new(yaml.read).result) || {}
-       config = (config["shared"] || {}).merge(config[env] || {})
+       config = YAML.load(ERB.new(yaml.read).result, symbolize_names: true) || {}
+       config = (config[:shared] || {}).merge(config[env.to_sym] || {})

        ActiveSupport::OrderedOptions.new.tap do |options|
-         options.update(NonSymbolAccessDeprecatedHash.new(config))
+         options.update(config)
        end
      else
        raise "Could not load configuration. No such file - #{yaml}"
      end
    rescue Psych::SyntaxError => e
      raise "YAML syntax error occurred while parsing #{yaml}. " \
        "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
        "Error: #{e.message}"
    end

つっつきボイス:「これはdeprecatedな機能を削除したのね」

Railsのdeprecationプロセス

「ちょっと解説すると、基本的にRailsでは何かの機能をなくすときに、いきなり消すのではなくて、まずdeprecation warningをログに出力するコードを追加して、次のメジャーバージョンアップで実際に消すという形で進めていきます」「みんなはそれを見て、消される前に対応することになりますね」

connected_toのキーワード引数databaseを非推奨化(代替なし)

connected_toのキーワード引数databaseはシャーディングのキーとして想定されていない場合に大量のバグが発生する。databaseキーワード引数が使われているテストとユースケースを検討した結果、これは削除すべきという結論に達した。
今後シャーディングをサポートする計画はあるが、それまでdatabaseキーワード引数は意味なしとなる。コネクションを新たに作成するアプリではestablish_connectionconnects_toを利用できる。
connected_todatabaseキーワード引数をリクエスト中または使い捨てでないコネクションで使うと、databaseというキーでバグの原因となる。
同PRより大意

# activerecord/lib/active_record/connection_handling.rb#L98
    def connected_to(database: nil, role: nil, prevent_writes: false, &blk)
+     if database
+       ActiveSupport::Deprecation.warn("The database key in `connected_to` is deprecated. It will be removed in Rails 6.2.0 without replacement.")
+     end
+
      if database && role
        raise ArgumentError, "connected_to can only accept a `database` or a `role` argument, but not both arguments."
      elsif database
        if database.is_a?(Hash)
          role, database = database.first
          role = role.to_sym
        end
        db_config = resolve_config_for_connection(database)
        handler = lookup_connection_handler(role)
        handler.establish_connection(db_config)
        with_handler(role, &blk)
      elsif role
        if role == writing_role
          with_handler(role.to_sym) do
            connection_handler.while_preventing_writes(prevent_writes, &blk)
          end
        else
          with_handler(role.to_sym, &blk)
        end
      else
        raise ArgumentError, "must provide a `database` or a `role`."
      end
    end

つっつきボイス:「シャーディングでうまくいかないことがあったみたいです」「a lot of bugsですか😆」

「ちょっと説明すると、Rails 6ではマルチプルデータベース機能が入ったんですけど、たとえばマスターとレプリカがある場合にconnected_toを使ってそのブロックの中でだけ一時的に接続先を変更することができるようになっていて、そこがシャーディングで不具合が起きてたということですね☺️」

「シャーディングをどのレイヤでやっているかにもよりますね: 最近のPostgreSQLだとデータベースレベルでシャーディングができたりしますし」「Railsは6.1でシャーディングをサポートするつもりらしいのでそのときに考えるということのようです」

「Railsでシャーディングをやりたいことって結構あるんでしょうか?」「それはもうめちゃありますよ: Railsでやるかどうかは別として、シャーディングしないとやっていけないようなユースケースはいろいろあります☺️」「なるほど」「まあシャーディングをgemでやるのかRailsの機能に入れるのかというのは議論の余地がありそうですけど、ユーザーがどれだけいるかわからないようなAction MailboxもRailsに機能として入ってくるぐらいなので、たぶんシャーディングもRailsに入るんでしょうね☺️」

シャーディングについて

「シャーディング(sharding)はデータベースでは水平分割とも言われる手法ですね: 雑に説明すると、たとえばIDが奇数のデータベースとIDが偶数のデータベースを用意しておくと、IDで検索したときにきれいに分散アクセスできる、みたいな感じです」「おぉ」「ソシャゲ開発の全盛期にはシャーディングがよく使われていましたけど最近あまり聞きませんね: 単に普及して当たり前になったのか、実はつらくて止めたのかは知りませんけど😆」

参考: 分割 (データベース) - Wikipedia
参考: シャーディング - Qiita

はみ出し: キーワード引数とRails

「以下は最初上のconnected_toのキーワード引数の話かな?と思ってつっつきで引用してみましたが、別の箇所でした😅」「最近話題の、Ruby 2.7でキーワード引数周りのbreaking changesに絡んだ話ですね☺️」「こちらの翻訳記事をどぞ↓」

Ruby 2.7: ハッシュからキーワード引数への自動変換が非推奨に(翻訳)

Rails

RailsのTZハックが不要になった話


つっつきボイス:「これはTwitterで見かけましたね☺️」「2本立ての記事で、TZ環境変数を設定したらRailsが速くなったけど実はRuby 2.6で解決されていたのでハック不要だったというお話」「おぉ」「これはいい話でしたね〜😋」「はてブでも結構上がってたかも」

jnchitoさんの振り返り記事

ここと次の記事で録音が途切れていました🙇。

Webpackerでやらかしがちなミス

# 同記事より
app/
  javascript/
    packs/
      application.js
      components/     # lots of files
      images/         # lots of files
      stylesheets/    # lots of files
      ...

つっつきボイス:「WebpackerのOverpackingってわかる😆」「とってもありそう😆」「詰め過ぎ💼」

シンプルなルール: Webpackerのpacksディレクトリのファイルのうち、対応する記述がアプリケーションのjavascript_pack_tagにないものはoverpackingである。
同記事より抜粋

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

Rails 6の細かな新機能をひたすら試す記事


qiita.comより


つっつきボイス:「ひとつひとつの記事は短いですけど、Rails 6のリリースノートに載っているかいないかみたいな細かい機能をひたすら試していてびっくりしました😳」「これは凄いな〜👍」「110件目って…」

今年の4月からずっとやってるんですね😳。

Pumaのphased-restart


つっつきボイス:「Pumaサーバーにphased-restartというのがあるってこれで初めて知りました」「Unicornサーバーのgraceful restartとは別なのかな?Pumaだとphased-restartをするとダウンタイムなしでコードを再読み込みできるということか」


puma.ioより
* サイト: unicorn: Rack HTTP server for fast clients and Unix

「サーバーの再起動周りは何かとややこしい😅」「日本語記事の方を見た感じではPumaのphased-restartはmaster processを使い回し、新しいworker processはサーバー起動時のmaster processからforkされるとある: これだけ見るとUnicornのgraceful restartと同じような感じ?🤔」「Unicornのgraceful restartは、たしかmaster processが死ぬんだった覚えがありますね🤔」「お〜、そうでしたっけ?」「だったはずです」「ということはPumaのphased-restartはUnicornのとは違うということなんでしょうね」

Railsサーバーの再起動について

「Unicornサーバーの再起動はあんまりいい思い出がないんですが😭、ちょっとだけこの辺の話をしますね」「はい〜😋」

「RailsのWebサーバーとして使われるUnicornやPumaは、起動するとまずmaster processが立ち上がって、外からのリクエストはたしかいったんmaster processが受けてそこからworker processをforkしてそこに振る形になります」「おぉ」

「こういうサーバーの再起動で何が問題になるかというと、上の記事にもあるように環境変数の更新なんですね😆: ご存知のとおり、Linuxでは親プロセスが死ぬとそこからforkした子プロセスも死ぬ仕組みになっているんですが、トランザクショナルな処理を実行中のworkerがいるときにmasterを殺してしまうと処理中のworkerも死んでデータが不整合になってしまうわけです😅」

「その辺を回避する仕組みがたとえばUnicornのgraceful restartで、中途半端な状態でworkerが殺されないようにいい感じに処理中のworkerを避けて落としていって、処理中のworkerがいなくなったら晴れて新しい環境に入れ替わることになってるんですが、実際には期待どおりに動かないこともあって😅」「😅」

「一般にサーバーの再起動は重たい処理なので、とてもリクエスト数の多いRailsサーバーをカジュアルに再起動するとその間サービスは止まりますし、再起動でメモリがいったん全解放されてしまうのでサービス復帰まで時間かかります」「他にも、リクエストの多いRailsサーバーを普通に再起動するとリクエストのキューが大量にたまって、復帰したけどリクエストをさばききれなくなってサーバーが繰り返し落ちるなんてこともあったりします😇」「このあたりはH2Oの作者のブログなどに詳しく載っていたと思います↓」

その他Rails

先ほど見つけたツィートです。


前編は以上です。

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

週刊Railsウォッチ(20191204後編)Rubyコードをトランスパイルするruby-next、Cloud Run正式リリース、2019年Web年鑑レポート、V言語ほか

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

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

Rails公式ニュース

Ruby Weekly

Rails: form_withで名前空間が異なるコントローラを簡潔に指定する方法

$
0
0

TL;DR

以下のようにurlを直接指定しなくても

<%= form_with model: @user, url: admin_users_path, local: true do |f| %>

こうやって簡潔に書けるよ

<%= form_with model: [:admin, @user], local: true do |f| %>

前提

環境

  • Rubyのバージョン: 2.5.1
  • Railsのバージョン: 5.2.0

フォルダ構成

  • app/
    • controllers/
      • admin/
        • users_controller.rb
    • models/
      • user.rb
    • views/
      • admin/
        • users/
          • new.html.erb

各ファイル概要

# users_controller.rb

class Admin::UsersController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    @user.save!
  end

  private

  def user_params
    params.require(:user).permit(:name, :age)
  end
end
<!-- new.html.erb -->

<%= form_with model: [:admin, @user], local: true do |f| %>
  <%= f.text_field :name %>
  <%= f.text_field :age %>
  <%= f.submit %>
<% end %>

ポイント

form_withの引数に[:admin, @user]を渡すことです。
こうすることで,/admin/usersというurlをRailsが生成してくれます。

これまで私はこの方法を知らず,urlオプションにヘルパーを直接指定していました。

<!-- new.html.erb -->

<%= form_with model: @user, url: admin_users_path, local: true do |f| %>
  <%= f.text_field :name %>
  <%= f.text_field :age %>
  <%= f.submit %>
<% end %>

冗長ですね。何度かこの方法で実装した後に,もう少しスマートにかけるのでは?と思いform_withのコードを読んだところ,しっかりコメントしてありました。

# For namespaced routes, like +admin_post_url+:
#
#   <%= form_with(model: [ :admin, @post ]) do |form| %>
#     ...
#   <% end %>

冗長だなーって思ったときは,きっとより良い書き方をRailsが用意してくれているはずなので,コードを見にいったり,公式ドキュメントを読んだりするのは大切ですね。

まとめ

  • コントローラの名前空間をわけたときは,form_withのキーワード引数(model:)に配列を指定すると,いい感じにurlを解決してくれます
  • コードを書いていて違和感を覚えたら,もっといい書き方がないか調べてみると良いかもしれません
    • (これはまとめというか自戒です)

関連記事

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


週刊Railsウォッチ(20191210後編)Ruby 2.7の変更点記事、mrubyで動くmitamae、画像系コラボレーションツールほか

$
0
0

こんにちは、hachi8833です。もういくつ寝るとRuby 2.7が出るんでしょうか。

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

今回も週刊Railsウォッチ第17回公開つっつき会を元にお送りいたします。お集まりいただいた多くの皆さま、ありがとうございました!😂🙇

⚓Ruby

⚓Ruby 2.7の変更点などなど


つっつきボイス:「ツイートの記事は、2.7で復活した機能や入りそうで入らなかった機能の話で、2つ目はjnchitoさんのQiita記事です」「Ruby 2.7でさらに速くなるのが期待できますね: Railsをちゃんと書いていれば、常に最新のRubyにするだけでRailsが速くなりますし😋」「ですね😋

「まあRubyの新機能を業務コードで率先して使うかどうかはまた別の話ですけどっ😆」「😆」「新しい機能はやっぱり楽しいんですけど、よっぽどうまくいくことがわかるまでは業務コードで使うのはためらいがある☺」「作り捨てのバッチあたりでならやってみてもいいかも😋

「キーワード引数の非シンボルキーは、例のキーワード引数の絡みで復活してましたね」「2.7で挙動が少し変わるようです↓」

# 同記事より
def hoge(*a, **k)
  p a
  p k
end
# (略)

# Ruby 2.6.2
hoge("a"=>1, b: 2)
#=> [{"a"=>1}]
#=> {:b=>2}

# Ruby 2.7
hoge("a"=>1, b: 2)
#=> []
#=> {"a"=>1, :b=>2}

Ruby 2.7: ハッシュからキーワード引数への自動変換が非推奨に(翻訳)

「Ruby 2.6で非推奨になっていた例のフリップフロップ..が実は2.6.4で復活していた!?」「え😅」「なくてもいいかもと言われていたフリップフロップ」「フリップフロップ全然使ってないしな〜解説無理😆」「どうやら意外に使われていたらしき☺

# 同記事より
10.times{|i| print i if (i==3)..(i==6)}
#=> "3456"

参考: Rubyのフリップフロップ - monamonamonad.github.io

Ruby: これはなくてもいいかも?と思う10の機能(翻訳)

「パイプライン演算子|>とメソッド参照演算子.:は既報通り2.7には入らないことになりましたね」「少なくとも2.7には入らないけど議論が進めば入るかも?🤔


「あと以下のお片付けならぬお型付けアドベントが気になりました😆」「まだそんなに埋まってないかな😆」(注: その後エントリ増えてました😋


「あとこちらもRuby 2.7や3の新機能がらみの記事です↓」「RubyConf 2019のMatzキーノートのまとめですね😋

なおこの記事では以下のRubyパターンマッチング記事をおすすめしていました↓。また「MatzによるとRubyのパターンマッチングは遅いのでパフォーマンスに注意して賢く使いましょう」という記述がありました。

参考: Introduction to Pattern Matching in Ruby | Toptal


toptal.comより

⚓Ruby 2.7のEnumerator#produceRuby Weeklyより)

# 同記事より
Enumerator.produce([0, 1]) { 
  |base_1, base_2| [base_2, base_1 + base_2]
}.take(10).map(&:first)
#=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# 同記事より
# ある月から平日に該当する日を取得
Enumerator.produce {
  Date.today+rand(30)
}.detect{ |date| !date.sunday? || !date.saturday? }
# => #<Date: 2019-11-15 ((2458803j,0s,0n),+0s,2299161j)>

つっつきボイス:「ruby-jp Slackで2.7のproduceが盛り上がってました」「フィボナッチやってる😆」「もしかするとElixirあたりにも同じ名前のメソッドあったりするかな?関数型言語あたりで見たことあるような気がする🤔」「injectを別の方法でやってる感」「今日は残念ながら出席できなかったkazzさんにproduce見せたらかなり面白がってました😋」「うまく書ければはまりそう😋

参考: instance method Enumerable#inject` (Ruby 2.6.0 リファレンスマニュアル)

後で2.7.0-preview3でproduceinjectをちょっと動かしてみました。

Enumerator.produce([1, 1]) {|base_1, base_2|
  [base_2, base_1 + base_2]
}.take(10).map(&:first)
#=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

(0..7).inject([1, 1]) {|fib, i|
  fib << fib[i] + fib[i+1]
}
#=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Ruby: `each`よりも`map`などのコレクションを積極的に使おう(社内勉強会)


追いかけボイス: Haskellのiterate関数がRubyのEnumerator#produceにちょっと似てるかもしれないと教わりました🙇

参考: The Fibonacci sequence - HaskellWiki

-- wiki.haskell.orgより
fibs = map fst $ iterate (\(a,b) -> (b,a+b)) (0,1)

⚓mitamae: mrubyでやれるitamae

# 同リポジトリより
# cat recipe.rb
include_recipe 'included'

directory '/tmp/etc'

file '/tmp/etc/hello' do
  content 'This is mitamae'
end

template '/tmp/etc/config.yml' do
  source 'config.yml.erb'
end

つっつきボイス:「この間BPSの社内Slackに貼っていただいたヤツです」「そうそう、mrubyで動くこのmitamaeをこの間知って、Rubyがなくても単独バイナリとして動かせるのがいいなと思いました👍

「itamaeだとitamaeを動かすためにRubyをインストールする必要があったりして大変です😅」「itamaeはいわゆるIaaC(Infrastructure as a Code)的なサーバーのプロビジョニングツールで、設定をRubyのコードで書くと↓実行するだけで一発でプロビジョニングできて、いい感じに差分管理もできます😋

# https://github.com/itamae-kitchen/itamae/wiki/Definitionsより
define :install_and_enable_package, version: nil do
  v = params[:version]
  package params[:name] do
    version v if v
    action :install
  end

  service params[:name] do
    action :enable
  end
end

「スクラッチのサーバーなんかだとRubyが入ってなかったりしますし、制約が厳しいOSだったりすると既存のRubyのバージョンが恐ろしく古いこともあるので、Itamaeを動かすためにRubyをどうやってインストールしようか考えるのが面倒😭」「😭」「そういうことを考えないで済むという点でこのmitamaeはいいなって思った次第です❤」「mrubyのいい使い方😋」「まだ動かしてないんですけど、READMEを見た感じではitamae sshが入ってないぐらいで後は普通のitamaeが動きそう👍」「コミットを見るとk0kubunさんがほとんどやってるみたいですね😳」「社内ではansibleが主流なんですけど😆、itamae使うならこれがいいかな〜」

追記(2019/12/10)

本記事タイトルで「mitamae gem」となっていたのは誤りでした。「mitamae」に修正しました🙇

⚓ruby-packer: Ruby CLIをシングルバイナリに


つっつきボイス:「まだ試していませんが、こちらはCRubyでシングルバイナリをコンパイルするようです」「CRubyでやったらどんだけバイナリでかくなるのやら😆」「どこまでリンクするかですよね」「やってみないとわかりませんが20MBぐらいになるのかな?🤔

後で雑にやってみました。63行程度のRubyコードをrubyc subtract.rb -o subtractしたところ怒涛のようにCのコンパイルが走って11MBになりました。Go言語で書くのとさほど変わらないぐらいのバイナリサイズという印象です。試していませんがRailsも一応コンパイルできるようです。

-rwxr-xr-x 1 hachi8833 staff  11M 12 10 12:20 subtract
-rwxr-xr-x 1 hachi8833 staff 1.1K 12 20  2018 subtract.rb

⚓シングルバイナリについて

「Rubyのシングルバイナリか〜、気持ちわかる☺」「READMEによると、この辺のライブラリがビルドに必要っぽい↓」

  • gdbm
  • libffi
  • ncurses
  • openssl
  • readline
  • yaml
  • zlib

「Linuxの内部に関わる話ですけど、このruby-packerはいわゆるライブラリに依存しないシングルバイナリをビルドできるというもののようですね: Rubyに限らず一般にプログラムはOSに入っているreadlineやらlibxmlといったライブラリを動的リンクして使うことがよくあります」「ふむふむ」「そういう動的にリンクされるライブラリは、Macだと拡張子が.dylib、Linuxだと.so、Windowsだと.dllになりますが、やってることは同じですね☺

「そうしたライブラリに依存するプログラムは、ライブラリのバージョンが変わったりライブラリがなくなったりすると不具合が出たり動かなくなったりするので、それらを1個のバイナリに全部焼き込んで依存しないようにしたシングルバイナリという形態のプログラムには一定の需要があります🧐」「おぉ」「その分バイナリのサイズはでかくなりますけど、その代わり同じOSならライブラリ構成とかが全然違っていても入れるだけで動かせます👍

⚓BusyBoxは便利

「この手のシングルバイナリで有名なのはBusyBoxというヤツ↓」「これ知らなかった😳」「BusyBoxは、lsとかsshみたいにサーバー管理に必要そうなLinuxコマンドとかがひととおり何でも入っているシングルバイナリのツールで、面白いのはコマンドのシンボリックリンクをBusyBoxバイナリにリンクしておけば呼ばれ方に応じて振る舞いが変わるところ」「こんないいものがあったなんて😂

参考: BusyBox - Wikipedia


busybox.netより

「gccすら入れられないような組み込み環境なんかでもBusyBoxさえ持ち込めば何とかなったりしますし、対応しているCPUアーキテクチャもやたら多くて、なかなか楽しい十徳ナイフ的なツールです🥰


はみだし:「もともと以下の記事↓で紹介したdipというツールがRubyなしでも動くシングルバイナリも提供されていて、どうやってそのシングルバイナリをビルドしているのか知りたくて追っているうちにruby-packerを見つけました: ただdipではruby-packerを使っているのかどうかよくわからなくて😅」「dipのリポジトリを今ざざっと見た限りではわかんないですね〜😆

docker-composeを便利にするツール「dip」を使ってみた

⚓その他Ruby


同リポジトリより


つっつきボイス:「お、Faradayってず〜っとバージョンが0.いくつのままでしたね😆」「一度バージョンが1.0になった後戻ったような覚えありますけどっ😆」「Ruby 2.7に対応したら1.0にするということのようです」「ま自分はHTTPClient派ですけど🤣」「そういえばウォッチでも話題になりましたね(ウォッチ20180615)」

「Faradayってクローラーとか作るときに使うんでしたっけ?」「クローラーも作れると思いますけど、もっとリッチなことができます☺」「Faradayはミドルウェアが使えますね↓」「そういえばあったわFaradayのミドルウェア」

「Faradayは直接使うより他のgemのdependencyで引っかかる印象があるんですよ: こっちのライブラリとあっちのライブラリがそれぞれFaradayの違うバージョンに依存してたりとか😭」「あぁ〜😳

⚓DB

⚓PostgreSQLカンファレンス2019まとめ


postgresql.jpより


つっつきボイス:「そうだぽすぐれカンファレンスやってた!」「Togetterでセッションごとにまとめてくれているので見やすそう😋」「PostgreSQLカンファレンスは大規模ですよね: 特に Project Tsurugi(劔)の話↓とか」「つるぎ?」「これを主催している神林さんは日本のPostgreSQL界隈ではめちゃ強い人💪で、たしか未踏プロジェクトにも出てたと思います」「おぉ」「何度か会ったことありますけど、PostgreSQLをL1キャッシュレベルで高速化するような人でした😆」「つぇ〜🤩

⚓クラウド/コンテナ/インフラ/Linux/Serverless

⚓今年のre:Invent


reinvent.awseventsjapan.comより


つっつきボイス:「re:inventちょうど今やってますね〜(注: つっつき中)」「やってるやってる〜😋」「既にいくつか記事も出てるので少し貼ってみました↓」「re:inventは自分もまだ追いかけられていませんが、重要なイベントなので後でキャッチアップして社内でre:inventサーベイするつもりです❤: EC2 Image Builder↓もよさそうですし」

「CodeGuru↓とか興味ある😋」「自動でコードレビューするやつでしたっけ」「でもお高い〜💸100行あたり0.75ドルだから、Railsのコードをうっかりかけたら死にそう😆」「死ねる😇」「インデント直すとかでも1000行で7.5ドルか〜😅」「よさそうだけどCodeGuru破産コワいし、今ならCodeClimateみたいな他のサービスの方がコストが安定しそうではありますね☺

⚓Buildpacksとは


つっつきボイス:「Buildpackはそこそこ前からあるようですが、どのぐらい普及しているんでしょう?」「Herokuは前からBuildpackやってますけどね☺」「上のスライドも開けてみたらBuildpackの話で、それで知りました」「まあDockerfileがこれだけ普及しましたし、Dockerfileは中身を追いかけられるのがいいところですし(追いかけるのつらいけどっ😆)」

⚓その他インフラ

FROM nginx:alpine

RUN apk add curl

HEALTHCHECK --interval=5s --timeout=3s CMD curl -f http://localhost/ || exit 1

つっつきボイス:「おぉ〜HEALTHCHECKは知らなかった😳」「どういうものなんでしょう?」「docker psするとこうやって表示できるのね↓」「どこかで使えそう😋」「おかしくなるとまずDockerリスタートしちゃいますけどっ😆

$ docker ps --format "table {{.Image}}\t{{.Status}}\t{{.Names}}"
IMAGE                    STATUS                      NAMES
kakakakakku/nginx:base   Up 19 seconds (unhealthy)   nginx05
kakakakakku/nginx:base   Up 27 seconds (healthy)     nginx04
kakakakakku/nginx:ng     Up 11 minutes (unhealthy)   nginx03
kakakakakku/nginx:ok     Up 11 minutes (healthy)     nginx02
kakakakakku/nginx:base   Up 11 minutes               nginx01

⚓CSS/HTML/フロントエンド/テスト

⚓Adbe XDのプラグイン

Mimic
URLを入力するだけで既存のWebサイトからXDプロジェクトに色、フォント情報、画像情報を一覧化してくれるプラグインです。

とか

VizzyCharts
csvファイルから数値を読み込んで、折れ線グラフ、円グラフ、棒グラフの3パターンが作成できます。

が社内デザインチームで色めき立っていました。


つっつきボイス:「VizzyChartsはCSSから値を読み込んでグラフを生成…ってExcelでできるのでは🤣」「🤣」「まあそれをXDだけでできるのがいいところかも😆

「今日お集まりの方の中でAdobe XD使ってる方は…いないっぽい😆: デザイナーからはどういうフォーマットでデザインを受け取ってます?」「やっぱりXDですね☺


adobe.comより

「自分たちはZeplin↓使うことが多いかな」


zeplin.ioより

「うちはFigma↓ですね」「これ知らなかった😳」「オンラインではよさそう😋」「これはXDで作ってFigmaにアップするんでしょうか?」「いえ、Figmaのデスクトップアプリがあるのでそこで作ってそのままプロトタイピングまでやれます」「へぇ〜」「マイクロインタラクションイージングとかはそんなに突き詰められないんですけど、オンラインで同期書き込みできるので同じものを複数人で見ながら操作したりできるのがいいですね😋」「Zeplinもそれができるのがありがたいです🙏」「Googleスプレッドシートみたい」


figma.comより

「少し変わった使い方としては、スクラムのカンバンをFigmaで作ってそのままFigma上で使ったりしてましたね」「おぉ〜そんな使い方が😳」「いわゆるカンバン機能みたいにして使うと😆」「デザインツールというよりコラボレーションツールみたいですね😋」「まあライフハックというか、draw.ioとかcacco.comみたいな感じで図形を編集しながらオンラインでやりとりできます😋」「カクー?」「caccoは日本のサービスですね」


cacoo.comより

「あとはGoogleというかG SuiteのJamboard↓も付箋をいっぱい貼れて便利ですね😋


gsuite.google.comより

「こういうコラボレーションツールもいろいろあるのでどれを使うか悩ましいですよね😄」(以下延々)

⚓言語・ツール

⚓新機能あれこれ

つっつきボイス:「今回は細かめの新機能を見繕ってみました😋

「gitに入った機能ですか」「あ〜、こうやって文字単位でもdiffを出してくれるのね↓」「RubyMineみたいなIDEにはもうある機能だと思いますけど😆」「gitでできるとCIでもやれたりするのはよさそう😋


同記事より


「BPS社内SlackでGitHubのコードジャンプに喜びの声が上がってました↑」


「みんなが待ち望んでいた機能🎉」「Slackのオートフォーマット機能のせいでこれまでどんだけ生産性が落ちていたかと😆」「Slackに何か貼り付けるとおかしなことになったりして、どうやったらこれを殺せるかSlackチャネルで真剣に相談してましたし😆

⚓その他

⚓\e[iがポイント


つっつきボイス:「これも楽しい記事😋」「ターミナルソフトウェアが制御文字をどのぐらい扱うかという話ですね☺」「BEL文字とか🛎、最近だとベルの代わりに画面が一瞬ピカッと光るとかもありますね⚡

参考: ベル文字 - Wikipedia

「昔々だとLinuxでとても時間のかかるコマンドを実行するときは最後にベルを鳴らしたりしましたね😆: ベル文字だとPCの基板上にあるスピーカーを直接鳴らすので、たとえばデータセンターでディスプレイを切り替えて作業していてもベルが鳴ってくれるんですよ🛎」「そういえば私もベル鳴らすコマンドを作ってシェルに入れました↓😆

# Mac用
afplay /System/Library/Sounds/Glass.aiff

⚓リニエンシー制度


つっつきボイス:「リニエンシー制度はちょっと前から日本にも導入されているらしいんですけど、談合を最初に白状した会社はお目こぼしにすることでいわゆる囚人のジレンマを解決するというものだそうです」「通報にインセンティブを付ける発想ですね☺

参考: 囚人のジレンマ - Wikipedia

⚓番外

⚓電池なしで動くものたち


つっつきボイス:「デイリーポータルZのこの記事面白かった!😋」「機械式計算機の使い方が誰もわからないオーパーツというかロストテクノロジー状態から使い方を模索していくのがサイコーです😆」「ちょっと長いですけど😆」「実はマニュアルこっそり持ってたってことは…ないかさすがに😆


「こちらの人工ニューロンも電池が要らないデバイスということで🔋


後編は以上です。おかげさまでつっつき後の親睦会も大勢で盛り上がりました🍻。ありがとうございます!

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

週刊Railsウォッチ(20191209前編)Pumaのphased-restartとUnicornのgraceful restart、RailsのTZハックが不要になった話ほか

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

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

Ruby Weekly

週刊Railsウォッチ(20191216前編)Rails 6.0.2がリリース、平成Ruby会議01開催、古いRailsのfindメソッド置き換えほか

$
0
0

こんにちは、hachi8833です。

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

今回は新しい試みとして、TechRacho記事でもお馴染みのWingdoor様が福岡エンジニアカフェで主催したつっつき会に東京からリモート接続する形で開催いたしました。Windoor様およびご参加いただいた皆さまありがとうございます!


つっつきボイス:(接続テストの後)「こういう遠隔イベントはやっぱりZoom↓が強い💪」(ご挨拶・会社紹介など)「それでは始めま〜す🔔」「よろしくお願いしま〜す🎉」「今回はモバイル開発やPHPやJSなどをやってる方もいらっしゃるようなのでそのつもりで進めます」


zoom.usより

「そういえば今度の土曜日は平成Ruby会議01ですね(注: イベントは終了しました)」「お、もう今週か😳」「そういえばなぜ平成😆」「平成生まれのRubyエンジニアが集う場ですって」「私アウト😇」「私アウト😇」「私アウト😇」「…の続きは、平成生まれだけでなく各世代で交流ですって😆


heiseirb.github.ioより

「キーノートは@koicさんに@yui-knkさん、2スロットもあるアツいカンファレンスですね🔥」「スライドもいろいろ上がってくると思いますので楽しみ😋


行ってまいりました。最高です❤。スライドは以下に続々アップ中です。

⚓臨時ニュース: Rails 6.0.2がリリース


rubyonrails.orgより

こちらはつっつきの後で出たリリースです。細かな改修のみで、セキュリティ関連の修正は見当たりませんでした。作り中のアプリで早速bundle updateしました。

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

以下から見繕いました。ドキュメントの修正が目立ちました。

⚓class_namesヘルパーを追加

<div class="<%= class_names(active: item.for_sale?) %>">
# actionview/lib/action_view/helpers/tag_helper.rb#L293
+     # Returns a string of class names built from +args+.
+     #
+     # ==== Examples
+     #   class_names("foo", "bar")
+     #    # => "foo bar"
+     #   class_names({ foo: true, bar: false })
+     #    # => "foo"
+     #   class_names(nil, false, 123, "", "foo", { bar: true })
+     #    # => "foo bar"
+     def class_names(*args)
+       safe_join(build_tag_values(*args), " ")
+     end

つっつきボイス:「class_name?」「これで何するんだろ?」「これはCSSのクラスでは?」「あ〜Rubyのクラスではなかったか😆」「ハッシュの値がtrueだったらキーをCSSのクラスとして出力するとかができるようになったのね☺」「元の書き方とどっちが読みやすいかはちと微妙ですけど😆

<!--Before:-->
<div class="<%= item.for_sale? ? 'active' : '' %>">

<!--After:-->
<div class="<%= class_names(active: item.for_sale?) %>">

「他の言語でこういう書き方してたのを取り入れたのかな?🤔」「…Vue.jsでこういう書き方があったような気がします」「お、そうでしたか😳

⚓インスタンス変数をloadに移動

インスタンス変数@records@loaded#exec_queriesではなく#loadに置くべき。#load#exec_queriesを実行し、@recordloaded?がtrueの場合しか代入されず、@loaded#loaded?がtrueの場合しか設定されず、どっちみち#loadで代入すべきなのでこの方が明快。なおこれはとある内部gemでインスタンス変数が代入される場所がおかしかったことで気づいた。
同PRより

# activerecord/lib/active_record/relation.rb#L628
    def load(&block)
-     exec_queries(&block) unless loaded?
+     unless loaded?
+       @records = exec_queries(&block)
+       @loaded = true
+     end

      self
    end
...
      def exec_queries(&block)
        skip_query_cache_if_necessary do
-         @records =
+         records =
            if where_clause.contradiction?
              []
            elsif eager_loading?
@@ -826,12 +829,11 @@ def exec_queries(&block)
              klass.find_by_sql(arel, &block).freeze
            end

-         preload_associations(@records) unless skip_preloading_value
+         preload_associations(records) unless skip_preloading_value

-         @records.each(&:readonly!) if readonly_value
+         records.each(&:readonly!) if readonly_value

-         @loaded = true
-         @records
+         records
        end
      end

つっつきボイス:「これはリファクタリングだなってわかりました」「インスタンス変数のメモ化周りを整理したということでしょうね☺

⚓Active Jobにdefault_retry_jitter設定を追加

  • config.active_job.default_retry_jitterは、失敗したジョブをリトライする際に算出されるdelay timeに”jitter”(ランダムなオフセット値)を適用する。デフォルトは0.15。
    guides/source/configuring.mdより大意

つっつきボイス:「おぉジッターじゃないですか: ジッターって自分で設定するものなんだろか?😆」「デフォルト値があってカスタマイズもできるみたいですね」

「お集まりの皆さんはジッター(jitter)ってわかります?」「電気方面で見たことある気がします」「ネットワーク用語などでは『ばらつき』みたいなネガティブな意味で使われるんですけど、ここではActive Jobのリトライのジッターなので違うニュアンスですね」「おぉ?」「たとえばジョブのリトライを5秒待つ場合、普通は5秒きっかり待つんですけど、それに1秒のジッターを与えるということは待ち時間を4秒から6秒の間でばらつかせるということだと思います」

参考: ジッター - Wikipedia

「ここはある程度想像ですけど、ジッターを与える理由があるとすれば、ワーカーがたくさんあってそれらが同時に失敗した後リトライがまったく同時に発生してしまうと負荷が集中してしまうので、ジッターを与えることで負荷をそこそこ分散させる、なんてのが考えられますね☺」「なるほど!」「特にサーバー負荷が高いせいでジョブが同時にたくさん失敗した場合は、ジッターがないとリトライのタイミングが揃いすぎてまた同じことが繰り返し起きてしまいますので😇」「ならしたいというか均質化したいというか」「みんな出口に同時に押し寄せると詰まるから適度に譲り合うみたいな😆

「ジッターがゼロだと永遠にリトライが終わらないことがあるかもしれないので、そういう場合にジッターを付ければ、たまたま最初にリトライしたジョブが成功して、ジッターの設定次第ですがジョブが少しずつでも片付いていくでしょうね: あえて設定するかどうかは別ですが😆


jitterはもともと「神経過敏でピリピリする」といったニュアンスのようです。

ジッターの連想でMichel Legrandアレンジの「Jitterbug Waltz」です。Jitterbugと「ジルバ」が同じものだと知ってびっくりした覚えがあります。

⚓入力値が正しくならない問題のリグレッションを修正

  hidden_field_tag('token', value: [1, 2, 3])
↓
<input type="hidden" value="">
# actionview/lib/action_view/helpers/tag_helper.rb#L94
        def tag_option(key, value, escape)
          case value
          when Array, Hash
-           value = build_tag_values(value)
+           value = build_tag_values(value) if key.to_s == "class"
            value = escape ? safe_join(value, " ") : value.join(" ")
          else
            value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
          end
          value = value.gsub('"', """) if value.include?('"')
          %(#{key}="#{value}")
        end
        private
          def build_tag_values(*args)
            tag_values = []

            args.each do |tag_value|
              case tag_value
-             when String
-               tag_values << tag_value if tag_value.present?
              when Hash
                tag_value.each do |key, val|
                  tag_values << key if val
                end
              when Array
                tag_values << build_tag_values(*tag_value).presence
+             else
+               tag_values << tag_value.to_s if tag_value.present?
              end
            end

            tag_values.compact.flatten
          end

つっつきボイス:「リグレッションを修正したようです」「hidden fieldのvalue: [1, 2, 3]が消える…だと?」「valueにarrayを渡すなんてことがあるとは😳

「arrayを渡すとどうなるのが正解なんでしょうね?🤔」「テストを見ると本来はスペース区切りになるのか↓」「そうでしたか!」「まあどうせ文字列になりますし😆」「自分ならこういう使い方はまずやりませんけど、そういう挙動なんだへぇ〜という気持ち😆

# actionview/test/template/tag_helper_test.rb#283
   str = content_tag("p", "limelight", class: [1, 2, 3])
    assert_equal "<p class=\"1 2 3\">limelight</p>", str

⚓データベースURLで=をサポート

=はPostgreSQLコネクションでデータベースURLフォーマットを用いてoptionsを渡すのに必要。
同PRより大意


つっつきボイス:「ぽすぐれ対応みたいです」「なるほど、こういうoptions=でオプションを渡せるようになったと」

# 同PRより
# config/database.yml

development:
  url: postgresql://localhost/railsdevapp_development?options=-cmysetting.debug=on

「以前も話題にしましたけど、postgresql://みたいな書き方ってデータベースごとに独自過ぎますよね😆」「オレオレ度高い😆

⚓番外: 不要なrequireを削除

# activesupport/lib/active_support/callbacks.rb#L3
require "active_support/concern"
require "active_support/descendants_tracker"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/class/attribute"
- require "active_support/core_ext/kernel/reporting"
- require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/string/filters"
require "thread"

つっつきボイス:「とてもささやかな修正ですが年末お掃除感🧹あったので」「最近地味に進められている、不要なrequireを消すヤツですね」「想像ですけどZeitwerk導入で要らなくなったのかな?🤔」「自動で探せそうな気もしますね☺

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

⚓番外: Active Recordの最適化

つっつきの後で見かけたツイートです。

⚓Rails

⚓RailsでネストしたAPIパラメータのバリデーションをActiveModel::Validationsでやってみた(Awesome Rubyより)


つっつきボイス:「ネストしたJSONパラメータのバリデータをこんな感じに作ってみたという記事っぽい☺

# 同記事より
# lib/affiliate/lead/params_validation/main.rb
module Affiliate
  module Lead
    module ParamsValidation
      class Main < Base
        validate :validate_customer

        private

        def validate_customer
          customer_validator = Customer.new(data[:customer])

          return if customer_validator.valid?

          add_nested_errors_for(:customer, customer_validator)
        end
      end
    end
  end
end

# lib/affiliate/lead/params_validation/customer.rb
module Affiliate
  module Lead
    module ParamsValidation
      class Customer < Base
        validates_presence_of :name

        validate :validate_address

        private

        def validate_address
          address_validator = Address.new(data[:address])

          return if address_validator.valid?

          add_nested_errors_for(:address, address_validator)
        end
      end
    end
  end
end

# lib/affiliate/lead/params_validation/customer/address.rb
module Affiliate
  module Lead
    module ParamsValidation
      class Customer
        class Address < Base
          validates_presence_of :postal_code
        end
      end
    end
  end
end

「こういうのってSwaggerというか最近だとOpenAPIとか使ってやるのかな?(ウォッチ20180806)」「同じ記事でgrape↓もおすすめされていて、ウォッチでもちらっとだけ取り上げたことがあったのを思い出しました(ウォッチ20170804)」「grapeはかなり昔からRailsで使われてますね☺」「APIやるgemなのか」「今はRailsもAPIモードありますけど😆」「grapeの定義の仕方は個人的にちょい苦手😅

「今回モバイル系の方もいらっしゃるのでおたずねしますけど、クライアントとサーバーの間の仕様の握りって何を使ってます?Excel仕様書?😆」「Excel仕様書もあるにはあります😆」「うちはGraphQLとか

「GraphQLでこういうネストの深いJSONとか複雑なデータ構造を扱うときってつらくなったりします?」「印刷して収まる範囲なら何とか」「シンプルなAPIならGraphQLでさくっとできそうですね😋」「GraphQLだとクライアントが投げるクエリによってはしれっと重くなったりしません?」「まあありますね😆」「インデックス張ってないところを取ろうとしたときとか😆」「クライアントは今のところそんなに重くないんで何とかなってます

「GraphQLの記事はひところに比べて落ち着いた印象ありますけど」「観測範囲では使われているところでは使われていますね☺」「まあちょろいSELECTクエリのために実装してAPI仕様を渡すよりはGraphQLの方が楽といえば楽かも😆

RailsでGraphQL APIをつくる: Part 1 – GraphQLとは何か(翻訳)

⚓secret_key_baseをお漏らししないために


つっつきボイス:「RubyのMarshal.loadにユーザ入力由来の値が入ると危険、そりゃそうだ😆」「あぁなるほど、ActiveSupport::MessageVerifierActiveSupport::MessageEncryptorがデフォルトでMarshalになっているのが潜在的に危険だからデフォルトをJSONにしようねという話か」「まあ言われてみれば😆

「今回はPHPやってる人もいますけど、PHPにも任意のオブジェクトをstringに変換する機能がありましたよね?たしかvar_dumpだったかな」「あります」「あれがRubyで言うMarshalに相当します」

参考: PHP: var_dump - Manual

⚓tapping_device gemに新機能

tap_on!tap_sql!です。

# 同記事より
def index
  @posts = Post.all
  tap_on!(@posts) do |payload|
    puts(payload.method_name_and_location)
  end
end
# 同記事より
tap_sql!(@posts, exclude_by_paths: [/activerecord/]) do |payload|
  puts("Method `#{payload.method_name}` generates sql: #{payload.sql}")
  puts("  From #{payload.filepath}:#{payload.line_number}")
end

つっつきボイス:「こちらは私の友だちのst0012さんが最近熱心にメンテしている、主にRailsを想定したRubyのデバッグ用gemで、それに新機能を足したという記事です☺

tap_sql!はSQLを生成したタイミングで取れるみたいな?」「そんな感じです」

Method `count` generates sql: SELECT COUNT(*) FROM "posts"
  From /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:3
Method `each` generates sql: SELECT "posts".* FROM "posts"
  From /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:16
Method `count` generates sql: SELECT COUNT(*) FROM "posts" WHERE "posts"."user_id" = ?
  From /PROJECT_PATH/rails-6-sample/app/views/posts/index.html.erb:31

tap_on!はメソッドがどこで呼ばれたかを追えるようです」「ほほぉ😋

Method: :eager_load_values, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation.rb:668
Method: :includes_values, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation.rb:669
Method: :eager_loading?, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation/calculations.rb:226
Method: :includes_values, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation/calculations.rb:226
Method: :has_include?, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation/calculations.rb:128
Method: :distinct_value, line: /Users/st0012/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/activerecord-6.0.0/lib/active_record/relation/calculations.rb:234

「新機能の実装がえらくしんどいと言ってました↓」「こういうふうにオブジェクトに触られた瞬間を全部追いかけたいと思ったらこういうgemを使うしかないですね〜☺

⚓実装をどっちに置くか


つっつきボイス:「この記事はRails以外でも通用する感じですね☺」「永遠の課題というか😆」「モバイルの人たちにも関係する話だ」

「最近だとAPIサーバー的な、データを処理するロジックだけサーバーでやって、後はクライアントでやるというのが世の中的に増えてきてる感じですが、モバイルの方から見ていかがでしょう?」「記事ではサーバーに寄せる方針でやってるようですね🤔」「実はサーバーもクライアントも一人で作っているのであれば、この辺は割とどうでもよかったりする😆」「たしかに😆

「ただね〜、バックエンドとフロントエンドを別の会社が作って連携するとなるとね…特にiOSとAndroidも別会社だったりするとなおさら😆」「😆」「そういう状況でサーバー側に機能を寄せるといろいろつらくなりがちなので、サーバーはAPIに専念する方がマシという気持ちにはなる☺」「そこら辺は案件によると言ってしまえばそれまでですが、割とサーバーよりはクライアント側で頑張る感じかなと」「なるほど」

「サーバー側で頑張るならいっそWebViewにしちゃうのがいいかなって思ったり😆」「ruby-jp Slackでもその辺が話題になってたみたいですね」「ある意味永遠のテーマ😆」「モデルをいくつも使うような複雑なバリデーションならサーバー側に寄せたいし」「iOSとAndroidが両方あるとそれぞれにロジック書くのはつらいし」「iOSとAndroidで表示がぶれるとイヤですね〜😅」「そこはクライアント側で頑張って欲しいですけど😆

参考: WebViewとネイティブのメリットデメリット - Qiita


「以下はruby-jp Slackで見かけた別のスライドですが、Monomicrolithという異様な言葉が気になりました😆」「これはKubeConのスライドでマイクロサービス寄りの話なので上とはつながりはないかな〜☺」「そうでした😅」「お集まりの皆さんの中でマイクロサービスで開発してる方は…?」「見事にいない😆


kubecon-jp.connpass.comより

「マイクロサービスをきれいに作れる気がしないとか大規模なシステムでないと効果が薄いとかはありますけど、絶対に守りたい機能をマイクロサービスで固く固く作ってソースもめったに変えないとかならあってもいいかも☺

⚓非推奨になったfind系メソッドを殺して回る(Hacklinesより)


つっつきボイス:「おぉ〜超なつかしい書き方↓、久しぶりに見た😆」「😆」「Rails 2の頃のfindメソッドですけど、自分Rails 3からだったので自分から書いたことありませんし(古いRailsで仕方なしに書いたことはありますけどっ😆)」

# 同記事より
Post.find(:all, conditions: { published_on: 2.weeks.ago }, limit: 5 ...)

「Synvertというgemを使って古い記法を修正したそうです↓」「へぇ〜こうやって変換してくれるんだ😳」「そういえばウォッチでも扱ったかな(ウォッチ20170908)」「AST解析してやってくれるのね😋

# 同記事より
# Handles find with hash options
$ synvert -r rails/convert_models_2_3_to_3_0

# Handles dynamic finders
$ synvert -r rails/convert_dynamic_finders

「信頼していいんでしょうか?😅」「まあ機械的に置換できるものならやっちゃっていいと思いますよ☺」「思わず記事の日付確認しちゃったけど新しい記事だし😆」「Rails 2ってRuby 1.8ぐらいの頃でしょうか?」「ですね: 今は亡きRuby Enterprise Editionとか使われてた頃😆」「😆」「ソースコードの冒頭に# coding: UTF-8とか書いてた時代😆

Ruby Enterprise Editionは2012年に終了していたんですね↓😳

参考: Welcome — Ruby Enterprise Edition

参考: Ruby で UTF-8 なのにマジックコメントが必要なケース - Qiita

⚓その他Rails



つっつきボイス:「↑そういえばこの情報って意外とまとまってそうでまとまってませんでしたね」「今はbin/railsが正というか推奨」「でも今でもbundle execって書きますけどっ😆

「binstubっていう、プロジェクトのbin/の下のコマンドは、以前はGitにコミットしないでくれということになってて.gitignoreにも登録されてたんですけど、Rails 5ぐらいからbinstubもプロジェクトにコミットすべきという話が出てコミットされるようになりましたね☺

binstubについてピンポイントの情報が見つからなかったので、やや近いStack Overflowを貼ります。

参考: Should I add the Rails 4 bin/ directory to git? - Stack Overflow


前編は以上です。

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

週刊Railsウォッチ(20191210後編)Ruby 2.7の変更点記事、mrubyで動くmitamae、画像系コラボレーションツールほか

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

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

Rails公式ニュース

Ruby Weekly

Awesome Ruby

Hacklines

Hacklines

Ruby: Object#tap、Object#then を使ってみよう

$
0
0

令和元年最初の年末です。いかがお過ごしでしょうか。私は年末進行まっただ中です😢
今回は聞いたことあるけどあんまり使う機会のなさそうな #tap#yield_self#then の話をしてみたいと思います。

Object#tap

仕様としてはAPIリファレンスを参照すると

self を引数としてブロックを評価し、self を返します。

ということなので、疑似実装としては以下のようになると思います。

class Object
  def tap(&block)
    yield self
    self
  end
end

#tap は古くから存在するメソッドで、1.8.3の頃から存在するようです。

Object#yield_self / Object#then

仕様としてはAPIリファレンスには

self を引数としてブロックを評価し、ブロックの結果を返します。

とあります。擬似的な実装は以下のようになると思います。

class Object
  def yield_self(&block)
    yield self
  end
end

#yield_self は ruby2.5から導入され、#then はそのエイリアスとして ruby2.6 から導入されました。

#then はMatzが直々のコミットです。導入された経緯は RubyKaigi2018のキーノートがとても印象的です(5:32〜)。

両者の違いをみてみる

method 実行内容 戻り値
#tap self を引数としたブロックの評価 self
#then / #yield_self self を引数としたブロックの評価 ブロックの結果

異なるのは戻り値となります。
レシーバー自身が戻るのが #tap
ブロックの評価値が戻るのが #then / #yield_self となります。

実際に使ってみる

では、#tap#then を使ってみて、両者を比較してみましょう。

  • #tap
hash = {}
#  => {}
hash.tap{ |h| h[:value] = 42 }
#  => {:value=>42}
hash
#  => {:value=>42}

  • #then
hash = {}
#  => {}
hash.then{ |h| h[:value] = 42 }
#  => 42
hash
#  => {:value=>42}

このように、ブロックを評価して hash[:value]42 を設定することは同じですが、#tap の場合は、ブロックの内容にかかわらずレシーバー自身が返り、 #then の場合はブロックの評価結果が返ります。

Array#eachArray#map と似てませんか

似てるなと思った方は勘が鋭いです。やってみましょう。

  • Array#each
[1, 2, 3].each{ |i| i*2 }
#  => [1, 2, 3]

ブロックの中で各要素に対する副作用が発生しなければ、レシーバーそのものが返ります。

  • Array#map
[1, 2, 3].map{ |i| i*2 }
#  => [2, 4, 6]

各要素をブロックで評価された、新しいArrayが返ります。

このように、 #tap は、 #each のオブジェクト版。 #then#map のオブジェクト版といえるのではないでしょうか。

うまい使いどころ

#tap#then のうまい使い方をご紹介します。

Railsコンソールでのデバッグ

Railsコンソールには reload! というコマンドがあり、実行するとプロジェクト内のソースコードを読み込み直してくれます。

ところが、変数に設定したオブジェクトの中身までは変更してくれません。

参考: Rails consoleでreload!する場合の注意点 - Qiita

これを回避してみたいと思います。

Railsコンソールのreload!はオブジェクトに効かない

まずは以下の例を見てみましょう。

以下のようなクラスがあるとします。

class User
  attr_accessor :age
end

Railsコンソールで以下のように実行します。

# (rails console)
> user = User.new
> user.age = 30
> user.age
#  ==> 30

Railsコンソールを開いたままで User クラスを変更します。

class User
  attr_accessor :age
  def age
    '18歳だよ'
  end
end 

Railsコンソールで reload! して #age の振る舞いを見てみます

> reload!
> user.age
#  ==> 30  

# (直ってないやん。仕方が無いのでオブジェクトの作り直し)
> user = User.new
> user.age
#  ==>  18歳だよ

#tapを使って回避する

変数にオブジェクトを格納するからマズいのです。
#tap を使ってワンライナーにしてみましょう。

> reload! && User.new.tap{ |user| user.age = 30 }.age
#  ==> 30

# (ソースの変更後、上キーを押して履歴実行)
> reload! && User.new.tap{ |user| user.age = 30 }.age
#  ==> 18歳だよ

実装変更の確認を高速にできるようになりました

ViewHelperをスッキリ書く

数値に , をつけてくれる number_with_delimiter というViewHelperがあります。

<%= number_with_delimiter(@number) %>

ところが number_with_delimiter の引数はnilを許容しません。普通ですと

<% if @number_or_nil %>
  <%= number_with_delimiter(@number_or_nil) %>
<% end %>

とか

<%= @number_or_nil.present? ? number_with_delimiter(@number_or_nil) : '' %>

みたいな読むのがつらいコードになっていくと思います。
(カンマ区切りの数値の出力が1箇所だけならいいですね)

#then&. を組み合わせると以下のようにスッキリ書けます。

<%= @number_or_nil&.then{ |num| number_with_delimiter(num) } %>

@number_or_nil が nil の場合、&. の後ろが評価されないため nil となります。
@number_or_nil が 数値の場合、ブロックの評価値である number_with_delimiter の変換結果が返ります。

nilがあり得るオブジェクトのデコレーターを適用したイメージになると思います。

APIで返すべきものを強調する

例えば以下のような実装があるとします

def spaghetti
  pot = Pot.new
  pot.put(water)
  spaghetti = Spaghetti.pickup(300)
  pot.put(spaghetti)
  pot.boil(7.minutes)
end

このコードは、 Pot#boil の評価値が返ります。スパゲッティかもしれませんし、煮え湯かもしれませんし、鍋かもしれませんし、7分かもしれませんし、nilかもしれません。
でも、#spaghetti が返却するべきはスパゲッティです。

たぶんバグってますね。
最終行に spaghetti と書いてもいいのですが、以下のように書いてもよいと思います。

def spaghetti
  Spaghetti.pickup(300).tap do |spaghetti|
    Pot.new
      .tap{ |pot| pot.put(water) }
      .tap{ |pot| pot.put(spaghetti) }
      .boil(7.minutes)
  end
end

とにかく#spaghetti はスパゲッティが返ります。
具体的には、ポットに水を入れて、スパゲッティを入れて、7分間ゆでられています。

#tap#then を見かけたら、結局何が返ってくるか?を頭に入れてから、ブロックの中身を読んでみるとよいと思います。

まとめ

#tap#then は一見複雑そうに見えますが、「まず何が戻るのか見る。それからブロックの中身を読む」を心がけると逆に可読性が上がる場合があります。

必ず可読性が上がるわけではありませんが、使えるかも?とおもったら是非お試しください。

関連記事

Ruby: `each`よりも`map`などのコレクションを積極的に使おう(社内勉強会)

週刊Railsウォッチ(20191223前編)Railsセキュリティ修正6.0.2.1と5.2.4.1リリース、Ruby 2.7.0-rc2リリース、ActiveRecordのコールバック回避ほか

$
0
0

こんにちは、hachi8833です。今年最後のRailsウォッチ前編をお送りします。Ruby 2.7のカウントダウンが始まっていますので、クリスマスにはリリースされるでしょう。

その後21日にrc2がリリースされました。

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

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

第18回目公開つっつき会は、来年1月09日(木)19:30〜にBPS会議スペースにて開催されます。今回から会場のPubスペースが隣の部屋に移ります。

週刊Railsウォッチの記事やここだけの話にいち早く触れられるチャンス!発言・質問も自由です。皆さまのお気軽なご参加をお待ちしております🙇

⚓臨時ニュース: Railsセキュリティ修正6.0.2.1と5.2.4.1がリリース

Rackで情報漏えい/セッションハイジャックの脆弱性が見つかったとのことです。どちらもRackのバージョンアップとAction Packのみの修正です。

ActionDispatch::Session::MemcacheStoreがまだ脆弱なので、dalli gemもアップデートが必須。
Changelogより大意

オレオレアプリは対応しました。

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

お詫び: 今回の録画に音声が入っていなかったため、最小限のつっつきボイスとなっています🙇🙇

以下から見繕いました。

⚓delegateがRuby 2.7の...記法に対応

# activesupport/lib/active_support/core_ext/module/delegation.rb#L202
-     definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"
+     definition = if /[^\]]=$/.match?(method)
+       "arg"
+     elsif RUBY_VERSION >= "2.7"
+       "..."
+     else
+       "*args, &block"
+     end

以下は先週も貼りましたが一応。

参考: Ruby 2.7 の変更点 - 「...」で全引数渡し - @tmtms のメモ
参考: Ruby 2.7 adds shorthand syntax for arguments forwarding – Saeloun Blog


つっつきボイス:「...はRuby 2.7の新機能か😳」「"*args, &block"と同等なんですね」

参考: $@ - シェルスクリプトに渡されたすべての引数

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

⚓bundle exec rubyでテストを実行できるよう修正

require: cannot load such file -- abstract_unit (LoadError)のようなエラーはRailsが初めての人にとってうれしくないので、いい感じにシンプルにした。またテストの依存関係も明確になり、サブフォルダでテストを実行できるようにもなった。
同PRより大意


つっつきボイス:「Railsに慣れていない人がくじけないようにとのことです」「修正ファイル144個は多いな〜😆」「修正されたissue #34025↓を見るとbundle exec ruby test/cache/stores/mem_cache_store_test.rbって実行してエラーになってたけど、こんなふうに実行したことないし😆

⚓yearが未定義の場合の扱いを修正

# actionview/lib/action_view/helpers/date_helper.rb#L832
      def select_year
-       if !2019/12/23time || 2019/12/23time == 0
+       if !year || 2019/12/23time == 0
          val = "1"
          middle_year = Date.today.year
        else
          val = middle_year = year
        end
        if @options[:use_hidden] || @options[:discard_year]
          build_hidden(:year, val)
        else
          options                     = {}
          options[:start]             = @options[:start_year] || middle_year - 5
          options[:end]               = @options[:end_year] || middle_year + 5
          options[:step]              = options[:start] < options[:end] ? 1 : -1
          options[:leading_zeros]     = false
          options[:max_years_allowed] = @options[:max_years_allowed] || 1000
          if (options[:end] - options[:start]).abs > options[:max_years_allowed]
            raise ArgumentError, "There are too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter."
          end
          build_select(:year, build_year_options(val, options))
        end
      end

つっつきボイス:「yearが定義されていない場合今までエラーになってたのね😳」「テストを見るとわかりやすいかも↓」

# actionview/test/template/date_helper_test.rb#473
  def test_select_year_with_empty_hash_value_and_no_start_year
    expected = +%(<select id="date_year" name="date[year]">\n)
    expected << %(<option value="2014">2014</option>\n<option value="2015">2015</option>\n<option value="2016">2016</option>\n<option value="2017">2017</option>\n<option value="2018">2018</option>\n)
    expected << "</select>\n"

    Date.stub(:current, Date.new(2018, 12, 18)) do
      assert_dom_equal expected, select_year({ year: nil, month: 4, day: nil }, { end_year: 2018 })
    end
  end

⚓マイグレーションのremove_columnsをリバーシブルに

removetype:を書いておけばリバースできるようになるそうです。

# 同PRより
class InvertibleChangeTableMigration < SilentMigration
  def change
    change_table("horses") do |t|
      t.column :name, :string
      t.remove :remind_at, type: :datetime
    end
  end
end

つっつきボイス:「マイグレーションのchangeremoveを書けるようになったそうです」「自分は使わないかな〜😆

⚓番外: 細かな修正

# railties/lib/rails/generators/rails/app/templates/config/puma.rb#L7
-threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
+threads_count = Integer(ENV.fetch("RAILS_MAX_THREADS", "5"))
threads threads_count, threads_count

つっつきボイス:「#28499は↑今までブロック渡しだったのを引数に変えた」「#38020はAction Textのドキュメントに補足↓」「そういえばWebpacker関連の記述って今のRailsガイドにほとんどありませんね😆」「WIPなガイドは他にもありますし☺

# guides/source/action_text_overview.md#L47
-Run `rails action_text:install` to add the Yarn package and copy over the necessary migration.
-Also, you need to set up Active Storage for embedded images and other attachments.
-Please refer to the [Active Storage Overview](active_storage_overview.html) guide.
+Run `rails action_text:install` to add the Yarn package and copy over the necessary migration. Also, you need to set up Active Storage for embedded images and other attachments. Please refer to the [Active Storage Overview](active_storage_overview.html) guide.
+
+After the installation is complete, a Rails app using Webpacker should have the following changes:
+
+1. Both `trix` and `@rails/actiontext` should be required in your JavaScript pack.
+
+```js
+// application.js
+require("trix")
+require("@rails/actiontext")
+```
+
+2. The`trix` stylesheet should be imported into `actiontext.scss`.
+
+```scss
+@import "trix/dist/trix";
+```
+
+Additionally this `actiontext.scss` file should be imported into your stylesheet pack.
+
+```
+// application.scss
+@import "./actiontext.scss";
+```

⚓Rails

⚓RailsとDDD

RailsはActiveRecordが密結合で実現しているためDDDのモデリングの考え方と相性が悪く、レイヤードアーキテクチャをやろうとすると型がないことなどDDDの考え方が適応しにくい
同記事より


つっつきボイス:「上みたいな記事を割と見かけるんですがやっぱり難しいんでしょうか?」「RailsでDDDをやるのは簡単ではありませんね🧐: Active Recordモデルを永続化に専念させるとか、いろいろやらないといけなくなりますし」「そうでしたか…RailsとDDDの記事は新しいのが少ないのはそういう事情があるのかも?🤔」「どうしてもやりたいならHanamiとかにする方がいいんじゃね?😆

参考: Rails 歴5年の僕が Laravel で開発するようになって思ったこと。|Kurashicom Engineers’ Blog|note

⚓Active Recordのコールバックを回避(Ruby Weeklyより)

Active Recordのコールバックにビジネスロジックを実装すべきでないと強く信じている。
同記事より抜粋

# 同記事より
class Address
  attr_accessor :disable_geolocation
  before_save :set_geolation, unless: :disable_geolocation

  private

  def set_geolocation
    # Hit some API or something...
  end
end

つっつきボイス:「disable_geolocationがtrueならコールバックしないようにしてる」「そういえば銀座Rails#15でもコールバック消し去りたい話が出てたのを見た覚えがあったんですが、この辺のツイートにあったのをやっと見つけました↓」「kamipoさんとyahondaさんの対談ですね☺

「とりあえず言っておきたいのは、コールバックとconcernsは相性が悪い」「あぁ!」

「そういえばkazzさんはコントローラのbefore_actionコールバックは例外的に好きって言ってました」「コントローラのbefore_actionも気をつけないとカオスになりますよ💀」「うぅ😢

⚓RubyConf 2019@Nashvilleまとめ


同記事より


つっつきボイス:「手描きのまとめが素敵だなと思って❤」「でも他の人が読むにはつらいという😆

⚓その他Rails

つっつきボイス:「RailsとReactとTerraformとスクラム開発やれたらマジ有能」


つっつきボイス:「Rackは思ったより難しくないし記事も多いので、どんどん作って動かしてみるのがいいと思います😋


同リポジトリより

参考: 海外記事翻訳シリーズ 【第 2 回】 Rack 仕様
参考: Rails と Rack - Railsガイド
参考: 第23回 Rackとは何か(1)Rackの生まれた背景:Ruby Freaks Lounge|gihyo.jp … 技術評論社


前編は以上です。

おたより発掘

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

週刊Railsウォッチ(20191217後編)Ruby 2.7の変更点とパターンマッチング、依存性自動アップデートツール、Stack Overflowアンケート2019ほか

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

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

Rails公式ニュース

Ruby Weekly

Rails: Bulletで検出されないN+1クエリを解消する

$
0
0

はじめに

普段、Railsを使って開発されている方であれば、関連するテーブルのデータを扱うときなど、N+1クエリを発行していないか、気をつけているかと思います。

また、うっかりN+1クエリを発行してしまうことを防ぐため、N+1クエリを自動で検出するBulletというgemを導入しているかたも多いかと思います。ただ、Bulletで検出されないクエリでもN+1になっているケースがあります。

この記事ではBulletでは検出されないが、N+1になっているクエリを改善する方法を紹介します。

N+1問題とは

N+1問題とは、1回のクエリで済むところをデータ量(N)の回数、クエリを発行してしまう問題のことです。
例えば以下のようなクエリはN+1クエリです。

Post Load (0.3ms)  SELECT "posts".* FROM "posts"
  ↳ app/views/posts/index.html.erb:14
  Comment Load (0.4ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
  ↳ app/views/posts/index.html.erb:17:in `map'
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 2]]
  ↳ app/views/posts/index.html.erb:17:in `map'
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 3]]
  ↳ app/views/posts/index.html.erb:17:in `map'

上の例ではpostsを読み込むのに1回、そこからpostsに関連するcommentsを読み込むのにpostsのデータ量である3(N)回、合計4回のSQLが発行されています。
データ量(N)が増えるほど、発行されるSQLが多くなりその分パフォーマンスが低下します。

このN+1クエリを自動で検出してくれる便利なgemがBulletです。Bulletを使うと、RailsのログやブラウザでN+1であることを確認できます。

N+1クエリの解消

N+1クエリの検証のため、サンプルのRailsアプリケーションを作成します。
サンプルアプリケーション作成後、一般的なBulletで検出されたN+1クエリの解消法を紹介します。そのあと、Bulletでは検出されないN+1クエリの解消法を紹介します。

各ライブラリのバージョンは以下です。

  • Rails: 6.0.2
  • Bullet: 6.0.2

まずはアプリとテーブルを作成します。

$ rails new test_n_plus_1
$ cd test_n_plus_1
$ rails g scaffold post name:string
$ rails g scaffold comment name:string post:references positive_count:integer negative_count:integer
$ bundle exec rake db:migrate

モデルの関連を定義します。

# app/model/post.rb
class Post < ActiveRecord::Base
  has_many :comments
end
# app/model/comment.rb
class Comment < ActiveRecord::Base
  belongs_to :post
end

Railsコンソールからデータを作成します。

$ rails c
>> post1 = Post.create(:name => 'first')
>> post2 = Post.create(:name => 'second')
>> post3 = Post.create(:name => 'third')
>> post1.comments.create(:name => 'first', positive_count: 3, negative_count: 1)
>> post1.comments.create(:name => 'second', positive_count: 10, negative_count: 4)
>> post2.comments.create(:name => 'first', positive_count: 3, negative_count: 1)
>> post2.comments.create(:name => 'second', positive_count: 10, negative_count: 4)
>> post3.comments.create(:name => 'first', positive_count: 3, negative_count: 1)
>> post3.comments.create(:name => 'second', positive_count: 10, negative_count: 4)

Bulletをインストールします。

Gemfileに以下を追加:

gem "bullet"
$ bundle install
$ bundle exec rails g bullet:install

Bulletで検出されるN+1クエリを解消する

まずはBulletで検出されるN+1クエリの解消法を紹介します。
以下のように、関連するテーブルデータをeach内で参照するケースを考えます。

<!-- app/views/posts/index.html.erb -->
<% @posts.each do |post| %>
  <tr>
    <td><%= post.comments.map(&:name) %></td>
  </tr>
<% end %>

Bulletを導入することでN+1クエリを検出した際、以下のようにログやブラウザでN+1クエリであることを知らせてくれます。

USE eager loading detected
  Post => [:comments]
  Add to your finder: :includes => [:comments]

Bulletで検出されたN+1クエリは以下のように解消します。

$ diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index afbce67..53b1e2d 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -4,7 +4,7 @@ class PostsController < ApplicationController
   # GET /posts
   # GET /posts.json
   def index
-    @posts = Post.all
+    @posts = Post.includes(:comments)
   end

このように、#includes を指定することで関連をまとめて取得、キャッシュし、最小限のクエリ回数で読み込まれるようになります。

変更前のクエリ
Post Load (0.3ms)  SELECT "posts".* FROM "posts"
  ↳ app/views/posts/index.html.erb:14
  Comment Load (0.4ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
  ↳ app/views/posts/index.html.erb:17:in `map'
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 2]]
  ↳ app/views/posts/index.html.erb:17:in `map'
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 3]]
  ↳ app/views/posts/index.html.erb:17:in `map'
変更後のクエリ
Post Load (0.2ms)  SELECT "posts".* FROM "posts"
  ↳ app/views/posts/index.html.erb:14
  Comment Load (0.8ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (?, ?, ?)  [["post_id", 1], ["post_id", 2], ["post_id", 3]]
  ↳ app/views/posts/index.html.erb:14

変更前はpostsの3(N)個のデータ量の回数、発行されていた3回のクエリが1回になりました。

Bulletで検出されないN+1クエリを解消する

次に、関連テーブルのカラムを参照するだけでなく、関連テーブルの該当カラムの合計を計算してそれを表示するケースを考えてみます。例えば、以下のようなケースです。

<!-- app/views/posts/index.html.erb -->
<% @posts.each do |post| %>
  <tr>
    <td><%= post.comments.sum(:positive_count) %></td>
    <td><%= post.comments.sum(:negative_count) %></td>
  </tr>
<% end %>

この場合、each内で都度、合計値を取得する余分なクエリが発生します。

Post Load (0.2ms)  SELECT "posts".* FROM "posts"
  ↳ app/views/posts/index.html.erb:14
   (0.2ms)  SELECT SUM("comments"."positive_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
  ↳ app/views/posts/index.html.erb:16
   (0.1ms)  SELECT SUM("comments"."negative_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
  ↳ app/views/posts/index.html.erb:17
   (0.1ms)  SELECT SUM("comments"."positive_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 2]]
  ↳ app/views/posts/index.html.erb:16
   (0.1ms)  SELECT SUM("comments"."negative_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 2]]
  ↳ app/views/posts/index.html.erb:17
   (0.1ms)  SELECT SUM("comments"."positive_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 3]]
  ↳ app/views/posts/index.html.erb:16
   (0.2ms)  SELECT SUM("comments"."negative_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 3]]
  ↳ app/views/posts/index.html.erb:17

また、BulletはN+1であると検出してくれません。
この場合、以下のようなscopeを定義します。JOINでテーブルを結合することで、1回のクエリで必要な情報を取得します。

# app/models/post.rb
scope :likes_count_per_id, -> {
  joins(:comments)
    .select(
      "posts.id,
       posts.name,
       SUM(comments.positive_count) as sum_positive,
       SUM(comments.negative_count) as sum_negative"
    ).group(:id)
}

このようなscopeを定義することで、each内では、取得したsum_positivesum_negativeを呼び出すことができます。
使い方は以下です。

# diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index afbce67..cfdc463 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -4,7 +4,7 @@ class PostsController < ApplicationController
   # GET /posts
   # GET /posts.json
   def index
-    @posts = Post.all
+    @posts = Post.likes_count_per_id
   end
<!-- app/views/posts/index.html.erb -->
<% @posts.each do |post| %>
  <tr>
    <td><%= post.sum_positive %></td>
    <td><%= post.sum_negative %></td>
  </tr>
<% end %>
変更前のクエリ
Post Load (0.2ms)  SELECT "posts".* FROM "posts"
  ↳ app/views/posts/index.html.erb:14
   (0.2ms)  SELECT SUM("comments"."positive_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
  ↳ app/views/posts/index.html.erb:16
   (0.1ms)  SELECT SUM("comments"."negative_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 1]]
  ↳ app/views/posts/index.html.erb:17
   (0.1ms)  SELECT SUM("comments"."positive_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 2]]
  ↳ app/views/posts/index.html.erb:16
   (0.1ms)  SELECT SUM("comments"."negative_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 2]]
  ↳ app/views/posts/index.html.erb:17
   (0.1ms)  SELECT SUM("comments"."positive_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 3]]
  ↳ app/views/posts/index.html.erb:16
   (0.2ms)  SELECT SUM("comments"."negative_count") FROM "comments" WHERE "comments"."post_id" = ?  [["post_id", 3]]
  ↳ app/views/posts/index.html.erb:17
変更後のクエリ
Post Load (0.1ms)  SELECT posts.id,
         posts.name,
         SUM(comments.positive_count) as sum_positive,
         SUM(comments.negative_count) as sum_negative FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" GROUP BY "posts"."id"
  ↳ app/views/posts/index.html.erb:14

変更前はデータ量(N)分、発行されていた6回(positive_count, negative_countで各3回)のクエリが1回になりました。

まとめ

Bulletには検出されていなくても、N+1クエリであったときに改善する方法を紹介しました。データ量がそれなりにあって、each内で都度、集計などの重い処理をする場合、思わぬパフォーマンスの低下を招くことがあります。

普段からlogでクエリの確認をすることは大事ですし、できる限り本番相当のデータを用意してパフォーマンスは問題ないか、N+1クエリを発行していないかを意識していきたいところです。

関連記事

Rails: JOINすべきかどうか、それが問題だ — #includesの振舞いを理解する(翻訳)

Rails 6のcookieに「purpose」メタデータが追加(翻訳)

$
0
0

概要

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

Rails 6のcookieに「purpose」メタデータが追加(翻訳)

Action Dispatchが提供するcookies.signedcookies.encryptedは、ユーザーによるcookie値の不正な改変を防ぐのに利用できます。

# ユーザーがcookie値を改変できないようにする
cookies.signed[:proxy_id] = current_admin.id

# このときのクライアント側のcookieの中身
# MTAx--fd47225e9e6de0710a4f84104d73fec1e4d94c65

# cookie値の改変だけでなく読み出しもできないようにする
cookies.encrypted[:current_zip] = current_user.zip

# このときのクライアント側のcookieの中身
# aHNzb2dxVkN1bE1MQTd0MnFsSkZ2dz09LS1tM2NHcVZQbjRoT0RVOVdvdE9FdHZnPT0%3D--be007d00dda3678f79fda9d2a7bcfacc6a760919

何が問題だったか

Rails 6より前は、上述のメソッドでcookieの中身しか署名されず、名前には署名されていませんでした。

このため、あるcookieから別のcookieに署名済みデータをコピペされる可能性があるというセキュリティ上の脆弱性がありました。

#17136で、cookieの値をコピーして別のユーザーのところで使える可能性がある例が示されました。

Rails 6以後

Rails 6では、#32937purposeメタデータがcookieに追加されました。

このメタデータではcookieの名前が設定に使われ、それがcookieに埋め込まれます。

これによって、あるcookieの値を別のところにコピペされることを防止します。

cookies.signed[:proxy_id] = current_admin.id

# このときのクライアント側のcookieの中身
# eyJfcmFpbHMiOnsibWVzc2FnZSI6Ik1UQXgiLCJleHAiOm51bGwsInB1ciI6ImNvb2tpZS5wcm94eV9pZCJ9fQ%3D%3D--ac22ece5b73ea1fbd3de8e925be57a173e9f8a2b

上の例では、--より前のデータはBase64でエンコードされた値で、そこにpurposeメタデータとexpiryメタデータが埋め込まれています。

このメタデータが設定されていない従来のcookieについては引き続き配慮されます。

この機能をあえて外したい場合は以下の設定が使えます。

config.action_dispatch.use_cookies_with_metadata = false

この設定はデフォルトでtrueになります。

関連記事

Ruby 2.7: ハッシュからキーワード引数への自動変換が非推奨に(翻訳)

RailsエンジンをRuboCopで徹底的に分離する:前編(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。画像はすべて元記事からの引用です。

RailsエンジンをRuboCopで徹底的に分離する:前編(翻訳)

FlexportのメインとなるバックエンドサービスはRuby on Railsモノリスです。弊社を立ち上げた頃はRailsのおかげでビジネスを素早く進めることができました。しかし、成長著しいスタートアップによくあることではありますが、チームが育つに連れて複雑さを管理するのが困難になってきました。

当初はRailsの利便性のおかげで生産性が向上しましたが、今やそのせいで何が起こっているかを理解するのも困難なありさまです。大量の双方向モデル関連付けやら、ActiveRecordで何でも読み書きできてしまっているとか、グローバルなapp/ディレクトリ構造やら暗黙の振る舞いやら、もうきりがありません。

複雑極まる状態になったアプリを解きほぐすために、私たちはRailsエンジンを使い始めました。Railsエンジンとは、あるRailsアプリの中に内蔵されるモジュールで、独自のディレクトリ構造や名前空間を備えています。しかしながら、Railsエンジンが提供するモジュラリティのほとんどは外面的なものにとどまります。エンジニアがエンジンの内部に直接手を突っ込んでごそごそやることを防いではくれません。

Flexportでは、Railsエンジンのデフォルトの振る舞いを拡張するために、エンジン内部に強めの制約をかけて分離する手法をいくつか編み出しました。本記事では弊社のアプローチとして、Railsエンジンに関連したRuboCopのcopを3つご紹介します。3つのcopはオープンソースとして公開していますので、コミュニティで広く共有いただけます。

オープンソースのステータスrubocop-flexportgemおよびリポジトリを一般公開しています。

Railsエンジンの分離について

「Railsエンジンの分離」とは、エンジンの外部にあるコードからエンジン内部を自由に読み書きできないようにすること、そしてその逆に、エンジン内部からエンジン外部を自由に読み書きできないようにすることを指します。

まずは簡単なコード例から。デフォルトのapp/の他にoceanとtrucking(運輸)という2つのエンジンがあるとします。ディレクトリ構造は以下のとおりです。

メインのRailsアプリと2つのエンジンのディレクトリ構造
分離されたRailsサブディレクトリには、それぞれにmodels/やcontrollers/やservices/といったディレクトリがあります。

さて、oceanチームのエンジニアは、出荷されたコンテナが宛先の港に到着する日時を知りたいとします。そこでoceanエンジン内にこんなハンドラを1つ追加しました。

oceanエンジン内の、到着するコンテナのハンドラ
その後、truckingチームもコンテナがいつ到着するのか知りたくなったとしましょう。truckingチームのエンジニアはoceanのハンドラに少々コードを追加して、truckingエンジン内に手を突っ込んでモデルを更新するようにしました。

oceanエンジンの内部からtruckingエンジンのモデルに直接アクセスしている
このやり方は便利ですが、エンジン間の越境は「関心の分離」に違反しています。oceanエンジンはtruckingエンジン内部のモデルについてもビジネスプロセスについても知識を持ってしまいました。oceanエンジンにはActive Recordモデルへの参照があるので、ocean側のコードがtrucking側のフィールドを勝手に設定してしまう可能性があります。

oceanエンジンはtruckingモデルに直接アクセスしてtruckingの内部に書き込めてしまう
「エンジンの強制分離」は、この種のエンジン間の越境をプログラム的に防止することです。

エンジン強制分離のメリット

弊社の主要な目標は、エンジン同士の関心の分離高い凝縮度、そして疎結合を達成することです。エンジン強制分離を導入することで、開発中にこれらの原則に違反すれば機能を早くリリースするのに便利な場合であっても、開発者をこれらの原則に従わせます。エンジン強制分離によって、戦術上においても以下のようなさまざまなメリットを得られます。

1. 自由な書き込みを防止する

Active Recordのモデルに直接アクセスすると、コードベースのどんな場所からでも.saveで自由気ままに書き込みできてしまいます。これでは書き込みパスをチームで一本化するのが難しくなり、コードもわかりにくくなります。

  • 「この配送に用いる運搬車両を変更する」といった単純なビジネスプロセスですら、バリデーションやコールバックや外部コードがあちこちに散らばってしまう可能性がある。
  • モデル自身にもモデルをまたがる重要なバリデーションを含めなければならなくなる: これによってバリデーションで関連付けが読み込まれるときのパフォーマンスが低下し、さらにN + 1クエリ問題につながることもあります。
  • メール送信やサードパーティシステムとの同期、イベント発火といった副作用があちこちでトリガされ、デバッグが困難になる可能性がある。

2. 自由な読み取りを防止する

Active Recordのモデルに直接アクセスすると、関連付け(association)を無視して他のモデルから自由気ままにデータを読めてしまい、次のような結果になってしまいます。

  • チームのモデルがコードベースの他の部分でどう使われているかが見通しにくくなる: それによって明確なインターフェースや所有権の境界を確定することも困難になり、当然リファクタリングも製品の改修も困難になります。
  • エンジニアがよそのチームのデータモデルから一部を読み込む場合、N+1問題を回避するために自分たちのincludesを定義してメンテナンスし続ける必要がある: よそのチームのデータモデルが変更されれば、こちらのincludesも更新が必要になり、コードを同期するのがつらくなります。

すぐに使えるisolate_namespaceモジュラリティ

デフォルトのRailsは、ささやかながらエンジンの分離機能を提供しています。isolate_namespaceメソッドは以下のように使います。

Railsエンジンでデフォルトで使える`isolate_namespace`
Railsエンジンのガイドによると、isolate_namespaceはコントローラ、モデル、ルーティングおよびその他のコードをエンジンの名前空間へと分離し、app/の下にある類似のコンポーネントから切り離します。

つまり、MyEngine内で定義されているMyModelOtherEngineからアクセスするには、名前空間なしのMyModelではなくMyEngine::MyModelを用いる必要があります。しかしサービスやモデルへのアクセスを禁止するわけではないので、引き続き以下のようにコードベースの他の部分からは自由に読み書きできてしまいます。

isolate_namespaceを適用すると、モデルの冒頭に必ずエンジンの名前空間を付けなければならなくなります。

これはこれで正しい手順ではありますが、弊社の経験ではほぼお飾りレベルの分離です。

Railsエンジン分離を強制するcopたち

Railsエンジンのデフォルトの分離の振る舞いを拡張するため、弊社はRuboCop用のcopを新たに作りました。RuboCopは弊社で愛用されていて、昨年公開したいくつかのcopも含め、社内で30個以上のcopをこしらえました。copに違反すると、ローカルのpre-commitフックでも社内CIパイプラインでも落ちるようになっています。

Railsエンジン分離で必要な保護は、主に次の2種類です。

  1. 外から中へのアクセス: エンジン外部のコードからエンジン内部のコードに手を突っ込む行為
  2. 中から外へのアクセス: エンジン内部のコードからエンジンの外のコードにちょっかいを出す行為

自分たちのすべてのコードが保護されたエンジン内に収まっていれば、1.は満たされるでしょう。しかし弊社の既存app/ディレクトリにも対応が必要なものがどっさり詰まっています。一般にエンジンの作者は両方向の結合に目を光らせなくてはなりません。弊社の頼もしいcopたちはこの2種類のアクセスを取り締まり、メインのapp/ではなくエンジンを使うよう後押しします。

NewGlobalModelでエンジン利用を促進

Flexport/NewGlobalModel copは、新しいモデルがメインのapp/modelsに追加されたときに違反キップを切ります。弊社ではこのディレクトリに置かれるモデルを「グローバルモデル」と呼ぶ慣習があり、モデルがメインappに置かれたことがこれでわかります。エンジニアは新しいモデルをメインappではなくRailsエンジンに追加することが奨励されます。

GlobalModelAccessFromEngineで外部へのアクセスを制限

Flexport/GlobalModelAccessFromEngine copは、エンジン内からメインappへの直接アクセスを取り締まります。以下の違反例をご覧ください。

エンジン内からグローバルモデルへの直接アクセスによる違反
ベストプラクティスは、モデルへの直接アクセスではなく、「ビジネスモデル中心の」serviceクラスを「メインappエンジンAPI」と私たちが呼んでいるところに追加することです。「メインappエンジンAPI」とは、app/の下のengine_api/で定義されるファイルの集まりです。これで次のように、エンジンでメインappへの明確なインターフェイスを使えるようになります。

グローバルモデルにアクセスするのではなく、正しく定義されたインターフェイスを用いる
MainApp::EngineApiはcopに強制されるものではなく、弊社内部での集約に用いる定番の手法です。エンジンのコードでこのcopを有効にすると、技術的にはエンジンのコードからメインappにあるどの非モデルコードにもアクセスできるようになります。これは、この後説明するcopによる外から中へのアクセス取り締まりに比べれば厳しくありません。

GlobalModelAccessFromEngine copは、次のように関連付けも調べてくれます。

エンジンで直接関連付けを用いた場合のRuboCop違反
エンジン内のモデルからグローバルモデルに関連付けを直接設定すると、本来分離されるべきモジュール同士がうっかり癒着してしまう可能性があります。

弊社では、エンジンのデータモデリングを、ちょうどネットワーク越しのサービスであるかのように扱う傾向があります。この場合、エンジン同士の境界を乗り越えてモデルを参照する外部キーIDを持たせるのが自然ですが、厳密に強制された外部データベースキーを持たせたいのではなく、背後のモジュールにおける「関心の分離」を隠蔽するORM関連付けを用いたいわけでもありません。

EngineApiBoundaryで外から中へのアクセスを制限

Flexport/EngineApiBoundary copは、エンジンの名前空間がエンジンディレクトリの外にある場合に警告します。以下の例は、MyEngineOtherEngineから保護しています。

MyEngineの外にあるコードからMyEngine内部にアクセスした場合のRuboCop違反
このcopは、次のように関連付けも調べてくれます。

Railsエンジンのpublic Ruby API

当然ながら、エンジン同士が何らかの形でやりとりする必要がしばしば生じます。このcopを用いて、エンジンの外のコードからエンジンとやりとりするためのAPIをエンジン作者が定義できます。

このAPIは、マイクロサービスが公開するネットワークAPIとある意味で似ていますが、エンジンのAPI呼び出しは、通常の同期的なRubyメソッド呼び出しであるのが普通です。以下のOtherEngineは、容認可能な方法でMyEngineを利用します。

外部のコードは、正しく定義されたAPI経由でMyEngineとやりとりすべき
エンジンの作者は、自分のエンジンのAPIをそのエンジン内部にあるapi/ディレクトリの下に定義します。定義方法は以下の2とおりです。

  1. api/にファイルを追加する: これらのファイルに定義されたコードはエンジンの外からアクセスできるようになります。たとえばapi/foo.rbを追加すればエンジン外部のコードからMyEngine::Api::Foo.bar(baz)のように呼び出せるようになります。
  2. api/の下に_whitelist.rbファイルを作成する: このファイルに記載されているモジュールはエンジンの外部にあるコードにアクセスできるようになります。このファイルには以下の形式でモジュール名を記述する必要があります。

エンジン内部での外部アクセスを許可するホワイトリスト_whitelist.rbの例
エンジンの外にあるファイルは、このホワイトリストのいずれかと前方一致するモジュールにアクセスできるようになります。また、api/ディレクトリ内で定義されているコードにもアクセスできます。

エンジンAPIのベストプラクティス

弊社のエンジニアは、エンジン間で値を交換するAPIとして、Active Recordではなく、PORO(Plain Old Ruby Object)またはDry::Structの値を用いることが推奨されています(現在は強制ではありませんが)。あるエンジンが他のエンジンのモデルを欲しがってActive Recordオブジェクトへの参照を取得すると、関連付けや.saveを用いて自由に読み書きできるようになってしまいます。

また、エンジニアはSorbetシグネチャでAPIを型付けすることも推奨されています。弊社では以下を確実に実行するためのcopを書くことを検討しました。(1)エンジンのAPIファイルにSorbetシグネチャがあること、(2)シグネチャにActive Recordモデルの型が含まれていないこと。

レガシー依存性

API以外にも、エンジンの作者はこのcopで「レガシー依存性リスト」ファイルを定義できます。これは、(理由を問わず)エンジンにこっそり直接アクセスすることを許されているファイルのバックログです。このレガシー依存性ファイルは、既存のコードをエンジンに移行するうえで素晴らしく有用であることがわかってきました。このcopを有効にしてレガシー依存性をひととおり与え、それから分離のためのリファクタリングをじわじわと進めるのです。

エンジン内部に引き続き直接アクセスするレガシー依存性リスト
(後編に続く)

関連記事

RuboCop作者がRubyコードフォーマッタを比較してみた: 前編(翻訳)


RailsエンジンをRuboCopで徹底的に分離する:後編(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。画像はすべて元記事からの引用です。

RailsエンジンをRuboCopで徹底的に分離する:後編(翻訳)

RailsエンジンをRuboCopで徹底的に分離する:前編(翻訳)

(前編の続き)

実際に使ってみる

弊社では2019年初頭からエンジン分離copを使い続けて成功を収めています。今やコードベースには40個のエンジンができあがり、Rubyコードの35%を締めています(cloc appcloc engines/**/appの比較)。このcopは、既存コードのリファクタリングにも、新規プロジェクトの開始時にも有用であることが実証されています。

訳注: AlDanial/cloc:はさまざまなプログラミング言語を対象にソースコードの空行やコメント行や実際の行数をカウントするツールです。

インクリメンタルな分離

弊社のドメインモデルでは、Martin Fowlerの「Bounded Context」に基づいてインクリメンタルなコード切り離しを進めるときに以下の手順に従っています。

  • 空のエンジンを1つ作成する(copは無効にしておく)
  • コードをエンジンに移し替える
  • エンジンでcopを有効にしてみる
  • RubyCop違反の発生場所を確認することで依存性のありかを突き止める
  • 「外から中」違反をひとつずつチェックして、そのファイルをエンジンに移動し、エンジンAPIを公開してファイルのユースケースをサポートする、もしくはそのファイルを_legacy_dependents.rbに登録する
  • 「中から外」違反をひとつずつチェックし、依存性を削除してメインのappエンジンAPIを作成する

新規プロジェクト

これらのcopがまっさらぴんのエンジン作成にも有用であることがわかってきました。2つのcopを最初から有効にしておけば、新しいエンジンのデータがモジュール化されてシステムの他の部分から切り離されます。このおかげでエンジンをモノリスで手軽に立ち上げられるようになり、(必要なら)後でネットワーク越しでアクセスする通常のサービスに切り出すのも楽にやれます。

copの特性

他のツールと同様、弊社のcopにも強みと制約があります。

強み

  • 越境違反を(実行時ではなく)静的解析で検出できるので、修正サイクルを軽快に回せる
  • チームのインクリメンタル分離作業が他から分離されて自分たちのペースで進められる
  • まっさらエンジンで最初から2つのcopを有効にしておくことで分離が強化される
  • copたちのおかげでインターフェイスファーストな開発が促進され、設計の質が高まるともっぱらの評判(個人の体験です)

制約

  • 「エンジンAPI」の定義がユルい: エンジンがActive Modelモデルを返すことを禁止できていない(今のところは!)
  • 「レガシー依存性」もユルい: 既存のレガシー依存性ファイルからエンジンへの直接アクセスを、警告なしに追加できてしまう
  • copの取り締まりをすり抜けようと思えばすり抜けられる(生SQLアクセスやメタプロを使う、RuboCopを止めてしまう)

その他の分離手法

弊社では、これら新しいcop以外にもモノリスでモジュール化を強制する方法をいろいろ調べてきました。

リードオンリーActive Record

弊社エンジニアのKevin Millerは、Railsのモデルを以下のように拡張することを社内レベルで提案しています。

  • .api_association: これは既存の.readonlyと同じだが、User.all.api_association.last.company.readonly? == trueのような関連付けチェインのみの形への強制も行う。これにより、あらゆる書き込みをサービスAPI経由のみでしか行えないように強制できそう。
  • .with_whitelisted_methods: 背後にあるモデルで、ホワイトリストに記載されていないメソッドをすべてエラーにし、背後の残念なモデルにかかわらずに特定のメソッドやカラムだけを公開できるようにする。

テスト時には明示的に定義された依存性だけを読み込む

スタートアップ企業Root社の良記事「The Modular Monolith: Rails Architecture」では、エンジン同士のモジュール分離を強制する方法について考察しています。テスト時に特定のエンジンだけをいくつか読み込むという方法です。

エンジンA、B、Cがあるとしよう。AはBに依存し、BはCに依存する。Cのテストを走らせるときは、エンジンCだけを読み込み、AやBは読み込まない。Bのテストを走らせるときは(BがCに依存するので)Cは読み込むがAは読み込まない。Aのテストを走らせるときはBやCを読み込む。
同記事より大意

この手法はまっさらエンジンではうまくいきそうですが、既存のコードベースをインクリメンタルにモジュール化するときはそれほどでもなさそうです。

Active Recordのsaveにフックをかける

ApplicationRecordクラスにフックをかけ、そのモデルが定義されているエンジンの外からsavesave!を呼び出そうとしたときにブロックするという手法です。

関連付けローダーにフックをかける

Active Recordの関連付けローダーにフックをかけて、エンジンを越境する関連付けの読み込みをブロックするという手法です。これは、あらゆる関連付けをcopで削除するという上述の手法と同等のように思われます。

ネットワークによる分離

エンジンを別アプリのインスタンスにデプロイして、やりとりをネットワーク越しに限定します。これは、ある意味で弊社で現在追求している手法と同じです。

実行時にメソッド呼び出しを計測する

aftersaveフックとメタプロを併用して、エンジンごと(またはモデルごと)にバックログ的なものを作成し、production環境でのエンジン内saveとエンジンの外からのsaveやコミットの割合を表示します。値が十分小さくなったら、Sentryのwarningとフルスタックトレースを併用します。

おまけ: オープンソースの哲学

ここで弊社のオープンソース哲学と、本記事でご紹介したcopたちのステータスについて簡単に述べておきます。

Frexportは、コミュニティに支えられている既存のリポジトリを利用可能なら、そこに置かせてもらうようにしています。しかし既存のリポジトリが要件に合わないこともあるので、その場合は弊社のコードを自社リポジトリに置く方が理にかなうと考えます。

RuboCopチームとの議論によれば、Railsエンジン分離copたちは後者に該当します。そこで弊社はそれらのcopを含む自社製copのリポジトリを新たに作成しましたが、他のupstream先は設けませんでした。

今後について

Railsエンジンは、弊社の複雑なモノリスの管理に役立つツールであることが実証されています。会社が成長し続けるにつれて、弊社の関心はバックエンドサービスをネットワークで分離して、GoogleのProtocol Bufferで定義された、より強固な「インターフェイスファーストAPI」を用いることに移りつつあります。弊社は今後も、コードの一部を新サービスに切り出す移行パスとしてRailsエンジンを使い続けるつもりです。そして弊社はRailsエンジンを移行中にも、新しいサービス内部のモジュールにも長期的に用いていくことを期待しています。

この方面についての皆さまの経験談についても知りたいと思います。ご意見などございましたらぜひ弊社までお知らせください。

関連記事

RailsエンジンをRuboCopで徹底的に分離する:前編(翻訳)

週刊Railsウォッチ(20200114前編)config_forのbreaking change、Active Storage variantをDBでトラッキング、SprocketsとWebpackの違いほか

$
0
0

こんにちは、hachi8833です。すっかり遅ればせながらあけましておめでとうございます🌅。2020年代も週刊Railsウォッチをよろしくお願いします🙇


つっつきボイス:「お〜リニューアルへの反応が、と思ったら今日のツイートでしたか😆: 一応昨年12月26日にリニューアルしたんですけどね☺」「😆

TechRachoのサイトデザインをリニューアルしました

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

今回は第18回公開つっつき会を元にお送りいたします。ご参加いただいた皆さん、ありがとうございます!

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

昨年末の公式情報を中心に見繕いました。

⚓新機能: Active Storage variantをデータベースでトラッキングするようになった

現在はvariantをリクエストされるとそれがサービス内に存在するかどうかをチェックし、ない場合はその場で生成して保存し、再利用に備えている。
この存在チェックによるvariant提供の遅延は容認できなくなることがある。サードパーティのサービスでは通常の環境で最大500msに達する。しかも、アップロード前に特定のキーでオブジェクトが存在するかどうかをチェックすると、S3の最終consistencyがトリガーされる。

Amazon S3では、全リージョンのS3バケットで新しいオブジェクトをPUTSするときのread-after-write consistencyを提供していますが、1つ注意点があります。オブジェクト作成前に(オブジェクトが存在するかどうかの確認のために)キー名をHEADやGETでリクエストすると、S3はread-after-writeの最終consistencyを提供します。

つまり、あるvariantを最初にリクエストして生成しS3に保存すると、以後そのvariantのサービスURLへのリダイレクトが失敗し続ける可能性がある。これによって画像が散発的に破損する。
このPRは存在チェックを行わないことで上の懸念を修正する。あるvariantが初めてリクエストされれば生成して保存し、以後はアプリケーションのデータベースでトラッキングする。これにより、ストレージサービスへのリモート呼び出しを行わずに、variantが生成済みかどうかを確認できるようになった。
同PRより大意


つっつきボイス:「ここで言ってるvariantは、画像をアップロードしたときに複数のサイズを生成して指定できるようにしているアレのことかなと」「サムネイル画像とかに使うヤツですね😋」「Active Storageでvariantが使えるようになってる?🤔」「Rails 6で入ってたと思います(ウォッチ20191111)」「たしかにvariant機能がないと画像アップロードとしてはとてもつらくなるでしょうし😆

「結構差分大きいな😳」「17ファイルも更新されてますね」「改修内容もわかりみ: hoge_bigみたいな画像があるかどうかを毎回S3にAPI経由で問い合わせて、なかったら作るとかやってるとそのチェックが重くなるでしょうね☺」「ふむふむ」「チェック先がファイルシステムとかだったら一瞬でしょうけど、API経由では遅くなるからデータベースに存在情報を持たせておけば、どんなストレージを使っていてもまずデータベースを参照することになるから高速化が期待できる」「なるほど!」「よくあるやり方☺

「ただこれをやると、今度はデータベースとの不整合が発生したときに面倒なことになりますけどっ😆」「S3にあるvariantを誰かが削除しちゃったりとか」「今日出席できなかったkazzさんは、データベースよりもメモリ上に置きたいな〜って」「それは無理でしょ😆: データベースが複数台構成で動かなくなると思います」「あ、そうか😳」「置き場所としてはRedisやmemcachedあたりか、さもなければデータベース上ぐらいでしょうね☺

参考: Redis
参考: memcached - Wikipedia

「あとサービスによってはvariantの生成がめちゃ重くなることがありますし、Pixivみたいな画像中心のアプリだとvariantが膨大になるでしょうし」「想像つきます😆」「Active StorageもそうですしCarrerWaveでもShrineでもPaperclipでもそうなんですけど、variantをいつ生成するかというのが共通のテーマになってますね: アップロード時にvariantをプリレンダリングするのか、あるいはここでやってるみたいにオンデマンドで生成するのかはだいたい悩ましいポイントです😭」「たしかに」「画像をバンバン扱うようなシステムではこの辺が大事🧐

「プリレンダリングならいったん生成が終われば後は速いんですけど、たとえば後でvariantの定義が変わると全部再生成しないといけなくなるのが厄介😇」「あ〜」「bigとsmallに後からmiddleを足すとか😆: 画像の数やサイズが大きいとめっちゃ重くなりますし😆」「それ用のrakeタスク的なものはだいたい必要になるので探せばたぶん何かしらあると思います」

⚓shrine gem

「shrineって、これも画像アップロード用のgemでしょうか?」「はい: 今だと一番いい感じに動いてくれるアップローダでしょうね」「おぉ〜😂」「Shrineは評判もいいですね」「欲しい機能はひととおりあるしメンテもされてますし☺: 去年バージョン3が出たんだったかな」


shrinerb.comより


「ちなみにBPSでやっているRails製の漫画のサービス↓では、漫画の画像サイズが大きいのでたしかプリレンダリングでやってたと思います: オンデマンドでやると最初の人のアクセスが激重になるので」「へ〜」(以下PDFやepubの話で盛り上がる)

⚓TagBuilderに条件値をハッシュで渡せるようになった

このPRはTagBuilderに条件値を渡すサポートを追加する。https://github.com/JedWatson/classnamesにヒントを得て、GitHubアプリケーションで使っているロジックを切り出して使っている。
RailsのビューでCSSクラスの適用に条件をつけるのはよくある手法だが、以下のように三項演算子の式展開だらけになりがち。

content_tag(
  "My username",
  class: "always #{'sometimes' if current_user.special?} another"
)

TagBuilderにハッシュのサポートを追加したことで、今後は以下のように書ける。

content_tag(
  "My username",
  class: ["always", "another", { 'sometimes' => current_user.special? }]
)

同PRより大意


つっつきボイス:「この書き方はどこかで見たような🤔: あ、去年見かけたVue.jsだかReactっぽい書き方できるヤツ?(ウォッチ20191216)」「それっぽいですね」「従来は条件付けした文字列を無理やり式展開で押し込んでたけど、配列の中で条件をハッシュで渡せるようになったと」「あ、そういうことですか😳」「たしかに論理的にはこっちで書ける方がいいですよね😋: 式展開と違ってコードのトラッキングもやりやすいですし👍」「下の書き方の方が好き❤」「rubocopのチェックとかも効きやすいでしょうし☺

⚓Rails::Application#config_forが共有設定をネスト含みでマージするようになった

自分は環境固有の設定のためにgemをいくつか使っているが、これをRailsの機能に統合できればと思う。しかし自分の設定はネストが深く、現在のRails::Application#config_forでは設定をマージできない。

# config/application.yml
shared:
  foo:
    bar:
      baz: 1
development:
  foo:
    bar:
      qux: 2

# 現在
config_for(:application)[:foo][:bar] #=> { qux: 2 }
# こうしたい
config_for(:application)[:foo][:bar] #=> { baz: 1, qux: 2 }

これがbreaking changeであることは承知している。デフォルトの振る舞い変更が難しいとかあれば、オプションとして制御できるようにするのはどうだろう。
同PRより大意


つっつきボイス:「これはRails標準のconfigの変更か: 書いてあるとおり超breaking changeですね😇」「おぉ?」

「以前からsettingslogicというgemやrails_configというgemがあるんですけど、それでも同じようなことをやれますね」「昔そのあたりをブログに書きましたけど探さないでください🤣」「🤣

編集部注: rails_config gemは現在configに名前が変わりましたが、紛らわしいので本記事では旧名のrails_configで表記します。

⚓Rails設定用gemの挙動

「settingslogicってたしかこんな挙動(メモ書きを始める)」

# default.yml
- production
  - categories
    - hoge
      - piyo
    - huga
    - foo

# production.yml
- production
  - categories
    - hoge

「settingslogicは、キーがかぶるとそれ以下を全部消して上書きする: rails_configの場合はキーがかぶるとマージ(と言っていいかどうかですけど)的に扱う」「まあマージでいいんじゃないでしょうか☺」「そしてsettingslogicはこの挙動が事故りやすいという😆」「そうそうっ😤」「default.ymlを更新するとproductionで上書きされて反映されなかったりするというのがありがちなパターン😇: 追加したはずのキーがなくてproductionでエラーを吐くとか😆」「地獄感ありあり☠

# settingslogicを使う場合
- production
  - categories
    - hoge

# rails_configを使う場合
- production
  - categories
    - hoge
      - piyo
    - huga
    - foo

「rails_configならdefaultの方で少なくともエラーにならないように設定しておいてからproductionの値を設定するんですけど、settingslogicはproductionに入れ忘れると死ぬ😇: そして従来のconfig_forも上書きする挙動なのでそれと同じことが起きます」

⚓breaking changeの影響は?

「たぶん今回の変更は同じキーがあったときにマージしたいということなので、config_forをrails_config風味にしたいということかなと😆: 思いっきりbreaking change」「どのぐらい影響出るんでしょう?🤔」「う〜ん、設定にもよるので予測難しいけどこれで死ぬことが割とあるかもしれませんね👹: こういうconfigはgemとかでカスタマイズされてることもよくあるので」「う〜む😅

「このPRはもうmasterにマージされちゃってますし😇」「設定を上書きじゃなくてマージする挙動になるならそんなに事故らないかな?」「でもたとえばdevelopmentとproductionで異なるSMTPサーバーを指定していた場合に、マージされるとdevelopment用のSMTPサーバーの設定もproductionに残ることになるから、これはこれで事故るんじゃないかな〜🤔」「あ〜ごめんなさい事故りますねたぶん😅」「知らないうちに設定が変わるのコワイ😱」「こんな変更をあっさりマージしちゃって大丈夫?っていう気持ちにちょっとなりますね…」「ちょっとドキドキする😓」「ドキドキする😇

「settingslogicとかを使ってる人にも影響あるんでしょうか?」「いえ、settingslogicなんかはRails本体のconfigとは違うところで設定するので関係ないですね」「そうでしたか😳」「Rails本体とはクラスの空間が違うので両者を共存することはできます☺

「このPRがmasterにマージされたのは昨年12月9日か〜」「6.0.2.1には入ったんだろうか?😅

後で調べると6-0-stableにはまだ入っていませんでした↓。

参考: rails/CHANGELOG.md at 6-0-stable · rails/rails

config_forではたとえばsharedの設定をdevelopmentで上書きできなくなるということですよね?」「ということでしょうね〜」「まあビジネスアプリの重要な設定をRailアプリ本体のconfigに直接書くことは普通しないと思いますし、自分はそういうときはrails_config gemとかを使うので、少なくとも自分のアプリは踏まないと信じる🤣」「🤣」「ただ他のgemが踏むかもしれませんけどっ😆

⚓Active SupportのRange#include?をdateやtimeの値に利用することが非推奨化された

dateやtimeのrangeに値が含まれているかどうかのチェックにRange#include?を利用することは非推奨化される。今後はRange#cover?を使うことが推奨される。
CHANGELOGより大意

参考: cover? — ActiveSupport::CompareWithRange


つっつきボイス:「今後はTimeWithZoneではcover?を使えと」

# activesupport/lib/active_support/core_ext/range/include_time_with_zone.rb#L11
    def include?(value)
-     if self.begin.is_a?(TimeWithZone)
-       cover?(value)
-     elsif self.end.is_a?(TimeWithZone)
+     if self.begin.is_a?(TimeWithZone) || self.end.is_a?(TimeWithZone)
+       ActiveSupport::Deprecation.warn(<<-MSG.squish)
+         Using `Range#include?` to check the inclusion of a value in
+         a date time range is deprecated.
+         It is recommended to use `Range#cover?` instead of `Range#include?` to
+         check the inclusion of a value in a date time range.
+       MSG
        cover?(value)
      else
        super
      end
    end
  end
end

「deprecatedの理由は何だろう?🤔」「issueに書いてありそう↓」「そっちでしたか😅」「include?だと実装の問題があるから使って欲しくないということみたい」

この拡張はどちらかというと削除したい。include?は、Range.to_aを返さない値で動くべきではない。現在の拡張を非推奨にして代わりにcover?を使うよう周知をお願いしてもよい?
同issueコメントより

⚓ActiveSupport::NumberHelper::RoundingHelper:round_modeが追加

    ```ruby
    number_to_currency(1234567890.50, precision: 0, round_mode: :half_down) # => "$1,234,567,890"
    number_to_percentage(302.24398923423, precision: 5, round_mode: :down) # => "302.24398%"
    number_to_rounded(389.32314, precision: 0, round_mode: :ceil) # => "390"
    number_to_human_size(483989, precision: 2, round_mode: :up) # => "480 KB"
    number_to_human(489939, precision: 2, round_mode: :floor) # => "480 Thousand"
    485000.to_s(:human, precision: 2, round_mode: :half_even) # => "480 Thousand"
    ```

つっつきボイス:「数値ヘルパーに:round_modeオプション、たしかに欲しいヤツ😋」「切り上げ切り捨て四捨五入ですね」「Ruby自体にそういう機能はありますけど😆」「その機能をnumber_to_なんちゃらヘルパーのオプションとして使えるようにしたんですね」「ビューの責務を考えたら、ヘルパーのオプションによる丸め処理はあくまでビューの表示のためのものだというのを示すのにはいいのかもしれませんね: 数値自体を丸めてるんじゃなくて表示の書式を整えてるだけだよ、みたいな☺」「このオプション、後で思い出せるかな😆

⚓Rails

⚓「WebpackをSprocketsのように使うな」


つっつきボイス:「Webpacker周りを追っててこの記事にたどりつきました: ここではお集まりの皆さまだけにこの記事のドラフト翻訳をお見せしますが、近日TechRachoで公開しますのでお楽しみに😋」「SprocketsとWebpackerは別物ですから😆」「今更ではありますけど、そのあたりをまとめて説明してくれてるのがいいなと思いました😋

参考: アセットパイプライン - Railsガイド
参考: rails/sprockets-rails: Sprockets Rails integration

「記事によると、Sprocketsでないとできないことがあるようです」「元々SprocketsとWebpackは役割が違いますからそうでしょうね: 個人的にはSprocketsとWebpackが両方あると混乱しそうだからやめときたいですけど😆」「Sprocketsを残すかどうかずっとモヤモヤしてたんですけど、これで踏ん切りがつきそうです😆

「RubyでJSのコードを扱いたい場合、特にRailsのコンテキストで扱いたい場合はたぶんSprocketsの方が向いていますね: WebpackはRailsのコンテキストでは動かないので🧐」「まさにそのあたりの話が記事にありました」「Railsのアプリケーションconfigを使ってJSの自動生成部分に値を埋め込みたい、みたいなのをやりたいときとかありますよね: たとえばアセットのプリコンパイル結果にRailsの動作モードを入れたいなんてのは(やるべきではありませんが)、Sprocketsじゃないとやれませんね☺

「そういえばRailsがWebpackに対応して間もない頃にダイジェストとか設定とかをWebpackでどう扱うかが議論になってた覚えがありますね😆: Railsのコンテキストにある値をWebpackでどうやってJSに渡すか、そのためにJSONを作って渡すとか何とか」「ちょっと無理やり感😆」「でもやりたいシチュエーションはありますから☺」「WebpackだとRailsのconfigの値を渡せないので、Railsのconfigと重複するのを承知でWebpackのconfigにも値を書くのか、とか」「それは気持ち悪いです😆」「でしょ😆」「この記事に『SprocketsをWebpackのように使うな』って書いてあるのはわかります😋

「RailsガイドにもWebpackerとSprocketsをどう扱うかという情報がほぼなくて困ってました😢」「まあRails 6でやるならもうSprocketsのことは忘れてWebpackしか存在しない世界だと思って進めるのがいいんじゃないでしょうか😆」「それもそうですね😂」「記事の人も、自分は併用を極力避けるけどSprocketsでないとできないことがあるのは知っておく価値はあるという感じでした」「そういう記事をひととおり読んでみれば、併用するのはやめておこうという気持ちになれますよ🤣


「ついでにyarn autocleanで不要なファイルをクリーンアップできるらしいことを知りました↓」「CIとかデプロイでは普通に使いそうですね☺

⚓Zeitwerkへの移行


つっつきボイス:「短い記事です」「これ系の記事はいっぱいありますし☺」「最後の方にある以下をRailsコンソールで実行するとdevelopmentモードでもZeitwerkの読み込みをチェックできるそうです」「読み込み順序に問題があればこれで確認できますね😋

# 同記事より
Zeitwerk::Loader.eager_load_all

⚓ネストあり/なしルーティングをRESTfulらしく実装する


つっつきボイス:「なるほどこの辺のお話↓」


同記事より

「ネステッドだと上から4番目の/magazines/:magazine_id/ads/:idみたいになりますよね: 知ってる人はとっくに知ってますが😆」「ふむふむ」「そしてネステッドとそうでないルーティングが両方定義されている場合はネステッドの部分を判別して書かないといけない: 以下のif !params[:landlord_id]はネステッドのときだけ通る↓、とか🧐」「なるほど」「この辺も知ってる人には今更ですが😆

# 同記事より
def show
  if !params[:landlord_id]
    id = params[:id]
    tenant = Tenant.find(id)
    render json: tenant
  else
    id = params[:id]
    landlord_id = params[:landlord_id]
    tenant = Landlord.find(landlord_id).tenants.find(id)
    render json: tenant
  end
end

「ただ個人的にはネステッドとそうでないルーティングを両方書くのは事故の元なのでやりたくありませんけどっ😆」「😆」「同じリソースに複数のルーティングがあるのってどうかと思いますし😆

「両方書かないといけなくなるシチュエーションってあるんでしょうか?」「本来は1つのURLでアクセスできるべきなんですけど、まああるとすればSEOの都合でもっと短いURLにしてくれという注文が来るとか😆、あとはフロントエンドのフレームワークが別のURLを要求してくるとかですかね〜」「なるほど!」「そもそも自分はネステッドルート好きじゃありませんし、おすすめもしませんが🤣

「ネステッドで厄介なのは、たとえば記事にあるこういうURL↓でlandlord_idの部分が今後変わったりしたときですね😇」「おぉ?🤔」「普通であれば、あるtenantのidに紐付けられるlandlord idが変われば上のURLはアクセスできなくなりますけど、下ならパーマネントなURLとしてtenantのidにアクセスできますね…あ、そういうパーマネントURLも用意して欲しいという要件はありそう」「たしかに」「ネステッドとそうでないルーティングってそこまで考えずに設計されてることもよくあったりしますし🤣

# 同記事より
Nested Path: localhost:3000/landlords/:landlord_id/tenants/:id
Un-nested Path: localhost:3000/tenants/:id

⚓GitHub Actions記事


つっつきボイス:「GitHub Actionsはまだ触れていないけど速くて安くていい感じみたいですね😋

「記事にもありますけどEOLが切れると致命的なんですよね: 業務システムだと古いRubyを使ってるとか普通によくあるので、ある日突然CIが動かなくなるとか😇」「😇

⚓初心者向けRailsパフォーマンス最適化のコツ5つ


つっつきボイス:「タイトルにもあるようにパフォーマンス初心者(noobs)向けの記事です」「定番の嵐😆」「せやなの嵐😆

見出しより大意:

  • 1. 半端に最適化しないこと
  • 2. プロファイリングとベンチマークをやってからにすること
  • 3. すごい技を繰り出すよりも足を引っ張る要素を取り除くのが肝心
  • 4. ほぼほぼデータベース(の使いこなし)でつまずく
  • 5. 速くなったっぽいなら速いのだ
  • 6. ほぼほぼRubyのせいではない

「4.といえば、データベースを使いこなせてなくてmap使って遅くなってるケースをちょくちょく見かけますね🤣」「あ〜ありそう🤣」「データベース側でsumすれば瞬殺なのにわざわざオブジェクトに落としてmapするとか🔰

「『ほぼほぼRubyのせいではない』も😆」「PHPでもどの言語でもちゃんと書けば速くなりますし🚄」「言語のせいにしてはいかんと☺

⚓2020年のベストクロスプラットフォームモバイル開発ツール


つっつきボイス:「あ〜モバイル開発か」「出たC#😆」「他にも懐かしいものがチラホラ」(以下延々)

見出しより:

「なぜか最後はRails😆」「RubyMotionっていうRubyでモバイルアプリを書けるアレかと思ったら違った😆

⚓その他Rails

つっつきボイス:「Ryan Biggさんは以下の翻訳記事↓でお世話になりました」

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


つっつきボイス:「見えないテキストボックスって、Railsガイドで言うハニーポットフィールド(おとりのフィールド)のことだと思うんですが、だんだん通用しなくなってきたんですね」「この辺のCAPTCHA方面は今いろいろアツいですよ😆」「自前で作るよりGoogleのCAPTCHA入れとけばいいんじゃね?😆

参考: Rails セキュリティガイド - Railsガイド
参考: CAPTCHA - Wikipedia

「ハニーポットって一般用語?」「invisible_captcha gemにもhoneypot:ってあるな↓」「ここに入力があったら例外吐けばいいんだから実装はそんなに大変じゃなさそうですけど☺

# https://github.com/markets/invisible_captchaより
class TopicsController < ApplicationController
  invisible_captcha only: [:create, :update], honeypot: :subtitle
end

「あとはどのぐらい効果があるかですね: ブルートフォース的なのはこれではじけるとしても悪意のあるアクセスをどのぐらい防げるのかな、とか」「ないよりはマシぐらいなのかな🤔」「ブラウザのオートコンプリートで入っちゃったりしますかね?🤔」「hiddenフィールドだったら入らないでしょうけど普通の入力フィールドだと入っちゃうでしょうね: invisible_captchaのREADMEにもオートコンプリートはオフにしろと書いてありますし」「ほんとだ」


つっつきボイス:「同書は初心者よりベテランにおすすめだそうです🎉」「特集1はたしかに初心者向けだけど特集2のWebpack/Sprocketsあたりから急にレベル高くなってる😆」「Rails経験があってもフロントの知見がないと手こずりそう😆」「途中からトップギア🏎



前編は以上です。

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

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

Rails公式ニュース

週刊Railsウォッチ(20200115後編)Ruby 2.7関連情報、Bootstrap 5は今年前半リリースか、PostgreSQLでやってはいけないリストほか

$
0
0

こんにちは、hachi8833です。Sprocketsは外してWebpackに一本化する決心がつきました。

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

今回も第18回公開つっつき会を元にお送りいたします。ご参加いただいた皆さん、ありがとうございます!

⚓Ruby

⚓Ruby 2.7関連ドキュメントや記事など

2.7情報は今年最初のRubyWeeklyを見る方が早そうです↓。


つっつきボイス:「Ruby Referenceは、英語の公式Rubyドキュメントの新しい部分をzverokさんがセミオートでかき集めてビルドしているドキュメントサイトです(ウォッチ20180413)」「そういえばあったな〜」

⚓Ruby Referenceを眺めて

「ちょうどRuby Referenceで見えたけど、このOpen3↓ってなぜ3なのかが一見わからないけど使ってみるとああなるほどってわかるヤツ😆」「これ何でしょう?」「stdinとstdoutとstderrを同時に開くライブラリですね☺」「だから3なのか〜」「と思うんですけどね(他に考えられない)😆」「capture3とかcapture2も謎😆」「もう少しマシな名前なかったのかと😆


rubyreferences.github.ioより

「たしかOpen3ってRuby以外にもあったはず↓」「なるほどPerlでしたか😳

参考: IPC::Open3 - perldoc.perl.org

⚓RubyのJITとパターンマッチ


つっつきボイス:「上はk0kubunさんのJITプログレス記事です」「2.6でJITが入ってから2.7までの進捗!」

見出しより:

  • Ruby 2.7リリース
  • Ruby 2.7で自分が実装した部分
  • Ruby 2.7で自分がコミットしなかった部分
  • Ruby 3.0で自分がやる予定の部分

「RailsでのJITは手こずってるみたいですね」「そこは確かに難しいでしょうね」「たとえば最適化をRubyに全部おまかせにするんじゃなくて、JITを走らせる/走らせたくないタイミングとかについてコードの側からある程度ヒントを与えるとかしないと、Railsみたいなリクエスト/レスポンス型のアプリの最適化は難しいのかなってちょっと思いますね🤔

後でk0kubunさん記事を急いで読んでみると「JITでのRails最適化はまだやれると思う」「以下の戦略でRailsベンチマークによく効く最適化の導入を考えている」とありました。

  • インストラクションベースの最適化を推し進める
  • インライン化を推し進める
  • Cで書かれたメソッドの種類の自動識別

⚓さっき拾ったツイートより

つっつきボイス:「kamipoさんがRailsのコード例で回答してくれてますね」「なるほど、インスタンス変数でチェックしてメソッド呼び出しを避ける最適化↓」「Rubyのメソッド呼び出しの遅さはライブラリのコードだと気になるでしょうね☺」「こういう部分にJITが効いてくれたら嬉しい!」

# 同PRより
      def read_attribute_before_type_cast(attr_name)
-       sync_with_transaction_state
+       sync_with_transaction_state if @transaction_state&.finalized?
        @attributes[attr_name.to_s].value_before_type_cast
      end

self.classで呼ぶと参照し直しになって遅くなる面もありますね」「20%速くなるってスゲエ😳」「それだけ呼び出し回数が多いということか😳

# 同PRより
      def pk_attribute?(name)
-       name == self.class.primary_key
+       name == @primary_key
      end

⚓Ruby Trunkより

いずれもまだcloseしてないissueです。

つっつきボイス:「#16488はAction Mailerに絡んだruby2_keywords周りの修正のようです(#38105)」「こういうエッジケースは指摘をもらわないとなかなか気づけないでしょうし☺」「そういえば2.7でruby2_keywordsが入ってましたね😳」「Ruby 3でbreaking changesになる部分をRuby 2.6以下のロジックでアクセスできるようにするものだったと思います」「後方互換性用でしょうか?」「あと移行もですね☺

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

module YourModule
  ruby2_keywords def delegating_method(*args)
    other_method(*args)
  end
end

「#16468はちょっと毛色を変えて素数判定周りです」「Prime.prime?のアルゴリズムを変更すると速くなるぞと😋

参考: ミラー–ラビン素数判定法 - Wikipedia

⚓unodos: 数列を推測するgem


つっつきボイス:「ruby-jpで見かけました」「なるほど、フィボナッチ的な数列を渡すと式を推測してくれると」「数学科出身のkazzさんにこのgemの話をしたら、そんなに大変じゃなく作れそうって言ってました」

# 同リポジトリより
require 'unodos'
Unodos[1,2].take(5) # => [1,2,3,4,5]
Unodos[1,2,4].take(5) # => [1,2,4,8,16]
Unodos[1,1,2,3,5].take(8) # => [1,1,2,3,5,8,13,21]
Unodos[1,1,2,4,3,9,4,16,5].take(10) # => [1,1,2,4,3,9,4,16,5,25]

「推測結果をruleで見られる↓のがいいですね😋」「自分の期待する式になるかどうかはわかりませんけど😆」「数列からどこまで複雑な式を生成できるかを遊んでみたら楽しそう❤」「ああやっとわかったかも😆」「つかそうやって遊ぶためのgemなのかなって😆」「コードゴルフ的にどれだけ長い式を出せるかという飛距離を競うとかありそう😆

# to see the generated rule
Unodos[4,1,0,1,4,9].rule # => "a[n]=4-4*n+n**2"
Unodos[1,2,4,5,7,8].rule # => "a[n]=-a[n-1]+3*n"

「と言ってる間にピザが到着しました🍕」「21:00になったら飲み食いしながらウォッチドラフトを眺めましょうか😆

⚓その他Ruby


つっつきボイス:「おおWindowsのRubyInstallerだ」「今もメンテされてるのがエライ🙏」「WSL2が出た後の世界で使うことがあるかどうかですけど😆」「そもそも矢印キーで履歴辿れなかったんですね😳」「Windowsのターミナルに対応するのは大変ですよ〜😆

参考: WSL2で劇的に変わるあなたのWebアプリケーション開発環境【その2:導入編】 | SIOS Tech. Lab


つっつきボイス:「公開つっつき会もアンチハラスメントポリシーを設定してもいい頃かも🤔」「RubyKaigiに準拠でおk😆」「公開しておくことに意義がありますし☺

後で見つけたツイートの「ぼっち対策」ってどんな感じで行われたのか気になります。私もぼっちなので。

⚓DB

⚓(公式)PostgreSQLでやってはいけないことリスト(Postgres Weeklyより)


つっつきボイス:「何しろ公式なので😆」「『table inheritance使うな』とか」「table inheritance、実はそんなにキライじゃないです😆: productionで使ったことありますし」「おぉ😳」「そういえばGitLabで問題になったことありませんでしたっけ?😆」「まあまあ😆」「moneyって型があるとは」「ぽすぐれはいろんな型ありますし☺

「そういえば!=という条件が遅いっていう話をkamipoさんがどこかにMySQLがらみか何かで書いてた気がします: 実際そうで、!=は基本的にデータを全精査しないと取れませんし」「言われてみればという感じ☺

参考: パフォーマンスの遅いSQL。インデックスを使わないSQLとは? | Oracle初心者でもスッキリわかる

使ってはいけないもの(同Wikiより):

  • エンコード
    • SQL_ASCII
  • ツール
    • psql -W or --password
    • rule
    • table inheritance
  • SQL
    • NOT IN
    • 大文字のテーブル名やカラム名
    • BETWEEN(特にtimestampで)
  • Date/Time
    • タイムゾーンなしのtimestamp
    • timez
    • CURRENT_TIME
    • timestamp(0)timestamptz(0)
  • テキスト
    • char(n)(固定幅のidにも使わないこと)
    • 上限指定のあるvarchar(n)をデフォルトにする
  • その他
    • money
    • serial

⚓PostgreSQLのささやかなベストプラクティス(Postgres Weeklyより)


つっつきボイス:「短い記事で、割と定番かなと」「運用向けという感じですね」

見出しより:

  • 接続文字列と環境変数を使う
  • credentialは定期的にローテートする
  • 主キーにはBIGINTかUUIDを使う
  • connection poolingを使う

「そういえば主キーにUUIDを使うのってどのぐらい普及してるんでしょうね: RailsでもUUID使うべきという人がいたりしますけど、ルーティングのURLがめちゃめちゃ長くなりますし😆」「😆」「まあRailsデフォルトの推測可能なサロゲートキーはそれはそれで悩ましいですけど😅

参考: UUID - Wikipedia

⚓クラウド/コンテナ/インフラ/Linux/Serverless

⚓AWSのモダンアプリケーション開発ホワイトペーパー


つっつきボイス:「はてブであがってた記事で、昨年末のre:inventでの発表内容にさらに手を加えたものだそうです」「まあこういうドキュメントは3年経っても『モダン』がついたままだったりしますけど😆」「最新はすぐ最新でなくなるの法則😆」「年号でやろう😆」「2020年版とか😆」「会場の本棚にもある定番のタネンバウム本↓も、いつのモダンやねんという感じですし🤣」「名著ですけど😆」「それは間違いない😆」「本の奥付見ると2004年ってなってるし😆」「モダンジャズが全然モダンじゃないのと同じ😆」「最早モダンにレトロな響きしか感じられない😆

参考: モダンオペレーティングシステム Tanenbaum, Andrew S(著) - ピアソン・エデュケーション | 版元ドットコム

⚓その他クラウド

つっつきボイス:「そもそも今まで無料だったとは💰」「今の請求項目はダミーみたいですけどもう少ししたら本気出すみたいです」「そういえばAWSのEIPだと、使っているIPは無料で使ってないIPは有料ですね🧐

参考: AWS Elastic IP の料金を理解する
参考: GCP 外部 IP アドレス料金

GCPは無料枠に該当しない場合に料金がかかるようになるようです。

⚓JavaScript

⚓Bootstrap 5は今年前半にリリースされそう


getbootstrap.comより

v4の12カラムはv5でも変わらないようです。


つっつきボイス:「個人的にこれが気になってました」「おそらくearly 2020リリースだろうということで、あとjQueryとIE10以下サポートが消えるそうです」「ついに消えますか!」「かつてはflexboxすら使えないIE9の地獄のブラウザハックを避けたかったのでBootstrapが欲しかったというところがありましたけど、その頃に比べれば、ないとやっていけないというほどではなくなったかもですね☺」「Bootstrap 3から4への移行に比べれば4から5への移行はそんなに大変ではないのかなという雰囲気ですね」「逆に5にしないといけない理由もそんなにないかも😆

v5でのJekyllからHugoへの移行ってBootstrapとどう絡むんだろうと思ったら、主にドキュメントサイトの話のようでした(#28014)。

⚓その他JS

つっつきボイス:「~.構文ってまだ確定じゃないですよね?」「プロポーザルの段階みたいです」「皆さんもRubyでやってるみたいに記号があるとつい使いたくなったりしません?😆」「あると使いたくなる的な😆

参考: ハンマーを持つ人には全てが釘に見える - 橋本商会


つっつきボイス:「社内Slackで見かけました: 前にもウォッチで取り上げたJSのprivate記法がついに入ったそうです(ウォッチ20190902)」「例の#でprivateを表すヤツ😆」「あくまでTypeScriptの中でですけど☺

class Greeter {
    #name: string;
    constructor(name: string) {
        this.#name = name;
    }
    greet() {
        console.log(`hello ${this.#name}`);
    }
}

const greeter = new Greeter("world");
console.log(greeter);                 // logs 'Greeter {}'
console.log(Object.keys(greeter));    // logs '[]'
greeter.greet();                      // logs 'hello world'

「変数がprivateかどうかを名前でわかるようにしたいという気持ちはちょっとワカル: たいてい物議を醸しますけど😆

⚓CSS/HTML/フロントエンド/テスト

⚓HTTP関連の動向

上の記事で言及されているWeb Authenticationのガイドも別途見つけました↓。


つっつきボイス:「asnokaze.hatenablog.comさんのブログがHTTPの仕様に関する記事が豊富だったので」「この人は有名ですね☺」「Web PackagingとかSigned HTTP Exchangesとかいろいろ提案されてるみたいでクラクラしてきました😅」「や〜っぱりHTTPは難しいですよ😆: 大学の授業で教えるときに去年動いたコードが今年突然動かなくなったりするの勘弁して欲しい😭」「Web Authenticationって一般用語かと思ったら固有名詞でした😅」「WebAuthnはブラウザの機能としてだいぶ前から進めているヤツですね☺

参考: WebAuthn - Wikipedia

⚓去年の流行りのスニペット


つっつきボイス:「楽しいスニペットがいろいろ並んでて、いたずら系も混じってます😆」「CodePenはプログラマーじゃなくても動かして楽しめるあたりがやっぱりよくできてますね😋」「こういうCSSアニメーションとか定番↓」

See the Pen
CSS Loading Animations
by Alex (@AlexWarnes)
on CodePen.

「ちょうど昨日学生にanime.js↓の話しましたよ😎」「お、こんなのあるんですね」「キレイなものを作るには相当気合が必要ですけどって学生にも言いましたし😆」(以下学生向けのコーディング課題などについて延々)

See the Pen
Easings animations with anime.js
by Julian Garnier (@juliangarnier)
on CodePen.

⚓その他フロントエンド

つっつきボイス:「パスワードチェックアップ拡張!」「お漏らし済みのパスワードを使うとChromeで怒られるようになるそうです」「ハッシュが一致するかどうかをチェックする感じでしょうね」「早速インストールしよっと😋」「だいぶ前に米国のPSNで流出したときにガチで引っかかったことあったから怒られる自信あるっ😆: お大丈夫と出た🎉」「私も『あんたのパスワード知ってるよ』って大昔に使ってたパスワードを通知する英文メール受け取ったことあります😅」「平文パスワードを保存している業界は天に召されて欲しい😇

参考: PlayStation Network個人情報流出事件 - Wikipedia


つっつきボイス:「お、このドキュメント良さそう❤


apple.comより

⚓言語・ツール

⚓乱数

参考: メルセンヌ・ツイスタ - Wikipedia — 通称「MT」


つっつきボイス:「MTって何だろうと思ったらメルセンヌ・ツイスタでした」「ちょうど弊社のメンバーが昨年のアドベントカレンダーで乱数記事出してましたね↓」「乱数は結構楽しいですよ〜😋: 速い乱数とか真の乱数とは何かとか」「Unixの/dev/random/dev/urandomの違いとか😆」「なんで2つあるんだろうと思って調べて納得するという」「そして忘れる🤣」「違いがあるということだけ覚えてる😆

参考: /dev/random - Wikipedia

記事のLaTeXや埋め込みがリニューアル後にちょっと乱れてますが近々修正します🙇

乱数について本気出して考えてみる

「他にも/dev/st0(巻き戻し可能なテーブデバイス)と/dev/nst0(巻き戻ししない)の違いとかね😆」「自宅でDDS3のテープデバイス使ってたことありますけど、音を立てて動くのが楽しいですよ❤: tarコマンドを文字どおりにテープバックアップに使ってましたし😆」「tarコマンドってtape archiveの略なんですよね😆」「mtコマンドで頭出ししたりとか😆」「仕事でDATにバックアップしてたことならありました😆」(以下延々)

参考: tape backup
参考: 【 tar 】コマンド――アーカイブファイルを作成する/展開する:Linux基本コマンドTips(40) - @IT
参考: mt - コマンド (プログラム) の説明 - Linux コマンド集 一覧表
参考: デジタル・データ・ストレージ - Wikipedia

⚓その他

このあたりからは親睦会で飲み食いしながらのつっつきとなりました😋

⚓2020年問題たち


つっつきボイス:「あ〜ガラケーのね😆」「絶賛発生中😆」「Perlでも2020年にバグが出たそうです」「ありゃ〜😆

「ガラケーで撮った写真を捨てられなくて持ち続けてる人って結構いると思いますよ」「メモリカード挿せないタイプだと捨てるに捨てられない😇」「大事な人の写真だったりするとなおさら😢」「ガラケーの時計が0時で止まるってひどい😆」「わかりやすく止まるだけマシかも😆

⚓その他のその他

つっつきボイス:「EnChromaは眼球保護メガネの研究中に偶然発見されたそうです」「矯正できるのが驚き😳」「そのままだと見えない色を認識可能な帯域に変換できればやれそうな感じですね」


つっつきボイス:「バッテリー爆発しない?😆」「数年で動かなくなるとか?😆」「そういえばSONYも昔NEWSっていうワークステーション出してましたね: たしか最初期のRubyはNEWSで開発したとか」

参考: NEWS (ソニー) - Wikipedia


つっつきボイス:「年末年始にまどマギの劇場版を見返してたら、こんな感じの赤外線キーボードをまどかが普通に使ってたな〜😆」「まどかマニアック😆

参考: 魔法少女まどか☆マギカ - Wikipedia

「こういうのだと、割と前からLeap Motionっていうジェスチャー入力デバイスがあって実は家にもあるんですけど、スキャンの解像度が高いせいなのか、触れないぐらいアツアツになるやんちゃデバイス🔥」「😆」「😆」「でも結構よくできてますし単価も安いし、何よりも注文するとすぐ届くのがエラくて、大学のUI系研究室とかに結構売れてましたね☺」「へぇ〜」「あと最近のOculus QuestっていうVRゴーグルは、インカムのカメラだけで手の動きをトラッキングできるようになってますし」「そういうの欲しいなってずっと思ってるんですけど、目ぼしいアプリがどのぐらいあるかが心配で😆」「自分はもっぱら寝ながらNetFlix見るのに使ってます😆」「PS VRで寝ながら動画を見たことならありますけどゴーグルがでかすぎて邪魔で😅

参考: Leap Motion - Wikipedia
参考: Oculus Questの機能 | Oculus


つっつきボイス:「コラッツの問題は、偶数だったら2で割って奇数だったら3かけて1足すとどんな数でも最後に必ず1, 4, 2, 1…となるかどうかという超難問」「へぇ〜」「ポール・エルデシュさんも『あの問題に関わると人生棒に振るからやめとけ』って言うレベル😆」「数学の難問ってそんなんばっかですか😆」「棒振り系多いですね😆

参考: コラッツの問題 - Wikipedia
参考: テレンス・タオ - Wikipedia


つっつきボイス:「LiDARってライダーって読むみたいで、ジャングルに埋もれた遺跡を空中からスキャンして発見するとかで大活躍してるらしいです」「自動車の自動運転で外界を認識するのにも使ってるみたいですね☺」「そんな凄いセンサーが10万以下で買えるということでIoT界の好き者たちがアツい眼差しを注いでいますね: そのツイートした方もIoT強者なんですけどその人のライブラリを使ったことあります😋

参考: LIDAR - Wikipedia
参考: ペルーのマチュピチュより古い遺跡の発見 - trendswatcher.net

「こういう長距離センサーは精度とかよりもノイズ除去周りの技術が決め手になったりしますね: 謎の誤検出を除去するのは大変😅」「最後はノイズとの戦いなんですね☺」「ライブラリのノイズ除去がイケてないとフィルタから自作しないといけなくなったりしますし😢」「自分はIoTでライブラリに助けてもらった経験ってあんまり覚えがありませんけど😆」「最近はかなりインテリジェントに処理してくれますよ☺: 特に屋外で使うセンサーは太陽光反射とかに対応しないといけませんし、そういうのはハードウェアで処理できないとソフトウェアだけではつらい」「そういえば大学でlaser range finder使ってた人いたな〜」「光波測距儀って書くと何だかカッコいい😍」(以下延々)

参考: TeraRanger One ToF Rangefinder B型(フレーム) - ロボショップ
参考: 光波測距儀 - Wikipedia

⚓番外

⚓3Dの次は

つっつきボイス:「変形ロボ!🤖」「ターミネーター2!」

⚓不気味の谷を超えたか


つっつきボイス:「このフェイク人間が個人的にヒットでした😆」「これはジワるw」「似すぎてて何だかチューリングテストされてる気分😆」「中国語の部屋でしたっけ」「紅白のAI美空ひばりですか😆」「あれってAIじゃないと思うんだけど😆

参考: 中国語の部屋 - Wikipedia

「この方面はやっぱりゲーム業界が進んでますね: レンダリングをそれっぽく見せる技術と、人間らしく振る舞わせる手法とか特にネトゲが強い💪」「映画の方が進んでるかと思ってました」「状況に合わせて最適に振る舞う手法はやっぱりゲーム業界が進んでると思いますね☺」「あ〜そうかも」「FPSとかMMO系なんかだと、たとえば20人が参加している状況で通信がちょっと途切れたりしても、キャラクターを人間らしく振る舞わせておくと通信が切れてないように見える、というのを昔からやってたりしますし☺」「演出の妙というか😆」「最近の映画も凄いですよ: 最新のスター・ウォーズで、役者が既に物故したモフ・ターキンをフルCGで再現してたんですけど自分マジで全然気づきませんでした😳」「たしかに精緻な表現は映画の方が進んでるかも☺

参考: ファーストパーソン・シューティングゲーム - Wikipedia
参考: MMORPG - Wikipedia
参考: グランド・モフ・ウィルハフ・ターキン - Wikipedia

⚓ダークエネルギーもなかった?

つっつきボイス: 「ダークマターではなくて?」「ダークエネルギーは宇宙の全エネルギーの3/4を占めてるとか見積もられてたんですけど、その仮定ががっつり違ってたみたいでした」「宇宙物理界隈は5年もするとがらっと変わったりするから大変😆

参考: ダークエネルギー - Wikipedia


後編は以上です。ご参加いただいた皆さまありがとうございました!😂

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

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

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Postgres Weekly

postgres_weekly_banner

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳)

$
0
0

概要

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

タイトルは内容に即したものにしました。画像は元記事からの引用です。

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳)

皆さんはアセットやJavaScript周りの変更で消耗してませんか?npm、Babel、ES6、Yarn、Webpack、Webpacker、Sprockets、これらのどこがどう違うのかさっぱりわからなかったりしますか?

Rails 6アプリケーションでJavaScriptエコシステム全体がどのように機能しているかという概念を急いで理解する必要にかられているそこのお方、あなたが探しているのはまさにこの記事です。

本記事の最後に、Rails 6プロジェクトにBootstrap 4とFontAwesome 6を追加する手順を記載しますのでご期待ください。

npm

npmは、JavaScript(正確にはNode.jsモジュール)のパッケージマネージャです。言ってみれば、JavaScript世界におけるRubygemです。

npm install <パッケージ名>

たとえばbootstrapをインストールするには以下を実行します。

npm install bootstrap

npmは、ダウンロードしたパッケージを./node_modulesに保存し、パッケージのリストを./package.jsonに保存します。

この時点では、npmとRailsがどう関係するのかについてはまだ何も説明していません。理由についてはこの先をお読みください。

yarn

yarnは、npmより新しいパッケージマネージャです。yarnはnpmのリポジトリからパッケージを取得する点は同じですが、その他の機能もあります。yarn.lockというファイルを自動生成し、必要なバージョンのnpmパッケージをそのファイルでロックします(RubyのGemfile.lockと同じようなものです)し、npmよりずっと高速です。

Rails 6アプリケーションでJavaScriptのライブラリが必要になった場合の操作は以下のとおりです。

  • 従来: JavaScriptのライブラリを提供するgemを追加し、app/assets/application.jsでライブラリをrequireします(これはSprocketsがコンパイルします)。
  • 現在: 同じ作業をyarn(https://yarnpkg.com)で行います。yarn add <パッケージ名>を実行してからrequireします(詳しくは後述します)。

原注: npmにもその後package-lock.jsonでロックする機能が追加されました。

ES6

ES6はJavaScriptの新しい標準です(JavaScriptの新バージョンと呼んでも結構です)。ES6にはクラス定義やデストラクタ、アロー関数といった極めて便利な機能が搭載されています。

さよならCoffeeScript、実は君のことがずっとキライだったよ。

訳注: CoffeeScriptはES6の仕様に影響を与えたという功績も指摘されています。

Babel

すべてのブラウザがES6を理解できるとは限らないため、ES6のJavaScriptがどんなブラウザでも動くようにするには、ES6のJavaScriptコードを読み取って旧来のES5 JavaScriptに変換するツールが必要になります。Babelはそのための変換を行うコンパイラです。

Webpack

BabelとYarn、それらの設定ファイルが揃ったら、次はアセットを自動コンパイルして環境を管理したりする手段が欲しくなります。

開発者はコードを書くこととアセットプリコンパイルの自動化だけに集中したいので、Webpackをバンドマスターとして使うことになります。Webpackはアセットを取り出して適切な個別のプラグインに渡します。プラグインは入力ファイルを適切なツールに処理させて、期待どおりの結果を得ます。

訳注: バンドマスター(バンマス)は、音楽のバンドにおける経営者を指します。

Webpackではたとえば以下のようなことができます。

  • ES6 JavaScriptコードを取り出す
  • babel-loaderプラグインでBabelにES6をコンパイルさせ、ES5 JavaScriptコードに変換する
  • できあがったpackをHTML DOMにインクルードできる形の1つのファイルにまとめる(<script type="text/javascript" src="path-to-es5-javascript-pack.js"></script>

Webpacker

Webpackerは、RailsアプリケーションにWebpackをいい感じに取り込めるgemです。Webpackerにはいくつかの初期設定ファイルが揃っているので(実際これだけで十分です)、設定のことを気にせずにコードを書き始められるようになります。

Webpackerのデフォルト設定では以下が指定されます。

  • 自分のJavaScript pack(つまりapplication.js)はapp/javascript/packs/の下に置かれること
  • ビューでJavaScript packをインクルードするにはjavascript_pack_tag '<pack名>'を使う(例: <%= javascript_pack_tag 'my_app' %>app/javascript/packs/my_app.jsをインクルードする)

本記事の最後に、これらをうまく動かせる極めて明快な例を示しますが、最初にSprocketsについて少しだけ説明しておく必要があります。


原注: 他にもextract_css: falseというデフォルト設定がconfig/webpacker.ymlにあります。これは、WebpackがCSS packをstylesheet_pack_tagで提供する方法を認識していたとしても提供をオフにする設定です。本記事はJavaScriptに特化しているのでこれ以上深追いしませんが、この機能がデフォルトでオフになっていることは頭の隅に置いておくとよいでしょう。これがデフォルトの振る舞いであり、バグではないことを知っておけばデバッグで無駄に時間を使わずに済みます。

もひとつ原注: rails assets:precompileを実行するとapp/assets/の下にあるものだけがプリコンパイルされると思っている方もいるかもしれません。実際には、Railsはapp/javascript/の下にあるWebpackのアセットと、app/assets/の下にあるSprocketsのアセットを両方コンパイルします。

Sprockets 4

Webpackと同様に、Sprokectsも「アセットパイプライン」です。アセットパイプラインとは、アセットファイル(JavaScriptやCSSや画像など)を入力に取り、それらを処理して欲しいフォーマットを出力として生成するものです。

Rails 6から、Sprocketsに代わってWebpack(er)がRailsアプリケーションでJavaScriptを書くための新しい標準となりました。しかしSprocketsは今もアプリケーションにCSSを追加するデフォルトの方法でもあるのです。

Sprocketsは以下のように使われていました。

  • 従来: config.assets.precompileにある利用可能なアセットをリストするのに使われた(Sprockets 3とRails 5の時代)
  • 現在: 同じことをapp/assets/config/manifest.jsのマニフェストファイルでやるように変更された(Sprockets 4とRails 6の時代)

Sprocketsのパイプラインを用いてアセットをインクルードしたい場合は以下のようにします。

  • CSSを書く(ここではapp/assets/stylesheets/my_makeup.cssとする)
  • app/assets/config/manifest.jslink_treelink_directorylinkを用いて、stylesheet_link_tagでそのCSSを利用できるようにする(例: link my_makeup.css
  • ビューにstylesheet_link_tagを書いてCSSをインクルードする(例: <%= stylesheet_link_tag 'my_makeup' %>

Sprocketsを使ってたときのようにWebpackを使わないこと

流れに逆らって時間を無駄に溶かしたくなければ、このセクションの内容を理解しておくことが重要です。そんな時間があればES6を学ぶことに少しでも回せれば理想ですが、少なくともこれだけは言えます。

Webpackがコンパイルするのはモジュールであるという点が、Sprocketsと異なっています

正確には「ES6モジュール」です(Rails 6のデフォルト設定の場合)。これは何を暗示しているのでしょうか?つまり、モジュール内で宣言されるものは、ある意味すべて名前空間化されるということです。というのも、これはグローバルスコープからアクセスできるようにするものではなく、むしろインポートしてから使うものだからです。いくつか例を挙げてみましょう。

Sprocketsでは以下のように書けます。

  • app/assets/javascripts/hello.js:
function hello(name) {
  console.log("Hello " + name + "!");
}
  • app/assets/javascripts/user_greeting.js:
function greet_user(last_name, first_name) {
  hello(last_name + " " + first_name);
}
  • app/views/my_controller/index.html.erb:
<%= javascript_link_tag 'hello' %>
<%= javascript_link_tag 'user_greeting' %>

<button onclick="greet_user('Dire', 'Straits')">Hey!</button>

かなりシンプルに理解できますね。ではWebpackerではどうなるのでしょうか?

「上の2つのJavaScriptファイルをapp/javascript/packsの下に移動してjavascript_pack_tagでそれぞれインクルードすればいいんじゃね?」とお思いの方、ちょっと待った。それではたぶん動かないでしょう。

その理由は、hello()は1つのES6モジュールとしてコンパイルされるからです(user_greeting()も同様に(別の)ES6モジュールとしてコンパイルされます)。つまり、ビューで両方のJavaScriptファイルをインクルードしたとしても、user_greeting()関数にとってhello()関数は存在しないのです。

ではWebpackでSprocketsと同じ結果を得るには以下のようにすればよいのでしょうか?

  • app/javascript/packs/hello.js:
export function hello(name) {
  console.log("Hello " + name + "!");
}
  • app/javascript/packs/user_greeting.js:
import { hello } from './hello';

function greet_user(last_name, first_name) {
  hello(last_name + " " + first_name);
}
  • app/views/my_controller/index.html.erb:
<%= javascript_pack_tag 'user_greeting' %>

<button onclick="greet_user('Dire', 'Straits')">Hey!</button>

残念ながら、これも同じ理由で動きません。greet_user()がコンパイルされると、モジュール内に隠蔽されてしまうため、ビューからgreet_user()にはアクセスできないのです。

いよいよ本セクションで最も肝心な部分にたどり着きました。

  • Sprocketsでやる場合: ビューはJavaScriptファイルで公開されているものとやりとり可能(変数アクセスや関数呼び出しなど)
  • Webpackでやる場合: JavaScript packに含まれているものにはビューからアクセスできない

ではJavaScriptアクションをボタンでトリガーするにはどうすればよいのでしょう?答えは、packから、ある振る舞いをHTML要素に追加することです。後はvanilla JSだろうとjQueryだろうとStimulusJSだろうとどれでもやれます。

以下はjQueryでやる場合の例です。

import $ from 'jquery';
import { hello } from './hello';
function greet_user(last_name, first_name) {
  hello(last_name + " " + first_name);
}
$(document).ready(function() {
  $('button#greet-user-button').on(
    'click',
    function() {
      greet_user('Dire', 'Strait');
    }
  );
});

/* ES6の場合はこうやれる: */
$(() =>
  $('button#greet-user-button').on('click', () => greet_user('Dire', 'Strait'))
);
  • app/views/my_controller/index.html.erb:
<%= javascript_pack_tag 'user_greeting' %>

<button id="greet-user-button">Hey!</button>

結論: Webpackでは、欲しい振る舞いをビューではなくpackでセットアップせよ

直前の例を用いて、繰り返し申し上げます。

何らかのライブラリ(たとえばselect2とかjQueryとか)が必要になった場合、そのライブラリをpackの中でインポートすればビューで使えるでしょうか?できません。あるpackでインポートしたライブラリはそのpackの中でお使いください。さもなければ本記事の次のセクションをお読みください。

原注: StimulusJSでJavaScriptコードを構成する方法やHTML要素に振る舞いをアタッチする方法を学びたいのであれば、StimulusJS on Rails 101がおすすめです。

「あらゆるものが隠蔽され名前空間化される世界」がどんなふうに動作するかを知りたい皆さまへ: ES6のあるモジュールがコンパイルされてES5コードに変換されると、そのモジュールの中身は1つの無名関数の内部に封じ込められ、その関数の外からはモジュール内で宣言された変数や関数に一切アクセスできなくなります。

(後編に続く)

関連記事

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

Rails 5: Webpacker公式README — Webpack v4対応版(翻訳)

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 後編(翻訳)

$
0
0

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 後編(翻訳)

(前編からの続き)

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳)

今でもJavaScriptコードをSprocketsで扱える

Webpackerドキュメントには以下のように書かれています。

(略)Webpackの主要な目的は「アプリのようなJavaScript」であり、画像やCSSのためではなく、ましてやJavaScript Sprinklesのためではない(これらは今後もapp/assetsの下に置ける)。
同ドキュメントより

つまり、ビューで何かJavaScriptを使いたいとか使う必要に迫られたときは、これまでどおりSprocketsを使えるのです。

  • app/assets/javascriptsディレクトリを作成する(複数形のjavascriptsになっていることに注意)
  • app/assets/config/manifest.jsを上に合わせて更新する(//= link_directory ../javascripts .js
  • ビューにjavascript_include_tagを書いて、Sprockets用JavaScriptファイルをインクルードする(Sprockets用はjavascript_include_tag、Webpacker用はjavascript_pack_tagであることに注意)
  • 後は好きにやる

個人的にはこの手段をできる限り避けていますが、このことは知っておく価値があります。


原注: manifest.jsconfig.assets.precompileの配列は、どちらもコンパイル対象のトップレベルに露出させるという目的は同じなのに、どうしてファイルが2つもあるのかが気になる方もいらっしゃると思います。その目的は後方互換性のためです。Sprocketsのアップグレード手順では、後者は利用しないことをおすすめしています。

Rails 6アプリケーションでbootstrap 4とfont-awesome 5を追加する手順

本記事をより深く理解いただくために、ここにある手順をそのまま適用することをおすすめします。JavaScriptの知見を深めるのに大いに役に立つでしょう。

1. Rails 6アプリケーションを新規作成する

rails new bloggy

以下にリストしたファイルを見てみましょう。目的は、皆さんにこれらを隅々まで理解してもらうことではなく、こういうファイルが存在するということを皆さんに知ってもらい、そこに何が含まれているのかというぼんやりとしたメンタルイメージを頭の隅に置いて、必要に応じていつでもそこに立ち返ることができるようにすることです。

Yarnのファイル:

  • package.json

Webpackerのファイル:

  • config/webpacker.yml
  • app/javascript/packs/application.js
  • app/views/layouts/application.html.erb

Sprocketsのファイル:

  • app/assets/config/manifest.json

2. ルートページを追加する

rails generate controller welcome index

ついでにconfig/routes.rbroot to: 'welcome#index'を追記します。

rails serverを実行し、問題なく動作することを確認します。

3. 必要なyarnパッケージを追加する

ここではbootstrap 4(jQueryとpopper.jsが必要です)とfont-awesome 5を追加したいと思います。

Yarnパッケージの検索エンジンで、自分に必要なパッケージを検索し(各パッケージのダウンロード数の多さに気づくことでしょう)、このチュートリアルを続行します。

yarn add bootstrap jquery popper.js @fortawesome/fontawesome-free

パッケージがyarnによって./bloggy/node_modules/にキャッシュされ、package.jsonも更新されます。しかしこのままではアプリケーションから利用できませんので対応しましょう。まずはJavaScript部分をインクルードすることにし、CSS部分は後でやることにします。

4. bootstrapとfont-awesomeのJS部分をインクルードする

アプリのレイアウトには既にjavascript_pack_tag 'application'が置かれています。これは、Webpackにapp/javascript/packs/application.jsのコンパイルを指示してその出力をレイアウトにインクルードします。bootstrapを追加するには、bootstrapをインクルードする専用のpackを別途作成するか、application.js packを使います。本物のアプリを作るわけではありませんので、ここでは後者をやってみましょう。

以下をapp/javascript/packs/application.jsに追加します。

require("bootstrap");
require("@fortawesome/fontawesome-free");

原注: ここでbootstrap/dist/js/bootstrap.minではなくbootstrapをrequireしていることにご注意ください。その理由は、ファイルパスを指定しない場合はどのファイルをインクルードすべきかという必要な情報をモジュールのpackage.json(つまりbloggy/node_modules/bootstrap/package.json)が提供するからです。bootstrap/dist/js/bootstrap.minをrequireすれば、それはそれで問題なく動くでしょう。


bootstrapとfont-awesomeの設定を続けましょう。Railsサーバーを起動してJavaScriptコンソールを開いてみると、application.jsでjQueryをrequireしていないにもかかわらず、問題なく動作していることを確認できます。

Webpackerを用いてbootstrapをインクルードする方法を他のチュートリアルで学んだ方の中には、他のチュートリアルのほとんどが最初にjQueryをrequireし、次にbootstrapをrequireしていることに気づいた方もいるかもしれません。これは実際には無意味です。

その理由がわかりますか?私たちはjQueryをyarnでインストールしているので、bootstrap自身がjQueryを自動でrequireできるのです。jQueryはapplication.jsの中で利用可能な状態になるので、jQueryをapplication.jsでrequireする必要はありません。すなわち、jQueryをapplication.jsの中で直接使う必要がない限り、実際にはjQueryをapplication.jsでrequireする必要はありません。

5. bootstrapとfont-awesomeの(S)CSS部分をインクルードする

私はSCSSを使うのが好きなので、bootstrapやfont-awesomeをインクルードする前にapplication.cssをapplication.scssにリネームし、コメントやSprockets用の指示を全部空にします。

続いて以下のコードを貼り付けます。

$fa-font-path: '@fortawesome/fontawesome-free/webfonts';
@import '@fortawesome/fontawesome-free/scss/fontawesome';
@import '@fortawesome/fontawesome-free/scss/regular';
@import '@fortawesome/fontawesome-free/scss/solid';
@import '@fortawesome/fontawesome-free/scss/brands';

@import 'bootstrap/scss/bootstrap';

原注: SprocketsはWebpackと異なり、インクルードするファイルを決定するためにnpmモジュールのpackage.jsonファイルを読み込みません。つまり、名前だけを指定してモジュールをインポートすることはできません。実際にインポートしたいファイル名とそのパスを指定する必要があります(拡張子はあってもなくても構いませんが)。


ついに準備が整いました。

ビューに何かボタンとアイコンを追加して、問題なく動作しているかどうかを確認しましょう。

app/views/welcome/index.html.erbファイルに<a href="#" class="btn btn-primary">Yeah <i class="far fa-thumbs-up"></i></a>を追加してRailsサーバーを実行し、primaryボタンとアイコンがbootstrapらしく表示されていることを確認します。

jQueryをあらゆるpackで利用する

jQueryのような依存関係を多くのpackで使う必要が生じた場合、それらをpackごとにいちいちrequireするのは面倒です。私好みのソリューションは、設定を変えて全packで利用可能にすることです(繰り返しますが、あくまでpack内での話であり、ビューでは使えません)。

これを行うには、config/webpack/environment.jsに以下をコピペします。

const { environment } = require('@rails/webpacker')
var webpack = require('webpack');

environment.plugins.append(
  'Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
  })
)

module.exports = environment

このスニペットによって、WebpackはjQueryモジュールを$という名前を介して全packに「提供」します。これは、各packの冒頭に以下を追加するのと同等です。

import $ from 'jquery';

お読みいただいた皆さまに感謝いたします。

関連記事

Rails 6+Webpacker開発環境をJS強者ががっつりセットアップしてみた(翻訳)

Rails 5: Webpacker公式README — Webpack v4対応版(翻訳)

Viewing all 1381 articles
Browse latest View live