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

Railsコンソールの思いがけない便利技(翻訳)

$
0
0

概要

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

Railsコンソールの思いがけない便利技(翻訳)

.irbrcを調整してインタラクティブなコンソールを使いやすくすると、生産性も目に見えて向上し、フラストレーションも減らせるのでやりがいのある作業です。最近のRuby Weeklyでは、このあたりのトピックを扱う記事をいくつも見かけます。

これまでも.irbrcにいろいろ手を加えて便利にしてきましたが、変更内容をリモートサーバーにも反映するのがいつも面倒でした(Herokuでも使えるようにするなど)。この問題で悩んでいるのは私だけではありません。もちろん方法はいくつもありますが、以下のようにアプリのコードに近い方法が最もしっくりきました。

# script/likeasir.rb

# productionのコンソールを開くときにあると便利なもの
# load 'script/likeasir.rb'

def event_store
  Rails.configuration.event_store
end

def command_bus
  Rails.configuration.command_bus
end

# ...

コンソールを開いたら、irbセッションで以下を実行してヘルパーを読み込みます。

load 'script/likeasir.rb'

ところが、弊社のKubaが上の読み込み手順を完全に不要にするうまい方法を教えてくれました。

# config/application.rb

module MyApp
  class Application < Rails::Application
    # ...

    console do
      module DummyConsole
        def event_store
          Rails.configuration.event_store
        end

        def command_bus
          Rails.configuration.command_bus
        end
      end
      Rails::ConsoleMethods.include(DummyConsole)
    end
  end
end

これで、bin/rails cを読み込めばcommand_busメソッドとevent_storeメソッドをいつでもirbセッションで使えるようになります。

以上、私がずっと思いつかなかった小技を紹介いたしました。

お知らせ

ARKADEMY.DEVに参加してArkencyのトップクラス教育プログラムコースにアクセスしましょう!「Railsアーキテクトマスタークラス」「アンチ”IF”コース」「忙しいプログラマーのためのブログ執筆コース」「Async Remoteコース」「TDD動画クラス」「ドメイン駆動Rails動画コース」以外にもさまざまなコースが新設中です。

関連記事

Railsコントローラのアクションがrenderで終わるとは限らない(翻訳)

The post Railsコンソールの思いがけない便利技(翻訳) first appeared on TechRacho.


Railsの技: カスタムヘルパーとStimulusで軽量コンポーネントを構築(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。

Railsの技: カスタムヘルパーとStimulusで軽量コンポーネントを構築(翻訳)

Railsのカスタムヘルパーのよさは見落とされがちですが、軽量コンポーネントのビルドや、Stimulusコントローラの定形コードを削減するときの最適なオプションにもなります。

Stiumulusの良い点は、マークアップの属性を見るだけで即座に機能を推測できることです。しかし、値やアクションを複数持つコンポーネントの場合は実装の詳細の一部を隠蔽することにメリットがあります。

私の過去記事『Building GitHub-style Hovercards with StimulusJS and HTML-over-the-wire』からコード例を引用します。

<div class="inline-block"
  data-controller="hovercard"
  data-hovercard-url-value="<%= hovercard_shoe_path(shoe) %>"
  data-action="mouseenter->hovercard#show mouseleave->hovercard#hide"
>
  <%= link_to shoe.name, shoe, class: "branded-link" %>
</div>

このhovercard_controllerurl値を渡される必要があり、かつマウスオーバーされたカードの表示と非表示用のアクションを追加する必要もあります。このコントローラはリンクタグでラップされており、アプリで用いるホバーカードの種類に応じてカスタムスタイルを適用できるようになっています。

このコントローラを数箇所で使うだけなら大した手間ではありませんが、さまざまな種類のホバーカードに対してこのコントローラを再利用したいのであれば、Railsのカスタムヘルパーを追加してみてはいかがでしょう。

利用法

app/helpersディレクトリの下に置かれたモジュールは、自動的にアプリのビューで利用できるようになります。

# app/helpers/hovercard_helper.rb
module HovercardHelper

  # ヘルパーを用いてStimulusコントローラ属性の繰り返しを避ける
  def hovercard(url, &block)
    content_tag(:div,
      "data-controller": "hovercard",
      "data-hovercard-url-value": url,
      "data-action": "mouseenter->hovercard#show mouseleave->hovercard#hide",
      &block)
  end

  # 独自の軽量「コンポーネント」をビルドする
  def repo_hovercard(repo, &block)
    hovercard hovercard_repository_path(repo), &block
  end

  def user_hovercard(user, &block)
    hovercard hovercard_user_path(user), &block
  end
end

ヘルパーを使えば、独自の「コンポーネント」をRubyで作成して実装の詳細を抽象化できるようになります。Rubyのブロックは強力なので、用途に応じてカスタマイズ可能な柔軟なコンポーネントを作れます。

<!-- app/views/timeline.html.erb -->

<%= user_hovercard(@user) do %>
  <%= link_to @user.username, @user %>
<% end %>

<%= repo_hovercard(repository) do %>
  <div class="flex items-center space-x-2">
    <svg></svg> <!-- Some icon -->
    <%= link_to repository.name, repository %>
  </div>
<% end %>

たとえばrepo_hovercardというヘルパーを作って、Repositoryモデルとレンダリングするブロックを受け取れるようにしたとします。これにより、ページのコンテキストに応じて表示内容を完全に制御でき、Stimulusイベントの適切な組み合わせを気にする必要もありません。

アプリのStimulusコントローラを変更したい場合も、コントローラが多くのビューに分散せず1箇所に集約されます。

参考情報

  • Rails APIドキュメント: Helpers

関連記事

Railsの技: 特定スコープ内でuniquenessバリデーションをかける(翻訳)

The post Railsの技: カスタムヘルパーとStimulusで軽量コンポーネントを構築(翻訳) first appeared on TechRacho.

Rails 7: belongs_to関連付けに変更トラッキングが追加(翻訳)

$
0
0

概要

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

Rails 7: belongs_to関連付けに変更トラッキングが追加(翻訳)

アップデートとしては地味でも、Railsユーザーにとって嬉しさ最大の機能が登場することがあります。Railsの最近のアップデートで、belongs_to関連付けの変更を監視する機能が導入されました。Rails wayに沿って進めたい開発者にとっては大歓迎です。

変更トラッキングは、Railsですぐ使える便利メソッドの一種です。Active Recordを使っていると、データベースで変更が生じているかどうかを調べられます。これはトリガーやイベントをビルドするときに便利です。サンプルを見てみましょう。

データベース構造に投稿(Post)とカテゴリ(Category)があるとします。

class Post
  belongs_to :category
end

class Category
  has_many :posts
end

投稿のタイトルが変更されたときのアクティビティを記録しておきたいとします。これは以下のように書けます。

class Post
  belongs_to :category

  before_update :record_activity

  private
  def record_activity
    if self.title_changed?
      # record an activity
    end
  end
end

title属性の値が変更されたかどうかはtitle_changed?メソッドで監視できます。

変更前

同じことを、投稿のカテゴリが変更された場合についてもやってみましょう。

class Post
  belongs_to :category

  before_update :record_activity

  private
  def record_activity
    if self.category_id_changed?
      # アクティビティを記録する
    end
  end
end

この書き方は目障りですね。categoryという関連付けがあるにもかかわらず、その関連付けの変更を追えないので、category_idの変更を監視するというかなり大雑把な方法を取らざるを得ません。これではRails wayから遠ざかってしまいます。

変更後

ありがたいことに、Rails 7でこれを行う方法が導入されます(#42751)。これなら以下のように書けます。

class Post
  belongs_to :category

  before_update :record_activity

  private
  def record_activity
    if self.category_changed?
      # アクティビティを記録する
    end
  end
end

この関連付け名_changed?メソッドは、元と異なる関連付けオブジェクトが代入されている場合はtrueを返し、次回のsave!で外部キーが更新されます。

また、関連付け名_previously_changed?を使えば、以下のように直前のsaveで関連付けが更新されて元と異なる関連付けオブジェクトを参照する場合はtrueを返すこともできます。

  post.category # => #<Post category_id: 123, title: "Welcome to Rails!">
  post.category_previously_changed? # => false

  post.category = Category.second # => #<Post category_id: 456, title: "Welcome to Rails!">
  post.save!

  post.category_previously_changed? # => true

関連記事

Rails 6.1: Active Storageのファイルをプロキシ経由で配信する(翻訳)

The post Rails 7: belongs_to関連付けに変更トラッキングが追加(翻訳) first appeared on TechRacho.

Railsの技: Turbo Frameとskeleton loaderでHotwireのコンテンツをlazy loadingする(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。

Railsの技: Turbo Frameとskeleton loaderでHotwireのコンテンツをlazy loadingする(翻訳)

HotwireはBasecampが提供する新しいフロントエンドツール集で、最小限のJavaScriptを書くだけで「リアクティブな」Railsアプリを構築できます。

Hotwireの機能の中で、サーバーサイドでレンダリングしたHTMLをリアルタイムでストリーミングできる機能が最大の目玉だと考える人もいますが、私の推しはTurbo Frameが追加されたことです。

Turbo Frameはiframeの超高性能版で、使ってみると実に感心します。Frameはページの一部を表現し、そこに独自のナビゲーションコンテキストを内包しています。

中でもひときわ強力なのは、Turbo Frameのlazy-loading(遅延読み込み)機能です。このパターンの例としては、普段よく目にするGitHubのactivityフィードがあります。

GitHub Activity Feed: Lazy load

最初にページの「外殻」を読み込み、続いてAJAX呼び出しを実行すると、コンテンツの残りをフェッチしてページを埋められます。これは遅いページの高速化に絶大な効果を発揮します。

しかし、この機能を使うとページコンテンツががたついてしまうという欠点があります。「読み込み中」のスピナー表示は小さい四角形ですが、表示されると多数のイベントがフィードされて四角形が下に展開されます。

これを解決する方法のひとつは、“skeleton screen”か”skeleton loader”を使うことです。このUIパターンでは、空白のコンテンツをプレースホルダとして用いることで、コンテンツが最終的に読み込まれたときのがたつきを軽減します。

この2つの概念は、ピーナッツバターとゼリーのように相性抜群です。

使い方

Turbo Frameの基本的なlazy-loadingは以下のようになります。

<turbo-frame id="feed" src="/feed">
 /feedが読み込まれるとこのコンテンツが置き換えられる
</turbo-frame>

ここにsrc属性を指定すると、Frameはページ読み込み時に自動的にAJAXリクエストを発行し、レスポンス内で<turbo-frame>にマッチする部分をそのコンテンツで置き換えます。

さらに、loadingプロパティに"eager"または"lazy"も設定できます。"eager"は直ちに読み込みを開始し、"lazy"はフレームがページに出現したときに読み込みを開始します。

GitHub ActivityのフィードをRailsで行うと、以下のような感じになるでしょう。

<!-- app/views/home.html.erb -->

<div>(他のコンテンツ)</div>

<%= turbo_frame_tag :feed, src: activity_feed_path, loading: :lazy do %>
  読み込み中...
<% end %>

この基本的な「読み込み中…」メッセージを独自のskeleton loaderに置き換えることでページをレベルアップできます。Tailwind CSSに組み込まれているanimate-pulseクラスのおかげで実に簡単です。

適当な灰色の四角形をフレームの初期コンテンツとして追加します。

<!-- app/views/home.html.erb -->

<div>(他のコンテンツ)</div>

<%= turbo_frame_tag :feed, src: activity_feed_path, loading: :lazy do %>
  <div class="flex flex-col space-y-6">
    <% 10.times do %>
      <div class="animate-pulse flex space-x-4">
        <!-- アバター -->
        <div class="rounded-full bg-gray-400 h-12 w-12"></div>

        <!-- 詳細 -->
        <div class="flex-1 space-y-4 py-1">
          <div class="h-4 bg-gray-400 rounded w-3/4"></div>
          <div class="space-y-2">
            <div class="h-4 bg-gray-400 rounded"></div>
            <div class="h-4 bg-gray-400 rounded w-5/6"></div>
          </div>
        </div>
      </div>
    <% end %>
  </div>
<% end %>

仕上げに、activity_feed_pathアクションが返すコンテンツをTurbo Frameにマッチするようラップしておきます。これによりフレームのコンテンツが自動的に差し替えられ、読み込み中のステートが更新されます。

class ActivityFeedController < ApplicationControler
  def show
    @events = Current.user.activity.last(20)
  end
end

注意: このレスポンスではFrameにsrc属性やloading属性を設定しないでください(設定すると無限ループになってしまいます)。

<!-- app/views/activity_feed/show.html.erb -->

<%= turbo_frame_tag :feed do %>
  <%= render partial: "feed_item", collection: @events %>
<% end %>

Turbo Frameの真の力を理解できれば、昔ながらのHTMLページをlazy-loadingできるようになり、アプリのあらゆる場所で役に立つでしょう。

参考情報

関連記事

HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか?前編(翻訳)

The post Railsの技: Turbo Frameとskeleton loaderでHotwireのコンテンツをlazy loadingする(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: Ruby 7 Alpha 1と2が公開、Rubyハッシュのショートハンド記法、iCare Dev Meetupほか(20210921)

$
0
0

こんにちは、hachi8833です。RubyKaigi Takeout 2021が終わりましたね。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

今週の週刊Railsウォッチは祝日が多いので短縮版でお送りします。また、来週の週刊Railsウォッチはお休みいたします🙇

🔗 特集: Rails 7 Alpha 1と2がリリース

なおつっつきの後で、Alpha 1と同じ日にAlpha 2もリリースされていたことに気づきました。


つっつきボイス:「Alpha 1出た🎉」「ちょっと前からDHHのYouTube動画チャンネルにもRails 7 Alphaプレビュー情報が出ていたのでそろそろ出る頃かもと思っていたところ」「DHHの動画はコードも見えるので比較的イメージを掴みやすいと思います: おすすめ👍」「あとで見ようっと」

🔗 Rails 7は選択肢を複数用意する方向へ

「これまでのRailsはConvention over ConfigurationのようなRails wayに沿って進めるのが望ましいという立ち位置でしたが、今回のRails 7 Alpha 1リリースを眺めていると、Hotwireが入るけどHotwire一択ではないという点がこれまでと違うかなという気がします」「あ、そうかも」「これまでRailsを使い慣れている人はRails wayから適切に外れるノウハウを身につけていて、人によってはたとえばTurbolinksを速攻で外したりすることもありましたよね」

turbolinks/turbolinks - GitHub

「Railsも成熟したんだなという気持ち」「Webpackも使いたければ使っていいよというのはありがたい🙏」「そういえば最近はリリースノートでも言及されているesbuildがいいらしいという噂もちらほら見かけますね」

evanw/esbuild - GitHub

以下はつっつき後に見つけたツイートです。

「リリースノートにはこれまでチェックしてきたActive Record暗号化(ウォッチ20210412)やmarginalia-gem風のQueryLog追加(ウォッチ20210906)などがフィーチャーされていますね」「Active Record暗号化うれしい😂」「load_asyncってもう入っているような気持ちになっていたけどまだだったか(ウォッチ20210222)」「そういえば少し前の銀座Rails#33でもload_asyncを取り上げていましたね↓」

「Rails 7へのアップグレード手順は、JS環境を完全にHotwireベースに乗り換えるなら作業量が増えそうだけど、今の構成のままでよければそこまで増えなさそうかな」「アップグレード楽しみです😋」「デフォルトのvariant(サイズ違いの画像)生成がmini_magickからvipsに変わるとは知らなかった(#42744)」

libvips/ruby-vips - GitHub

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

今回は以下の公式更新情報を取り上げます。

🔗 sass-railsへのデフォルト依存を削除

最近のWebアプリケーションでは、Tailwind、Bootstrap、BulmaなどのCSSフレームワークを使うことが増えている。Railsは、まだすべてを手書きしているかのようなモデルごとのスタイルシート生成を行うべきではない。
また、Sassはdart-sassに注力することを選んだが、dart-sassはRailsがデフォルトで採用していないあらゆる依存関係を必要とする。そこで、Sassへの依存を減らしてオプションとして追加することにした。
同PRより


つっつきボイス:「sass-rails gemが消されたのかと思ったら、デフォルトではsass-ralisを使わないことにしたのね: Sassを引き続き使いたかったらSprocketsかWebpack-dev-serverかそれらに相当する何らかのアセットプリコンパイラが必要ということになるでしょうね」「たしかに」「もちろん必要なら自分でSassをインストールすればOK」「sass-ralisがデフォルトでなくなるのはそれはそれで嬉しいかも」

rails/sass-rails - GitHub

「アセットプリコンパイル、結局よくわからずじまいでした」「アセットプリコンパイルは果たしてサーバーサイド側の責務なのか、それともフロントエンド側なのかという問題は以前からありますね」「ありますあります」「Railsはこれまでサーバーサイドでアセットプリコンパイルするという考え方でしたね」「それが今回デフォルトからは消えたということか」「RailsコマンドにSassインストール用のオプションが入るといいですよね」「まだオプションは見当たらないけどあるとよさそう」

「ところでDHHはTailwind cssを推しているような気がしますね」「Tailwind、まだ使ったことないんですけど流行っているんでしょうか?」「いわゆるユーティリティ系CSSフレームワークでは世間的には有名ですね: フロントエンド界隈ではTailwindを使うことがデファクトというわけではないと思いますが、個人的にはユーティリティ系CSSフレームワークを使うならTailwindでもいいのではとは思います」「なるほど」

参考: Tailwind CSS - Rapidly build modern websites without ever leaving your HTML.

「TailwindはIDEがサポートする前提で人間が手書きする感じではないのが個人的に何となく合わないんですが、かといってSassならいいというほどでもないんですよね」「Tailwindが生成するCSSを見ていると、CSSのカスケーディングという仕様が現代のコンポーネント化されることを想定したWebページに合わなくなってきてつらいよなあ、という気持ちになります」

🔗 parse_floatvalid_float?をRuby 2.7以降向けに最適化

# actionview/lib/action_view/helpers/number_helper.rb#450
        def parse_float(number, raise_error)
          Float(number)
        rescue ArgumentError, TypeError
          raise InvalidNumberError, number if raise_error
          result = Float(number, exception: false)
          raise InvalidNumberError, number if result.nil? && raise_error
        end
# activesupport/lib/active_support/number_helper/number_converter.rb#176
        def valid_float?
          Float(number)
        rescue ArgumentError, TypeError
          false
          Float(number, exception: false)
        end

つっつきボイス:「ActiveSupport::NumberHelper#parse_floatActionView::Helpers::NumberHelper#valid_float?を、例外を投げ直さないようにする形で高速化したのね」「ここで使ったFloat(..., exception: false)はRuby 2.7以降のみと書かれている」「つまりRailsで必要な最小限のRubyバージョンがRails 7で2.7.0以上に変更されることで↓、Ruby 2.7の機能が使えるようになったということでしょうね」「たしかにif RUBY_VERSION的なコードがありませんね」「そういうコードは少ない方がみんなが幸せになれる」

Rails requires Ruby version 2.7.0 or later. It is preferred to use latest Ruby version. If the version number returned is less than that number (such as 2.3.7, or 1.8.7), you’ll need to install a fresh copy of Ruby.
edgeguides.rubyonrails.org Getting Started with Railsより

🔗 preload_link_tagを修正


つっつきボイス:「preload_link_tagって使ったことないかも」「ああ、リンクタグにrel="preload"を追加するビューヘルパーなのか」「一部の画像でas="image"が設定されなかったのが修正されたということのようですね👍

# #Lactionview/test/template/asset_tag_helper_test.rb#248
  PreloadLinkToTag = {
-   %(preload_link_tag '/sprite.svg') => %(<link rel="preload" href="/sprite.svg" as="image" type="image/svg+xml">)
+   %(preload_link_tag '/sprite.svg') => %(<link rel="preload" href="/sprite.svg" as="image" type="image/svg+xml">),
+   %(preload_link_tag '/mb-icon.png') => %(<link rel="preload" href="/mb-icon.png" as="image" type="image/png">)
  }

🔗 schema_cache_ignored_tables設定オプションが追加


つっつきボイス:「特定のテーブルをスキーマキャッシュにダンプしないようにできるオプションが追加されたんですね」「正規表現も使える」「これはどんなときに嬉しいんでしょうか?」「おそらく、Railsのマイグレーションで管理されていない外部データベースを読み込み専用で参照しているような状況で、スキーマがキャッシュされると困るときに欲しい機能でしょうね」「なるほど、ちょうどそういう感じの作業が降ってきたところです😅」「既存の方法でも全テーブル単位とかでダンプをオフにすることはできたと思いますが、コンフィグでより細かく設定できるようにしたということだと思います」「お〜」

  • スキーマキャッシュのダンプ時にテーブルを無視するコンフィグオプションを追加
    アプリケーションがスキーマキャッシュのダンプ時に特定のテーブルを無視するよう設定可能になった。
    この設定オプションは以下のようにテーブルの配列にすることも、正規表現にすることも可能。
config.active_record.schema_cache_ignored_tables = ["ignored_table", "another_ignored_table"]
config.active_record.schema_cache_ignored_tables = [/^_/]

Eileen M. Uchitelle
同Changelogより

🔗 ActiveModel::APIが追加


つっつきボイス:「Alpha 1のコミットの中からたまたま見つけました」「Action PackとAction Viewが参照するModelとして期待するAPI群をActiveModel::APIに切り出すことで、ActiveModel::Modelの見通しを良くしたリファクタリングのようですね」「ActiveModel::API、わかりやすそう」「persisted?はデフォルトでfalseになるのか、へ〜」

# activemodel/lib/active_model/api.rb(ドキュメントは略)
# frozen_string_literal: true

module ActiveModel
  module API
    extend ActiveSupport::Concern
    include ActiveModel::AttributeAssignment
    include ActiveModel::Validations
    include ActiveModel::Conversion

    included do
      extend ActiveModel::Naming
      extend ActiveModel::Translation
    end

    def initialize(attributes = {})
      assign_attributes(attributes) if attributes

      super()
    end

    def persisted?
      false
    end
  end
end

現在のActiveModel::Modelは、Action PackやAction Viewとやりとりするための最低限のAPIとして定義されている。
その名のとおりこれをincludeしてActive Recordモデルを作成できるが、モデル作成機能はごくわずかしかない。たとえばActiveModel::Attributesincludeすることが非常に多い。
ActiveModel::Modelの実装を新たなActiveModel::APIに移動することで、Action PackやAction Viewとやりとりするための最小限のAPI定義を維持できる。
これでActiveModel::ModelにはActiveModel::APIだけがincludeされるようになり、後方互換性を失わずにActiveModel::Modelに機能追加できるようになる。

なお、このプルリクは以前更新しそびれていた古い#42042と同じ。
同PRより

🔗Rails

🔗 iCare Dev Meetup #25開催


つっつきボイス:「昨日(2021/09/15水)開催されたiCare Dev Meetup #25、HotwireやBiTemporal Data Modelの話など、いろいろ充実したイベントでしたね」「イベントやってたの知らなかった…」「毎月開催されていますよ」

以下は#icare_meetup - Twitter検索 / Twitterより。

「早くも動画や資料が公開されていてありがたいです🙏」「いい内容でしたし動画もそれほど長くありませんので視聴をおすすめします👍

「BiTemporal Data Modelは、適用開始年月日と適用終了年月日を持つデータモデルで、昔からよく取沙汰される話題ですね」

「神速さんのRailsアップグレードの話もよかった: このアップグレード環境を構築するのは大変そうだけど、それを作れればしくみとして回せるようになるんだなと思えました」

「yasaichiさんの資料はしばらくお待ちくださいということですが、動画ではもう見られます」

つっつき後にyasaichiさんの資料が公開されました↓。

「そういえばTwitterもだいぶ前にCassandraを導入しようとして断念したことがありましたね: 新しい技術は一見よさそうに見えても、実際に使ってみるまでわからないところがあるから慎重にならざるを得ない」「ピクスタさんもこれが最終決定ではないそうなのでこれからですね」

参考: Apache Cassandra - Wikipedia
参考: Twitterが、Cassandraの本採用を断念。「いまは切り替えの時期ではない」 - Publickey

「ところで、yasaichiさんの発表はこれより前のバージョンを見た覚えがありますよ: たしか以前の銀座Railsかな」「お、例の『Ruby on Railsの正体と向き合い方』(ウォッチ20190401)とは別でしょうか?」「それではなくて、今回のと近いタイトルだったと思います: こういうふうに同じテーマを定期的に追い続けてくれる方がいると、状況の移り変わりなどもわかってとても助かります🙏

つっつき後に見つけました↓。

「yasaichiさんの発表を見ていて、必ずしも毎回新しいテーマでなくてもいいんだなって思ったので、機会があったらまたデータベースVIEWの話をしてもっと啓蒙してみようかな」「データベースVIEWは以前銀座Rails#10で発表していましたね↓」「3年経ったからそろそろ頃合いかも」

RDBMSのVIEWを使ってRailsのデータアクセスをいい感じにする【銀座Rails#10】

「ところで自分は聞いたことはありませんが、yasaichiさんの発表内容はこちらのtextafmポッドキャスト↓を普段から聞いていた人にはお馴染みだったようです」「そうそう、こうやっていろんな形で普及に努めているんだなって思いました」

🔗 その他Rails


つっつきボイス:「Railsクイズ、皆さんもぜひやってみるとよいと思います👍: 自分たちが普段やっていることなら見当がつくけど、2番のような、既にプロファイルが存在するのにuser.build_profileするという普段まずやらないような問題は難しかった」

🔗Ruby

🔗 ハッシュのショートハンド記法

参考: Shorthand hash syntax in Ruby - rubyonrails-talk - Ruby on Rails Discussions


つっつきボイス:「そうそう、RubyKaigi Takeout 2021最終日の感想戦でES2015風のハッシュのショートハンド記法をRuby 3.1に追加する話が出ていましたね」「結局マージされたんでしたっけ?」「c60dbcdでマージされていました」「お〜、割と前から要望があった構文ですけど、RubyKaigiの勢いに乗ってマージされたんでしょうね」

参考: Allow value omission in Hash literals · ruby/ruby@c60dbcd

参考: オブジェクト初期化子 - JavaScript | MDN

// developer.mozilla.orgより
// Shorthand property names (ES2015)
var a = 'foo', b = 42, c = {};
var o = {a, b, c};

RubyKaigi Takeout 2021のスライド(Day3)

「TechRacho翻訳記事でお世話になっているBrandon Weaverさんが早くもこの新構文で遊んでみた記事をアップしていました↓」


今回は以上です。

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

週刊Railsウォッチ: 責任あるモンキーパッチの当て方、gem脆弱性スキャンツール、Docker Desktop課金プラン改定ほか(20210907後編)

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

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

Rails公式ニュース

The post 週刊Railsウォッチ: Ruby 7 Alpha 1と2が公開、Rubyハッシュのショートハンド記法、iCare Dev Meetupほか(20210921) first appeared on TechRacho.

Rails 7 Alpha 1、2がリリースされました(リリースノート翻訳)

$
0
0

Ruby on Rails 7 Alpha 1、Alpha 2が立て続けにリリースされました。

今回のRails 7 Alphaでは、GItHubのリリースタグ↓にChangelogがありません。代わりにRails 7 Alpha 1リリースノートの翻訳を掲載いたします。

詳しくは以下のコミットリストやAlphaプレビュー動画をご覧ください。

Rails 7.0 Alpha 1: JavaScriptへの新回答、実行時暗号化、クエリ生成元のログ出力、Zeitwerk一元化

概要

MITライセンスに基づいて翻訳・公開いたします。

Rails 7の最初のAlpha版リリースにようこそ。このリリースには、JavaScriptの扱いに関するきわめてエキサイティングな新回答、Active Recordの実行時暗号化への目覚ましいアプローチ、SQLクエリ生成元のログ出力、非同期クエリ読み込み、オートローディングのZeitwerk一元化、他にも多くの機能が盛り込まれています。

通常であればRailsのAlpha版をリリースすることはありませんが、フロントエンドの新しいアプローチが非常に抜本的な変更を伴うことを考慮し、通常の「Beta版->リリース候補版->最終版」というフローに乗せる前にもう少し検証を進めておくのがベストと考えました。

今年中にRails 7最終版をリリースできるよう、新機能のテストにどうかご協力をお願いいたします。

フロントエンドに関する新しい回答のすべて

これまで5年近くに渡り、RailsでモダンなJavaScriptを書くためのデフォルトの回答はWebpackerでしたが、次の段階に進むときが来ました。ブラウザでのES6やESMサポート強化、HTTP/2の普及、そしてimport mapsというエキサイティングな新標準が登場したことによって、Rails 7のJavaScriptをNodeに依存しないアプローチへの道が開かれました。しかもnpmパッケージの利用を諦めずにです。

従来のTurbolinksとRails UJSが、StimulusTurboを組み合わせたHotwireに置き換えられたことで、優れたRailsアプリケーションを書くためのかつてないほど完成度の高いフロントエンド向け詰め合わせセットアップを実現しました。node_modulesディレクトリに数千個ものnode依存関係を置く必要もモジュールバンドラーの設定で苦しむこともなくなり、JavaScript開発にありがちな困難から解放されます。

それと同時に、Railsで「JavaScript + CSSバンドラー」を必要とする方向けに、両者の統合も劇的に改善されました。rails newで2つの新しいコンパニオンgemを-javascript [bundler]オプションや-css [bundler]オプションで有効にすることで、import mapsを用いた新規アプリケーションの作成時や変更時に、esbuildrollup.jsWebpackTailwind CSSPostCSSDart SassBootstrapを簡単に使えるようになります。

Active Recordの実行時暗号化機能

HEYから切り出されたActive Recordの属性暗号化機能を追加しました。これにより、従来の保存時(at-rest)および通信時(in-transit)の暗号化に加えて、実行時(at work)1の暗号化機能もアプリケーションで提供できるようになります。

これによってただちに得られる実用上のメリットのひとつは、機密属性の暗号化によるセキュリティレイヤを追加できることです。たとえば、攻撃者がデータベースやデータベースのスナップショットやアプリケーションログにアクセスできたとしても、暗号化済みの情報を理解できなくなります。そうした犯罪者たちを考慮しないとしても、正当な理由でアプリケーションログを確認するときに顧客の個人情報が露出せずに済みます。

しかしより重要な点は、Active Record暗号化を用いてアプリケーション内の機密情報をコードレベルで定義できることです。これにより、機密情報へのアクセス方法をコントロールすることも、その周辺にサービスを構築することも可能になります。例として、暗号化済みデータを保護する「監査可能な」Railsコンソールや、コントローラのparamsを自動的にフィルタする組込みシステムのチェックを想像してみてください。

詳しくはedgeguidesで暗号化属性の利用法に関する完全なガイドを参照してください。

Marginalia gem方式のタグ付けによるクエリ生成元のトレース

10年ほど前にBasecampから切り出されたMarginalia gemは、SQLコメントにタグ付けすることでクエリ生成元をトレースします。この外部gemがQueryLogsクラスに昇格してActive Recordに取り込まれました

非同期クエリ読み込み

コントローラのアクションで、互いに関連のない2つのクエリを読み込む必要があるときに、Relation#load_asyncで同時に実行できるようになりました。実行に100msかかる複雑なクエリが3つある場合、これまでは300msかけて1つずつ実行していました。これらのクエリがパラレル実行可能になり、3つのクエリにかかる時間は合計100msで済みます。

Zeitwerk一元化

Railsのオートローディング機能は魔法のようなクオリティオブライフ(QOL)を実現する機能のひとつであり、あるのが当然と思われがちなほどです。かつて信頼を寄せられていたconst_missingによるアプローチは、動作が気まぐれで機能も不足していましたが、Zeitwerkコードローダーへの一元化という形で置き換えが完了しました。特に古いアプリケーションではアップグレード時に注意しておきたい問題点がいくつかありますが、アップグレードガイドを読めばすぐにでも対応を開始できるでしょう。

その他のハイライト

  • spring gemがデフォルトから外されました(コンピュータが高速になったことで大規模アプリケーション以外での必要性が下がったため)。
  • ActionController::Live#send_streamは、コントローラのアクションでオンザフライ生成されるファイルを手軽にストリーミングできます。
  • パラレルテストが、CPUコア数とテスト数を比較した結果に応じてパラレル化をスケールするようになりました。
  • Active Storageで、より高速かつセキュアなlibvipsデフォルトのvariantプロセッサとして使われるようになりました。

私たちから皆さんへのメッセージ

昨年Rails 6.1をリリースして以来、3,000件を超えるコミットがRails 7に取り込まれました。これは数百人にのぼるコントリビュータたちによる成果です。その中には今年初めてコントリビュートした200人以上の新コントリビュータたちも含まれており、長年Railsのコードベースに手を加え続けてきた約6,000人のコントリビュータリストに彼らも名を連ねました。

関連記事

HotwireはRailsを「ゼロJavaScript」でリアクティブにできるか?前編(翻訳)


  1. at-work encryptionはHEY独自の用語のようです。詳しくはhttps://www.hey.com/security/を参照してください。本記事では「実行時」をat-workの仮訳としています。 

The post Rails 7 Alpha 1、2がリリースされました(リリースノート翻訳) first appeared on TechRacho.

Railsの技: to_sqlでActive Recordが生成するクエリを調べる(翻訳)

$
0
0

概要

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

Railsの技: to_sqlでActive Recordが生成するクエリを調べる(翻訳)

joinsや複雑なwhere句を含んでいたり、複数テーブル間で特定の値をSELECTするような込み入ったActive Recordクエリを書こうとしているときに、Active RecordのDSLをすべて思い出すのは難しいでしょう。

joins(:orders)で書くべきか、それともjoins(:order)か?where(role: { name: 'Manager' })と書くべきか、それともwhere(roles: { name: 'Manager' })と書くべきか、といった具合です。

Railsコンソール上でこうしたクエリのテストを繰り返すのはよい考えですが、コードを実行するとたまに以下のような奇妙な結果が返って頭を抱えることもあります。

#<MyModel::ActiveRecord_Relation:0x23118>

この結果にアクセスしようとすると、以下のような謎エラーが発生してクエリが吹き飛んでしまうこともあります。

ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  missing FROM-clause entry for table "permission"
LINE 1: ...."id" = "permissions_users"."permission_id" WHERE "permissio...

使い方

問題点をデバッグするために、生成されたSQLを検査したくなることがあります。

実際にはActive Recordの機能を使えばとても簡単です。クエリでto_sqlメソッドを呼び出せば、SQLを実行する代わりに完全なクエリを出力してくれます(SQLが無効な場合にも出力されます)。

User.where("email LIKE '%?'", "gmail.com").to_sql
#=> "SELECT \"users\".* FROM \"users\" WHERE (email LIKE '%'gmail.com'')"

なるほど、%?構文で失敗していることがわかりました。

User.where("email LIKE ?", "%gmail.com").to_sql
#=> "SELECT \"users\".* FROM \"users\" WHERE (email LIKE '%gmail.com')"

この方法は、特にデータベースで複数のテーブルを扱っている場合に有用です。

User.joins(:permissions).where(permission: { key: :edit_posts }).to_sql
#=> "SELECT \"users\".* FROM \"users\" INNER JOIN \"permissions_users\" ON \"permissions_users\".\"user_id\" = \"users\".\"id\" INNER JOIN \"permissions\" ON \"permissions\".\"id\" = \"permissions_users\".\"permission_id\" WHERE \"permission\".\"key\" = 'edit_posts'"

今度はwhereに以下のように複数形のpermissionsを書く必要があることがわかりました。

User.joins(:permissions).where(permissions: { key: :edit_posts })

この技のおかげで、複雑なクエリのデバッグに要する時間を大幅に節約できました。また、期待どおりのSQLクエリをRailsが生成しているかどうかを確認するときにも、この技を使って複雑なクエリを検証するようになりました。

参考情報

関連記事

Railsの技: 特定スコープ内でuniquenessバリデーションをかける(翻訳)

The post Railsの技: to_sqlでActive Recordが生成するクエリを調べる(翻訳) first appeared on TechRacho.

Railsの技: email_address_with_nameで表示名付きのメールを送信する(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。

Railsの技: email_address_with_nameで表示名付きのメールを送信する(翻訳)

ほとんどのメールソフトでは、メールアドレスの直前に以下のように表示名を追加できます。

To: Matt Swanson <matt@example.com>

小さな工夫ですが、表示名があるとメールアドレスが読みやすくなります。Railsには、文字列を手動で操作せずにこのスタイルでメールアドレスを整形するヘルパーユーティリティが用意されています。

利用法

email_address_with_nameを使うと、以下のようにメールアドレスの直前に名前を標準的な形で追加できます。

ActionMailer::Base.email_address_with_name("swan3788@gmail.com", "Matt Swanson")
#=> "Matt Swanson <swan3788@gmail.com>"

このヘルパーはRailsのあらゆるメイラーで利用できます。

class UserMailer < ApplicationMailer
  default from: 'notifications@example.com'

  def welcome_email
    @user = params[:user]

    mail(
      to: email_address_with_name(@user.email, @user.display_name),
      subject: 'You have a new message'
    )
  end
end

オプション

このヘルパーは以下のようにnilもいい感じに扱ってくれます。

ActionMailer::Base.email_address_with_name("swan3788@gmail.com", nil)
#=> "swan3788@gmail.com"

さらに文字のエスケープも自動で対応します。

ActionMailer::Base.email_address_with_name("mike@example.com", "Michael J. Scott")
#=> "\"Michael J. Scott\" <mike@example.com>"

ActionMailer::Base.email_address_with_name("chip@example.com", 'John "Chip" Smith')
#=> "\"John \\\"Chip\\\" Smith\" <chip@example.com>"

参考資料

関連記事

The post Railsの技: email_address_with_nameで表示名付きのメールを送信する(翻訳) first appeared on TechRacho.


Rails 7のActive Record暗号化機能(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。

Rails 7のActive Record暗号化機能(翻訳)

Rails 7のActive Recordに、実にクールな新機能が導入されることになりました。モデル内で使える強力なencrpyts宣言によって呼び出される、アプリケーションレベルの暗号化機能です。この新機能は、アプリケーションコードとデータベースの間に暗号化の層を提供します。要するに、ActiveRecord::Encryptionを用いたデータがActive Recordオブジェクトに読み込まれると平文になり、データベースに置かれると暗号化されます。

本記事では、この新機能の使い方の概要を説明し、いくつかの優秀な機能を紹介するとともに、制限事項についても触れます。

本題に入る前に、Edgeガイドの素晴らしいドキュメントを紹介しておかないといけませんね。

参考: Active Record Encryption — Ruby on Rails Guides

本記事(英語版)では簡単のため、この新機能を単に「暗号化(encrypts)」と呼ぶことにします(他の呼び方を思いつかないので😉)。

暗号化機能登場の背景

暗号化機能は、@jorgemanrubiaによるプルリク#41659の形でRailsにマージされました。プルリクの説明では、HEY(訳注: Basecampのサービス)で用いられている暗号化機能を抽出したものだそうです。この機能が導入するまでの経緯に関心のある方は、Jorgeによる以下の興味深いブログ記事をどうぞ。

決定論的暗号化1について

ここで、決定論的暗号化(deterministic encryption)と非決定論的暗号化(non-deterministic encryption)について簡単に触れておきます。いたって単純な話ではありますが、暗号化機能の利用方法を理解するうえで重要なポイントとなります。

ここで言う暗号化とは、「何らかのテキスト入力(平文)に対してある関数を適用し、テキスト出力(暗号文)を得ること」とお考えください。

この関数が決定論的である場合、同じテキストに対してその関数を適用すると必ず同じ結果が得られます。

この関数が非決定論的である場合、ある値を暗号化したときの出力は予測できなくなります。理論上は1回目と2回目で同じ出力を得られる可能性もありますが、その確率は極めて小さくなります。非決定論的な暗号化がデフォルト設定となっている場合、同じ平文を暗号化するたびに、ほぼ確実に異なる暗号文が出力されます。

モデルの属性を暗号化する場合に決定論的暗号化を用いると、同じ平文をデータベース内で2つの行に保存すると、暗号文の値も同じになります。逆に非決定論的暗号化を用いると、同じ平文をデータベース内で2つの行に保存したときの暗号文の値は一般に異なります。後ほど説明しますが、この決定論的/非決定論的の違いは、暗号化済みデータをクエリできるかどうかに影響します。

セットアップ

暗号化を使うための設定はそれほど多くありませんが、注意しておきたい点がいくつかあります。

キー

主に必要となるのは、キーセットを生成してcredentialファイルに追加することです。bin/rails db:encryption:initを実行すれば、ファイルに追加するためのキーが以下のように生成されます。

Add this entry to the credentials of the target environment:

active_record_encryption:
  primary_key: zxMXS0hBbpa5BzRKPv9HOSF9etBySiHQ
  deterministic_key: 0pM2UHHBQr1kf1irO6JgakcSOXu0r1Vn
  key_derivation_salt: I5AkViD0UJhSqK3NY49Zvsls3ZoifyXx

primary keyは、非決定論的暗号化で用いるルート暗号化キーを導出するのに使われます。なお、credentialファイル内のprimary_keyの値には、キーをリストで複数書くこともできます。

deterministic_keyは、決定論的暗号化で用いられます。上述のとおり、このキーを用いて同じデータに対して暗号化を行うと何度やっても同じ結果が得られます。現時点の暗号化機能では、決定論的暗号化で用いるキーをリスト形式で複数書くことをサポートしていません。決定論的暗号化を完全に無効にしておきたければ、このキーを提供しないでおくのが確実です。

key_derivation_saltは、暗号化キーの導出に使われます。

アプリの設定

暗号化APIではさまざまなオプションが用意されていて、どのオプションもconfig.active_record.encryption名前空間の下で定義されています。これらのオプションを使う場合は、このAPIドキュメントを熟読することをおすすめします。一読すれば、ほとんどのオプションに合理的なデフォルト値が設定されていることがわかるでしょう。

config.active_record.encryption.extend_queriesについて少し解説を加えます。このオプションはデフォルトではfalseになっていますが、これをtrueにすると以下が許可されます。

  • 暗号化済みカラム内で平文データをクエリできるようになる(config.active_record.encryption.support_unencrypted_dataも有効にする必要あり)
  • 暗号化スキームを複数サポートできるようになる
  • uniquenessバリデーションのサポートが有効になる

データベース

暗号化された文字列やテキスト属性がデータベースに保存されるときには、通常の文字列やテキストではなく、書き込み時にシリアライズされ読み出し時にデシリアライズされる複雑なデータ構造として保存されます。このデータ構造のおかげで暗号化済みテキストに加えていくつかのメタ情報も保存でき、アプリがテキストの暗号化方式を知る手がかりをある程度得られるようになります。

メタ情報が追加されるため、ストレージで最大250バイトのオーバーヘッドが発生します。

edgeガイドでは、カラムを暗号化する場合は255バイトのstringフィールドを510バイト2に増やしておくことを推奨しています。textフィールドのオーバーヘッドについては、一般に無視できる範囲であるとされています。

呼び出し

いよいよ暗号化を使うときが来ました。

最も基本的なユースケースでは、あるカラムを暗号化するのに必要なのは、モデル内の暗号化したい属性にencrypts宣言を追加することだけです。たとえばDogモデルにあるtoy_locationというフィールドを暗号化したい場合は以下のように書きます(イヌはよくおもちゃを隠しますよね)。

class Dog < ApplicationRecord
  encrypts :toy_location

簡単でしょう?

書き込み

暗号化された属性への書き込みは、完全に透過的に行なえます。いつもRailsでやっているように書き込めばよいのです。

> dog = Dog.create!(name: 'Bruno', toy_location: 'top secret')

データベース内に保存されている内容を直接表示してみると、次のようになります。

> result = Dog.connection.execute('SELECT toy_location FROM dogs LIMIT 1').first
   (1.4ms)  SELECT toy_location FROM dogs LIMIT 1
#=> {"toy_location"=>"{\"p\":\"oVgEJvRaX6DJvA==\",\"h\":{\"iv\":\"WYypcKysgBY05Tum\",\"at\":\"OaBswq+wyriuRQO8yCVD3w==\"}}"}

この値はシリアライズされたJSONなので、以下のようにparseしてみましょう。

> JSON.parse(result['toy_location'])
#=> {"p"=>"oVgEJvRaX6DJvA==", "h"=>{"iv"=>"WYypcKysgBY05Tum", "at"=>"OaBswq+wyriuRQO8yCVD3w=="}}

するとハッシュが得られました。ハッシュ内にあるキーのほとんどはActiveRecord::Encryption::Properties::DEFAULT_PROPERTIESで定義されています。pはペイロードで、平文を暗号化した暗号文を表します。hは、暗号化操作に関連する情報を含むヘッダーのハッシュです。ivは平文を暗号化したときの初期化ベクトル(initialization vector)です。詳しくは次のセクションで説明します。atは、復号化の際に暗号文が改変されていないことを確認するためのauth_tagです。暗号化の設定や利用方法によっては、DEFAULT_PROPERTIESハッシュ以外にもヘッダーが追加されることがあります。

読み出し

暗号化された属性を持つモデルをRailsで読み込むと、暗号化された値がシームレスに復号化されます。上で作成したDogモデルを名前で検索してみましょう。

> Dog.find_by!(name: 'Bruno').toy_location
#=> <Dog id: 1, name: "Bruno", toy_location: "top secret", created_at: "2021-05-28 22:41:23.142635000 +0000", updated_at: "2021-05-28 22:41:23.142635000 +0000">

ご覧のように、暗号化された値がモデルインスタンス上で自動的に人間が読める形の属性に変換されます。なかなかよくできています。

検索

Brunoというイヌを、名前ではなくtoy_locationで検索したいときは、以下のようにフィールドが暗号化されていないときと同じように行なえます。

> dog = Dog.find_by!(toy_location: 'top secret')
  Dog Load (2.1ms)  SELECT "dogs".* FROM "dogs" WHERE "dogs"."toy_location" = ? LIMIT ?  [["toy_location", "{\"p\":\"oVgEJvRaX6DJvA==\",\"h\":{\"iv\":\"WYypcKysgBY05Tum\",\"at\":\"OaBswq+wyriuRQO8yCVD3w==\"}}"], ["LIMIT", 1]]
#=> #<Dog id: 1, name: "Bruno", toy_location: "top secret", created_at: "2021-05-28 22:41:23.142635000 +0000", updated_at: "2021-05-28 22:41:23.142635000 +0000">

クエリの文字列が、先ほどデータベースの内容を覗いたときに見えた暗号化済みJSON文字列に自動的に変換されていることにご注目ください。

初期化ベクトルと決定論について

決定論的暗号化を使う場合、同じ平文値を持つすべてのレコードで、暗号化に同一の初期化ベクトルが使われます。これはActive Recordが同一入力に対して同じ暗号文を生成するためのものであり、暗号化済みデータを検索するにはこれが前提条件となります。Railsの内部では、決定論的に暗号化されたデータの場合は平文から初期化ベクトルを生成しますが、そうでない場合は初期化ベクトルをランダムに生成します。

同じ平文を持つ2つの行が、それぞれ異なる初期化ベクトルで暗号化されると、データベースに保存されるシリアライズ済みJSONはまったく異なるものになります。

暗号化済みデータを検索可能にするには、ここで保存される値を完全に同じにする必要があります。

つまり、同じ平文を持つ2つの行では、シリアライズされたハッシュに保存される値がすべて同一になる必要があります。そしてRailsはまったく同一のハッシュをその場で再計算して、検査したい文字列にマッチする行を見つけられます。

まさに決定論の最たるものですね。

平文の検索について

最初から暗号化しておく余裕を取れない場合はどうすればよいでしょう。たとえば、既に存在するDogのテーブルにあるtoy_locationカラムが暗号化されていない場合はどうすればよいでしょうか。

上で生成したクエリを見ればわかるように、Dogのレコードのtoy_locationカラムに「top secret」という平文がある場合、この平文はクエリで検索できません。また、平文が保存されているDogのレコードをメモリに読み込もうとすると、平文の復号化を試みるときに問題が発生する可能性が高いでしょう。

ひとつの方法は、平文データを事前に暗号化データに変換しておくことです。私にはこれが理想的な方法に思えます。しかし何らかの理由でそうしたデータ移行を避けたい事情が生じるかもしれません。

そのような場合は、平文値を引き続き平文のままで保存し、新規または更新データを暗号化するオプションを利用できます。暗号化データと平文データの共存サポートを有効にするには、Rails設定のconfig.active_record.encryption.support_unencrypted_dataオプションをオンにします。

この挙動を有効にすると、平文の復号化を試みたときにエラーの発生を防止でき、カラムの平文と暗号文の間にデータのミスマッチがある場合にも検索できるようになります。

この設定を有効にして先ほどのクエリを再実行してみると以下のようになります。

Dog Load (0.3ms)  SELECT "dogs".* FROM "dogs" WHERE "dogs"."toy_location" IN (?, ?) LIMIT ?  [["toy_location", "{\"p\":\"Bd+/TzEysF2CCQ==\",\"h\":{\"iv\":\"R2IUJJ+EmnDnZvQP\",\"at\":\"zqG5WAJql1zgctRCPpoBkQ==\"}}"], ["toy_location", "top secret"], ["LIMIT", 1]]

これで、暗号化済みコンテンツを持つレコードや、そのコンテンツの平文を持つレコードを検索できるようになりました。完璧ですね。

大文字小文字を区別しない検索

デフォルトの検索では大文字小文字が区別されます。何らかの理由で大文字小文字を無視して検索したい場合は、いくつかのオプションがあります。

  • オプション1

Dog.where(toy_location: ['Top secret', 'top secret'])のように、マッチする必要のある大文字小文字のバリエーションをすべて含んだクエリを送信します。

  • オプション2

encrypts宣言でdowncase: trueを指定します。これによって、テキストを小文字(downcase)に変換してから保存するようになります。Active Recordは、クエリ実行時に検索テキストを自動的に小文字に変換します。この方法の欠点は、大文字の情報がすべて失われてしまうことです。下世話な話(downer)で恐縮です。

  • オプション3

encrypts宣言でignore_case: trueを指定し、さらにoriginal_カラム名original_toy_locationなど)をデータベースに追加します。

以下のように、大文字を含む「Top secret」というテキストを登録したとします。

Dog.create!(name: 'Max', toy_location: 'Top secret')

このときtoy_locationカラムには小文字に変換された「top secret」が保存され、original_toy_locationカラムには大文字を含んだままの「Top secret」が保存されます。

これで常にtoy_locationカラムに対して検索が行われるようになり、toy_location属性はoriginal_toy_locationからメモリに読み込まれて生成されるようになります。

ここでひとつ知っておきたい点があります。このtoy_locationカラムは決定論的に暗号化されています(だからこそ検索が効くわけです)が、original_toy_locationカラムは非決定論的に暗号化されるようです。original_toy_locationカラムの検索をサポートする必要はないので、これは理にかなっています。同じ平文値を持つ2つのレコードでtoy_locationカラムとoriginal_toy_locationカラムの値を比較してみると、このことを確認できます。以下のようにtoy_locationカラムには同じ値(初期化ベクトルやペイロードなど)が保存されていて、検索可能かつ小文字変換済みになっています。しかしoriginal_toy_locationカラムの値は互いに異なっており、検索不能かつ大文字小文字が維持されています。

{ "toy_location"          => "{\"p\":\"Bd+/TzEysF2CCQ==\",\"h\":{\"iv\":\"R2IUJJ+EmnDnZvQP\",\"at\":\"zqG5WAJql1zgctRCPpoBkQ==\"}}",
  "original_toy_location" => "{\"p\":\"5syLqDK6GCbBDw==\",\"h\":{\"iv\":\"KBGp4FrI7oL4/a3p\",\"at\":\"JnH6hxLX35cAwroImk2XqQ==\"}}" },
  "toy_location"          => "{\"p\":\"Bd+/TzEysF2CCQ==\",\"h\":{\"iv\":\"R2IUJJ+EmnDnZvQP\",\"at\":\"zqG5WAJql1zgctRCPpoBkQ==\"}}",
  "original_toy_location" => "{\"p\":\"0246w4+SSqqlJw==\",\"h\":{\"iv\":\"1uEnjlCNot9sYNgR\",\"at\":\"UhkhK6YlOTxJg75juqIMGA==\"}}" }

その他のクールな機能

Railsの暗号化には、これまで紹介した機能以外にも多くの機能が備わっています。本を書いているわけではないので詳しくは解説しませんが、そうした機能もいくつか紹介しておきます。

これまで見てきたのは単純な文字列の暗号化でしたが、実はリッチテキスト属性も暗号化可能です。

また、以前利用していた暗号化スキームの利用についてもサポートされています。つまり、当初は非決定論的に暗号化していたカラムを、後で決定論的な暗号化に切り替えられるということです。この機能を使う前には、ぜひドキュメントの隅々まで目を通しておくことをおすすめします。

(非決定的な)キーのローテーションも可能です。素晴らしい機能ですが、現時点では決定論的暗号化には対応していない点にご注意ください。

キーのローテーションに関連する話として、暗号化に用いたキーへの参照を暗号化データ自身に保存する設定も可能です。

決定論的暗号化を使っている場合は、unique制約がサポートされます。暗号化済みカラムの一意性を担保する必要がある場合はいくつか注意点がありますので、使う前に必ずガイドを読んでください。

暗号化済みカラムは、デフォルトでRailsのログから自動的に除外されます。この機能を無効にするオプションも提供されています。

ここで触れておきたいのは、この実装はモジュール化されていて、かなりのレベルでカスタマイズ可能な点です。暗号化オプションの多くは、属性単位でもグローバルなレベルでも設定可能です。

暗号化機能の制約

魅力たっぷりの暗号化機能にもいくつかの制約があります。私が気になった制約を以下にリストアップしました。暗号化の機能は多岐に渡っていて、適用可能なユースケースもさまざまなので、気になる制約も人によって変わってくるでしょう。

  • あいまい検索: 暗号化が提供する検索機能では、検索テキストとの完全一致が必須になります。すなわちLIKEクエリなどが使えないということです。これは、暗号化済みカラムに対するクエリは、生SQLではなく、すべてRailsとActive Recordを経由する必要があるということでもあります。
  • リッチテキストの検索: リッチテキストを暗号化できるのは明らかですが、現時点では非決定論的にしか暗号化できません。つまりリッチテキストは検索できません。
  • 決定論的検索は複数キーをサポートしない: 決定論的な暗号化および検索を使う場合、同時に2つ以上のキーを利用できない点に注意が必要です。キーの変更が必要な場合は、何らかの特殊な操作が必要になりそうです。
  • Railsコンソールでデータが見える: 自明のことと思われるかもしれませんが、万一悪意のある人がRailsコンソールにアクセスすれば、暗号化済みデータをオブジェクトに読み込んで一日中平文を見ることができてしまう可能性があります。Jorge氏のブログ記事によると、HEYでは暗号化に加え、コンソール拡張機能を用いてコンソールアクセスを保護および監査しているそうです。残念ながらこの拡張機能は同社のプライベートgemであり、現時点ではRailsで利用できません3
  • 決定論的暗号化はセキュリティが低下する: 実装そのものの問題ではないと思いますが、決定論的暗号化を用いれば、暗号化された属性に同じ値を持つ2つの行は、暗号を逆解析することはできなくても、一方の行の平文値を突き止められれば他方の行の平文値も判明します。非決定論的暗号化にはこのような弱点はありません。

まとめ

一言でいうと、Railsの暗号化は相当興味をそそられる機能です。他の人たちがこの暗号化機能をどう活用し、今後どう進化するかを見ていくのは素晴らしいことだと感じています。私の予感では、この暗号化機能を使い始める人は今後増え、その人たちがコードを深く調べていくうちに(決定論的暗号化で複数キーがサポートされていないなどの)さまざまな問題が解消されるのではないかと睨んでいます。

関連記事

Rails 7 Alpha 1、2がリリースされました(リリースノート翻訳)


  1. 訳注: 「決定論的暗号化」「非決定論的暗号化」は仮訳です。決定論という語についてはWikipediaを参照。 
  2. 訳注: 原文では512バイトでしたが、edgeガイドに記述されている510バイトとしました。 
  3. 訳注: その後Basecampからリリースされました。Privacy-aware Rails consoles with console1984 and audits1984 

The post Rails 7のActive Record暗号化機能(翻訳) first appeared on TechRacho.

Railsのパターンとアンチパターン3: ビュー編(翻訳)

$
0
0

概要

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

Railsのパターンとアンチパターン3: ビュー編(翻訳)

Ruby on Railsのパターンとアンチパターンシリーズの第3回目にようこそ。前回までの記事では「一般的なパターンとアンチパターン」「Railsのモデル関連のパターンとアンチパターン」をそれぞれ取り上げました。今回は、Railsのビュー関連のパターンとアンチパターンをいくつか紹介します。

Railsのビューは、問題なく高速に動作するかと思えば、さまざまな問題が発生することもあります。本記事は、自信を持ってビューを扱えるようになりたい方や、このテーマについて詳しく知りたい方におすすめです。それでは早速始めましょう。

おそらくご存知かと思いますが、Railsフレームワークでは「設定より規約(Converntion over Configuration)」が重視されます。RailsはMVC(Model-View-Controller)パターンが重要なので、当然ビューのコードにも「設定より規約」が当てはまります。マークアップ(ERBファイルやSlimファイル)、JavaScript、CSSファイルもこれに含まれます。一見すると、ビュー層はとてもわかりやすくて簡単だと思うかもしれませんが、最近のビュー層にはさまざまな技術が混在していることをどうかお忘れなく。

ビューでは、JavaScript、HTML、CSSを使います。この3つはコードの混乱やばらつきにつながる可能性があり、そうなると長期的にあまり意味のない実装になってしまいます。ちょうど今回は、Railsのビュー層でよくある問題とその解決法をいくつか見ていく予定です。

ビューの「重量挙げ」

この種のミスはそれほど頻繁には起きませんが、いったん発生すると足手まといになります。ビューの中についついドメインロジックやクエリを直接書いてしまう人をよく見かけますが、これではビュー層の内部で汗水たらして重量挙げしているようなものです。面白いのは、Railsが実際にはこのような書き方を禁止していないという点です。Railsにはこの種の「セフティーネット」が存在せず、ビュー層ではどんな無茶な書き方も可能になっています。

定義に従えば、MVCパターンのビュー層には表示のためのロジックを置くべきであり、ドメインロジックやデータのクエリをそこに混ぜるべきではありません。RailsのERB(Embedded Ruby)ファイルを使ってRubyのコードを書くと、そのコードが評価されてHTMLになります。indexページで曲のリストを表示するWebサイトを例に取ると、ビューのロジックはapp/views/songs/index.html.erbファイルに置かれます。

ビューの「重量挙げ」の意味と、ビューでやってはならないことを説明するために、以下のコード例を見てみましょう。

<!-- app/views/songs/index.html.erb -->

<div class="songs">
  <% Song.where(published: true).order(:title) do |song| %>
    <section id="song_<%= song.id %>">
      <span><%= song.title %></span>

      <span><%= song.description %></span>

      <a href="<%= song.download_url %>">Download</a>
    </section>
  <% end %>
</div>

上のコードにある巨大なアンチパターンは、songをマークアップの中でフェッチしていることです。データを取得する責務は、コントローラか、さもなければコントローラから呼び出されるサービスに委譲すべきです。コントローラで何らかのデータを準備しておいて、その後ビューで追加のデータをフェッチしている人をたまに見かけます。これは悪い設計であり、クエリの頻度を増やしてデータベースに負荷をかけるため、Webサイトが遅くなります。

代わりに、@songsインスタンス変数をコントローラのアクションで公開し、マークアップでは以下のようにそのインスタンス変数を呼び出すべきです。

class SongsController < ApplicationController
  ...

  def index
    @songs = Song.all.where(published: true).order(:title)
  end

  ...
end
<!-- app/views/songs/index.html.erb -->

<div class="songs">
  <% @songs.each do |song| %>
    <section id="song_<%= song.id %>">
      <span><%= song.title %></span>

      <span><%= song.description %></span>

      <a href="<%= song.download_url %>">Download</a>
    </section>
  <% end %>
</div>

これらのコード例は完全からほど遠いことにご注意ください。コントローラのコードを読みやすくしてSQLパスタを避けたい方は、ぜひ前回の記事をご覧ください。また、ビュー層にロジックを置いてしまうと、他の開発者たちがそのロジックをあてにしてソリューションを構築してしまう可能性が高まります。

Railsにある機能を活用しよう

ここでは簡単な説明にとどめます。フレームワークとしてのRuby on Railsには、特にビューで重宝するヘルパーがどっさり用意されています。こうした気の利いたヘルパーを活用すれば、ビューを短期間で手軽に構築できます。Railsに慣れていないと、つい以下のようにERBファイルの中にHTMLを丸ごと手書きしたくなってしまうかもしれません。

<!-- app/views/songs/new.html.erb -->

<form action="/songs" method="post">
  <div class="field">
    <label for="song_title">Title</label>
    <input type="text" name="song[title]" id="song_title">
  </div>

  <div class="field">
    <label for="song_description">Description</label>
    <textarea name="song[description]" id="song_description"></textarea>
  </div>

  <div class="field">
    <label for="song_download_url">Download URL</label>
    <textarea name="song[download_url]" id="song_download_url"></textarea>
  </div>

  <input type="submit" name="commit" value="Create Song">
</form>

このHTMLでも、以下のスクリーンショットのように「New Song」フォームをそれらしく表示できます。

New song form

しかしRailsでこんなプレーンHTMLを手書きする必要はありませんし、手書きするべきではありません。form_withビューヘルパーを使えば、Railsが代わりにフォームのHTMLを生成してくれます。form_withはRails 5.1で導入され、それまでの古いform_tagform_for(こちらに慣れている人もいるかもしれませんね)を置き換えるためのものです。それでは、form_withを使うとコード量がどれだけ減るかを見てみましょう。

<%= form_with(model: song, local: true) do |form| %>
  <div class="field">
    <%= form.label :title %>
    <%= form.text_field :title %>
  </div>

  <div class="field">
    <%= form.label :description %>
    <%= form.text_area :description %>
  </div>

  <div class="field">
    <%= form.label :download_url do %>
      Download URL
    <% end %>
    <%= form.text_area :download_url %>
  </div>

  <%= form.submit %>
<% end %>

form_withは単にHTMLを生成するだけではなく、CSRF攻撃を防ぐための認証トークンも生成します。したがって、ほぼどんな場合でも指定のヘルパーを使う方が身のためです(指定のヘルパーはRailsフレームワークでスムーズに動きます)。仮にフォームをプレーンHTMLで送信しようとすると、リクエストに認証トークンが存在しないため失敗します。

Railsには、form_withlabeltext_areasubmit以外にも多数のビューヘルパーが用意されています。これらのビューヘルパーは、あなたが楽に生きられるために存在しているので、詳しく知っておくべきです。中でも光り輝いているのは、間違いなくlink_toでしょう。

<%= link_to "Songs", songs_path %>

上のコードから以下のHTMLが生成されます。

<a href="/songs">Songs</a>

ヘルパーをひとつひとつ説明しているときりがありませんし、本記事のテーマからも外れますので、ヘルパーの詳細は割愛します。Webサイトで必要なヘルパーについてはRails Action Viewヘルパーガイド(英語)で探すことをおすすめします。

ビューコードの再利用と構成

ここに完全無欠なWebアプリケーションがあると想像してみましょう。ユースケースも完璧なのでifelse文は使われておらず、コントローラでフェッチしたデータをHTMLタグに押し込めるコードだけがあるとします。こんなアプリケーションはハッカソンや夢の中になら存在するかもしれませんが、現実のWebアプリケーションは、ビューをレンダリングするときに大量の分岐や条件判断を行います。

ページの一部を表示するロジックが複雑になりすぎてしまったら、どうするべきでしょうか?何か方法はあるのでしょうか?一般的な回答としては、モダンなJavaScriptライブラリかフレームワークで複雑なものを構築するということになるのでしょうね。しかし本記事のテーマはRailsのビューなので、ビューでどんな方法が使えるかを見てみることにしましょう。

カスタムヘルパーを後付けする

たとえば、ある曲の下にコールトゥアクション(CTA)ボタンを表示したいとします。しかしここでひとつ問題があります。ある曲にダウンロード用のURLが存在する場合と、何らかの理由でダウンロード用のURLが存在しない場合のどちらの可能性もあります。そういうときに、つい以下のようなコードを書きたくなってしまうかもしれません。

<!-- app/views/songs/show.html.erb -->

...

<div class="song-cta">
  <% if @song.download_url %>
    <%= link_to "Download", download_url %>
  <% else %>
    <%= link_to "Subscribe to artists updates",
                artist_updates_path(@song.artist) %>
  <% end %>
</div>

...

上のコード例を独立したプレゼンテーションロジックと思えば、それほど悪くなさそうですよね?しかし今はよくても、条件付きレンダリングが増えるとコードが読みづらくなってしまいます。そして条件が増えれば増えるほど、どこかでレンダリングがおかしくなる可能性も高くなります。

対抗手段のひとつは、この部分を別のヘルパーに切り出すことです。ありがたいことに、Railsではカスタムヘルパーを手軽に書ける方法も用意されています。app/helpersディレクトリの下に以下のようなSongsHelperモジュールを書けます。

module SongsHelper
  def song_cta_link
    content_tag(:div, class: 'song-cta') do
      if @song.download_url
        link_to "Download", @song.download_url
      else
        link_to "Subscribe to artists updates",
                artist_updates_path(@song.artist)
      end
    end
  end
end

これで、ある曲のshowページを開くと引き続き同じ結果を得られます。しかしこのコード例はもう少し改善できます。コード例では@songインスタンス変数が使われていますが、@songnilになる場所ではこのヘルパーが使えなくなる可能性があります。そこで、インスタンス変数という外部依存をなくすために、以下のようにヘルパーに引数を渡す方法が使えます。

module SongsHelper
  def song_cta_link(song)
    content_tag(:div, class: 'song-cta') do
      if song.download_url
        link_to "Download", song.download_url
      else
        link_to "Subscribe to artists updates",
                artist_updates_path(song.artist)
      end
    end
  end
end

続いて、ビューで以下のようにヘルパーを呼び出します。

<!-- app/views/songs/show.html.erb -->

...

<%= song_cta_link(@song) %>

...

これで先ほどと同じ結果がビューで表示されます。カスタムヘルパーを利用するメリットは、今後ヘルパーでリグレッションが発生しないようにするためのテストを書ける点です。デメリットは、カスタムヘルパーがグローバルに定義されてしまうことで、ヘルパー名がアプリ全体で重複しないよう注意する必要があります。

Railsでカスタムヘルパーを書くのがあまり好きでなければ、draper gemでいつでもView Modelパターンを導入できます。あるいはView Modelパターンを手作りで導入しても構いません(それほど複雑にはなりません)。Webアプリを作り始めたばかりであれば、今のうちにひと手間かけてカスタムヘルパーを書いておき、もしカスタムヘルパーがつらくなるようだったら別のソリューションを使うことをおすすめします。

ビューをDRYにする

私がRailsを使い始めた頃に心底惚れ込んだのは、信じられないほど簡単にマークアップをDRYにできることでした。Railsにはパーシャル(再利用可能なコード)を作成してどこでもインクルードできる機能があります。たとえば、songsを複数の場所でレンダリングしていて、同じコードが複数のファイルにある場合は、songパーシャルを作成するとよいでしょう。

たとえば以下のような曲のshowページがあるとします。

<!-- app/views/songs/show.html.erb -->

<p id="notice"><%= notice %></p>

<p>
  <strong>Title:</strong>
  <%= @song.title %>
</p>

<p>
  <strong>Description:</strong>
  <%= @song.description %>
</p>

<%= song_cta_link %>

<%= link_to 'Edit', edit_song_path(@song) %> |
<%= link_to 'Back', songs_path %>

しかし上を同じマークアップで別のページにも表示したいとします。その場合は、app/views/songs/_song.html.erbのようにファイル名冒頭をアンダースコアにした新しいファイルを作成します。

<!-- app/views/songs/_song.html.erb -->

<p>
  <strong>Title:</strong>
  <%= @song.title %>
</p>

<p>
  <strong>Description:</strong>
  <%= @song.description %>
</p>

<%= song_cta_link(@song) %>

以後、songパーシャルをインクルードしたい箇所で以下のように書くだけで利用できるようになります。

...

<%= render "song" %>

...

Railsは_songパーシャルが存在するかどうかを自動探索して、存在する場合は表示します。カスタムヘルパーの例と同様、パーシャル内では以下のように@songインスタンス変数を取り除いておくのがベストです。

# app/views/songs/_song.html.erb
<p>
  <strong>Title:</strong>
  <%= song.title %>
</p>

<p>
  <strong>Description:</strong>
  <%= song.description %>
</p>

<%= song_cta_link(song) %>

続いてsong変数をパーシャルに渡し、再利用性を高めて他の場所でも利用できるようにしておく必要があります。

...

<%= render "song", song: @song %>

...

最後に

今回の記事は以上です。今回はRailsビューで踏む可能性のあるパターンとアンチパターンをいくつか解説しました。要点を以下にまとめます。

  • UIでは複雑なロジックを避けること(ビューに重量挙げさせないこと)
  • Railsのビューヘルパーにどんな機能があるかを知っておくこと
  • コードをカスタムヘルパーやパーシャルで構造化し、再利用すること
  • インスタンス変数に頼りすぎないこと

次回は、Railsのコントローラにおけるパターンと、気をつけないとかなり痛い目に合うアンチパターンをいくつか取り上げます。どうぞご期待ください。

それでは次回またお会いしましょう!

原文追記

Rubyのマジックに関する記事が公開されたらすぐ読みたい方は、元記事末尾のフォームにて「Ruby Magic」ニュースレターをご購読いただければ、新着記事を見逃さずに読めるようになります。

関連記事

Railsのパターンとアンチパターン1: 概要編(翻訳)

Railsのパターンとアンチパターン2: モデル編とマイグレーション(翻訳)

The post Railsのパターンとアンチパターン3: ビュー編(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか(20211019後編)

$
0
0

こんにちは、hachi8833です。発表された新型MacBook Proのノッチが…

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 ruby/debugのChrome用リモートデバッグ機能


つっつきボイス:「BPS社内Slackに貼っていただいたツイートです」「これはいい機能👍

ruby/debug - GitHub

「これは?」「ruby/debugの中からHTTPサーバーを起動してそこにChromeでアクセスすると、Chromeからruby/debugを制御できるという、ruby/debugの新機能です」「お〜動画でChromeの中にデバッガコンソールが見えた」「いわゆるリモートデバッグ機能そのもの」「Chrome拡張を使うのかと思ったら不要なんですね」「Chrome Devtoolsで公開されているChromeの独自機能を使っているようです」

現時点ではmasterブランチに入っています。

🔗 Rubyアプリを最適化する(Ruby Weeklyより)


つっつきボイス:「ちょっと眺めた限りではシングルスレッドで複数のIOをチェインして同時処理させる話のようですね」

参考: class Thread (Ruby 3.0.0 リファレンスマニュアル)

Backend#chainを使うと、こんなふうにI/O操作をチェインできるらしい↓」

# 同記事より
Thread.current.backend.chain(
  [:write, @conn, "#{len.to_s(16)}\r\n"],
  [:splice, r, @conn, len],
  [:write, @conn, "\r\n"]
)

「このあたりのRESPOND_FROM_IO_PROGRAMの内側がインタプリタとかASTパーサーっぽく見える↓」

# 同記事より
# program references:
# 0 - headers
# 1 - io
# 2 - @conn
# 3 - pipe r
# 4 - pipe w
# 5 - chunk_size
RESPOND_FROM_IO_PROGRAM = [
  [:write, 2, 0],
  [:loop,
    [:splice, 1, 4, 5],
    [:break_if_ret_eq, 0],
    [:store_ret, :len],
    [:write_cte_chunk_size, 2, :len],
    [:splice, 3, 2, :len],
    [:write, 2, "\r\n"]
  ],
  [:write, 2, "0\r\n\r\n"]
]

def respond_from_io(request, io, headers, chunk_size = 2**14)
  formatted_headers = format_headers(headers, true, true)
  r, w = IO.pipe
  Thread.backend.submit(RESPOND_FROM_IO_PROGRAM, formatted_headers, io, @conn, r, w)
end

「お、同じ部分を今度はDSL化して読みやすくした感じですね↓」

# 同記事より
RESPOND_FROM_IO_PROGRAM = Polyphony.io_program(
  :headers, :io, :conn, :pipe_r, :pipe_w, :chunk_size
) do
  write :conn, :headers
  io_loop do
    splice :io, :pipe_w, :chunk_size
    break_if_ret_eq 0
    store_ret :len
    write_cte_chunk_size :conn, :len
    splice :pipe_r, :conn, :len
    write :conn, "\r\n"
  end
  write :conn, "0\r\n\r\n"
end

「最後のまとめを見ると、この記事ではトップの層でRubyのデータ構造を使い、I/O操作のような単純な処理は下の層のCで操作することで並列実行しやすくしたようですね」「Rubyでもこんなふうにすると速くできるよと」「Linuxのio_uringと似たアプローチとありますね」

本記事ではRubyアプリのパフォーマンス最適化のためにプログラムを2つの層に分離するアプローチを紹介しました。つまり、Rubyのデータ構造を用いて低レベル処理を表現するRubyのトップ層と、それらの処理を最適な形で実行するCの実装の層です。このアプローチでは、chunkedエンコーディングによるHTTPレスポンス送信、受信データ解析、I/Oのループ処理などの複雑な処理を長時間行う場合に特に有効です。
上述のようにこのアプローチはLinuxのio_uringで用いられているものと似ています。考え方は同じで、(I/O)操作をデータ構造で表現し、その実行を最適化された下の層(io_uringではカーネル、Rubyの場合はC拡張)に移行しています。
同記事より

参考: Transfer-Encoding - HTTP | MDN
参考: io_uringで高速IO処理(?) | κeenのHappy Hacκing Blog

「ところでさっきのRESPOND_FROM_IO_PROGRAMの中身はリテラルになっていますけど↓、こうするとRubyのインタプリタが仕事しなくて済むようになってJITが効きやすくなるのかもと思いました」「たしかにリテラルですね」

# 同記事より抜粋
RESPOND_FROM_IO_PROGRAM = [
  [:write, 2, 0],
  [:loop,
    [:splice, 1, 4, 5],
    [:break_if_ret_eq, 0],
    [:store_ret, :len],
    [:write_cte_chunk_size, 2, :len],
    [:splice, 3, 2, :len],
    [:write, 2, "\r\n"]
  ],
  [:write, 2, "0\r\n\r\n"]
]

🔗 HTTPI: RubyのHTTPクライアント向けの共通インターフェイス(Ruby Weeklyより)

savonrb/httpi - GitHub


つっつきボイス:「HTTPI、見たことなかった」「新しそうなライブラリですね」

# 同リポジトリより
require "httpi"

# create a request object
request = HTTPI::Request.new
request.url = "http://example.com"

# and pass it to a request method
HTTPI.get(request)

# use a specific adapter per request
HTTPI.get(request, :curb)

# or specify a global adapter to use
HTTPI.adapter = :httpclient

# and execute arbitrary requests
HTTPI.request(:custom, request)

「これは何をするものなんでしょう?」「Rubyにはこのサイトにも書かれているようなHTTPClientやNet::HTTPのようないわゆるHTTPクライアントライブラリがたくさんありますけど、それぞれのインターフェイスはまちまちなので、それらを統一して呼べるようにして、HTTPI.adapter = :httpclientみたいにアダプタを切り替えるだけでHTTPクライアントライブラリを切り替えられるということでしょうね」「なるほど、RubyのHTTPクライアントライブラリ向けのラッパーでしたか」

「ただ、HTTPクライアントライブラリを使い分ける機会はそうそうないと思いますけど」「ライブラリをこれと決めたら普通はそのまま使いますよね」「Active Jobを経由せずにSidekiqを直接使う話(ウォッチ20211018)と似ているかも」「MySQLからPostgreSQLに移行することがめったにないのもそうですね」「ライブラリごとに機能も違ってくるので、ライブラリ間の差異を共通化レイヤで吸収するのはそれなりに大変」

「HTTPIは新しいライブラリですし、今すぐproductionで使うものでもないので、もしかすると練習用として作ってみたのかなと想像してみました: こういうのを自分でやってみると楽しく勉強できると思います」「特定のHTTPライブラリのバグを切り分けるのに使えるかもしれませんね」

🔗 その他Ruby


つっつきボイス:「ruby/specリポジトリで、Ruby 3.0の新機能や機能変更のspecを書いて欲しいという募集だそうです」

#823を見ると、specを書いて欲しい3.0の機能がちゃんとリストになっているのがいいですね」「完了のチェックボックス、この間見たときより増えてるみたいです」「これならコントリビュートしやすそう」「やってみようかな」

その後もチェック済みは着々と増えているようです: Pull requests · ruby/spec

「お、IBM720なんてエンコーディングがあるんですって(#16233)」「聞いたことないですね」

参考: MFT で使用できるコード・ページ - IBM Documentation

🔗DB

🔗 SpannerにPostgreSQL互換インターフェイスが追加(Publickeyより)


つっつきボイス:「そうそう、GoogleのSpannerにこの機能が入りましたね: Spannerはかなり高価だったんですが、最近値下がりして以前より使いやすくなりつつあるので、PostgreSQL互換のインターフェイスが使えるようになればより手を出しやすくなりそう」

参考: Cloud Spanner  |  Google Cloud

「AWSにはRDSやAurora PostgreSQLがありますけど、これまでGCPにはマルチAZやフェイルオーバーまですべて備えたようなRDS的なサービスというとCloud SQLしかなかったと思うので、今回の発表でSpannerがGCPでの選択肢に入ってきそうですね」「なるほど」「AWSからSpannerを使おうとするとネットワーク的に遠いという問題はありますが」「GCPからSpannerを使う方が無理がなさそうですね」「レイテンシはその方がよいでしょうね」

参考: Amazon RDS(マネージドリレーショナルデータベース)| AWS
参考: Amazon Aurora PostgreSQL の特徴 | MySQL PostgreSQL リレーショナルデータベース | アマゾン ウェブ サービス
参考: Cloud SQL ドキュメント  |  Google Cloud

「Spannerはスケーラビリティがすごく高くてオートスケールも強いんですが、これほどの高性能が必要な案件はなかなかないでしょうね」「あ〜」「あるとすれば、ソシャゲやワクチン予約受付サイトのようにユーザー数がいくらでも増える可能性やアクセスが短時間にものすごく集中する可能性もあって、かつランニングコストに見合うサービスかな」「なるほど」「そのぐらいの規模になるとAWSのオートスケールだとアクセス急上昇に間に合わないかも」「ソシャゲだとSpannerはちょっともったいなさそうですけどね」

「その意味では、AWSのAurora PostgreSQLは負荷に応じてオートスケールできて、しかもRDSと値段がほとんど変わらないのがいいんですよ」「たしかに値段がほぼ同じなら迷わずAurora PostgreSQLを選ぶでしょうね」「SpannerはDBインスタンスを立てるのに比べて安くないので悩ましいところ」

「まだGCPやSpannerは本格的に運用したことはありませんが、Spannerは選択肢のひとつとしておくとよさそう👍


「ところで元記事ではSpannerがNoSQLとして紹介されていたけど、NoSQLだったかな?」「公式ブログ↓を見ると、当初はNoSQLキーバリューストアとして設計されたけどリレーショナルモデルも採用したとありますね」「なるほど、自分の中ではSpannerは無限といってもいいぐらいにスケールできるRDBという位置づけだったけど、合ってるみたいでよかった」

参考: NoSQL から新しい SQL へ : グローバルなミッションクリティカル DB へと進化を遂げた Cloud Spanner | Google Cloud Blog

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

🔗 AWS Lambdaバトル


つっつきボイス:「これは自分も見ましたけど、ベンチマークのソースコードを見るとJSONを取ってきてDynamoDBに格納しているだけで、実行環境での起動以外ではほとんどリソースを使っていないんですよ」「そんなに軽い処理なんですか?」「コードに分岐すらありません」

Aleksandr-Filichkin/aws-lambda-runtimes-performance - GitHub

# aws-lambda-runtimes-performance/ruby-lambda/app.rb
require 'json'
require 'aws-sdk-dynamodb'

 $client = Aws::DynamoDB::Client.new

def create(event:,context:)
  body = event["body"]
  book =JSON.parse(body)
  id = SecureRandom.uuid
  book["id"]=id

  table_item = {
    table_name: "book",
    item: book
  }
  $client.put_item(table_item)
  { statusCode: 201, body: JSON.generate(book) }

end

参考: Amazon DynamoDB(マネージド NoSQL データベース)| AWS

「このベンチマークは実行環境のプロセス起動を比較しているようなものなので、ビジネスで使うサービスを運用するうえではあまり参考にならないかなと思います」「なるほど」「Rubyのグラフはメモリーリークっぽく上昇してはいますけど、自分の印象ではRuby十分速いと思いました」

「ちゃんとベンチマークするのは難しい…」「言語が違えば実装も変わりますし、特にJSONパーサーはライブラリによって速度がかなり変わります」「それもそうですね」


なお、LambdaバトルでNodeが遅い件について、AWS_NODEJS_CONNECTION_REUSE_ENABLEDがオンになっていない可能性があるのではというissueが立っている↓とBPS社内メンバーからメンションをもらいました。

参考: Optimize NodeJS function warm starts by enabling TCP connection reuse by bilalq · Pull Request #6 · Aleksandr-Filichkin/aws-lambda-runtimes-performance

🔗JavaScript

🔗 jQuery UIとjQuery Mobileが開発終了(Publickeyより)


つっつきボイス:「今回はたまたまPublickeyの記事が多めになりました」「jQuery UIとjQuery Mobileがついに」

「ツイート↓で見かけましたけど、jQueryは今でもすごく使われているんですね」「実際jQueryで十分な場合も多いですし、ライブラリの種類も豊富だし、デザイン系も含めるとjQueryを扱える人は多いですね」「なるほど」

「jQueryがこれだけ人気を得た理由のひとつは、複数ブラウザで共通に利用できたことでしょうね: 今はその役割はほぼ終わったと思いますが」「そうですね」「蓄積されたライブラリは簡単にはなくならないと思います」

「jQueryは複数ブラウザで使えるJSライブラリのさきがけだったんでしょうか?」「Prototype.jsも同じぐらいの時期だったかな: 一時期はPrototype.jsが優勢だったこともありましたけど今は消えた」「Prototype.jsは2015年までメンテされてたんですね」

参考: Prototype JavaScript Framework - Wikipedia

「そういえばjQueryとPrototype.jsを両方置くと名前空間が衝突するのを思い出した↓」「そうだったかも」「どちらも$を使うからですね」「jQuery.noConflict();、なつかしい」

参考: prototype.jsと同時に使うには - jQuery 日本語リファレンス

🔗言語/ツール/OS/CPU

🔗 計算量とオーダー


つっつきボイス:「はてブでバズっていました」「O(m)みたいに書くビッグオー記法以外にもΘΩとかいろんな記法があるんですね」「私もビッグオーしか知りませんでした」「みっちり書かれてる」

参考: ランダウの記号 - Wikipedia

「計算量やオーダーの概念はプログラマーにとって大事ですし、アルゴリズムごとにオーダーもありますけど、現実のユースケースだとデータや検索にlocality(局所性)が絡んだりして、きれいに正規分布しないことも多いですよね」「そうそう」「アルゴリズムの特徴を知っておくのは大事だけど、計算量を覚えるだけだとあまり有効でない気がします」

参考: 参照の局所性 - Wikipedia

「具体的な計算量はググればわかるので、むしろ計算量という概念があることは知ってて欲しいですよね」「コードレビューで3重ネストがあったときに、これだと計算量がこのぐらいになるから早期脱出を入れようとか、そういうやりとりはしますね」「そうそう、丸暗記しなくてもいいので、コードに関してやりとりするときにその視点は持ってて欲しい」

「計算量やオーダーは知っていて損はしないので読んでおくといいと思います👍


後編は以上です。

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

週刊Railsウォッチ: Ruby 3.1にYJITマージのプロポーザル、Rubyのmagic historyメソッド、JSのPartytownほか(20211012後編)

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

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

Ruby Weekly

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ: ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか(20211019後編) first appeared on TechRacho.

‘rails runner’セッションをプロらしく拡張する(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。

rails runnerセッションをプロらしく拡張する(翻訳)

Railsコンソールの思いがけない便利技(翻訳)

弊社のPawełが上の記事でRailsコンソールをチューンナップする方法を紹介していましたが、実はrunnerメソッドでランナーセッションを拡張できることを彼に伝えたいと思います。

ランナーセッションにはRails::ConsoleMethodsのようなモジュールがないので、拡張が少々不便です。そのため、ランナースクリプトで利用可能なメソッドを追加するのはそれほど簡単ではありませんが、ランナーセッションの開始時と終了時に実行されるコードを追加することなら可能です。

たとえば、スクリプトの評価が終了したかどうかをターミナルで確認しなくても済むよう何らかの通知を送信できますし、時間の計測や弊社のRails Event Storeのメタデータを設定すれば、セッション中の変更内容も簡単に確かめられます。

私たちのプロジェクトで使われている例を紹介しましょう。ここでは経過時間を記録し、Rails Event Storeのメタデータを設定し、Slack通知を送信します。これは以下のコードをconfig/application.rb に追加するだけでできます。

runner do
  session_id = SecureRandom.uuid
  script = ARGV.join(" ")
  Rails.configuration.event_store.set_metadata(
    causation_id: session_id,
    correlation_id: session_id,
    script: script,
    locale: I18n.locale.to_s,
  )
  t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  notify_slack(
    username: "Script runner",
    text: "[#{Rails.env}] Runner script session #{session_id} has started: '#{script}'",
    channel: "notifications",
    icon_emoji: ":robot_face:"
  )

  at_exit do
    notify_slack(
      username: "Script runner",
      text: "[#{Rails.env}] Runner script session #{session_id} has finished: '#{script}' (elapsed: #{Process.clock_gettime(Process::CLOCK_MONOTONIC) - t} seconds)",
      channel: "notifications",
      icon_emoji: ":robot_face:"
    )
  end
end

このスニペットを使えば、rails runnerでスクリプトを実行(あるいはインラインのRubyコードを評価)するたびに、Rails Event Storeインスタンスのメタデータが設定され、runnerセッションの開始時と終了時にSlack通知が送信されるようになります。

お知らせ

ARKADEMY.DEVに参加してArkencyのトップクラス教育プログラムコースにアクセスしましょう!「Railsアーキテクトマスタークラス」「アンチ”IF”コース」「忙しいプログラマーのためのブログ執筆コース」「Async Remoteコース」「TDD動画クラス」「ドメイン駆動Rails動画コース」以外にもさまざまなコースが新設中です。

関連記事

Rails: Zeitwerkオートロードの「1ファイルにクラスを複数置けない」問題を回避する

The post ‘rails runner’ セッションをプロらしく拡張する(翻訳) first appeared on TechRacho.

Rails 7: ActiveRecord::Base.loggerがclass_attributeで7倍高速化(翻訳)

$
0
0

Rails 7: ActiveRecord::Base.loggerがclass_attributeで7倍高速化(翻訳)

私たちの見解では、このプルリクはRails 7におけるきわめてシンプルかつ大きなパフォーマンス改善です。最近のRubyで、クラス変数の読み取りにインラインキャッシュが導入されました(#177631。これにより、クラス変数の値解決で複雑な継承ツリーをたどるかわりにキャッシュから値を読み取れるようになりました。Rubyでクラス変数が読み込まれると、継承ツリーにある各クラスをチェックして、そのクラス変数がツリー内の他のクラスに設定されていないことを確認する必要があります。

もうお気づきかと思いますが、これはO(n)問題になります。ツリー内のノード数が増えるにつれて、読み取りのパフォーマンスは線形に低下します。

それでは、1個のモジュールを継承するクラス、30個のモジュールを継承するクラス、最後に100個のモジュールを継承するクラスを使ったデモを見てみましょう。

require "benchmark/ips"

MODULES = ["B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH", "II", "JJ", "KK", "LL", "MM", "NN", "OO", "PP", "QQ", "RR", "SS", "TT", "UU", "VV", "WW", "XX", "YY", "ZZ", "AAA", "BBB", "CCC", "DDD", "EEE", "FFF", "GGG", "HHH", "III", "JJJ", "KKK", "LLL", "MMM", "NNN", "OOO", "PPP", "QQQ", "RRR", "SSS", "TTT", "UUU", "VVV", "WWW", "XXX", "YYY", "ZZZ", "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLL", "MMMM", "NNNN", "OOOO", "PPPP", "QQQQ", "RRRR", "SSSS", "TTTT", "UUUU", "VVVV", "WWWW"]
class A
  @@foo = 1

  def self.foo
    @@foo
  end

  eval <<-EOM
    module #{MODULES.first}
    end

    include #{MODULES.first}
  EOM
end

class Athirty
  @@foo = 1

  def self.foo
    @@foo
  end

  MODULES.take(30).each do |module_name|
    eval <<-EOM
      module #{module_name}
      end

      include #{module_name}
    EOM
  end
end

class Ahundred
  @@foo = 1

  def self.foo
    @@foo
  end

  MODULES.each do |module_name|
    eval <<-EOM
      module #{module_name}
      end

      include #{module_name}
    EOM
  end
end

Benchmark.ips do |x|
  x.report "1 module" do
    A.foo
  end

  x.report "30 modules" do
    Athirty.foo
  end

  x.report "100 modules" do
    Ahundred.foo
  end

  x.compare!
end

キャッシュなしのRubyでは以下の結果になります。

Warming up --------------------------------------
            1 module     1.231M i/100ms
          30 modules   432.020k i/100ms
         100 modules   145.399k i/100ms
Calculating -------------------------------------
            1 module     12.210M (± 2.1%) i/s -     61.553M in   5.043400s
          30 modules      4.354M (± 2.7%) i/s -     22.033M in   5.063839s
         100 modules      1.434M (± 2.9%) i/s -      7.270M in   5.072531s

Comparison:
            1 module: 12209958.3 i/s
          30 modules:  4354217.8 i/s - 2.80x  (± 0.00) slower
         100 modules:  1434447.3 i/s - 8.51x  (± 0.00) slower

それではキャッシュありのRubyの結果を見てみましょう。

Warming up --------------------------------------
            1 module     1.641M i/100ms
          30 modules     1.655M i/100ms
         100 modules     1.620M i/100ms
Calculating -------------------------------------
            1 module     16.279M (± 3.8%) i/s -     82.038M in   5.046923s
          30 modules     15.891M (± 3.9%) i/s -     79.459M in   5.007958s
         100 modules     16.087M (± 3.6%) i/s -     81.005M in   5.041931s

Comparison:
            1 module: 16279458.0 i/s
         100 modules: 16087484.6 i/s - same-ish: difference falls within error
          30 modules: 15891406.2 i/s - same-ish: difference falls within error

Rubyのmasterブランチでは、モジュール100個をincludeするとモジュール1個のincludeの8.5倍遅くなります。しかしキャッシュを使えば、モジュール1個のincludeとモジュール100個のincludeのパフォーマンスは変わらなくなります。

それでは、Railsコアチームがこのパフォーマンス向上をどのようにRailsに取り入れたかを見てみましょう。

変更前

ActiveRecord::Base.loggerは継承ツリーに63個2のモジュールを持つcvar(クラス変数)です。以下を実行すればこのことを確かめられます。

ActiveRecord::Base.ancestors.size

# => 62

ActiveRecordコアのコードを見てみると、ロガーは以下のように定義されています。

mattr_accessor :logger, instance_writer: false

ここがクラス変数としてではなくmattr_accessorとして定義されているので、このままでは最新のRubyで導入されたパフォーマンス改善が効いてくれません。

変更後

Railsへのプルリク#42237によって、loggerの定義方法が以下のように変更されました。

class_attribute :logger, instance_writer: false

それではパフォーマンスを比較して改善を確かめてみましょう。

Calculating -------------------------------------
              logger      1.700M (± 0.9%) i/s -      8.667M in   5.097595s
             clogger     11.556M (± 0.9%) i/s -     58.806M in   5.089282s

Comparison:
             clogger: 11555754.2 i/s
              logger:  1700280.4 i/s - 6.80x  (± 0.00) slower

7倍近い高速化は大きな改善です!現実のRailsアプリケーションが強化された見事な例です。

注意

この変更にはいくつかの注意点があります。

ActiveRecord::Base.loggerclass_attributeになったため、@@logger で直接アクセスできなくなります。また、サブクラスに logger = を設定しても親クラスのロガーを変更できません。

logger はほとんどの場合単なるメソッドとして使われているので、きわめてささいな不都合に過ぎませんが、注意するに越したことはありません!

この改修が皆さんのRailsアプリのコードベースで効くかどうかを今のうちに調べておきましょう。

関連記事


  1. 訳注: Rubyのこの機能は2021年5月に#4340でmasterブランチにマージされているので、利用できるのはRuby 3.1以降となります。 
  2. 訳注: Rails 7 alpha2では70個でした。参考まで。 

The post Rails 7: ActiveRecord::Base.loggerがclass_attributeで7倍高速化(翻訳) first appeared on TechRacho.

Rails: 提案「コントローラから`@`ハックを消し去ろう」(翻訳)

$
0
0

概要

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

週刊Railsウォッチで絶賛された記事です。Rails Wayから外れるのでRails中級以上向けですが、Rails初心者も知っておいて損はありません。

太字は訳で追加いたしました。

  • 2018/06/14: 初版公開
  • 2021/10/21: 更新

Rails: 提案「コントローラから@ハックを消し去ろう」(翻訳)

少し前に私が書いた記事で、Railsコントローラをメンテしやすくするために私が使っていた、伝統的でない戦略をいくつかご紹介しました。考え方そのものは今でもまったく問題ないと思っていますが、その中でもとりわけ気に入っているものについては、Railsで標準になるべきだとも思っています。

そのために本記事は、Railsのコントローラでデータの読み込みやアクション間での共有、ビューとのやりとりの手法を変更すべきであるという提案を皆さんに納得していただくための事例を作成するために執筆しています。

「Rails Way」のおさらい

私の事例を紹介する前に、「Rails Way」のどの点が素晴らしく、どの点が不十分であるかをしっかり理解しておくことが重要と考えます。それによって、私からの提案がさらに明瞭になればと願っています。

データの読み込みやビューとのデータのやり取りを、冗長な(Rails的でない)方法で行うと次のようになります1

def show
  user = User.find params[:id]
  render :show, locals: { user: user }
end

運のよいことに、Railsフレームワークの開発者はどのアクションも最終行が似たり寄ったりであることに気付いたので、テンプレートに渡す必要のあるデータを何度も書くことにきっと嫌気がさしたのでしょう。そしてこうした定型コード(boilerplate)を減らすために賢い方法を2つ編み出しました。

  • @変数のハック
  • 暗黙のレンダリング

では、これら2つのRails的手法を適用するとどうなるかを見てみましょう。

def show
  @user = User.find params[:id]
end

暗黙のレンダリングでは、レンダリングするテンプレートをアクション名で決定します。私はこちらのRails Wayを愛していますので、これは変えたくありません。問題にしたいのは、もうひとつの@変数のハックの方です。

@変数のハック

そもそも「@変数のハック」とは何でしょうか。Railsフレームワークの開発者は、何らかのハッシュ的なものを用いてビューに変数を渡すのではなく、その変数にマーキングするだけの方がよいだろうという決定を下したのです。

マーキングはどのようにして行われるかご存知でしょうか?Ruby自身には、変数にマーキングする方法が2とおりあります。変数の先頭に$記号または@記号を付けることでマーキングできます。

Rubyで$記号を付けると、値がグローバルになるのでよくありません。では@記号はどうかというと、通常であれば、その変数は同一インスタンス上のあらゆるメソッド呼び出しで共有されるべきであるという指定になります。これはRubyや多くのオブジェクト指向言語でネイティブサポートされている「インスタンス変数」と呼ばれるものです。

Railsフレームワークの開発者は、コントローラではインスタンス変数の使いみちがあまりないことに気付きました。コントローラは、概念上(開発者目線では、ですが)以下を行います。

controller = UsersController.new request
response = controller.show

もちろん内部では他にもいろいろやっているのは承知のうえで、概念上は以下にまとめられます。

  • インスタンスを1つ作成する
  • メソッドを1つ呼び出す
  • 作成されたオブジェクトを渡す

これは本質的にオブジェクト指向的というより関数型的です。

コントローラオブジェクトはさほど長生きしませんし、呼び出すメソッドはたった1つなので、コントローラではインスタンス変数の出番があまりありません。Railsフレームワークの開発者はここに目をつけて、@というマーカーを別の目的に転用する決断を下したのです。

技術的にはインスタンス変数であることは変わりませんが、Railsはこれらの変数を監視してビューにコピーします。これによって、ビューに渡す変数のハッシュを指定する必要がなくなりました。

ある機能を(言語設計者の)意図しない目的に転用するという意味で、私はこれをハックと呼んでいます。「ハックだからよくない」ということではありませんのでお間違いなきよう。定形コードを削減するために未使用の機能を転用するのは賢いやり方です。私が問題にしたいのは、このハックそのものではなく、実際にはこのハックでも満たせないニーズがあるという点に尽きます。

何が問題なのか

@変数のハックのどのあたりに問題があるのでしょうか?@マーカーを転用したことが問題なのではなく、読み込みのパターンが複数のアクション間で共有されてしまっていることが問題なのです。次のように、いくつものアクションが同じようなデータを欲しがるというのはよくあることです。

def show
  @user = User.find params[:id]
end

def edit
  @user = User.find params[:id]
end

他のupdatedestroyなどのメンバーアクションも同様です。Railsには、甚だしい繰り返しをコールバックで解決する手法があります。上のように書く代わりに、以下のように書けます。

before_action(only: %i[show edit]) { @user = User.find params[:id] }

def show
end

def edit
end

しかしコールバックによる手法にはいくつもの問題があります。

  • only:except:を用いて特定のアクションだけを対象にしようとするとエラーが発生しやすくなります。これらのリストがちゃんとメンテされていないばかりに誤ったデータを読み込んでいたアプリを山ほど目撃してきました。
  • アクションの実際の動作が見えづらくなります。アクションを眺めただけでは、一見何もしてないように見えてしまいます。
  • 現実世界の巨大なコントローラでコールバックを把握づらくなる可能性があります。コールバックが親クラスで定義されていたりモジュールとしてincludeされていればなおさらです。
  • ソースコードの順序がシビアになります(あるbefore_actionフックで別のbefore_actionで読み込んだデータが必要になるなど)

「メソッドでやろうよ」

私の提案するソリューションはいたってシンプルです。一言で言えば「メソッドでやろうよ」です。

私の提案では私たちのニーズがすべて勘案されているので、よくできた小さな定形コードを素早く構築できます。定形コードはまさに@ハックが殺そうとしていたものなので、これでは歴史を繰り返しているように思われるかもしれません。私のアイデアがお気に召すかどうかを皆さんにご検討いただくためにも、どうかもうしばらくお付き合いください。最終的には、ささやかなメタプログラミングを用いてあらゆる定形コードをラップし、(@ハックの)メリットを失うことなく簡潔なコードに作り変えます。

メソッドを定義する

典型的なオブジェクト指向システムにおいて、あるコンポーネントが他のコンポーネントからデータを取得する最もシンプルなソリューションと言えば何だかおわかりでしょうか?最初のコンポーネントが、その情報を担当する次のコンポーネントにメッセージを送信することです。この「メソッドを介するメッセージ送信」は、オブジェクト指向プログラミングの核となるアイデアです。

さて、データをビューに送信しようとするのではなく、両者の立場を逆転させて、ビューが自分の欲しいデータをコントローラに問い合わせる形にしてはどうでしょうか?つまり、データを要求するメッセージはビューからコントローラに送信し、コントローラはレスポンスでデータを返すことになります。

ビューでは以下のような感じになります。

<%= controller.user.name %>

そしてコントローラは次のような感じになります。

def user
  User.find params[:id]
end

これは単なるpublicメソッドであり、オブジェクト指向としてはごく普通の考え方です。このメソッドは別のテンプレートからも使えます。このuserをeditテンプレートとshowテンプレートのどちらも欲しがっているのであれば、どちらも同じメソッドを呼べばよいのです。indexテンプレートが欲しがっていなければ、indexテンプレートで呼ばなければよいのです。必要もないのにデータを読み込むことはしません。コールバックの定義でonly:except:のリストを今後もメンテし続ける必要もありません。

メモ化

userの属性を大量に出力したいが、コントローラで新しいインスタンスを毎回読み込むのは嫌だ。そんなときは次のように読み込みをメモ化(memoize)しましょう。

def user
  @user ||= User.find params[:id]
end

上のコードではあえてインスタンス変数を用いていますが、これはインスタンス変数の本来の用い方です(同一インスタンス内にある異なるメソッド呼び出し同士でデータを共有する)。上のコードではインスタンス変数がnilの場合が考慮されていませんので、もう少しちゃんと書くと次のようになります。

def user
  return @user if defined? @user
  @user = User.find params[:id]
end

ヘルパー

もはや概念上は「ハック」ではなくなりましたが、その分テンプレートはわずかに冗長になりました(コントローラも冗長になりましたが、これについては後述します)。変数アクセスごとにcontroler.変数のようなプレフィックスをいちいち付けたくありません。次のようなヘルパーメソッドでコントローラへプロキシしてみてはどうでしょうか。

module UsersHelper
  def user
    controller.user
  end
end

これでテンプレートは以下のように書くだけで済みます。

<%= user.name %>

これはこれでありがたいのですが、データ読み込み系メソッドごとにヘルパーメソッドをいちいち書くのはだるくて仕方ありません。ありがたいことに、Railsにはこんなときに使える手があります。コントローラのどのメソッドでも、helper_methodを呼んでおきさえすればRailsがヘルパーメソッドを代わりにこしらえてくれます。これでコントローラのデータ読み込み部分は次のように書けます。

def user
  return @user if defined? @user
  @user = User.find params[:id]
end
helper_method :user

メソッドを「代入可能」にする

これらのメソッドを代入可能にする(訳注: =で終わるいわゆるセッターメソッドを定義する)と、読み込みの振る舞いをアクション間でもっとうまく共有できることにも気が付きました。たとえば何らかのアクセス制御を行うとしましょう。定義はおそらく次のようになります。

def users
  return @users if defined? @users
  @users = policy_scope User.all
end
helper_method :users

def user
  return @user if defined? @user
  @user = users.find params[:id]
end
helper_method :user

ここでは、データ読み込みメソッド(users)のひとつを用いて、他のメソッド(user)の実装を支援しています。before_actionコールバックによる方法とは異なり、ソースコードの順序はまったく影響しません。単にメソッドを呼んでいるだけなので、userを先に定義しても構わないのです。

ここまでは何もかもうまくいってる感がありますね。今度はindexアクションで検索もできるようにしたいとしたらどうでしょうか?indexアクションの定義は次のようになるでしょう。

def index
  @users = users.search params[:q]
end

ここでやっと例の@ハックに立ち戻りました。もし(インスタンス変数でない)usersデータ読み込みメソッドに検索結果を代入すると、showアクションでも現状のuserの実装に合わせて自前で検索を行うはめになります。データ読み込みの振る舞いの一部(policy_scopeなど)はアクション間で共有したいが、その他の振る舞い(検索)は共有したくない、というのは一般によくある問題です。

この問題も、代入によって解決できます。次のように、あるアクションでデータを絞り込めるよう、別のメソッドを定義してみてはどうでしょう。

private

def users= assigned_users
  @users = assigned_users
end

これで次のようにindexアクションを定義できます。

def index
  self.users = users.search params[:q]
end

先ほどメモ化を実装しておいたので、同じインスタンス変数が使われれば(繰り返しますが、このインスタンス変数は同一インスタンス内のメソッド呼び出し間でデータを共有するのに使われます)、indexビューでusersを呼び出すとpolicy_scopeや検索が適用されたリストを取得できます。showアクションでuserを呼び出せば、policy_scopeのみが適用され、検索は除外されます。

この代入メソッド(users=)はprivateにしてあります。理由は、このメソッドはそのアクション内(さもなければbefore_actionフック)でしか使われないからです。このメソッドをコンポーネントの外(ビューなど)で使う理由はまったく思い当たりません。

テストを書く

この提案におけるもうひとつの絶大なメリットは、テストの書きやすさです。これらのメソッドはいずれもpublicなので、テストでまったく普通に呼び出せます。たとえばindexアクションで検索が正しく行われているかどうかをテストしたい場合、従来の方法では、おそらく以下のような感じのテストを書くでしょう(RSpec構文とFactoryBotを利用)。

it 'searches for the given query' do
  create :user, last_name: 'Doe'
  create :user, last_name: 'Jackson'

  get '/users', params: { q: 'Doe' }

  expect( response.body ).to have_content 'Doe'
  expect( response.body ).not_to have_content 'Jackson'
end

上のテストコードには、出力されたテンプレートにキーワードが含まれているかどうかをチェックすることでコントローラが正しく振る舞っていると見なすという、暗黙の前提があります。しかし、そのページに何かのはずみでJacksonという単語が紛れ込んでしまえば、テストは「正しく機能していない」という理由で失敗するでしょう。しかしこの失敗は本当の失敗ではなく、false positive(偽陽性)です。

それでは、同じテストを先ほどのpublicメソッドで書き直してみましょう。

it 'searches for the given query' do
  expected = create :user, last_name: 'Doe'
  create :user, last_name: 'Jackson'

  get '/users', params: { q: 'Doe' }

  expect( controller.users ).to eq [expected]
end

こちらのテストはさらに頑丈になりました。欲しいレコードが見つかることと、それ以外のレコードが検索されないことを確認しています。false positiveをうっかり引き起こすような副作用の起きる余地はありません。

定形コードを減らす

皆さまがここまで辛抱強く読んでくださり、そして私の推す提案を気に入っていただければ幸いです。しかし、まだ「定形コードを何度も書くのがだるい」という問題が残されています。何やかやで、現在のデータ読み込みメソッドは以下のようになっています。

def user
  return @user if defined? @user
  @user = User.find params[:id]
end
helper_method :user

private

def user= assign_user
  @user = assign_user
end

えっへん!Rubyには、この手の共通パターンをシンプルに書くのにうってつけのメタプログラミングという強い味方があるのです。上のコードで提供したいものは、結局次の2つに集約されます。

  • データ変数の名前
  • そのデータ変数の読み込み方法

開発者が普段から慣れ親しんでいるRSpecとある程度対になるよう、この新しいメソッドにletと命名しました。データ読み込みにメタプログラミングを用いると、次のような感じで書けます。

let(:user) { User.find params[:id] }

簡潔でありながら、先ほどの定形コードによる方法のメリットを何ひとつ失っていません。Lettableモジュールでどんなメタプログラミングが使われているのか、じっくりご覧ください(Gist)。

module Lettable
  def let name, &blk
    iv = "@#{name}"

    define_method name do
      return instance_variable_get iv if instance_variable_defined? iv
      instance_variable_set iv, instance_eval(&blk)
    end
    helper_method name

    define_method :"#{name}=" do |value|
      instance_variable_set iv, value
    end
    private :"#{name}="
  end
end

上のコードをapp/controllers/concernディレクトリに置き、ApplicationControllerでこのコードをextendすれば完了です。コードが引き締まり、巨大なライブラリを持ち出す必要もなくなりました。letという名前がお気に召さないのであれば、好きに変えていただいて構いません。

コード例

上のコードを用いるとコントローラをどんなふうに書けるか見てみましょう(Gist)。

class WidgetsController < ApplicationController
  let(:widgets) { Widget.all }
  let(:widget) { widgets.find_or_initialize_by id: params[:id] }

  def new
    render :form
  end

  def edit
    render :form
  end

  def create
    save
  end

  def update
    save
  end

  def destroy
    widget.destroy
    redirect_to widgets_url, notice: 'ウィジェットは削除されました'
  end

  private

  def save
    if widget.update secure_params
      redirect_to widget, notice: 'ウィジェットは保存されました'
    else
      render :form
    end
  end

  def secure_params
    params.require(:widget).permit :name
  end
end

私はcreateupdateのどうでもいいような差分を消し去るのが大好きなので、テンプレート名をformとし、パーシャルレンダリング用のダミーテンプレートは一切用いませんでした。もちろん、このあたりの書き方は好みに応じて変えていただいても一向に構いません。

このletによる手法を既存のコントローラに導入したとしても、コントローラで他の部分を書き直す必要はありません。皆さんのお好きなようにコーディングしていただければ結構です。letは、単にアクション間やコントローラ-ビュー間でデータ読み込みを共有し、テストを楽にするためのものに過ぎません。

影響を受けたgem

私の提案は、decent_exposure gemのライブラリから影響を受けていることをここに認めるものであります。このライブラリのおかげで最初の着想に触れることができました(ダジャレを狙ったわけではありません2)。decent_exposureで好きになれなかったのは「暗黙の読み込み」の部分でした。これをやると隠蔽が甚だしくなり、カスタマイズが難しくなるからです。

decent_exposureを用いることも検討しましたが、暗黙の読み込みはどうしても使いたくありませんでした。巨大なライブラリを導入しなくても、ひとかけらのメタプログラミングさえあれば十分やれることに気付いたときに、それをconcernに置くのがよいという決定を下したのです。

ツイートより

関連記事

Railsコードを改善する7つの素敵なGem(翻訳)

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


  1. 訳注: 本記事のコード例はあくまで説明のためのものです。週刊Railsウォッチ20181015『Rails初心者とバレる書き方』もご覧ください。 
  2. 訳注: 「exposed me to the idea」とdecent_exposureのシャレと思われます。なおdecent exposureは「個人情報の適度な露出」という流行語です。 

The post Rails: 提案「コントローラから`@`ハックを消し去ろう」(翻訳) first appeared on TechRacho.

銀座Rails #37 開催レポート

$
0
0

morimorihogeです。Kaigi on Railsは良かったですね。RafaelのKeynoteほんとよかった。

先日の記事でもお知らせした通り、銀座Railsの運営を引き継ぎまして、去る2021/09/24に銀座Rails #37が開催されました。本記事はそちらの開催レポートになります。

銀座Railsについて

銀座Railsは3年以上継続して毎月開催されているRuby/Rails勉強会・発表コミュニティです。
技術的な内容にこだわらず、コミュニティ活動やRails開発者が興味を持つような開発一般の周辺トピックを扱っています。
過去の開催についてはConnpassのサイトをご覧ください。

銀座Rails#37 レポート

Google Play IAB(In-App Billing) 〜Railsでのサーバサイド対応のすべて〜

三星祐也(@Y_MITSUBOSHI)さんの発表。

Google Playのアプリ内課金を実装する場合のRails側での対応について、必要な部分を丁寧に解説しています。
この手のプラットフォーム課金サービスは、概ね考え方の互換はありつつもプラットフォームによって用語や詳細実装が異なるため、なんだかんだで真面目に対応しようとするとがっつりドキュメントを読み込んで参考実装を動かしてみたりなどフィジビリティチェックに手間がかかる部分ですよね。
本発表はここまで丁寧に!というレベルで解説してくれているので、そうしたフィジビリティチェックにかける時間を大幅に短縮してくれる内容でした。僕も仕事で使うことになったら再度参照させていただきます 🙇‍♂️

Kaigi on Rails 2021タイムテーブル完全解説

大倉雅史(@okuramasafumi)さんの発表。

10/22-23に開催されたKaigi on Rails 2021運営から発表内容のat a glance解説という形で発表いただきました。
2日間にわたって開催されるイベントということもあり、盛りだくさんな内容となっていました。国内ではRubyKaigiRubyWorld Conferenceに並ぶ大規模参加イベント(だと僕は認識しています)ということもあり、発表内容に注目が集まりました。

本記事投稿時点で開催は終わっていて僕ももちろん参加したのですが、いろいろと見どころ盛りだくさんでした。最後のRafaelのKeynoteはとても印象に残った発表で、僕がなぜRuby/Railsが好きなのかを自分の中で思い返せる内容でした。後日公開されるなら改めて見直したい。

スポンサーセッション: リンクアンドモチベーション株式会社

スポンサーであるリンクアンドモチベーションさまの発表。

直近開催予定だった進化するエンジニアキャリアパスの在り方【クラスメソッド×リンクアンドモチベーション】の紹介をされていました。
リンクアンドモチベーション様は経営・人事向けのイメージが強いかもしれませんが、実は銀座Railsの立ち上げからスポンサーをして下さっているなどエンジニア向けのイベント開催や支援も意欲的に行われています。

スポンサーセッション: 株式会社DeNA

スポンサーであるDeNAさまの発表。
ヘルスケア事業本部福島様より「ヘルスケアのプロジェクトにおけるRailsの利用事例とお知らせ」という題で発表頂きました。
kencomのバックエンドではRailsが使われているということで、そのアーキテクチャの紹介などされていました。
個人的には既に6年間運用していて蓄積されたデータも多そうで、ビジネスとしてもやれること多そうだなと感じました。ご興味ある方はぜひ。

DeNA TechCon 2021 Autumnのご紹介もされていました。
本記事投稿時点では既に開催終了しておりますが、DeNAさまの技術イベントは定期的に開催されていますので Twitter: @DeNAxTechをフォローしていただけるとそうした技術イベントの情報が収集できるかと思います。

Ruby/Railsと遠隔講義:受講者評点がリモートでも伸びた理由

yasulabさんの発表。

Ruby on Railsチュートリアルは日本語話者のRailsエンジニアならお世話になった方も多いのではないでしょうか。
本発表ではこのRailsチュートリアルを使った遠隔学習について、Webテキストだけでなく解説動画や反転学習を導入していった取り組みのお話をされていました。

Railsチュートリアルは演習主体の内容になるので知識面は講義の前にWebテキストや動画といった教材を使って学習しておき、講義中には主に課題で詰まったところを質問するような形にしていくのは実際効率的かなと感じました。
僕自身今も大学の非常勤でWebサイト開発の演習授業を担当していることもあり、とても興味深い内容でした。

なお、英語版のRuby on Rails Tutorialとは違う機能やコンテンツが大幅に拡充されており、単なる日本語訳版ではない独自版となっているとのことですので、これからやるぞという人は電子書籍版を購入されると良さそうですね。
スライド後半にもあるように企業向けに研修支援もされているとのことですので、自社でRailsエンジニアを育成される予定のある企業はこちらも検討されると良いかもしれません。

また、RailsチュートリアルはYouTubeチャンネルがあり、そちらで過去の発表などを閲覧することができますので興味のある方はご参照下さい。

既存のRESTful なRails プロジェクトに、GraphQLの導入を検討した話

ゲストスピーカー 飯塚 浩也(@0317_hiroya)さんの発表。

元々8年間医師をされていた経歴の中エンジニアに転身し、現在Beatfit社でCTOをされているという立場の中で、小規模なエンジニア組織の中で事業を進めていくための技術選定の成功・失敗についてお話いただきました。

ベンチャーという資金・時間的リソースの制約に大きく振り回される中、エンジニア採用という視点で見て新しい技術を使ってエンジニアから魅力的に見せたいという部分と実際に開発しているチームの持っている技術スタック、経験値のバランスなど、小さい企業でエンジニアをされている人なら一度は遭遇したことがあるのではないかと思います。
エンジニアとしては新しくて良いとされている技術を使いたいが、その技術のノウハウや実績が手元のメンバーでは心もとない状況というのは僕も経験があり、そういった場合に無理せず枯れた選択肢を選ぶ、というバランス感覚はサービスをリリースしていくというビジネス視点から大事な点かと思います。

なお、先に紹介したRailsチュートリアルのYouTubeチャンネルにて、飯塚さんのインタビューも投稿されています。

まとめと次回予告

というわけで、遅くなりましたが銀座Rails#37の開催報告でした。

次回銀座Rails#38は本記事を投稿している今週金曜の2021/10/29 19:00より開催します。まだ未登録な方はぜひご登録下さい。

ゲストスピーカーには笹田耕一さんをお呼びし、先日のRubyKaigi takeout 2021でもお話されたdebug gemについてお話いただきます。
debug gemは先日open chromeによるChrome Devtoolsを使ったリモートデバッグがサポートされたりなど、現在進行形で意欲的な開発が進められているので見どころです!

その他銀座Railsに関する公式情報はTwitter: @GinzaRailsにて発信しておりますので、お気軽にフォローいただければと思います。

The post 銀座Rails #37 開催レポート first appeared on TechRacho.


週刊Railsウォッチ: insert_allやupsert_allのタイムスタンプ自動更新、app/contextsにロジックを置くほか(20211025前編)

$
0
0

こんにちは、hachi8833です。供給そんなにヤバいのかしら。


つっつきボイス:「電子部品の他に鉄も値上がりしてると聞いてますね」「あ〜」「給湯器の値上がりが著しいとか」「新型MacBook、部品のあるうちに買っとくのがいいのかな…」「Appleはそれなりに部品の流通を確保していると思いますけど、どれかが滞ったら詰まったりして」「欲しいときに買うのが一番」

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

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

今回は以下の公式情報から見繕いました。

🔗 insert_allupsert_allでタイムスタンプを自動更新するオプションが追加

このプルリクは、insert_allまたはupsert_all(および関連するメソッド)でレコードが作成された場合にタイムスタンプのカラム(created_atcreated_onupdated_atupdated_on)を自動設定するオプションを提供する。現時点では、これらのカラムを確実に設定するクリーンな方法は、カラム自体にデフォルトを設定するか、さもなければ属性として明示的に渡したうえでさらに既存レコードのcreated_atを上書きしないようon_duplicateのSQLをカスタマイズするしかなかった。
同PRより


つっつきボイス:「insert_allupsert_allのタイムスタンプって自動更新なのでは?と思いましたけど、このプルリクが出たということは今までは自動更新じゃなかったんですね」「record_timestamps:オプションをtrueにすればinsert_allupsert_allでタイムスタンプが自動更新されるようになったようです↓」

# activerecord/lib/active_record/insert_all.rb#L10
-   def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
+   def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil, record_timestamps: nil)
      raise ArgumentError, "Empty list of attributes passed" if inserts.blank?

      @model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s)
      @on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
+     @record_timestamps = record_timestamps.nil? ? model.record_timestamps : record_timestamps

record_timestampsはデフォルトがnilか」「falseを明示的に設定すると今まで通りになるんですね」

# activerecord/test/cases/insert_all_test.rb#L410
+ def test_upsert_all_does_not_implicitly_set_timestamps_on_create_when_model_record_timestamps_is_true_but_overridden
+   with_record_timestamps(Ship, true) do
+     Ship.upsert_all [{ id: 101, name: "RSS Boaty McBoatface" }], record_timestamps: false
+
+     ship = Ship.find(101)
+     assert_nil ship.created_at
+     assert_nil ship.created_on
+     assert_nil ship.updated_at
+     assert_nil ship.updated_on
+   end
+ end

🔗 コントローラの_htmlサフィックスの挙動を修正


つっつきボイス:「これはi18n関連ですね」「以下のようにキーに_htmlというサフィックスを追加するとhtml_safe?がtrueになってそのままビューに出力される機能は前からありますね」「ウォッチでも何度か話題になりました(ウォッチ20180723)」「う、知らなかった」「以下のhelloはエスケープされるけど、hello_htmlhtml_safe?がtrue、つまりサニタイズ済みとして扱われる↓というものです」「なるほど〜」

# actionpack/test/abstract/translation_test.rb#L20
              translation: {
                index: {
                  foo: "bar",
+                 hello: "<a>Hello World</a>",
+                 hello_html: "<a>Hello World</a>",
+                 interpolated_html: "<a>Hello %{word}</a>",
+                 nested: { html: "<a>nested</a>" }
                },
                no_action: "no_action_tr",
              },

参考: 4.4 安全なHTML変換 — Rails 国際化 (i18n) API - Railsガイド

「そして今回のプルリクを見ると、今まではコントローラとビューで_htmlサフィックスの挙動が違っていたらしい」「え、コントローラでも使えるんですか?」

これは#27872をやり直したもの。

html_safeへの変換の一般的な動作を、Active Supportのprivateなモジュールに抽出するコミットを追加して、Action ViewとAction Packで異なっている挙動を合わせ忘れることのないようにした。

これにより、#39989で実現されたメモリ節約の一部が元に戻される(Action Viewの実装ではチェックが必要な可能性のあるキーごとにhtml_safeオプションをビルドするので)。Action Viewのループ内だけでオブジェクトのアロケーションをメモ化する方法が見つからなかったので、これについては妥協することにした。メモリを節約する方法についてアイデアがあれば求む。
同PRより

AbstractControllerが改修されているので↓、コントローラの実装はビューと別だったみたい: ActiveSupport::HtmlSafeTranslation.translateに切り出して共通化したことで修正したようですね」「なるほど」「この機能をコントローラで使ったことはなかったな〜」

# actionpack/lib/abstract_controller/translation.rb
# frozen_string_literal: true

+require "active_support/html_safe_translation"
+
module AbstractController
  module Translation
    mattr_accessor :raise_on_missing_translations, default: false

...

      i18n_raise = options.fetch(:raise, self.raise_on_missing_translations)
-     I18n.translate(key, **options, raise: i18n_raise)
+
+     ActiveSupport::HtmlSafeTranslation.translate(key, **options, raise: i18n_raise)
    end
    alias :t :translate

    # Delegates to <tt>I18n.localize</tt>. Also aliased as <tt>l</tt>.
    def localize(object, **options)
      I18n.localize(object, **options)
    end
    alias :l :localize
  end
end

🔗 ArelにFILTER句のサポートを追加


つっつきボイス:「PostgreSQLとSQLite3の場合にfilterメソッドが使えるようになった」「MySQLはサポートされてないのか残念」

機能リクエストの多かったrails/arel#518を再度オープンした(rails/arel#460にもある)。
以下のRubyコードを書くことで、

Model.all.pluck(
  Arel.star.count.as('records_total').to_sql,
  Arel.star.count.filter(Model.arel_table[:some_column].not_eq(nil)).as('records_filtered').to_sql,
)

以下のSQLが出力されるようになる。

SELECT
  COUNT(*) AS records_total
  COUNT(*) FILTER (WHERE some_column IS NOT NULL) AS records_filtered
FROM models

サポート対象はPostgreSQL 9.4以降(2014年12月、リリースノート)とSQLite 3.30以降(2019年10月、リリースノート
参考:

「FILTER構文って何でしょう?」「上のModern SQLサイトによると、以下のように集計関数に条件を指定できるとありますね: 集計関数をSELECT文で条件付けするよりも簡潔に書けそう」「お〜!」

# modern-sql.comより
SUM(<expression>) FILTER(WHERE <condition>)

「以下みたいにCASE WHENでもやれますけど↓、条件が増えてくるとどんどん行数が増えてしまう」「それは読みづらそう…」「filterメソッドはそういうときに便利でしょうね👍

# modern-sql.comより
SUM(CASE WHEN <condition> THEN <expression> END)

参考: Window関数のFILTER句を極める

🔗 ビューのplain textモードの箇条書きを改善


つっつきボイス:「to_plain_textの改善だそうです」「箇条書きがネストしたときの書式を改善したのね」「そもそもto_plain_textというメソッドがあったことを知らなかった」「plain textモードを使う人って少なそうですけど、いるんでしょうね」

# 同PRより
• Item 0
• Item 1
  • Item A
    1. Item i
    2. Item ii
  • Item B
    • Item i
• Item 2

「今実装を見てますけど、" " * (depth - 1)とか"\n#{text}"のあたりが何というか生々しいですね」「自分でゴリゴリ実装したときのような感じが出てる」

# actiontext/lib/action_text/plain_text_conversion.rb#L93
+     def indentation_for_li_node(node)
+       depth = list_node_depth_for_node(node)
+       if depth > 1
+         "  " * (depth - 1)
+       end
+     end
+
+     def list_node_depth_for_node(node)
+       node.ancestors.map(&:name).grep(/^[uo]l$/).count
+     end
+
+     def break_if_nested_list(node, text)
+       if list_node_depth_for_node(node) > 0
+         "\n#{text}"
+       else
+         text
+       end
+     end

🔗 CSRF防止戦略のカスタマイズをサポート

概要
このプルリクは、protect_from_forgeryでのカスタムCSRF防止戦略を渡すサポートをAPIドキュメントで公式に追加する。
その他
現在のRailsは、CSRF保護戦略のカスタマイズを偶然サポートしている。protection_method_classにはクラスまたはシンボルオブジェクトを渡せるし、.to_s.classify呼び出しも両方で同じように振る舞う。
そこで@rafaelfrancaに相談した結果、このメソッドの振る舞いを変更してcase/when文で既存のCSRF防止戦略を明示的に返すようにし、かつ早期リターンによってクラスを戦略として渡せるようにした。
同PRより


つっつきボイス:「protection_method_classでCSRF防止の挙動を変えられるようにしたんですね: csrf-tokenの生成ポリシーを修正したいことはあるかもしれないので」「どんなときに変更したいんでしょうか?」「CSRFトークンの生成をRailsサーバー側以外で行いたい時とかかなあ」「なるほど」「たぶん自分でカスタマイズすると相当複雑になると思いますが」

参考: 3 クロスサイトリクエストフォージェリ (CSRF) — Rails セキュリティガイド - Railsガイド

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

🔗 has_secure_password利用時にpassword = nilしても値が残る問題を修正

# 更新情報より
user.password = 'something'
user.password = nil
# before:
user.password # => 'something'
# now:
user.password # => nil

つっつきボイス:「Active Modelのpassword=セッターでnilを代入してもpasswordリーダーで読み出すと消えていなかったのが修正されたのね」「これは修正しないといけないヤツ」「修正はinstance_variable_setを1行追加しただけなんですね↓」

# activemodel/lib/active_model/secure_password.rb#L95

        define_method("#{attribute}=") do |unencrypted_password|
          if unencrypted_password.nil?
+           instance_variable_set("@#{attribute}", nil)
            self.public_send("#{attribute}_digest=", nil)
          elsif !unencrypted_password.empty?
            instance_variable_set("@#{attribute}", unencrypted_password)
            cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
            self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
          end
        end

🔗Rails

🔗 Zeitwerkにアップグレードした話(Ruby Weeklyより)


つっつきボイス:「RailsアプリのオートローダーをZeitwerkにアップグレードしたときのノウハウ記事です」「ファイルやクラスのリネームも発生したのか」「今どきはたいていZeitwerkになっていると思いますけど、アプリが大きいと後からZeitwerkに乗り換えるのは大変でしょうね」

Rails: Zeitwerkオートロードの「1ファイルにクラスを複数置けない」問題を回避する

🔗 RailsにSorbetを導入

以下はつっつき後に見つけたツイートです。


つっつきボイス:「freee会計などを手掛けているfreeeがRubyの静的型チェッカーSorbetを導入した記事です」「記事でやっているようにビジネスロジックを中心に少しづつ型を追加していくのがよさそうですね」「SorbeとYARDを両方書くのは大変、たしかに」

「Sorbetを使っている会社のリストを見ると、開発元のStripe以外にShopifyなども使ってますね」「以前から型注釈を欲しいと思っていた会社が使っているんでしょうね」「SorbetはLanguage Server Protocolに対応しているのがありがたい」

参考: Official page for Language Server Protocol

先週土曜日のKaigi on Rails 2021のクロージングキーノートでも、RafaelさんがShopifyでSorbetを導入したことを話していましたね。

参考: Keynote by Rafael França - Kaigi on Rails 2021

🔗 Arel入門


つっつきボイス:「ArelはActive Record内部のクエリ生成APIですね」「Arel職人を目指す記事なのかな」「arel_tableとかを見ると昔のトラウマが😅

# 同記事より
Organization.where(
    Organization.arel_table[:id].in(
    Comment.where(
      Comment.arel_table[:user_id].eq(user.id)
    ).distinct.pluck(:organization_id)
  )
)

参考: ActiveRecordを支える技術 - Arelとは何者なのか? (全5回) その1 - TIM Labs

「Arelを使うとこんなふうに書ける↓」「gt(2)はgreater than 2なんですね」「自分は普通にwhereとプレースホルダ?で書きますけど、事情によってはArelで書くこともたまにあります」

# 同記事より
users[:id].in([1,2,3]).to_sql
=> "`users`.`id` IN (1, 2, 3)"

users[:id].gt(2).to_sql
=> "`users`.`id` > 2"

users[:id].eq(3).to_sql
=> "`users`.`id` = 3"

「Arelでjoinが絡んだりコンポジションしたりするうちにだんだん複雑になりがち」「そうそう」「Arelはありがたい存在だけど、毎回Arelで書く気にはなれないな〜」

# 同記事より
users.join(photos, Arel::Nodes::OuterJoin).on(users[:id].eq(photos[:user_id]))

「BPSだとkazzさんがよくArelを使ってたかも」「Railsで汎用的なモジュールを書こうとするとArelが必要になってくることがあるんですよ」「なるほど」

追記: 今週金曜日の銀座Rails#38で、@osyoさんが『AST を使って ActiveRecord の where の条件式をブロックで記述しよう』というタイトルでお話しされるそうです。

🔗 Railsのビジネスロジックを「contexts」で整理(Ruby Weeklyより)


つっつきボイス:「また新しめのパターン」「contextという言葉のメタ度が高くてどうとでも解釈できそうな感じ」

「contextは、Elixir言語で動くPhoenixフレームワーク↓が由来と記事に書かれていました」「Elixir、知らない世界」「Elixirは見た目がRubyに似てて、型が書けるそうです」

参考: Elixir (プログラミング言語) - Wikipedia

app/contexts/の下にファイルを作って、そこにビジネスロジックを置くスタイルなんですね」「見た感じでは、GoF本で言うところのFacade(ファサード)を普通にcontextsに置いている感じかな🤔

# app/contexts/accounts.rb
module Accounts
  def self.active_users
    User.all.active
  end

  def self.account_details(id)
    account = Account.find(id)
    # ...
  end
end

# app/contexts/accounting.rb
module Accounting
  def self.create_invoice
    # business logic magic
  end
end

参考: ギャング・オブ・フォー (情報工学) - Wikipedia — GoF

「ElixirのフォーラムにcontextsとFacadeのことが書かれている↓: まさにFacadeパターンですね」

参考: Contexts in Phoenix 1.3 and Facade Pattern - Phoenix Forum / Chat / Discussions - Elixir Programming Language Forum

「記事では、Service Objectを使いたくないのでcontextにしたそうです」「Service Objectはクラスがやたらと増える傾向があるので、Facadeパターンを使うのはわかる: 自分もその方が好みです」「たしかに」

参考: Facade パターン - Wikipedia

「そういえばService Objectに置くのはたいていFacadeか、もうひとつ何かのパターンのどっちかだと以前おっしゃってましたね」「もうひとつはCommandパターンですね: RailsでService ObjectというとこのCommandパターンを使ったものを指すことが多いようです」「なるほど」「Commandパターンだと基本的に1クラス=1機能になるけど、Facadeパターンはそこにこだわらない感じ」

参考: Command パターン - Wikipedia

Railsのパターンとアンチパターン4: コントローラ編(翻訳)

🔗 GitLabコメント欄にmermaid構文でグラフを書く


つっつきボイス:「GitLabのコメント欄でmermaidというグラフ生成構文を使ってグラフを生成できるそうです」「元記事を見るとGitLab 10.3と随分昔からあるようなので、最近これを発見したのかも」「PlantUMLも使えるのね」

参考: mermaid - Markdownish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.
参考: PlantUML: シンプルなテキストファイルで UML が書ける、オープンソースのツール

# docs.gitlab.comより: mermaidの例
graph TB

  SubGraph1 --> SubGraph1Flow
  subgraph "SubGraph 1 Flow"
  SubGraph1Flow(SubNode 1)
  SubGraph1Flow -- Choice1 --> DoChoice1
  SubGraph1Flow -- Choice2 --> DoChoice2
  end

  subgraph "Main Graph"
  Node1[Node 1] --> Node2[Node 2]
  Node2 --> SubGraph1[Jump to SubGraph1]
  SubGraph1 --> FinalThing[Final Thing]
end


docs.gitlab.comより

「コメント欄でちょっぴりグラフを書くのにいいのかも」「自分はDraw.ioを開いてスクショを貼る方が早いかな」「それもそうですね」

後で調べると、draw.ioドメインはセキュリティ上の理由でdiagrams.netドメインに移行していました。

参考: diagrams.net
参考: Blog - Open source diagramming is moving to diagrams.net, slowly


前編は以上です。

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

週刊Railsウォッチ: ruby/debugをChromeでリモートデバッグ、Rubyアプリの最適化ほか(20211019後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: insert_allやupsert_allのタイムスタンプ自動更新、app/contextsにロジックを置くほか(20211025前編) first appeared on TechRacho.

週刊Railsウォッチ: YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか(20211026後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Ruby

🔗 YJITリーダーによるYJIT紹介記事


つっつきボイス:「以下↓はYJITリーダーであるMaxime ChevalierさんによるYJIT紹介記事ですが、翻訳リクエストを受けてひとまず一次翻訳終えました: リクエストありがとうございます🙏」「お〜翻訳楽しみ」「Ruby 3.1でYJITがマージする流れになっていますね: YJITはデフォルトでオフのはずなので、使わないときは意識せずに済むはず」「お〜YJIT楽しみ」

「続編記事としてNoah GibbsさんによるYJITお試し方法の紹介記事も出ていました↓」

以下はYJITのベンチマークサイトです。

🔗 YJITがマージされた

「ちなみについさっき(注: つっつき時点の10/21夜)YJITがマージされたというツイート↓を見かけたんですが、YJITがでかくてコミット数が多かったせいかdev環境のSlackボットや通知周りがエラーになって、いったんcloseされていました」「ありゃ残念」「ドンマイ」「分解するのは大変そう」

つっつきの後、無事YJITがmasterにマージされました🎉

参考: ruby-trunk-changes 2021-10-21 - ruby trunk changes

その後、OpenBSDでYJITを無効にするコミットや、JITでMJITを有効にするオプションを追加するコミットも追加されていました(YJITとMJITは同時には利用できないそうです)。

🔗 safe_regexp: 正規表現をタイムアウト

grosser/safe_regexp - GitHub

# 同リポジトリより
# normal
/a/.match?('a') # -> true in 0.0001ms
SafeRegexp.execute(/a/, :match?, 'a') # -> true in 0.13568ms

# bomb
require "safe_regexp"
regex = /aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?aa?/
value = "a" * 46
regex.match? value # false in ~60s
SafeRegexp.execute(regex, :match?, value) # -> SafeRegexp::RegexpTimeout

# methods without arguments
regex = "^(The '(?<first_group>.*)' parameter of the (?<second_group>.*))$"
SafeRegexp.execute(regex, :names) # -> ["first_group", "second_group"]

つっつきボイス:「以下のregular-expressions.infoというサイトで知りました↓: サイトの人がこのgemを試したかどうかまではわかりませんでしたが」「今日のBPS Webチーム内発表で触れていたgemですね」「SafeRegexpを使うことで、正規表現がデフォルトで1秒以上かかるとタイムアウトしてエラーをraiseするそうです」

参考: Preventing Regular Expression Denial of Service (ReDoS)

「今Ruby本家でも正規表現にタイムアウトを入れようかという話が出ていましたね」「はい、RubyKaigi Takeout 2021のMartin先生の発表↓で触れていた#17837などですね」「これは?」「最近流行りのcatastrophic backtracking攻撃に対してRubyが対抗策を議論しているissueのひとつです」

参考: Regular Expressions: Amazing and Dangerous by Martin J. Dürst - RubyKaigi Takeout 2021

「READMEではThreadTimeoutを使わずに作ったとありました」「同じくREADMEによると、正規表現用に別プロセスを立ち上げて、タイムアウトしたらkill -9で止めるということをやってるらしい: 正規表現エンジンの中にタイムアウトを仕込むのではなく、エンジンの外側でやるという戦略かな」「シェルで強制終了するような感じなんですね」

「そういえば手元でsafe_regexpをちょっと試してみたところ、ヤバいパターンを食べさせたらコンソールが固まって、Ctrl-Cでも戻らなくなっちゃいました😢」「kill -9で止めるしかないでしょうね: 別プロセスにしないとRubyのメインプロセスがCPUタイムを食い尽くしてしまうので、OSのことも考えて正規表現用のプロセスを分けたんでしょうね」「なるほど」

「重たい別プロセスをRubyで立ち上げるのは泥臭いですが、ひとつの方法でしょうね」「テスト環境で使うとか、特定の正規表現を手元で検証するときにはよさそう👍

はじめての正規表現とベストプラクティス10: 危険な「Catastrophic Backtracking」前編

🔗 ripperのドキュメント化が進行中


つっつきボイス:「RubyのパーサーライブラリであるRipperは以前からドキュメントがないと言われていましたが、この方が頑張ってコードを読んでドキュメントを書き進めているそうです」「お〜すごい!」「READMEの内部リンクがまだ切れてるのでファイルを直接開いてください」

参考: class Ripper (Ruby 3.0.0 リファレンスマニュアル)

「RuboCopの作者の@bbatsovさんも、ドキュメントがないなどもろもろの理由でRipperを使うのをやめたそうです↓」「ドキュメントがなくても使う人はいるでしょうけど、やっぱりドキュメント大事」「動きがだいたいわかってても、ドキュメントがないと不安になりますよね」「RuboCopも使えるものならRipper使いたかったでしょうね」「ドキュメント化大変だろうけど頑張って欲しいです🙏

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

🔗 Rubyのワンライナーcookbook


つっつきボイス:「Rubyのワンライナー向けオプションはたくさんある分、Perlの-pieオプションに比べると長めになってしまう傾向がありますけど、Rubyの-neオプションあたりなら使いますね」

# 同記事より
$ # sample stdin data
$ printf 'gate\napple\nwhat\nkite\n'
gate
apple
what
kite

$ # print all lines containing 'at'
$ # same as: grep 'at' and sed -n '/at/p' and awk '/at/'
$ printf 'gate\napple\nwhat\nkite\n' | ruby -ne 'print if /at/'
gate
what

$ # print all lines NOT containing 'e'
$ # same as: grep -v 'e' and sed -n '/e/!p' and awk '!/e/'
$ printf 'gate\napple\nwhat\nkite\n' | ruby -ne 'print if !/e/'
what

参考: Perlのワンライナーでテキストの一括置換 - console.lealog();

「Rubyは標準機能が強力だし普段からRuby書いてるので、ワンライナーにもいいですよね」「いいっす」「ただ自分がRubyであまりワンライナー書かないのは、カスタマイズできない作業環境でRubyが使えるとは限らないからというのもあるんですよ」「その意味ではPerl強いですよね」「Perlはたいていの環境で最初から使えますね」

「Pythonも入っているとは限らない」「あってもPython 2系か3系かという罠があったりしますし」「さすがに新しい環境でPython 2は減ったと思いますけど、古い環境だと油断できない」

参考: Python 2.7.x と 3.x の決定的な違いを例とともに | POSTD

「macOSだとRubyが入っているけど、バージョンが古いんですよね」「1.8とかだったらどうしよう」「1.8はもう別物😆

後でBig Sur備え付けのRubyバージョンを調べてみました。Ruby 2.6は2022年3月にEOL(end-of-line)を迎えますが、果たしてmacOSはちゃんとバージョン上げてくれるでしょうか?
参考: Ruby Maintenance Branches

$ /usr/bin/ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.x86_64-darwin20]

🔗CSS/HTML/フロントエンド/テスト/デザイン

🔗 セキュリティヘッダークイックリファレンス(StatusCode Weeklyより)


つっつきボイス:「HTTPのヘッダーのうち、セキュリティ関連のヘッダーのクイックリファレンスだそうです」「▼をクリックすると詳細が表示されるのね」

X-Content-Type-Optionsはたしかに使う」「X-Frame-Optionsもそういえばあった」

# 同サイトより
X-Content-Type-Options: nosniff
# 同サイトより
X-Frame-Options: DENY

「クロスオリジン関連のヘッダーにCross-Origin-Opener-Policy(COOP)やCross-Origin-Embedder-Policy(COEP)というのもあるんだ、へ〜細かい」「たしかに細かい」

# 同サイトより
Cross-Origin-Opener-Policy: same-origin-allow-popups
# 同サイトより
Cross-Origin-Embedder-Policy: require-corp

CORSはよく聞くヤツですね」「知ってるものが出てきた」「ちなみにCORSのヘッダーは、ない状態が最もセキュア」

# 同サイトより
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

参考: Rails アプリケーションのセキュリティ対策(CORS/CSP/HSTS)

「こうしたセキュリティ関連ヘッダーは適切に設定すべきですね: 特にSPAは1つのページ内であらゆるデータを読み書きするようになるので、一箇所でも不備があると他の部分にも影響しがち」「たしかに」「セキュリティヘッダーのリファレンスとして便利そうなサイト👍」「ヘッダーが多すぎないのが嬉しいです😂

参考: シングルページアプリケーション - Wikipedia — SPA

🔗 Priority Hintsによるリソース読み込み最適化


つっつきボイス:「Priority HintsはWebページ内の要素の読み込みで優先度を指定できる新しめの仕様ですね」「以下の記事は2018年の時点でPriority Hintsをチェックしていました↓」

「お〜、Blink(Chromeなどで使われているレンダリングエンジン)ではこれらに優先順位を設定できるのね↓」「『Priority Hintsはヒントであってディレクティブ(指示)ではない』、なるほど」

参考: Blink (レンダリングエンジン) - Wikipedia


同記事より

「具体的には以下のようにタグでimportance="low"のように指定する↓と、ブラウザ側で優先順位を割り当てる」「"low""high""auto"の3つか」「画像のimgにも指定できるんですね」「その場合CSSで画像サイズをきちんと指定しておく方がいいかも」

<!-- 同記事より -->
<!-- include trial token in the <head> -->
<meta http-equiv="origin-trial" content="{Replace with origin trial token}">

<!-- Lower the priority of script file -->
<script src="script.js" importance="low"></script>

<!-- Alter the priority of images -->
<img src="Background.jpg" width="400" importance="low">
<img src="Sunset.jpg" width="400" importance="high">

<!-- Note that importance="auto" is the default based on the spec if not specified -->
<img src="Flower.jpg">

「優先順位の仕様↓は定められているけど、細部の解釈はブラウザ依存になる可能性があるかもしれませんね」「あ〜たしかに」


同記事より(一部)

importanceを全部Highにしたりするのはダメなのかな?」「無意味だと思いますよ😆 : 上の仕様の上下矢印などを見た感じでは、仮に全部Highにしたとしても優先順位は同じにならないでしょうね」「あ、たとえばHighに下向き矢印がある項目は、それより低くなる可能性があるということなんですね」

「Priority Hints指定なし(左)とあり(右)では以下のように変わるんですね↓」「わかりやすい〜」

「優先順位をここまで追い込んで使うことはすぐにはないかもしれないけど、importance="high"asyncを指定する↓のは効果高そうな感じなので、これなら書いてもいいかなと思いました」

<!-- 同記事より -->
<script src="async_but_important.js" async importance="high"></script>

🔗言語/ツール/OS/CPU

🔗 Crontab.guru


つっつきボイス:「なるほど、crontabのスケジューリングの設定を支援するサイトですか」「これ面白いですね」「たしかGitHubのドキュメントでこのCrontab.guruが参考としてリンクされてたのを見たことがありますよ」

参考: crontab - Wikipedia

後で見つけました。

crontab guru を使うと、クーロン構文の生成および実行時間の確認に役立ちます。 また、クーロン構文の生成を支援するため、crontab guru のサンプルリストもあります。
ワークフローをトリガーするイベント - GitHub Docsよりより

🔗 crontabの罠

「ところでcrontab形式って、実は標準というものがないんですよ」「え?」「そうなんです😢

「たとえば、Linuxだと曜日は日曜始まりで0-6が割り当てられていて、さらに7も利用可能なので、07が日曜日になる: これなら7で割った余りで曜日を出すみたいな処理がやりやすい」「なるほど!」

参考: crontab 曜日設定などメモ - Qiita

「でもAmazon EventBridge↓(旧Amazon CloudWatch Events)のCrontab形式はかなり特殊で、曜日が日曜始まりの1-7になってる」「え〜それヤバいじゃないですか!」「きっとハマる自信ある」「誰もが一度はハマります」

参考: ルールのスケジュール式 - Amazon CloudWatch Events

「さらに厄介なのは、EventBridgeではcrontabで指定できる時刻がUTCのみという点」「え〜!JSTとかで書けないんですか?」「ローカルタイムが指定できないので、時刻を指定するたびに変換してあげないといけません」「ややこしい…」

参考: 協定世界時(UTC) - Wikipedia

「ちなみにLinuxのcrontabだとTZ=タイムゾーン名でローカルタイムを指定できます↓(指定し忘れると悲惨ですが)」「そういえばそうですね」「Linuxのcrontabに慣れているほどAWSでハマりがち」

参考: cron - How do you set the timezone for crontab? - Ask Ubuntu

「ところが面白いことに、AWSのマネージメントコンソール経由だとEventBridgeでローカルタイムを指定できるんですよ(↓証拠写真)」「へ〜!」「これについてさんざん調べたんですが、APIレベルではできない😢」「残念…」

「そういったわけで、crontab周りはクラウドの設定をレビューするときの要チェックポイントです」


後編は以上です。

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

週刊Railsウォッチ: insert_allやupsert_allのタイムスタンプ自動更新、rails/contextsにロジックを置くほか(20211025前編)

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

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

StatusCode Weekly

statuscode_weekly_banner

The post 週刊Railsウォッチ: YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか(20211026後編) first appeared on TechRacho.

ruby/debugのChrome Devtools連携をRailsで動かす

$
0
0

morimorihogeです。涼しくなってようやく生きていける感じになって何よりです。

今回はruby/debugに新しく導入されたChrome Devtools連携リモートデバッグ機能を動かしてみたので、そちらを紹介してみようと思います。

ことの起こり

新しいRuby標準デバッガとして開発が進んでいるruby/debugですが、先日こんなTweetがありました。

なにこれすごくない!?と思い、今回の記事を書くに至りました。

動きとしては、デバッガのコンソールで open chrome コマンドを実行するとURLが表示され、そのURLにChromeでアクセスすると、Chrome搭載のChrome Devtools(JavaScriptデバッグなどで使えるやつ)を使ってRubyのデバッガにアタッチできる、というもののようです。発想がすごい。

今回はこれを普段使っているRailsアプリで実際に使ってみよう、という話になります。

まずは動作確認

まずは最もシンプルな形で動かしてみましょう。今回の動作環境はWindows 10 WSL2上のUbuntu環境ですが、macOSでも同じことができるはずです。

以下のような sample.rb を用意します。 debugger がデバッガ呼び出しに当たります。

require 'debug'

test_var = 'test'

debugger

そうしたら、debug gemをインストールして実行します(ruby 3.0.2p107で動作確認していますが、2.7等でも動作します)。

$ gem install debug
$ ruby sample.rb

すると、下図のように debugger 部分でコードが停止し、rdbgコンソールが開きます。

ここで open chrome 命令を実行すると、devtools:// で始まるURLが表示されます。

このURLにアクセスするとChromeが開発者コンソールの画面を開きますので「Sources」タブを開いてみましょう。

はい。いい感じにデバッガが開きました。右上の方にstep over他一般的なデバッガ用の操作ボタンがあるので、ソースを見ながらこの辺りを見ていくことができます。
また「Console」タブに移動すると、デバッガが停止している状態のコンテキストで任意のRubyコードを実行できます。

ChromeのデバッグコンソールでRubyのコードを動かせる、というところでなんか楽しくなってきましたね 🎉

Railsで動かす: テストコード編

さて、うまいこと動いたので次は手持ちのRailsのコードでやってみましょう。
※今回普通に手元の業務で使ってるコードでやったのでモザイク多めですが、それだけ普通のRailsプロジェクトでももう使えると思っていただければ幸いです

まずはbyebugの代わりにdebug gemに入れ替えるため、Gemfileを編集します。Bye-bye buybug!今までありがとう!

group :development, :test do
  # gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'debug'

あとは普通にbundleしましょう。

ローカルでrspecなどのテストコードを動かしている場合、もうあとはデバッガ起動したい部分にdebuggerを埋め込み、open chromeするだけで対応できます。
一方で、使っている環境がDockerを使っている場合、追加の設定が必要になります。

open chrome コマンドはデフォルト設定では127.0.0.1、ポートは任意でLISTENするようになっていますが、これだとローカルのブラウザ環境からアクセスするのにはうまくいきません。
docker-composeコマンドでRailsコンテナに入ってrspecなどの実行を行っている場合には、以下のようなコマンドでコンテナを立ち上げましょう。
これでコンテナ外からruby/debugの開いたポートにアクセスすることができるようになります。

docker-compose run --rm -p '23456:23456' \
  -e RUBY_DEBUG_HOST=0.0.0.0 \
  -e RUBY_DEBUG_PORT=23456 \
  rails bash

この状態でdebuggerを含むテストを実行し、開いたrdbgコンソールからopen chromeすると、以下のように指定したホスト:ポートでLISTENしてくれます。

あとは先ほどと同じようにアクセスしますが、Windows/WSL2環境ではChromeで開く際にURL末尾のホストIPを0.0.0.0から127.0.0.1にする必要がありました
これは、アプリケーションコンテナからは 0.0.0.0:23456 でLISTENしていますが、ホストからはコンテナに繋ぐ必要があるためかなと思うのですが、docker psの結果では 0.0.0.0:23456->23456/tcp で開いているようにもなっており、WSL2のポート開放周りの問題というか仕様?かもしれません。

というわけで、attachできました。sample.rbの場合と同様に操作可能です。

step intoでライブラリコードに潜っていくことももちろんできます。

Consoleタブでputsするとちゃんとrpsec側の実行中コンソールに出ます。

Railsで動かす:Railsサーバー編

さて、今度は実際に動かしているRailsサーバー中でデバッガをアタッチしてみます。

まず、pumaを使っている場合(恐らく現代の大多数のRails)、少なくとも現行のdebugバージョン(1.3.1)だとmultiple worker環境でうまく動かない(後述)ようなので、puma.rbを修正する、または環境変数WEB_CONCURRENCYを1に指定します。

# config/puma.rb の該当行をよしなに編集する
# Worker数を1プロセスに指定(threadsは複数でも動作するようだ)
workers 1
# preload_app!が有効になっている場合、コメントアウトする
#preload_app!

この状態でPumaを起動するとPuma起動時に「in single mode…」と出ますので、こうなっていればOKです。

あとはこれまで通りにやればOKです。docker-composeからRailsサーバーを起動するようにしている場合は以下のように環境変数、公開ポートを指定してやれば良いでしょう。

services:
  rails:
    environment:
      # use ruby/debug
      - RUBY_DEBUG_HOST=0.0.0.0
      - RUBY_DEBUG_PORT=23456
    ports:
      - '23456:23456'

docker-compose up で起動したら、docker attach #{コンテナ名} とすることでRailsサーバーのプロセスに接続できます。
この状態でdebuggerを含む処理が実行されるとその時点でデバッグコンソールが立ち上がります。

※docker attachしたコンソールから抜けたい場合はCtrl-CではなくCtrl-P Ctrl-Qで抜けましょう。Ctrl-Cだとプロセスが終了してしまいます。このあたりはDockerの使い方に関する部分なので、よく分からなければDockerのドキュメントなどを参照して下さい。

Railsサーバーあれこれ
今回pumaで最初うまくいかなくて色々試しましたが、Webrick(rails s -u 'webrick')は問題なし、pumaはmultiple workerの時のみChrome Devtoolでアタッチした直後に接続が切れてしまいうまくいきませんでした。
ReaderThreadError等が出ているので、pumaがcluster modeの場合の何かが問題なのかもしれません。

まとめ

debug gemは今まさに活発に開発が進行中で、どんどん新機能や対応が進んでいます。今回のChrome Devtools連携機能についてもOpen Chrome automaticallyというPRが上がっていたりなどまだまだ改善が進みそうな気配です。

お使いのRailsプロジェクトでもサクッと気軽に使い始められるdebug gem、皆さまもぜひ試してみましょう 😊

The post ruby/debugのChrome Devtools連携をRailsで動かす first appeared on TechRacho.

Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of

$
0
0

概要

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

週刊Railsウォッチ20210823 ActiveRecord::QueryMethods#in_order_ofを追加もどうぞ。

Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of

RailsでActive Recordを利用していて、クエリの結果が特定の順序に並んでいることを期待する場合があります。

たとえば、ブックリーダー用のRailsアプリケーションがあり、読了した本や今呼んでいる本やこれから読みたい本をトラッキングできるとします。

このアプリケーションをシンプルに構築するには、UserBookを作成します。このモデルにはbook_iduser_idstatusという3つのカラムがあります。statusカラムはreadcurrently_readingto_readのいずれかの値を取れるようになっています。

変更前

ユーザーの本をto_readcurrently_readingreadの順で表示するには、以下のような実装が考えられます。

user = User.first

# Arelを利用
result = user.user_books.
           order(
             Arel.sql(
               %q(
                  case status
                  when 'to_read' then 1
                  when 'currently_reading' then 2
                  when 'read' then 3
                  end
               )
             )
           )

# クエリをかけてからレコードを並べ替える
result = user.user_books.where(status: %w[to_read currently_reading read])

# 以下のアプローチか
# 「オランダ国旗問題」のソリューションを使える
# https://en.wikipedia.org/wiki/Dutch_national_flag_problem

ordered_result = result.collect{ |user_book| user_book.status == "to_read" } +
                 result.collect{ |user_book| user_book.status == "currently_reading" } +
                 result.collect{ |user_book| user_book.status == "read" }

指定の順序で最終的な結果をマッピングするには、ArelでSQL文を書くか、クエリレコードを反復しなければなりません。

変更後

Rails 7のActiveRecord::QueryMethodsに、上述の問題を解決するin_order_ofメソッドが追加されました(#42061)。

この新しい変更を用いれば、上述の実装は以下のようになります。

user = User.first

result = user.user_books.in_order_of(:status, %w[to_read currently_reading read])

#=> #<ActiveRecord::Relation [#<UserBook id: 3, user_id: 1, status: "to_read">, #<UserBook id: 4, user_id: 1, status: "to_read">, #<UserBook id: 5, user_id: 1, status: "currently_reading">, #<UserBook id: 6, user_id: 1, status: "read">]>

UserBook.in_order_ofは以下のクエリを生成します。

SELECT "user_books".* FROM "user_books" /* loading for inspect */ ORDER BY CASE "user_books"."status" WHEN 'to_read' THEN 1 WHEN 'currently_reading' THEN 2 WHEN 'read' THEN 3 ELSE 4 END ASC LIMIT ?  [["LIMIT", 11]]

ただしMySQLの場合、CASEの代わりにFIELD関数が用いられます。

SELECT "user_books".* FROM "user_books" /* loading for inspect */ ORDER BY FIELD("user_books"."status", 'to_read', 'currently_reading', 'read') ASC

Rails 7には既に、ActiveRecordのin_order_ofと同様に振る舞うEnumerable#in_order_ofも追加されています(関連記事)。

Enumerable#in_order_ofはEnumeratorに対して動作しますが、ActiveRecordのin_order_ofActiveRecord::Relationオブジェクトに対して動作する点が異なります。

関連記事

The post Rails 7: クエリ結果を任意の順序にできるActiveRecord::QueryMethods#in_order_of first appeared on TechRacho.

週刊Railsウォッチ: Rails 7アセットパイプライン解説記事、ロジックをapp/operatorsで整理ほか(20211101前編)

$
0
0

こんにちは、hachi8833です。直近ですが、明日11/2(火)19:30より「大江戸Ruby会議09 出前Edition」がオンライン開催されます。Rails界隈で知られた「あの人」や「あの人」も登壇するそうです。皆さんもぜひ!


また、Kaigi on Rails 2021の全動画がYouTubeチャンネル↓で公開されました 🎉

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

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

以前の公式更新情報で拾いきれなかったものから見繕いました。

🔗 Action MailテンプレートにDOM idを追加

自分のチームでは、CapybaraとSeleniumの代わりにCypressですべてのエンドツーエンドテストをやっている。その中で、すべてのメイラーでMailer Previewのアクションがエラーなしに開いてレンダリングできることをテストしている。メイラーやメイラーのアクションがたくさんあるのでバグの早期発見に役立っている。
自分たちが直面した問題は、メールのtofromsubjectなどにある重要なdd要素のほとんどに一意のセレクタがなくDOMで追いかけづらいというもの。このプルリクはこの問題を修正する。
私たちのCypressによるテストは以下のような感じになっている。

context('Mailer Preview', () => {
  it('works for all mailer actions', () => {
    cy.visit('/rails/mailers')

    cy.get('li a').each($a => {
      const href = $a.attr('href');
      cy.log(href)
      cy.visit(href)

      cy.get('#from').then($dd => {
        cy.log('FROM :' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('from@example.com')
      })

      cy.get('#to').then($dd => {
        cy.log('TO: ' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('foo@bar.com')
      })

      cy.get('#subject').then($dd => {
        cy.log('SUBJECT: ' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('Test subject')
      })

      cy.get('#mime_type').then($dd => {
        cy.log('MIME TYPE: ' + $dd.get(0).innerText)
        //expect($dd.get(0).innerText).to.eq('HTML email')
      })

      cy.get('[download="icon.png"]').should('exist')
    })
  })
})

同PRより


つっつきボイス:「Action MailテンプレートのDOM idを増やしたそうです」「そうそう、idがあるとシステムテストを書いているときにとても取りやすいんですよ」「たしかに」「順序で指定したりすると後で仕様が変わったときにハマりやすいので、これはありがたい修正👍」「自分もテストのためにこんな感じでフィールドにidを振ったりしたことがあります↓」

<!-- railties/lib/rails/templates/rails/mailers/email.html.erb#56 -->
<dl>
    <% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %>
      <dt>SMTP-From:</dt>
-     <dd><%= @email.smtp_envelope_from %></dd>
+     <dd id="smtp_from"><%= @email.smtp_envelope_from %></dd>
    <% end %>
    <% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %>
      <dt>SMTP-To:</dt>
-     <dd><%= @email.smtp_envelope_to %></dd>
+     <dd id="smtp_to"><%= @email.smtp_envelope_to %></dd>
    <% end %>

    <dt>From:</dt>
-   <dd><%= @email.header['from'] %></dd>
+   <dd id="from"><%= @email.header['from'] %></dd>

    <% if @email.reply_to %>
      <dt>Reply-To:</dt>
-     <dd><%= @email.header['reply-to'] %></dd>
+     <dd id="reply_to"><%= @email.header['reply-to'] %></dd>
    <% end %>

    <dt>To:</dt>
-   <dd><%= @email.header['to'] %></dd>
+   <dd id="to"><%= @email.header['to'] %></dd>

    <% if @email.cc %>
      <dt>CC:</dt>
-     <dd><%= @email.header['cc'] %></dd>
+     <dd id="cc"><%= @email.header['cc'] %></dd>
    <% end %>

    <dt>Date:</dt>
-   <dd><%= Time.current.rfc2822 %></dd>
+   <dd id="date"><%= Time.current.rfc2822 %></dd>

    <dt>Subject:</dt>
-   <dd><strong><%= @email.subject %></strong></dd>
+   <dd><strong id="subject"><%= @email.subject %></strong></dd>

    <% unless @email.attachments.nil? || @email.attachments.empty? %>
      <dt>Attachments:</dt>

🔗 BASIC認証に無効な認証文字列が渡されたときの挙動を修正

BASIC認証で保護されているコントローラに、コロンが抜け落ちている誤った認証情報でリクエストを送信すると、以下のようにNoMethodError: undefined method 'bytesize' for nil:NilClassエラーが発生する。

class UsersController < ApplicationController
   http_basic_authenticate_with name: "king", password: "secret"

   def index
     render plain: "Something"
   end
end
credentials=$(echo -n king secret | base64)
curl 'http://localhost:3000/users' -H "Authorization: Basic $credentials"

同PRより


つっつきボイス:「BASIC認証の認証情報が不備だった場合のエラーがこれまでNoMethodErrorだったのを、has_basic_credentials?がfalseを返す形に修正したようです」「たしかにNoMethodErrorだと違いますよね」「今まではNoMethodErrorがhas_basic_credentials?を突き抜けてしまっていたのか」

# actionpack/test/controller/http_basic_authentication_test.rb#115
  test "has_basic_credentials? should fail with credentials without colon" do
    @request.env["HTTP_AUTHORIZATION"] = "Basic #{::Base64.encode64("David Goliath")}"
    assert_not ActionController::HttpAuthentication::Basic.has_basic_credentials?(@request)
  end

🔗 inflectorに登録した略語を削除できるようにした

ActiveSupport::Inflectorの略語(acronym)を削除しようとすると実装が壊れ、別の略語を登録しようとするとTypeErrorが発生する。

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "activesupport", require: "active_support/all"
end

ActiveSupport::Inflector.inflections do |inflect|
  inflect.clear :acronyms
  inflect.acronym "HTML" # => '[]=': no implicit conversion of String into Integer (TypeError)
end

これは#clearinstance_variable_set "@#{scope}", []のように@acronymsに新しいArrayを設定していたのが原因。デフォルトの初期値はHashになっている↓。

# activesupport/lib/active_support/inflector/inflections.rb#L78-L79
def initialize
  @plurals, @singulars, @uncountables, @humans, @acronyms = [], [], Uncountables.new, [], {} 

このプルリクでは、ActiveSupport::Inflector::Inflectionsの#clear#clear(:all)`を拡張して、従来できなかった略語の削除をできるようにもしておいた。
同PRより


つっつきボイス:「Inflectorは単数・複数形単語の登録や独自の固有名詞・略語の登録を行える機能ですね↓: #clearなどで削除できない問題と、その後で略語を再登録できない問題が修正された」「日本語だとあまり使わない機能だと思いますが、たしかに登録できるなら削除もできて欲しいですよね」

参考: Rails API ActiveSupport::Inflector::Inflections

# api.rubyonrails.orgより
acronym 'RESTful'
underscore 'RESTful'           # => 'restful'
underscore 'RESTfulController' # => 'restful_controller'
titleize 'RESTfulController'   # => 'RESTful Controller'
camelize 'restful'             # => 'RESTful'
camelize 'restful_controller'  # => 'RESTfulController'

acronym 'McDonald'
underscore 'McDonald' # => 'mcdonald'
camelize 'mcdonald'   # => 'McDonald'

🔗 ジェネレータのCSSプロセッサリストにBootstrapとBulmaを追加

# railties/lib/rails/generators/rails/app/app_generator.rb#L265
-     class_option :css, type: :string, desc: "Choose CSS processor [options: tailwind, postcss, sass]"
+     class_option :css, type: :string, desc: "Choose CSS processor [options: tailwind, bootstrap, bulma, postcss, sass... check https://github.com/rails/cssbundling-rails]"

つっつきボイス:「ci skipとあるのはだいたいドキュメント関連の改修」「そういえばCSSフレームワークのBulmaは、以前#43110でsass-railsへのデフォルト依存が削除されたときに見かけました(ウォッチ20210921)」「Bulmaは使ったことないな〜」

参考: Bulma: Free, open source, and modern CSS framework based on Flexbox

🔗 Railsガイドのスタイル改修


つっつきボイス:「Railsガイドのスタイルにいくつか細かな修正が入っていました」「地道な修正大事ですね」

「ガイドの目次ドロップダウンを開いたらEscキーで閉じられるようにする、なるほど」「edgeガイドに反映されていました」

// guides/assets/javascripts/guides.js
    document.addEventListener("keyup", function(e) {
      if (e.key === "Escape" && guides.classList.contains("visible")) {
        guides.classList.remove("visible");
      }
    });

#43250の修正はどこだろう?」「モバイル表示の左右マージンが調整されて、フッターのmargin-bottomがわずかに小さくなっていた↓」「diffを見る方が早いかも」


同PRより(編集部で横並びに変更)

#42989はダークモードのdiffが見づらかったのを修正」「お〜なるほど」「個人的にダークモードってどうも不要な文明感がありますけど😆」「同じく」



同PRより

🔗Rails

🔗 Rails 7.0のアセットパイプライン周り解説記事


つっつきボイス:「これはいい記事でしたね👍」「歴史と現状と見通しのまとめが凄いですね」「ふわっとさせずに詳細を解説しきっていて、Simpackerにも言及しているのがさすが」

hokaccha/simpacker - GitHub

参考: Simpacker: Rails と webpack をもっとシンプルにインテグレーションしたいのです - クックパッド開発者ブログ

「この間話題にしたPropshaft(ウォッチ20211018)も、記事によるとRails 7で主要な選択肢のひとつになりそうで、Propshaftは思っていたより進んでいるんですね」「Rails 7は今はAlpha2ですが、次のAlpha3あたりでPropshaftが入るかもしれませんね」「お〜」

後で調べると、Propshaftはv7.0.0.alpha2ブランチではまだジェネレータのapp_base.rbには取り込まれていませんでしたが、masterブランチapp_base.rbには取り込まれていました。

Rails 7: import-map-rails gem README(翻訳)

🔗 ビジネスロジックをapp/operatorsで整理(Ruby Weeklyより)


つっつきボイス:「記事ではTrailbrazer↓を使ったりしてみたけど自分に合わなかったのでapp/operatorsで整理したらしい」「Trailbrazerにも名前の似たOperationsという概念があるんですね」

trailblazer/trailblazer - GitHub

「app/operatorsは前回取り上げたapp/contextsに似ている感じでしょうか?(ウォッチ20211025)」「記事冒頭でも前回のcontext記事↓を引用しているので意識はしているでしょうね」

参考: Organizing business logic in Rails with contexts

「前回のcontextsもそうでしたけど、この記事のコードに出てくる何とかOperatorもまさにGoF本で言うFacade(ファサード)ですね↓」「なるほど、名前が違う感じですか」

# 元記事より
# app/operators/invoice_operator.rb

class InvoiceOperator < BaseOperator
  def update(params:)
    @record.assign_attributes(params)

    # do things before updating the invoice eg:
    # update/create related records (like InvoiceItems)
    # a few more examples:
    calculate_total 
    calculate_vat

    state = @record.save

    # do your other business eg.:
    # send emails,
    # call external services and so on

    Result.new(state: state, record: @record)
  end

  def create(params:)
    @record.assign_attributes(params)

    # do things before creating the invoice eg:
    # create related records (like InvoiceItems)
    # a few more examples:
    calculate_total
    calculate_vat

    state = @record.save

    # do your other business eg.:
    # send emails,
    # call external services and so on

    Result.new(state: state, record: @record)
  end

  private

  def new_record
    Invoice.new
  end

  def calculate_total
    # you can write the logic here, 
    # or call a class that handles the calculation
  end

  def calculate_vat
    # you can write the logic here, 
    # or call a class that handles the calculation
  end
end

参考: Facade パターン - Wikipedia

「よりシンプルなパターンを作る試みは常にありますけど、実際に使ってみたときにシンプルになるとは限らないのが悩ましいところなんですよ」「もしかするとService Objectも出た当初はシンプルだと言われてたのかも」

「コントローラにActive Recordのメソッドチェーンがたくさんあるのは確かに気持ちよくないので、operatorsやcontextsでFacadeにするのもわかる: 個人的にはRailsでActive Recordが使えるのが嬉しい点だと思うので、そこまでしてActive Recordを直接触らせない形にしなくてもいいかなという気持ちが少しあります」「たしかに」「規模が大きくなればまた違ってくると思いますが」

「元記事末尾によると、このoperatorsはまだ長期のバトルテストは経てないそうです」「アプリが育ってきたときの設計は一概にどれがいいとは言えませんが、大きくする予定のないアプリなら別にoperatorsでやってもいいんじゃないかな」

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

🔗 RailsアプリのJSテストコードのカバレッジ(RubyFlowより)


つっつきボイス:「RailsプロジェクトにおけるJSテストのコードカバレッジの記事のようですね」「記事に出てくるIstanbulはJS製のカバレッジツールなのか↓」「simplecov(Ruby製のカバレッジツール)も使ってる」

istanbuljs/istanbuljs - GitHub

simplecov-ruby/simplecov - GitHub

「記事ではIstanbulで集計したJSテストカバレッジのダンプをRSpecからトリガーしているようですね↓」

# 同記事より
# spec/rails_helper.rb

RSpec.shared_context "dump JS coverage" do
  after { dump_js_coverage }
end

RSpec.configure do |config|
  config.include_context "dump JS coverage", type: :system

  ...

「JSのテストコードが多いRailsプロジェクトで使いそうなテクニックですね: もっともフロントエンドとバックエンドでリポジトリが分かれているプロジェクトだと少し工夫が必要そうですが」「あ、たしかに」

🔗 GitLab 14.4リリース

つっつきボイス:「14.4が出たのでBPS社内のGitLabもアップデートしておくかな」「DASTって何だろうと思ったらDynamic Application Security Testingなんですね」「静的なセキュリティスキャン機能ですね: 最近のGitLabはこういった機能をよく追加していますね」「なるほど」「よく見たらDASTはGitLabのUltimate版のみなのでFree版では使えないことが判明」「う、残念」

参考: GitLab Pricing | GitLab

「他の機能はというと、VSCodeからのGitLabリモートリポジトリ参照」「おぉ?」「ローカルにcloneしていないプロジェクトをVSCodeから読み取り専用で参照できるようですね: おそらく動画↓で動いているVSCode拡張用のインターフェイスをGitLab側に用意したということかな」「リモートリポジトリをちょっと見たいときにローカルにcloneしなくてもVSCodeで見られるのはよさそうですね😋

「GitLabは機能が着々と増えていますし、セキュリティパッチもちゃんと出し続けているので、出たらとりあえず当てることにしています」

🔗 その他Rails


つっつきボイス:「この間のruby/debug + Chrome Devtoolsの記事にアンサーが付いてたので拾いました」「そうそう、この方は明日10/29(注: つっつきは前日10/28夜でした)の銀座Rails#38に登壇されるosyoさんです」

以下は登壇後の資料ツイートです。

「そしてその後もrdbgについてTwitter上でosyoさんとやり取りしました↓」「あ、続きがあったんですね」「とりあえずの結論としてはbundle exec経由だとGemfileに追加しないとrdbgが動かないけど、binstub経由ならGemfileに追加しなくてもrdbgが動くということになりました」「お〜そうだったんですね」「Ruby界隈はTwitterでつぶやくと即誰かがコメントしてくれるから便利😊」「そうそう😊

【翻訳+解説】binstubをしっかり理解する: RubyGems、rbenv、bundlerの挙動


前編は以上です。

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

週刊Railsウォッチ: YJITがRuby 3.1向けにマージ、ripperのドキュメント化、crontabの罠ほか(20211026後編)

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

The post 週刊Railsウォッチ: Rails 7アセットパイプライン解説記事、ロジックをapp/operatorsで整理ほか(20211101前編) first appeared on TechRacho.

Viewing all 1425 articles
Browse latest View live