Quantcast
Channel: hachi8833の記事一覧|TechRacho by BPS株式会社
Viewing all 1830 articles
Browse latest View live

Rails 7: ActiveRecord::Relationにstructurally_compatible?が追加(翻訳)

$
0
0

概要

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

参考: 週刊Railsウォッチ20210830『ActiveRecord::Relation#structurally_compatible?が追加

Rails 7: ActiveRecord::Relation#structurally_compatible?が追加(翻訳)

Railsの優れている点のひとつは、SQLコードをまったく書かずにデータベースオブジェクトやクエリを簡単に扱えることです。これが可能なのは、Railsのコアライブラリの中で間違いなく最高峰であるActive Recordライブラリのおかげです。

Active Recordクエリインターフェイスを使えば、findgroupjoinsといったさまざまなクエリ操作を実行できます。

また、wherenotor操作を用いてリレーションをチェインすることも可能です。ただしorand操作については、チェインする2つのリレーションに構造上の互換性を持たせておく必要があります。

2つのリレーションに構造上の互換性を持たせるには、両者が同じモデルを対象とし、かつwheregroupが定義されていない場合)またはhavinggroupが存在する場合)だけが異なっていなければなりません。

従来は、2つのリレーションが構造上の互換性を持っているかどうかを簡単に確かめる方法がありませんでした。リレーションの値をイテレーションするか、orandで発生するArgumentErrorをキャッチする必要がありました。

しかし、クエリでorandを実行する前に2つのリレーションが構造上の互換性を持っているかどうかを簡単にチェックできるActiveRecord::Relation#structurally_compatible?がRails 7に追加されました(#41841)。

たとえばPostモデルに :content:user_idstatusという属性があり、Commentモデルに:post_id:user_id:contentという属性があるとしましょう。Posthas_many :commentsを持ち、Commentbelongs_to :postを持っています。

構造上の互換性を持つリレーションが2つあるとします。

  relation_1 = Post.where(status: 'active')
  relation_2 = Post.where(id: current_user.id)

また、構造上の互換性を持たないリレーションも2つあるとします。

  relation_3 = Post.where(status: 'active')
  relation_4 = Post.joins(:comments).where(comments: { user_id: current_user.id})

変更前

これらのリレーション間でorクエリを実行しなければならなくなった場合、ArgumentErrorをキャッチします。

  relations = [[relation_1, relation_2], [relation_3, relation4]]
  relations.each do |relation|
    left = relation.first
    right = relation.last
    begin
      left.or(right)
    rescue ArgumentError
      # 構造上の互換性を持たないリレーションが失敗したらArgumentErrorをrescueする
    end
  end

変更後

Rails 7では、リレーション同士が構造上の互換性を持つかどうかをクエリ実行前にチェックできます。

  relations = [[relation_1, relation_2], [relation_3, relation4]]
  relations.each do |relation|
    left = relation.first
    right = relation.

    if left.structurally_compatible?(right)
      left.or(right)
    end
  end

これは、例外をキャッチせずにリレーションを手軽にクエリできる素晴らしい改善です。このような小さなヘルパー機能が豊富にあるからこそ、Active Recordが優秀なライブラリとなり、Railsフレームワークが開発者にとってより使いやすくなっています。

関連記事

Rais 7のbyebugがruby/debugに置き換わる(翻訳)

The post Rails 7: ActiveRecord::Relationにstructurally_compatible?が追加(翻訳) first appeared on TechRacho.


Rails: 5.1以降のtagヘルパー記法はcontent_tagより便利

$
0
0

更新情報

  • 2018/11/01: 初版公開
  • 2022/01/13: 更新

こんにちは、hachi8833です。少し前の話ですが、Rails 5.0->5.1で変更されたビューヘルパーのtagメソッドについてメモします。

なお5.1と5.2では変化はありません。

Railsビューヘルパーのtagメソッド

きっかけはDHHによる以下のissueです。

# #25195より
# 以前は<span class="bookmark"></span>を次で生成していた
content_tag :span, nil, class: 'bookmark'
# 今後は次でできる(<span>など閉じタグが必要なタグや<br>など閉じタグ不要なタグのインデックスを保持するので) 
tag.span class: 'bookmark'

# 以前は<div id="post_1">を次で生成していた
content_tag :div, post.title, id: dom_id(post)  
# 今後は以下でできる
tag.div post.title, id: dom_id(post)

# 以前は<br>を以下で生成していた
tag :br, nil, true
# 今後は以下でできる(spanのときと同様。このタグは閉じタグ不要であることがわかっている)
tag.br

# ネストもサポートできるとよい: <div id="header"><span>hello</span></div>を以下で生成する
tag.div(id: 'header') { |tag| tag.span 'hello' }

その後#25543でマージされ、5.1に取り入れられました。Rubyらしく書けるtag.タグ名記法はデフォルトでHTML 5をサポートします。

かつ、content_tagはレガシ記法であるとされ、その記述は5.2の現在も変わっていません。ただしcontent_tagは今のところ非推奨にはなっていないようです。非推奨にするには影響が大きすぎるからだろうと思いました。

追記(2022/01/13)

content_tagはその後もRails 7まで変わらず利用できます。

その代り、tagの古い記法(tag('br')など)は#25543の時点で非推奨になっており、今後廃止の予定です。

参考: Rails 5.1 からは tag(:br) より tag.br を推奨 - Qiita

追記(2022/01/13)

tagのレガシー記法はその後も削除されておらず、Rails 7でも利用できます。

Rails 5.2.1で確認

5.2.1でrails newし、Postをscaffoldで生成し、レコードを2つ作ってから上のcontent_tag記法とtagの新しい記法をそれぞれ試してみました。

<!-- view/posts/index.html.erb -->
...
<%= content_tag :span, nil, class: 'bookmark' %>
<%= tag.span class: 'bookmark' %>

<% @posts.each do |post| %>
  <%= content_tag :div, post.title, id: dom_id(post) %>
  <%= tag.div post.title, id: dom_id(post) %>
<% end %>

<%= tag :br, nil, true %>
<%= tag.br %>

<%= tag.div(id: 'header') { |tag| tag.span 'hello' } %>
...

content_tag :spantag.spanも動きました。

ビューでtag.div tag.p('Hello world!')と書けば<div><p>Hello world!</p></div>のようにネストできたり、以下のようにdoendブロックで文字列を渡せたりする機能もcontent_tagと同じですが、記法がシンプルなのが嬉しい点です。

<%= tag.p do %>
  パラグラフの文章
<% end %>

使い勝手やHTML 5対応を考えると、基本的にcontent_tagよりもtag.タグ名を使うのがよさそうです。

関連記事

Rails tips: ビューの`content_tag`のあまり知られていないオプション(翻訳)

The post Rails: 5.1以降のtagヘルパー記法はcontent_tagより便利 first appeared on TechRacho.

Rails 7: caching?とuncachable!ヘルパーが追加(翻訳)

$
0
0

概要

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

Rails 7: caching?とuncachable!ヘルパーが追加(翻訳)

よく「キャッシュの無効化はコンピュータサイエンスの難問であり、バグを引き起こす可能性がある」と言われますが、実際、キャッシュすべきでないものをキャッシュするとバグやセキュリティ脆弱性の温床になることがあります。

Railsにはフラグメントキャッシュの仕組みが組み込まれていて、レンダリングされたビューの一部をフラグメントとして保存します。以後のリクエストでは、再レンダリングする代わりに保存済みのフラグメントが使われます。

ただしフラグメントキャッシュは深刻なバグを引き起こす可能性があります。たとえば、商品ごとに固有の署名済みURLを生成するAWS S3 URLヘルパーを使う場合や、リクエスト固有の認証トークンを出力するフォームヘルパーを書く場合を考えてみましょう。このような場合は、フラグメントキャッシュは避ける方がよいでしょう。

変更前

cacheヘルパーを使ってフラグメントキャッシュを実装できます。

views/products/index.html.erb

  <table>
    <thead>
        <tr>
        <th>Title</th>
        <th>Description</th>
        <th>Image</th>
        </tr>
    </thead>

    <tbody>
        <% @products.each do |product| %>
          <% cache(product) do %>
              <%= render product %>
          <% end %>
        <% end %>
    </tbody>
  </table>

views/products/_product.html.erb

  <tr>
    <td><%= product.title %></td>
    <td><%= product.description %></td>
    <td><%= image_tag(generate_presigned_url(product.image_url)) %></td>
  </tr>

しかしこのコードでは、毎回一意の署名済みURLを生成してもレンダリングのたびにキャッシュされた方の商品(product)が取得されてしまうというバグがあります。これを解決するには、productパーシャル内にcacheableを書く必要があります。これで、誰かがproductパーシャルをキャッシュしようとすると、ActionView::Template::Errorエラーが発生します。

変更後

  <tr>
    <%= uncacheable! %>
    <td><%= product.title %></td>
    <td><%= product.description %></td>
    <td><%= image_tag(generate_presigned_url(product.image_url)) %></td>
  </tr>

上のようにuncacheable!を書いておくと、以下のようになります。

  ActionView::Template::Error (can't be fragment cached):
    1: <tr>
    2:     <%= uncacheable! %>
    3:   <td><%= product.title %></td>
    4:   <td><%= product.description %></td>
    5:   <td><%= image_tag(generate_presigned_url(product.image_url)) %></td>

  app/views/products/_product.html.erb:2

また、caching?ヘルパーは、現在のコードパスがキャッシュされているかどうかのチェックや、キャッシュの強制に使えます。

  <tr>
    <%= raise Exception.new "This partial needs to be cached" unless caching? %>
    <td><%= product.title %></td>
    <td><%= product.description %></td>
  </tr>

この変更に関する議論について詳しくは、#42365をご覧ください。

関連記事

Rails 7: ActiveRecord::Relationにstructurally_compatible?が追加(翻訳)

The post Rails 7: caching?とuncachable!ヘルパーが追加(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: Webpackerが公式に引退宣言、『Everyday Rails』日本語版がRails 7に対応ほか(20220124前編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

今回は以下の中からChangelogが更新されているものを中心に見繕いました。久しぶりに更新が少ないですね。

🔗 atomic_writeの競合状態の挙動を修正

production環境で見かけたatomic_write関連のバグがある(他にもさまざまなところで見た)。スタックトレースによれば、FileUtils.touchはそのbasedirが存在しなくなったパスを操作していて、それが原因でErrno::ENOENT例外が発生していた。どこかで競合条件が発生しているのかもしれない。いずれにしろFile.probe_stat_inはデバッグには使えそうにないので、atomic_writeが爆発したとしても爆発させるべきではないだろう。
同PRより


つっつきボイス:「privateメソッドprobe_stat_inENOENT例外に対応していなかったのを修正したようですね↓」

# activesupport/lib/active_support/core_ext/file/atomic.rb#L56
  def self.probe_stat_in(dir) # :nodoc:
    basename = [
      ".permissions_check",
      Thread.current.object_id,
      Process.pid,
      rand(1000000)
    ].join(".")
    file_name = join(dir, basename)
    FileUtils.touch(file_name)
+ rescue Errno::ENOENT
+   file_name = nil
  ensure
    FileUtils.rm_f(file_name) if file_name
  end

「ときどき見かけるENOENTってどう読むのか謎だったんですが、Error No Entoryの略だったんですね」「Eで始まるのがいかにもPOSIXのエラーっぽいので探してみるとやはりそうだった↓」「POSIXにこんなにたくさんエラーがあるとは知らなかった〜」

参考: Man page of ERRNO

ENOENT
そのようなファイルやディレクトリはない (POSIX.1-2001)
通常は、このエラーは、指定されたパス名が存在しないか、パス名のディレクトリプレフィックスの構成要素のいずれかが存在しないか、指定されたパス名が壊れた (dangling) シンボリックリンク、の場合に発生する。
linuxjm.osdn.jpより

参考: POSIX – Wikipedia

🔗 preload_link_tagrel: 'modulepreload'を使うよう修正

このプルリクは、preload_link_tagmoduleスクリプトのプリロードをサポートするように修正する。
type: 'module'が指定されると、rel: 'preload'ではなくrel: 'modulepreload'を使うようになる。
背景
従来は、preload_link_tagでのタグのレンダリングとレスポンスのearly hintの両方について、rel=preloadが(moduleスクリプトを含む)すべてのスクリプトで使われていた。
型が合わない(スクリプトはプリロードされるがモジュールはインクルードされていない)ので、ブラウザはこのプリロードされたリソースを無視する
同PRより


つっつきボイス:「お、今まではrel属性にpreloadを指定してJavaScriptファイルをプリロードしていたのをmodulepreloadでやるように修正されたのね↓」「JavaScript用のリンク種別なのか」

# actionview/lib/action_view/helpers/asset_tag_helper.rb#L319
      def preload_link_tag(source, options = {})
        href = path_to_asset(source, skip_pipeline: options.delete(:skip_pipeline))
        extname = File.extname(source).downcase.delete(".")
        mime_type = options.delete(:type) || Template::Types[extname]&.to_s
        as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
        crossorigin = options.delete(:crossorigin)
        crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
        integrity = options[:integrity]
        nopush = options.delete(:nopush) || false
+       rel = mime_type == "module" ? "modulepreload" : "preload"

        link_tag = tag.link(**{
-         rel: "preload",
+         rel: rel,
          href: href,
          as: as_type,
          type: mime_type,
          crossorigin: crossorigin
        }.merge!(options.symbolize_keys))

-       preload_link = "<#{href}>; rel=preload; as=#{as_type}"
+       preload_link = "<#{href}>; rel=#{rel}; as=#{as_type}"
        preload_link += "; type=#{mime_type}" if mime_type
        preload_link += "; crossorigin=#{crossorigin}" if crossorigin
        preload_link += "; integrity=#{integrity}" if integrity
        preload_link += "; nopush" if nopush
        send_preload_links_header([preload_link])
        link_tag
      end

参考: リンク種別: modulepreload – HTML: HyperText Markup Language | MDN

🔗 ServerTimingミドルウェアのドキュメント更新

  • 0547b16で入ったActionDispatch::ServerTimingのAPIドキュメントを追加
  • rails_on_rack.mdガイドで抜けていたミドルウェアや解説を更新
    同PRより

つっつきボイス:「これはドキュメントの更新もれの修正ですね👍」「そういえばServerTimingミドルウェアは少し前に追加されていましたね(ウォッチ20211011)」「そうそう、Server-Timingはパフォーマンス測定用にレスポンスに追加できるヘッダー」

参考: rails/server_timing.rb at 0547b1646d09a80d0685a03c932fb0ba09c3e614 · rails/rails
参考: Server-Timing – HTTP | MDN


後で調べると、Rails 7のproduction環境とtest環境ではデフォルトでServerTimingがオフになっていました↓(development環境ではオンでした)。

$ RAILS_ENV=production dip rails middleware
Creating enno_docker_backend_run ... done
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use Rack::Runtime
use RailsSameSiteCookie::Middleware
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Rack::Attack
use Rack::Attack
run App::Application.routes

Rails 7のdevelopment環境でChrome DevToolsの[Network]タブを見ると、こんな感じで出力されていました↓。DevToolsの[Timing]タブをクリックするとグラフも表示されます。

Server-Timing: cache_read.active_support;dur=0.063720703125, cache_write.active_support;dur=0.029296875, start_processing.action_controller;dur=1.03662109375, sql.active_record;dur=4.445556640625, !render_template.action_view;dur=27.783203125, render_partial.action_view;dur=16.796875, render_template.action_view;dur=12.628173828125, render_layout.action_view;dur=22.005126953125, process_action.action_controller;dur=68.377197265625

🔗Rails

🔗 Webpackerが引退


つっつきボイス:「とうとうWebpackerが引退とは」「Rails 7リリースやここ最近のRailsの動きから、うすうす予感していた人は多いでしょうね」「Twitterを見ていてもそんな印象でした」

つっつき後に、Webpackerは以下のshakapackerに引き継がれると教わりました↓。

shakacode/shakapacker - GitHub

「Rails 7で入ったjsbundling-railsを使えば↓引き続きJavaScriptのバンドリングはできるので、TypeScriptなどを使う人はjsbundling-railsに移行してくれということでしょうね」「移行ガイドも出ていますね」

rails/jsbundling-rails - GitHub

参考: Webpacker 5->jsbundling-rails移行ガイド jsbundling-rails/switch_from_webpacker.md at main · rails/jsbundling-rails

「ちなみにHotwireのdiscussionサイトで見つけたんですが、UJSはRails 7では削除されていないそうです↓」「互換性についてだけturbo-railsのREADMEに書いておいたのね: rails-ujsはTurboと共存できるように名前空間が別になっていますね👍

We stopped removing UJS, and instead just document the compatibility: https://github.com/hotwired/turbo-rails#compatibility-with-rails-ujs
discuss.hotwired.devよりDHHのコメント

参考: Rails UJS/Turbolinks -> Turboアップグレードガイド turbo-rails/UPGRADING.md at main · hotwired/turbo-rails


なお、Rails 7でbin/rails new --helpすると以下のようにJavaScriptビルドツールの選択肢が表示されます。importmapwebpackesbuildrollupから選択できます。なおデフォルトのimportmapの場合はjsbundling-railsはインストールされず、importmap-railsがインストールされます。

-j, [–javascript=JAVASCRIPT] # Choose JavaScript approach [options: importmap (default), webpack, esbuild, rollup]

Rails 7: import-map-rails gem README(翻訳)

なお私はesbuildが速いという評判を見かけたのでRails 7でjsbundling-rails+esbuildにしてみました。

参考: Node.js のビルドツール「esbuild」について!
参考: [Web フロントエンド] esbuild が爆速すぎて webpack / Rollup にはもう戻れない | Kabuku Developers Blog

🔗 メモリリークとmemory bloat(Ruby Weeklyより)

memory bloatの日本語定訳が見当たらないので英ママとしました。


つっつきボイス:「メモリリークとmemory bloatの違いを解説するのかな?: メモリリークは完全にバグですが、memory bloatはメモリが非効率な状態になることですね」「なるほど」「実際はmemory bloatであるものをメモリリークと呼ぶのは、仕様をバグと呼ぶようなものかもしれませんね」

参考: メモリリーク – Wikipedia

後で探すとQuoraで見つかりました↓。

参考: (4) プロダクションの Rails サーバーの利用メモリがひたすら増加していくような挙動を観測したとき、どう対応するのがよいですか? – Quora

これは使われないメモリへの参照が残るバグとしてのmemory leakではなくて、ちゃんと開放してるにもかかわらずメモリ使用量が減らないmemory bloatだからです。
jp.quora.comより

「実際、Railsに限らず大規模なデータを扱うサーバーサイドアプリケーションではmemory bloatは多かれ少なかれ生じるものなので、これはもう仕方ないとも言えます」「そうなんですね…」「もちろんメモリ使用量を監視していればmemory bloatに気づけますが」

「記事ではいくつかmemory bloat検出用ツールが紹介されていますね↓」

brynary/rack-bug - GitHub

binarylogic/memorylogic - GitHub

noahd1/oink - GitHub

「ちなみに3つ目のoinkは英語でブタの鳴き声を表す擬音ですね」「へ〜」「もしかするとmemory bloatを監視・検出するツールの動作を、トリュフを見つけるブタの鳴き声になぞらえているのかなと想像してみました」「あ〜それありそう」

参考: oinkの意味・使い方・読み方|英辞郎 on the WEB

🔗 rails_same_site_cookie: RailsのSameSite cookieを自動設定

pschinis/rails_same_site_cookie - GitHub

参考: SameSite cookies – HTTP | MDN
参考: いまさら聞けないSameSite CookieとGoogle Chrome 80 | ecbeing


つっつきボイス:「私のRailsアプリ(5.2)でSameSite cookieを自力で設定するのが面倒だったんですが、以下の記事でこのrails_same_site_cookieを知って、作り中のRails 7アプリでこれを入れてようかなと検討中です」「ほほぉ〜」

「このgemを入れたらすぐ使える感じですね」「はい、これといったコンフィグなしで導入できて、期待どおりにローカル環境ではSameSite=Noneのcookieでsecureがオンになってくれました」「なるほど、Rackの機能を使わずに独自にcookieを設定しているようですね: この機能がRailsに公式に入ってもよさそうな気もしますが、この種のcookieのデフォルト設定は注意が必要なので、Rails本家にはすぐには入らないんじゃないかな🤔」「あ〜そうなのかも」

「Qiita記事でもcookieのテストをUser-Agentごとに書いてる↓: テストを書かないと振る舞いがよくわからなくて困りそうなcookieみたい」「う〜む、もう少し調べます」

# https://qiita.com/jnchito/items/1290a7b1dbf0ccf40c7b より
require 'rails_helper'

RSpec.describe 'Cookies', type: :request do
  describe 'Cookie の SameSite 属性' do
    before do
      user = create :user
      login_as user
    end
    it 'User-Agent指定無しの場合 SameSite=None がつく' do
      get new_user_session_path
      expect(response.headers['Set-Cookie']).to match /_your_app_session=.*SameSite=None/
    end

    it 'SameSite=Lax がデフォルトになる Chrome 80 では SameSite=None がつく' do
      mac_chrome_80 = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.42 Safari/537.36      '
      win_chrome_80 = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.16 Safari/537.36'

      get new_user_session_path, headers: { 'User-Agent' => mac_chrome_80 }
      expect(response.headers['Set-Cookie']).to match /_your_app_session=.*SameSite=None/

      get new_user_session_path, headers: { 'User-Agent' => win_chrome_80 }
      expect(response.headers['Set-Cookie']).to match /_your_app_session=.*SameSite=None/
    end

    it 'SameSite=Noneの扱いにバグがある iOS12 Safari では SameSite=None がつかない' do
      iphone_ios12_user_agent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1'
      get new_user_session_path, headers: { 'User-Agent' => iphone_ios12_user_agent }
      expect(response.headers['Set-Cookie']).to include '_your_app_session='
      expect(response.headers['Set-Cookie']).not_to include 'SameSite'
    end
  end
end

後で探すと、Railsへのプルリクを1件見つけましたがcloseしていました。上のテストにもあるようにSafariでSameSite=Noneが期待どおりに動かない問題があったようです。

参考: Add SameSite to Cookies by cfabianski · Pull Request #28297 · rails/rails — closed
参考: cookieのSameSite属性 おぼえがき – kasei_sanのブログ — Safariのバグについて書かれています

🔗 bullet gemがRails 7に対応

flyerhzm/bullet - GitHub


つっつきボイス:「bullet gemがRails 7で動くようになった🎉」「この間まで動きませんでしたが、さっきGemfileでコメント解除したら無事インストールできました」

🔗 mrujs: TypeScriptで書かれたujs

paramagicdev/mrujs - GitHub

参考: Form submit with turbo-streams response, without redirect – General – Hotwire Discussion — このコメントでmrujsを知りました


つっつきボイス:「Rails 7のフォームで確認ダイアログを出す方法を調べていて見つけました」「モダンujsと書かれているので、ujsの機能をTypeScriptで書き直した感じかな: 確認ダイアログのようなujsの機能をひととおり使えるらしい」「ちなみにjnchitoさんの記事↓のおかげで最終的にTurboで確認ダイアログを表示できたので、mrujsはとりあえず使わずに済みました」

参考: Rails 7.0 + Ruby 3.1でゼロからアプリを作ってみたときにハマったところあれこれ – Qiita

🔗 その他Rails

つっつきボイス:「RSpecによるテスト入門書『Everyday Rails』がRails 7に対応したそうです」「お〜、継続的にアップグレードされているんですね」「無料アップグレードできるのか〜」「期間限定で割引セールですって」「いつ買ったか思い出せないけど電子書籍で買ってたはず…(探す)あった、2014年にLeanpubで買ってた」

「電子書籍をいろんなサイトで買うと後から探すのが大変ですよね」「PDFやepubをどこからダウンロードしたかわからなくなる問題、あるある」


前編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Ruby 2.5〜3.1ベンチマーク、Opal 1.4、JRubyが20歳に、2022年のCSSほか(2022018後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: Webpackerが公式に引退宣言、『Everyday Rails』日本語版がRails 7に対応ほか(20220124前編) first appeared on TechRacho.

Rails UJS・Turbolinks -> Turboアップグレードガイド(翻訳)

$
0
0

概要

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

hotwired/turbo-rails - GitHub

Rails UJS・Turbolinks -> Turboアップグレードガイド(翻訳)

Turboは、従来Rails UJSが行っていたリンク変更やフォーム送信をXMLHttpRequestsに変える機能を置き換えます。Rails UJSおよびTurbolinksからTurboへの移行を完了するには、アプリケーションのconfig/application.rbconfig.action_view.form_with_generates_remote_forms = falseを設定する必要があります。しかし、すべてのアプリケーションが一挙に移行可能とは限りません。また、Rails UJSをTurboと共存させる必要が生じることもあるでしょう。そのために必要な手順を以下に示します。

1. Rails UJSでTurbo互換のセレクタを使うようにする

Rails UJSは、Railsフレームワークで直接提供されるか、さもなければ古いjquery-ujsプラグインによって提供されます。どちらもそのままにできますが、Turboと互換性のあるバージョンにアップグレードするか(#42476を参照)、アプリで必要なJavaScriptファイルをベンダリングして自分で調整する必要があります。

訳注

Rails UJSで書かれたアプリケーションをHotwireの新しいTurboフレームワークに移行する場合、移行の間(あるいは今後もずっと)TurboとUJSを共存させたい場合もあります。そのためには、フォーム送信の扱いを区別する手段が必要です。フォームでdata-turbo=trueを指定すると、Rails UJSはそのフォームを処理せずにTurboに処理を任せるようになります。
#42476より

2. Gemfile内のturbolinks gemをturbo-railsに置き換える

Gemfile内のgem 'turbolinks'を消し去ってgem 'turbo-rails'に置き換えたいことでしょう。しかし、UJSが発行する古いスタイルのXMLHttpRequestsがTurboでも動くようにするためには、古いTurbolinksの振る舞いを修正して、それらのリクエストをHTTP 302互換にする必要があります(Turbolinks呼び出しをTurbo呼び出しにする)。

まず、app/controllers/concerns/turbo/redirection.rbファイルを追加する必要があります。このconcernをApplicationController内でTurbo::Redirectionとしてincludeしてください。

module Turbo
  module Redirection
    extend ActiveSupport::Concern

    def redirect_to(url = {}, options = {})
      turbo = options.delete(:turbo)

      super.tap do
        if turbo != false && request.xhr? && !request.get?
          visit_location_with_turbo(location, turbo)
        end
      end
    end

    private
      def visit_location_with_turbo(location, action)
        visit_options = {
          action: action.to_s == "advance" ? action : "replace"
        }

        script = []
        script << "Turbo.clearCache()"
        script << "Turbo.visit(#{location.to_json}, #{visit_options.to_json})"

        self.status = 200
        self.response_body = script.join("\n")
        response.content_type = "text/javascript"
        response.headers["X-Xhr-Redirect"] = location
      end
  end
end

次に、test/helpers/turbo_assertions_helper.rbファイルを追加する必要もあります。このヘルパーをActionDispatch::IntegrationTestincludeしてください。

module TurboAssertionsHelper
  TURBO_VISIT = /Turbo\.visit\("([^"]+)", {"action":"([^"]+)"}\)/

  def assert_redirected_to(options = {}, message = nil)
    if turbo_request?
      assert_turbo_visited(options, message)
    else
      super
    end
  end

  def assert_turbo_visited(options = {}, message = nil)
    assert_response(:ok, message)
    assert_equal("text/javascript", response.try(:media_type) || response.content_type)

    visit_location, _ = turbo_visit_location_and_action

    redirect_is       = normalize_argument_to_redirection(visit_location)
    redirect_expected = normalize_argument_to_redirection(options)

    message ||= "Expected response to be a Turbo visit to <#{redirect_expected}> but was a visit to <#{redirect_is}>"
    assert_operator redirect_expected, :===, redirect_is, message
  end

  # 「Content-Typeがtext/javascriptの非GETリクエスト」かどうかという
  # 簡単なヒューリスティックでTurbolinksのリクエストを検出する
  #
  # 技術的にはTurbolinks-Referrerリクエストヘッダが設定されていることも
  # チェックするが、そのためには`turbo:`オプションを指定して
  # POSTやPATCHなどのテストメソッドのヘッダーをオーバーライドして渡す
  # 必要がある
  #
  # `request.xhr?`チェックは利用できない
  # (X-Requested-Withヘッダーは、後続のリクエストで漏洩しないよう
  # コントローラのアクション実行後にクリアされるため)
  def turbo_request?
    !request.get? && (response.try(:media_type) || response.content_type) == "text/javascript"
  end

  def turbo_visit_location_and_action
    if response.body =~ TURBO_VISIT
      [ $1, $2 ]
    end
  end
end

3. packファイルに含まれるTurbolinksをTurboに置き換える

おそらくpackファイルにrequire("turbolinks").start()のような記述があると思いますが、これをimport "@hotwired/turbo-rails"に変更する必要があります。Turboはインポート時に自動的に起動されてwindow.Turboに割り当てられるので、何も起動する必要はありません。

4. Turbolinks名前空間をすべてTurbo名前空間に置き換える

  • document.addEventListener("turbolinks:before-cache" ...)のようなイベント名にあるTurblinks名前空間は、Turboのturbo:before-cacheに置き換える必要があります。
  • 同様に、Turbolinks.visit呼び出しもTurbo.visitに置き換える必要があります。

  • DOM要素のdata-turbolinks-actionなどの属性も、data-turbo-actionのように置き換える必要があります。

  • data: { turbolinks: false }もすべてdata: { turbo: false }に置き換えることを忘れずに。

5. オプション: モバイルアダプタ向けの後方互換shimを提供する

Turbolinksモバイルアダプタを用いて構築したネイティブアプリも移行する必要がある場合は、Turbolinksの呼び出しをTurbo呼び出しに変換するshimが必要になるでしょう。Basecampでは以下のshimが必要でした。

// モバイルアプリ向けの互換性shim
window.Turbolinks = {
  visit: Turbo.visit,

  controller: {
    isDeprecatedAdapter(adapter) {
      return typeof adapter.visitProposedToLocation !== "function"
    },

    startVisitToLocationWithAction(location, action, restorationIdentifier) {
      window.Turbo.navigator.startVisit(location, restorationIdentifier, { action })
    },

    get restorationIdentifier() {
      return window.Turbo.navigator.restorationIdentifier
    },

    get adapter() {
      return window.Turbo.navigator.adapter
    },

    set adapter(adapter) {
      if (this.isDeprecatedAdapter(adapter)) {
        // 古いモバイルアダプタはvisitProposedToLocation()をサポートしない
        adapter.visitProposedToLocation = function(location, options) {
          adapter.visitProposedToLocationWithAction(location, options.action)
        }

        // 古いモバイルアダプタはvisit.location.absoluteURLを利用するが、
        // TurboではLocationクラスを廃止してDOM URL APIに変えたので利用できない
        const adapterVisitStarted = adapter.visitStarted
        adapter.visitStarted = function(visit) {
          Object.defineProperties(visit.location, {
            absoluteURL: {
              configurable: true,
              get() { return this.toString() }
            }
          })

          adapter.currentVisit = visit
          adapterVisitStarted(visit)
        }
      }

      window.Turbo.registerAdapter(adapter)
    }
  }
}

// デスクトップアプリによってrequireされる
document.addEventListener("turbo:load", function() {
  const event = new CustomEvent("turbolinks:load", { bubbles: true })
  document.documentElement.dispatchEvent(event)
})

関連記事

Rails 7: import-map-rails gem README(翻訳)

速報: Basecampがリリースした「Hotwire」の概要

The post Rails UJS・Turbolinks -> Turboアップグレードガイド(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: Rubyコンパイラの歴史動画、RubyのWebAssembly対応進む、ぼっち演算子の注意点ほか(20220126後編)

$
0
0

こんにちは、hachi8833です。昨晩体調を崩して銀座Rails#41に参加できず…😢

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 RuboCopのRuby 3.1対応


つっつきボイス:「お〜、例のハッシュ値省略記法がRuboCopに追加された」「RuboCopのデフォルトは省略するスタイルになったんですね」「こうやって目を慣らしていくと新しい記法が使われるようになるかな」

# 同記事より
# EnforcedShorthandSyntax: always (default)

# bad
{foo: foo, bar: bar}

# good
{foo:, bar:}


# EnforcedShorthandSyntax: never

# bad
{foo:, bar:}

# good
{foo: foo, bar: bar}

参考: Hashの値の省略記法 – NaCl非公式ブログ

&だけで書ける無名ブロックの委譲、初めて見るとちょっと驚くかもしれないけど個人的には割と好き」「&にブロックが入ると思えばいいのかな」

# 同記事より
# EnforcedStyle: anonymous (default)

# bad
def foo(&block)
  bar(&block)
end

# good
def foo(&)
  bar(&)
end

# EnforcedStyle: onymous

# bad
def foo(&)
  bar(&)
end

# good
def foo(&block)
  bar(&block)
end

参考: プロと読み解く Ruby 3.1 NEWS – クックパッド開発者ブログ

🔗 ruby-nextがRuby 3.1に対応(RubyFlowより)

ruby-next/ruby-next - GitHub


つっつきボイス:「以前もウォッチ20191204で取り上げたruby-nextもRuby 3.1の新構文に対応したそうです」「ruby-nextは古いRuby構文に変換するポリフィルですね」

参考: Polyfill (ポリフィル) – MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

🔗 RubyとApache Arrow


つっつきボイス:「Red Data ToolsプロジェクトでRuby 向けのApache ArrowライブラリRed Arrowを手掛けているSpeeeの村田賢太さんと、クリアコードの須藤功平さんによる公開論文だそうです」「お〜興味深い内容: 情報処理学会のような学術団体がこういうのを公開するのは大事ですね」

参考: Red Data Tools – Rubyでデータ処理!
参考: Apache Arrowのご紹介 – クリアコード

🔗 VSCode向けSorbet拡張機能がリリース


つっつきボイス:「SorbetのVSCode拡張機能をリリースしたのは、やはりSorbetを作ったStripeですね」「あ、ほんとだ」

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

🔗 Rubyのぼっち演算子の注意点(Ruby Weeklyより)


つっつきボイス:「safe navigation operator(&.)は、ぼっち演算子のことですね」「ぼっち演算子に気をつけようみたいな記事かな」「ぼっち演算子は個人的にあまり使わないようにしてます」

「お、こんな罠が: 値が両方nilだとtrueが返ってしまう↓」「うわ、怖い」「Rubyではnil == nilがtrueになるからこうなりますね」「これは気をつけないと」

# 同記事より
  def owner?(explorer)
    explorer&.id == owner_id    # explorerとowner_idが両方nilだとtrueが返される
  end

Rubyのぼっち演算子はRailsの`Object#try`より高速(翻訳)

🔗 gemをアップデートしたら不要な課金が大量に発生(Ruby Weeklyより)


つっつきボイス:「gemをアップデートした直後に、普段はほとんど動きのなかったユーザーへの課金額が急上昇したそうです↓」「これは怖い」「即死…」「記事の最後は平謝りですね」


同記事より

「急いでStripeのAPIをロックしてgemをロールバックし、不要な課金をされた人に返金することで対応したんですね」「課金を扱うシステムはほんとに要注意: それと課金額もチェックするのが重要」「あ、たしかに」「Mongoid gem(Ruby向けMongoDBドライバ)をアップデートしたらorの挙動が変わって、本来不要な課金がStripeに送信されてしまったのか…」「うわー」

🔗 RubyのWebAssemblyのWASI対応が進む(Publickeyより)

元記事: RubyがWebAssemblyのWASI対応へ前進。ブラウザでもサーバでもエッジでもどこでもWebAssembly版Rubyが動くように - Publickey


つっつきボイス:「そういえばPublickeyに掲載されていたこのWebAssemblyの記事もよかった↑」「あ、st0012さんに以下のコミット↓があることを教わっていたのに危うく貼り忘れるところでした」

参考: Feature #18462: Proposal to merge WASI based WebAssembly support – Ruby master – Ruby Issue Tracking System

「記事は『これは何を意味するのでしょうか?』などを丁寧に解説していいですね👍」「WASI向け変更をコミットしたkateiさんという方がこのたびRubyコミッターに加わったそうです」「おめでとうございます🎉

つっつき後に以下の記事が公開されていました。

🔗 Rubyアソシエーションの開発助成金

「そういえば昨年のRubyアソシエーション開発助成金でWasm対応のプロジェクトがありましたよね」「ほんとだ、『MRIのWebAssembly対応よるポータブルなRubyプログラムの実現』というエントリですね」

参考: 2021年度Rubyアソシエーション開発助成金 公募結果発表

「開発助成金の成果が実ったのが素晴らしい」「Rubyアソシエーションに寄付をする意義を実感できますね」「だいぶ前のRubyWorld Conferenceのときに自分も現地で寄付しました: 現地入りは寄付をする機会になりやすいと思います」「早く現地参加できるようになるといいですよね」

「ところで、GitHub Sponsors経由でもRubyアソシエーションに寄付できるといいな」「あ、たしかに寄付の動線が複数ある方が寄付しやすくなりますよね、GitHub Sponsors経由ならリポジトリにバッジも表示されますし」「もちろん今もRubyアソシエーションに寄付すれば芳名帳に掲載されます」

参考: GitHub Sponsors

🔗 Rubyコンパイラの歴史動画(Ruby Weeklyより)


つっつきボイス:「例のシェイプ記事やスタンプ記事↓の翻訳でお世話になったShopifyのChris Seatonさんが、昨年11月にコロラド州デンバーで開催されたRubyConf 2021で発表した動画だそうです」「なるほど、Rubyインタプリタ内で行われるコンパイラの歴史をたどる動画ですね: これはいい👍」「コアな話が面白そう」「歴史を把握している人が解説してくれているのがありがたいですね: 後でちゃんと見よう」

おそらく以前もウォッチで紹介した以下のサーベイを下敷きにしていると思われます↓。

参考: The Ruby Compiler Survey

Rubyオブジェクトの未来をつくる「シェイプ」とは(翻訳)

Rubyの整数オーバーフローチェックを「スタンプ」で一掃する(翻訳)

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

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

🔗 AWSがRuby SDKコード生成ツールsmithy-rubyをdeveloper preview公開(Ruby Weeklyより)

awslabs/smithy-ruby - GitHub

smithing
{名} : 鍛造、金属工芸


つっつきボイス:「AWSがSmithyのRuby版をdeveloper previewで公開したそうです」「公式サイト↓を見ると、SmithyはIDL(インターフェイス記述言語)を書くとそれを元に各種言語向けのコードを生成してくれるツールのようですね」

// awslabs.github.io/smithy/ より
namespace example.weather

service Weather {
    version: "2006-03-01",
    resources: [City],
    operations: [GetCurrentTime]
}

resource City {
    identifiers: { cityId: CityId },
    read: GetCity,
    list: ListCities,
    resources: [Forecast],
}

参考: インタフェース記述言語 – Wikipedia

「AWS SDKはこのSmithyで作られているんじゃないかな」「そのようですね」「IDLで定義を書いてツールにかけるとさまざまな言語向けのマッパーを一気に生成するというのは、Javaなどでも複数言語対応や複数プロトコル対応などによく使われています」

参考: AWS SDK とツール
参考: Java IDL入門

「SmityはAWSがこれまで内部で使っていたものをオープンソースとして公開したんでしょうね: AWS内部での開発にはこの種のツールが不可欠なはずなので」「あれだけAPIがたくさんあるとツールは必要でしょうね」

「AWSがかつてすべてをAPI化すると決めたときに、APIは厳守する代わりにAPI内部の実装には何を使ってもよいと宣言したんですよ」「へ〜!」「だからこそIDLからコードを生成するツールが不可欠」「API内部の言語やフレームワークを問わないというのは開発者にとってはありがたいですね」「この種のツールはいろいろありますが、AWSの中で使われているものがオープンソース化されたというのは自分たちが使うかどうかは別にして面白いですね」

参考: The Secret to Amazons Success Internal APIs
参考: ASCII.jp:なぜAmazonはマイクロサービスに舵を切ったのか?


「ちなみに、AWSとは別にRuby向けのGemsmithとRubysmithというプロジェクトもあります↓」

bkuhlmann/gemsmith - GitHub

bkuhlmann/rubysmith - GitHub

🔗 AWS LambdaがES Modulesをサポート開始(Publickeyより)


つっつきボイス:「なるほど、AWS Lambdaでパッケージ化されたES Modulesもインポートできるようになったんですね」「今まではCommonJSのみだったのか」

参考: JavaScript モジュール – JavaScript | MDN
参考: CommonJS – Wikipedia

「こういうふうに拡張子が.mjsのES Modulesもバンドルして読み込めるようになったのね↓」

// https://aws.amazon.com/jp/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/ より
// this file is named index.mjs – it will always be treated as an ES module
import { square } from './lib.mjs';

export async function handler() {
    let result = square(6); // 36
    return result;
};

// lib.mjs
export function square(x) {
    return x * x;
}

「こちらが従来のCommonJSの読み込みか↓: 今後は拡張子が.cjsだとCommonJSのモジュールとして扱われるので、同じバンドルzipファイルにES ModulesとCommonJSを両方含められるようになる」「なるほど」

// https://aws.amazon.com/jp/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/ より
// CommonJS require syntax
const { SSMClient, GetParameterCommand } = require("@aws-sdk/client-ssm");

const ssmClient = new SSMClient();
const input = { "Name": "/configItem" };
const command = new GetParameterCommand(input);
const init_promise = ssmClient.send(command);

exports.handler = async () => {
    const parameter = await init_promise; // await inside handler
    console.log(parameter);

    const response = {
        "statusCode": 200,
        "body": parameter.Parameter.Value
    };
    return response;
};

「そしてES Modulesの方が速いらしい↓」「お〜」


Using Node.js ES modules and top-level await in AWS Lambda | AWS Compute Blogより

🔗JavaScript

🔗 Stimulusのベストプラクティス


つっつきボイス:「最近Stimulusが気に入ってしまって、またStimulus記事を拾いました」

「あとこちらのGlitchは、Stimulusの公式ドキュメントからリンクされていたWeb IDE環境で、Stimulusも使えるそうです↓」「NodeやReactやSQLiteなどはトップページでフィーチャーされているのにStimulusはなぜかフィーチャーされていませんけどね☺

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

🔗 AVIFフォーマット


つっつきボイス:「AVIFという画像ファイル形式は初めて見る」「AV1フォーマットのIフレームを静止画として表示するとは!」

AVIF は、ロイヤリティフリーなコーデックを目指して AOMedia が開発した AV1 を、画像に転用したものだ。AV1 Image File Format の略とされ、AV1 の I フレームを静止画として表示するイメージだ。
同記事より

参考: AV1 – Wikipedia
参考: 次世代型画像ファイル形式「AVIF」とは?概要やメリット・デメリット、将来性などを解説! | 株式会社ipe (アイプ)

「記事の中でGoogleのWebPフォーマットにも触れていますけど、WebPはもうだいぶ普及しているんでしょうか?」「直接使うことについてはわからないけど、アセットのコンパイルなどではサポートされていますね」

参考: WebP – Wikipedia

「ここでサンプル画像を見られる↓」

参考: WebP DEMO | labs.jxck.io

「AVIFだと元画像よりサイズが小さくなってますね」「お〜AVIFのlossyなサンプルではWebPとAVIFで画像に違いがあるし、しかもAVIFはアニメーションもできるのか」「アニメーションの圧縮率も高い!」

「従来のGIFアニメーションは効率もよくないし色数も制約されるし、今となってはいにしえの規格でしょうね」「たしかに」

参考: GIFアニメーション – Wikipedia

🔗言語/ツール/OS/CPU

🔗 Intel CPUとAMD CPU


つっつきボイス:「そうそう、この記事見た: libffiがマップされているはずが、AMDでRubyを動かすとsasscがマップされていたという」「IntelのAVX-512の命令が、そのとき使っていたAMD64に入っていなかったのか…」「はてブでもこんなコメントが付いていた↓: CPUアーキテクチャをまたがるとたまにこういうことが起きますね」

Intel CPUとAMD CPUの混在による問題に直面した話 | 開発ブログ | Elastic Infra

sasscはなぜかビルドオプションにデフォルトで -mtune=native つけてて狂ってるんだよな〜。まだそうだったのか

2022/01/16 07:57

sass/sassc - GitHub

「sassのissuesには上がってないみたい」「sasscのREADMEを見たら、sasscは非推奨になったのでDart Sassを使うようにと書かれていますね」

参考: Sass: Dart Sass

🔗 Vim作者のインタビュー


つっつきボイス:「Vimの作者ってこういう方なんですね」「意外に技術的な話は少なくて、最後に『夜ふかしすると生産性が落ちるよ』と書かれていました」「まあそうですけど😆」「Vimの強さについて語りたおすとかではなく?」「もともとお金にするつもりもなかったようで、意外に淡々とした調子でした」


後編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Webpackerが公式に引退宣言、『Everyday Rails』日本語版がRails 7に対応ほか(20220124前編)

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

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

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ: Rubyコンパイラの歴史動画、RubyのWebAssembly対応進む、ぼっち演算子の注意点ほか(20220126後編) first appeared on TechRacho.

Rails 7: data-turbo-confirmはformタグに書く

$
0
0

Rails 7で、フォームの送信ボタンをクリックしたときにTurboで確認ダイアログを出す方法がなかなかわからず、すっかりはまってしまいました。

結論から言うと、Rails 7とTurboでは、data: { turbo_confirm: 'Are you sure?' }は以下のようにform_withのオプションに渡すと表示されるようになります。

<!-- Turboによる確認ダイアログが出る -->
 <%= form_with(url: check_path, data: { turbo_confirm: 'Are you sure?' }) do |form| %>
  <%= text_area_tag(
    :typotext,
    @received_string,
    class: 'form-control font-monospace'
    ) 
  %>

  <%= form.submit '送信',
    class: "btn btn-primary"
  %>
<% end %>

このときのHTMLは以下のとおりです。

<form data-turbo-confirm="Are you sure?" action="/check" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="vDRTAkY01fGxRxRd3iQtv3xfQPie28H1VPFqQjV85FwrF7P7rYQIaY152zJVOzhH7eWFmRqKW3RgQtooBsmRTg" autocomplete="off" />
      <textarea name="typotext" id="typotext" " class="form-control font-monospace col-lg mb-3" >
</textarea>

      <input type="submit" name="commit" value="送信" class="btn btn-primary" data-disable-with="送信" />
</form>

以下のように送信ボタンのform.submitdata: { turbo_confirm: 'Are you sure?' }を付けても効きませんでした。ujsのときはここにdata: { confirm: 'Are you sure?' }を書けばよかったのですが。

<!-- Turboによる確認ダイアログが出ない -->
 <%= form_with(url: check_path) do |form| %>
  <%= text_area_tag(
    :typotext,
    @received_string,
    class: 'form-control font-monospace'
    ) 
  %>

  <%= form.submit '送信',
    class: "btn btn-primary",
    data: { turbo_confirm: 'Are you sure?' }
  %>
<% end %>

この書き方に気づいたのは、以下のStack overflowの記事でした(質問内容は直接関係ありません)。

参考: New Rails 7 Turbo app doesn’t show the data-turbo-confirm alert messages don’t fire (turbo-rails 7.1.0 and 7.1.1) – Stack Overflow


参考: Rails 7.0 + Ruby 3.1でゼロからアプリを作ってみたときにハマったところあれこれ – Qiita

jnchitoさんの上の記事にあるように、link_tobutton_toの場合は、少し工夫は要るものの、data: { turbo_confirm: 'Are you sure?' }を追加できます。

しかしform.submitの場合は、送信ボタンではなく、form_withに渡す必要があります。なお、submit_tagform.buttonの場合も同様にform_withに渡さないと効きませんでした。

参考: Rails7 で button_to の data-confirm の挙動が変わって data.confirm から form.data.confirm に変わってました – 好きなものだけ書く。ポジティブに。


なお、data-turbo-confirmはTurbo 7.1.0から利用可能になっていました。

参考: Add data-confirm behavior to turbo by virolea · Pull Request #379 · hotwired/turbo

サンプルコードでは<form>タグにdata-turbo-confirm="Are you sure?"属性が追加されているので、これが正しいようです。

<form action="/action" method="post" data-turbo-confirm="Are you sure?">
  <!-- ... -->
</form>

関連記事

Rails 7 : rails newのフロントエンド関連オプションの組み合わせを調べてみた

The post Rails 7: data-turbo-confirmはformタグに書く first appeared on TechRacho.

Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)

$
0
0

概要

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


whitefusion.ioより

画像は元記事からの引用です。

  • 2018/04/18: 初版公開
  • 2022/01/20: 更新

Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)

近年、RailsアプリにService Objectを追加するメリットを説く記事が次から次へと量産されています。私は本記事において、それがなぜ正しくないかを述べたいと思う次第であります。もっとよい方法はあるのです。

私はこれまで、Service Objectに関するネット上の議論にときおり参加しては、問題に対するまっとうな解決方法としてService Objectが正しくない理由について繰り返し見解を述べてきました。実際、私は多くの場合においてService Objectよりもっとよい解決方法があると考えるのみならず、Service Objectはオブジェクト指向設計原則への配慮が損なわれている兆候を示すアンチパターンとして取り扱っています。

Service Objectを追加したところであんたのRailsコードベースが今より良くなることはねえと言ったらどうするよ?

Service Objectを追加したところで
あんたのRailsコードベースが今より良くなることはねえと言ったらどうするよ?

このような深遠なポイントを細切れのツイートやコメント欄を追って理解するのは大変です。そこで私は、私の見解を正確に表すいくつかの現実的なコードを詳しく追って記事にすることにしました。

私が使う「アンチパターン」という用語については、StackOverflowにうまい説明があったので引用します。

「アンチパターン」とは、ソフトウェア開発においてプログラミング上の悪手と考えられる特定のパターンのことです。デザインパターンが、よい開発手法と一般的に考えられている定型化された共通のアプローチであるのと対照的に、アンチパターンは望ましくないものです。
https://stackoverflow.com/a/980616より

私がService Objectを好きになれない理由を説明するために、昔ある顧客のプロジェクトで当時の開発チームが書いたコードを私が継承した中から、いくつかを詳しく見ていくことにします。そのアプリは未だにpublic betaなので当時の状況についてはあまり立ち入ったことは書けませんが、メディア(画像や動画)にレーティングするソーシャルプラットフォームであり、レーティングは特定のコールバックスタイルの操作(データ処理のアルゴリズム更新や、さまざまなユーザーのタイムラインへのアクティビティの追加など)でトリガされる、とだけ言っておきましょう。

比較的シンプルなデータモデルが1つあり、UserオブジェクトとMediaオブジェクトにbelongs_toで属しているRatingオブジェクトをそこに作成できます。以下のコード例はいずれもproductionファイルそのままではなく、抜粋です。

class Rating < ActiveRecord::Base
  belongs_to :user
  belongs_to :media
end
class Media < ActiveRecord::Base
  has_many :ratings
end

以下を見ればおわかりのように、前任の開発者は、ユーザーから届くレーティングを扱うのにMediaRatingというService Objectをこしらえたのです。これはコントローラから呼び出されます。

class MediaRating
  def self.rate(user, media, rating)
    mr = MediaRating.new(user)
    rating_record = mr.update_rating(media, rating)
  end

  def initialize(user)
    @user = user
  end

  def update_rating(media, rating)
    rating_record = @user.ratings.where(media: media).first
    if rating_record.nil?
      # 何か作成する
    else
      # 何か更新する
    end

    # データのアルゴリズム処理の実行やら
    # タイムラインでのソーシャル活動の追加やらを行う
  end
end

対応するコントローラのコードは次のとおりです。

media = Media.find(params[:media_id])
rating = params[:rating].to_i
MediaRating.rate(current_user, media, rating)

このコードが最初に書かれたのはかなり前であることを念頭に置いてください。最近のService Object愛好者たちなら、提示するAPIをもう少し形式的に書くので、このService Objectを私が書き直してもよいのであれば、きっとこう直すことでしょう。

# これはGemfileに追加
gem 'smart_init'

class UserMediaRater
  extend SmartInit

  initialize_with :user, :media, :rating
  is_callable

  def call
    rating_record = @user.ratings.where(media: @media).first
    # etc.
  end
end

# コントローラからの更新済みコマンド
UserMediaRater.call(current_user, media, rating)

問題が始まるとき

私のコードは満更でもありませんよね?そこそこクリーンですし、よく構造化され、楽にテストできます。私がService Objectを書き直したコードを今皆さんが見ているわけですが、問題はこの洗練されたコードが実際より相当簡略化されたものだという点です。コードベース内の実際のコードは74行ものスパゲッティコードになっていて、メソッドが別のメソッドを呼び、それがまた別のメソッドを呼んでいるという具合です。というのも、データのアルゴリズム処理だのタイムライン更新だのが全部1個のService Objectに押し込められていたからです。実際のフローは以下のような具合でした。

コントローラ > Service Object > Rateメソッド > Ratingの更新 > 何か別の更新メソッド + (アルゴリズムの実行 > 関連データの更新)、そしてキャッシュの無効化 + アクティビティをタイムラインに追加

そんなわけで、私がコードベースをエディタで新たに開いて、ユーザーがレーティングしたメディアオブジェクトの作成や更新を行っているコードのブロックをちょっと見ようとするたびに、本質的でない大量の付加機能をかき分けて基本のコードパスにたどり着かなければなりませんでした。

「ちょっとちょっとぉ、さすがにその開発者のService Objectの書き方はいくらなんでもマズいでしょ!他のオブジェクトに(おそらく他のService Objectであっても)そうやって処理を押し込めるんじゃなくて、もっとシンプルかつ対象を絞り込んで書けばよかったのに」とおっしゃる方もいるでしょう。

まあまあ、もうしばらくお付き合いください。標準的なRails MVCパターンに含まれるコードを抽出してService Objectに置く必要があると言われていた本来の理由は、要するに複雑なコードフローを単独の関数に分割しやすくなるからだということでした。しかしここで問題なのは、ルールを強制するものが何もないということです。これっぽっちもないんです!シンプルなService Objectを書くことは間違いなく誰でもできます。しかし、メソッドをたらふく抱え込んであっという間にスパゲッティコード化してしまう複雑なService Objectも、同じぐらい間違いなく誰にでも書けるのです。

私が何が言いたいかおわかりでしょうか?つまり本質的にService Objectパターンそのものには、コードベースを読みやすくする力も、メンテしやすくする能力も、関心を適切に分離する手腕もありはしないということです。

あるパターンが、シンプルなものから複雑なものまでほぼ無限に、ほぼあらゆる種類のプログラミングスタイルを無節操に許容して促進するのであれば、そんなものは開発者にとって最早有用なパターンでも何でもありません。

ではどうすればいいんでしょうかね?

私がそこそこの量のコードを書き始めるときは(データを受け取って、他の機能に配慮しながら作成なりレコード更新なりを行わなければならないぐらいの規模感であれば)、大抵の場合最も適切なモデル上に(訳注: わざと)クラスメソッドを1つ書くところから始めます。まま、そう慌てないで。これがService Objectの上を行く優秀なパターンと言いたいのではありません。「ここにうまくはまるパターンがないかなぁ」と私があれこれ考えるよりも前にこれを行っている点にご注目いただきたいのです。

もし仮に、Rating自身のクラスメソッドを用いてメディアのレーティングを行っていたとしたら果たしてどうなっていたか、見てみましょう。

class Rating < ActiveRecord::Base
  belongs_to :user
  belongs_to :media

  def self.rate(user, media, rating)
    rating_record = Rating.find_or_initialize_by(user: user, media: media)
    rating_record.rating = rating
    rating_record.save

    # データのアルゴリズム処理の実行やら
    # タイムラインでのソーシャル活動の追加やらを行う
  end
end

コントローラのコードはこんな感じに書き直します。

media = Media.find(params[:media_id])
rating = params[:rating].to_i
Rating.rate(current_user, media, rating)

私はこのコードを目にして、やっと安堵の息をつくことができました。というのも、レーティングのコードが直接Rateモデルに置かれていることで、この機能がコードの影響を最も強く受けるデータ構造に近い場所に配置されているからです。このコードベースをエディタで開いてレートの処理を見たければ、Rateモデルを見ればよいのです。実に素直なコードです。

とはいうものの、このコードにはまだ1か所、非常にうれしくない点があります。要は、私は(クラスメソッドではなく)インスタンスメソッドを呼ぶことと、可能な限りRailsの関連付けを使うのが好きなのです。今のコードは私にとって、あたり一面クラスメソッドだらけになる「コードの匂い」と、本来意図されている関連付けや標準的なOOP原則を回避したがる「コードの匂い」がプンプン漂ってきます。この場合、コントローラで@media.rateなどと書けないなんて残念です。要するに私はメディアのオブジェクトを立ち上げてそれをレーティングしたいのです。こんな明確なインターフェイスをなぜ使えないんだと思うわけです。

真の友はconcernとPORO

次はモデルのクラスメソッドから複雑なコードを切り出す必要があると確信できたら、ガラクタをモデルのインスタンスメソッドに押し込めるようなパターンなんかではない、もっとよいパターンを見つけたいと思います。いわゆる「ファットモデル」に伴う問題とは、突き詰めれば、分割したコードの置き場所としてみんなが真っ先にService Objectを推奨するのは一体なぜか、ということです。

しかし、現実にはファットモデルのデメリットは、1個のオブジェクトに大量のメソッドを書くことのデメリットに比べればたかが知れています。後者は、そうした大量のメソッドが(そしておそらく関連する単体テスト)1つのファイルでひしめき合うことです。しかし、現実には単一のオブジェクトが多くのメソッドを持つ「ファットモデル」のデメリットはさほどではなく、そうした多くのメソッドは(そしておそらく関連する単体テストも)すべて1つのファイルにまとまります。本当に必要なのは、ある重要な機能のコード片が、別の重要な機能のコード片に干渉しないようにしてコードを理解しやすくするための手法であり、そしてどのコード片を別のオブジェクトに再配置してまとめるかという何らかの経験則なのです。

さて、このメディアレーティング業務で何ができそうかちょっと見てみましょう。私なら最初に、立ち向かうべきコードのひと塊をconcernに切り出すでしょう(concernとは、標準的なRubyのmixinをRailsで少々拡張しただけのものです)。このconcernにRatableという名前を付けましょう。

module Ratable
  extend ActiveSupport::Concern

  included do
    has_many :ratings
  end

  def create_or_update_user_rating(user:, rating:)
    rating_record = ratings.find_or_initialize_by(user: user)
    rating_record.rating = rating
    rating_record.save

    # データのアルゴリズム処理の実行やら
    # タイムラインでのソーシャル活動の追加やらを行う

    rating_record
  end
end

このMediaクラスにも嬉しい点があります。has_many :ratingsディレクティブを取り出してこの新しいconcernに置いておけるからです。

class Media < ActiveRecord::Base
  include Ratable
end

コントローラのコードはこんな感じに書き直します。

rating = params[:rating].to_i
Media.find(params[:media_id]).create_or_update_user_rating(
  user: current_user,
  rating: rating
)

既にかなり改善された感がありますね。後はコントローラでメディアのオブジェクトを検索して、わかりやすい名前のインスタンスメソッドを1つ呼び出せばおしまいです。可能な限り最善の手法によってRailsらしさにあふれた、親しみやすいインターフェイスです。

しかしまだ問題が1つ残っています。
このcreate_or_update_user_ratingメソッドが頑張りすぎています。ここではデータベースアクセスを扱うのが自然ですが、データのアルゴリズム処理だのタイムラインの更新だのは、結果が出てからトリガされるべきですし、別の場所で定義されるべきアクションのように思えます。

標準のRails wayなら、このコードをActiveRecordのコールバックに置くところでしょう。コールバックに置くのは別に問題ありませんし、うまくはまりそうなら私は喜んで使います。しかしこの場合は、主に行うべき2つの機能が、それと隣合わせになっている特定のメディア、レーティング、ユーザーオブジェクトと関連しているだけで、それ以外に何の関連もないように思えます。

そこで、この機会にドメインモデルを少々適用し、余分な機能をconcernからさらに切り出して別のPORO(pure old Ruby object)に切り出してみましょう。create_or_update_user_ratingメソッドは、これらの新しいオブジェクトを指すようにして小ざっぱりかつシンプルにします。

  def create_or_update_user_rating(user:, rating:)
    rating_record = ratings.find_or_initialize_by(user: user)
    rating_record.rating = rating
    rating_record.save

    # 追加機能をPOROまたは関連するモデルに切り出しましょう
    # バックグラウンドジョブにカプセル化すれば
    # さらに改善できますね
    # 読者の練習用に取っておきます
    Rating::Processor.run(rating_record)
    Timeline::Activities.add_for_rating(rating_record)

    rating_record
  end

もはや考えるまでもなく、Rating::ProcessorTimeline:: ActivitiesもService Objectではありません。これらはPOROであり、OOPパラダイムに注意深く配慮しながらモデリングされています。オブジェクトの1つは私が「Processorパターン」と呼んでいるもので、入力を取って処理し、どこかに出力を保存します。もう1つは「Collectionパターン」で、項目の追加や削除と、それらのアクションの結果を管理します。ここでは特別なことや独自の技は何ひとつ使われていません。しかしそこが重要なのです。

ここで代わりにService Objectパターンの利用を試みることもやろうとおもえばやれました。その場合おそらく、UserMediaRaterをリファクタリングしてProcessNewRatingAddTimelineActivityForRatingなどのService Objectを呼び出す形になるでしょう。しかし、concernとPOROの読みやすさと良い構造化にどれだけ太刀打ちできるでしょうか?本質的には関数であるものがぎっしり詰まったapp/servicesフォルダを見て心が折れるくらいなら、クラス名とデータ構造と、そして読みやすく使いやすい設計のオブジェクトメソッドを備えた本物のドメインモデリングを手にすればよいのです。

最後に申し上げたいポイントは「Service ObjectではなくconcernとPOROを使うことで、インターフェイスが改善され、関心(concern)が正しく分離され、OOP原則が健全に使われるようになり、コードを把握しやすくなる」です。

テストの戦略について語る余地がなくなってしまいましたが、concernやより高度なPOROを使うと、テストでService Objectとはまた別の問題が持ち上がるのではないかとご心配な方のために、有用なリソースをいくつかご紹介します。

Railsのconcernをモデルレベルやコントローラレベルで有用なPOROパターンと組み合わせることが、こんなときに使われがちなService Objectと比べていかに優れているかについてはお話したいことがいくらでもありますので、今後の記事をぜひお楽しみに。

忙しい方向けのまとめ: Service Objectはアカンやつであり、ほとんどの場合もっとよいソリューションがあります。どうかそちらをお使いください。お読みいただきありがとうございました!

(罵倒でない、十分吟味した)ご意見・ご感想は@jaredcwhiteまでどうぞ 😊

お知らせ

Intersectは、Whitefusion社が提供するJekyllベースのオープンなWebブログです。弊社についての詳細や弊社の目指すものについては会社概要をご覧ください。

付録: 社内Slackより

ServiceObjectに複雑なロジックを委譲(切り取り&ペースト)だけしてもアカン。PORO設計などのモデリングをしないとキレイにならないよと言っていそう。


私がよくやるのはこれかな: 「標準のRails wayなら、このコードをActiveRecordのコールバックに置くところでしょう。コールバックに置くのは別に問題ありませんし、うまくはまりそうなら私は喜んで使います。」
よくオブジェクトAのafter_saveでオブジェクトBのデータを更新してオブジェクトBのafter_saveにオブジェクトAのデータ更新が書いてあって、無限ループとかあるので、このあたりがうまくはまる/はまらないってところなのかなあ。


Service Objectをcomplex facadeとして使う場合、facadeの先がある程度パッケージとして固まっていてくれるとここまでごちゃらない気がしますが、Rubyだと苦しいかなあ。
Javaで言うprotectedみたいなのでうまくServiceの先がカプセル化されていて、不用意に直接呼び出しされないようになっていればいいんですが、Rubyだとオープンクラスだし、強制しづらそう。
service objectだけがその先のオブジェクトを触れる、みたいな強制の仕方をしようとすると、マイクロサービスみたいな話になるのかなあ。

ツイートより

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

Rails tips: Service Objectパターンでリファクタリング(翻訳)

Rails: Service Objectはもっと使われてもいい(翻訳)

The post Service Objectがアンチパターンである理由とよりよい代替手段(翻訳) first appeared on TechRacho.


Rails 7: Active ModelからActiveModel::APIが切り出された(翻訳)

$
0
0

概要

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

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

参考: 週刊Railsウォッチ20210921 ActiveModel::APIが追加

Rails 7: Active ModelからActiveModel::APIが切り出された(翻訳)

私たちはRailsで作業するときにActive Recordに慣れ親しんでいます。Active RecordはMVCフレームワークにおけるモデルであり、システムでビジネスデータやビジネスロジックの表現を担当する層です。Active Recordは、アプリケーション内のリッチなオブジェクトをRDBMS上のテーブルに接続するORM(Object-relational mapping)でもあります。

Railsで、データベース内のどのテーブルとも紐付けられていない普通のクラスが、モデルのような機能を必要とすることがあります。こういうときはRailsのActive Modelの出番です。Active Modelは、Active Recordの機能の一部を必要とするクラスを開発するときに使われるライブラリです。

たとえばname属性とid属性を持つPersonクラスがあるとします。これらの属性をバリデーションする機能を追加したい場合は、 ActiveModelを使うことで以下のようにPersonクラスを実装できます。

class Person
  include ActiveModel::Model

  attr_accessor :name, :email
  validates :name, :email, presence: true
end
irb> person = Person.new(name: "Sam", email: "sam@example.com")

irb> person.name
# => "Sam"

irb> person.email
# => "sam@example.com"

irb> person.valid?
# => true

irb> person.persisted?
# => false

ActiveModel::Modelincludeすれば、以下のような機能も使えるようになります。

  • モデル名を調べる
  • ActiveModel::Conversion(変換)
  • ActiveModel::Translation(翻訳)
  • ActiveModel::Validator(バリデーション)

ActiveModel::Modelincludeしたクラスでは、Active Recordオブジェクトと同じようにform_withrenderなどのAction Viewヘルパーメソッドが使えるようになります。

変更前

Rails 7より前のActiveModel::Modelは、Action PackやAction Viewとやりとりする最小限のAPIとして動作します。

「Model」という名前から分かるように、ActiveModel::ModelincludeすればActive Recordのようなモデルを作成できます。しかしActiveModel::Modelでは、基本的なActiveModel::Attributes機能がサポートされていませんでした。

attributeメソッドを使おうとして失敗する様子をちょっと見てみましょう。

class Person
  include ActiveModel::Model

  attribute :name, :string
  attribute :email, :string
end

NoMethodError (undefined method `attribute' for Person:Class)

これを解決するには、PersonクラスにActiveModel::Attributesincludeする必要があります。

変更後

Rails 7以降は、ActiveModel::Modelの実装がActiveModel::APIに移動します(#43223)。

ActiveModel::API がAction PackやAction Viewとやりとりするときの定義は最小限に抑えられています。これにより、ActiveModel::Modelは、今後Active Recordのモデルのような機能を追加して拡張できます。

ActiveModel::ModelActiveModel::APIだけをincludeするようになりました。このおかげで、後方互換性を維持しながらモデルに機能を追加できるようになりました。

関連記事

Rails 7: caching?とuncachable!ヘルパーが追加(翻訳)

The post Rails 7: Active ModelからActiveModel::APIが切り出された(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: Sidekiqが10歳に、BuildKiteのテストを高速化、フィーチャーフラグほか(20220131前編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

公式の更新情報をRailsウォッチが先回りしてしまったので、コミットリストの中からChangelogが変更されたものを中心に見繕いました。ドキュメントの修正が多いですね。

🔗 Active Storageのリダイレクトモードでレスポンスをストリーミングしないようにした

従来はリダイレクトモードとプロキシモードのどちらもレスポンスがストリーミングされていたため、新しいスレッドが作成されてコネクションプールでコネクションがリークする可能性があった。しかし実際にはリダイレクトモードでデータを送信しないのでストリーミングは不要。
Luke Lau
同Changelogより

# activestorage/app/controllers/active_storage/base_controller.rb#L4
class ActiveStorage::BaseController < ActionController::Base
- include ActiveStorage::SetCurrent, ActiveStorage::Streaming
+ include ActiveStorage::SetCurrent

  protect_from_forgery with: :exception
# activestorage/app/controllers/active_storage/blobs/proxy_controller.rb#L9
class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
  include ActiveStorage::SetBlob
+ include ActiveStorage::Streaming
# activestorage/app/controllers/active_storage/representations/proxy_controller.rb#L9
class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
+ include ActiveStorage::Streaming
+

つっつきボイス:「リダイレクトモードではデータを送信しないのでスレッドを作る必要はないと」「たしかに」「ActiveStorage::Streamingは通常使わなくてもよいのでBaseControllerから外してProxyControllerに移動したんですね」

参考: §5.1 リダイレクトモード — Active Storage の概要 – Railsガイド

参考: Rails API ActiveStorage::Streaming

🔗 YAML.loadYAML.unsafe_loadに置き換えた

# activesupport/lib/active_support/encrypted_configuration.rb#L51
      def deserialize(config)
-       YAML.load(config).presence || {}
+       doc = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(config) : YAML.load(config)
+       doc.presence || {}
      end

つっつきボイス:「yamlパーサーであるpsych gemのバージョンアップに対応するためにYAML.loadYAML.unsafe_loadに置き換えられた」「そういえば少し前にpsych 4.0.0でbreaking changeが入っていましたね」

ruby/psych - GitHub

参考: Ruby の Psych.safe_load(YAML.safe_load)の引数が Psych v4.0.0 から非互換になる – Secret Garden(Instrumental)

🔗 Object#instance_variable_namesを高速化


つっつきボイス:「これはActive Supportの最適化ですね」「instance_variable_namesって何に使うんでしょうか?」「文字どおりインスタンス変数のリストを返すメソッドですね」「あ〜なるほど」

# activesupport/lib/active_support/core_ext/object/instance_variables.rb#L27
- def instance_variable_names
-   instance_variables.map(&:to_s)
+ if Symbol.method_defined?(:name) # RUBY_VERSION >= "3.0"
+   # Returns an array of instance variable names as strings including "@".
+   #
+   #   class C
+   #     def initialize(x, y)
+   #       @x, @y = x, y
+   #     end
+   #   end
+   #
+   #   C.new(0, 1).instance_variable_names # => ["@y", "@x"]
+   def instance_variable_names
+     instance_variables.map(&:name)
+   end
+ else
+   def instance_variable_names
+     variables = instance_variables
+     variables.map! { |s| s.to_s.freeze }
+     variables
+   end
+ end
end

「Ruby 3.0からSymbol#nameが使えるようになったので、Stringのアロケーションをそれで避けたのか」「Ruby 3.1だと1.5倍ぐらい速くなっている!」

🔗 ActiveSupport::LoggerThreadSafeLevel#addを削除


つっつきボイス:「不要になったので消したっぽい」「Ruby 2.7以上なら不要なメソッドで、現在のRailsがRuby 2.7以上が必須になったので消したんですね、なるほど」

参考: §1.2 Rubyバージョン — Rails アップグレードガイド – Railsガイド

🔗 ガイド: カウンタキャッシュに追記


つっつきボイス:「これはドキュメントの更新ですね」「reset_countersというメソッドがあるとは知らなかった」「あるモデルの主キーを更新すると、そのモデルの主キーを参照して計算しているカウンタキャッシュの値が実際の参照モデル数と一致しなくなってしまうので、reset_countersで再計算させる」

# guides/source/association_basics.md#L1040
-Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`.
+Counter cache columns are added to the owner model's list of read-only
+attributes through `attr_readonly`.

+If for some reason you change the value of an owner model's primary key, and do
+not also update the foreign keys of the counted models, then the counter cache
+may have stale data. In other words, any orphaned models will still count
+towards the counter. To fix a stale counter cache, use [`reset_counters`][].

+[`reset_counters`]: https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html#method-i-reset_counters

🔗Rails

🔗 Railsのminitestを1分で終わらせる(Ruby Weeklyより)


つっつきボイス:「BuildKiteでテストが速くなったのかと思ったら、factory_botのファクトリーを修正して高速化したのね」

thoughtbot/factory_bot - GitHub

「そういえば、BuildKiteはRailsフレームワークのテストにも使われていますね↓」「そうそう、Railsで複数バージョンのテストを回すのに使っている: CIの設定をいろいろ変えてテストするときにこういうのを使いますね」「CircleCIの仲間みたいな感じですか」

参考: BuildKite — Ruby on Rails/Rails
参考: 最先端の CI/CD ツール – CircleCI

「1分でテストが終わるってすごい」「個別のテストが高速ならやれるでしょうね」

🔗 Page ObjectでRailsのシステムテストをメンテ可能にする(Ruby Weeklyより)


つっつきボイス:「Page Objectってどこかで聞いたような」「こういうクラスを作ってテスト用のヘルパーにするという趣旨みたい↓」「TestPageとかRegistrationPageとかがそうなんですね」

# 同記事より
# test/pages/test_page.rb
class TestPage
  include Rails.application.routes.url_helpers

  attr_accessor :test, :page

  def initialize(system_test, page)
    @test = system_test
    @page = page
  end

  def visit
    @test.visit page_path
  end

  def page_path
    raise "Override this method with the page path"
  end
end

# test/pages/registration_page.rb
class RegistrationPage < TestPage
  def register(user)
    @test.fill_in "Email", with: user.email
    @test.fill_in "Password", with: "12345671"
    @test.fill_in "Password confirmation", with: "12345671"

    @test.click_on "Sign up"
  end

  def page_path
    new_user_registration_path
  end
end

# test/pages/dashboard_page.rb
class DashboardPage < TestPage
  def assert_logged_in
    @test.assert_selector "h1", text: "Dashboard"
  end

  def page_path
    dashboard_path
  end
end

「ログインセッションを取るためのPage Objectぐらいはあってもいいけど、個人的にはあまり増やしたくない気持ちがありますね」「というと?」「テストコードにロジックを持つクラスを作ると、テストコードをテストしないといけなくなるので」「あ〜たしかに!」「そうなんですよ」「テストコードはベタがいいというヤツですね」

「もちろん、ログインセッションのように毎回同じことをやることがわかっている部分をこういうふうにPage Objectで自動化する↓のはありだと思いますし、実際にやります」

# 同記事より
# test/pages/registration_page.rb
class RegistrationPage < TestPage
  def register(user)
    @page.fill_in I18n.t("attributes.user.email"), with: user.email
    @page.fill_in I18n.t("attributes.user.password"), with: user.password
    @page.fill_in I18n.t("attributes.user.password_confirmation"), with: user.password

    @page.click_on I18n.t("buttons.register")
  end

  def page_path
    new_user_registration_path
  end
end

🔗 Rails Best Practicesサイト


つっつきボイス:「お、年季の入ったサイト」「昔からあるRails Best Practicesというサイトで、rails_best_practices gemのチェック結果ではここを見るように言われるんですが、古くなっているのはあるかなと思って取り上げてみました」

flyerhzm/rails_best_practices - GitHub

「どれどれ、トップのエントリの内容↓がいきなり古い↓」「ありゃ」「今はTime.zone.nowではなくTime.currentを使うべき」「そうそう、Time.currentですよ〜」「一番新しいエントリが2014年か」「2010年のエントリが一番多いみたいですね」

# rails-bestpractices.comより
Time.zone.now
Time.zone.today

参考: When should DateTime.now.utc vs. Time.current.utc be used in Rails? – Stack Overflow

「rails_best_practices gemは今も更新されているみたいだけど」「名前のとおりRailsのベストプラクティスをチェックするという位置づけのようですが、今だとrubocop-railsと役割がちょっとかぶっていそう」「たしかに似てそうですね」「rubocop-rails↓は広く使われていて更新も盛んなので、自分だったらrubocop-railsを使うかな」「自分もrubocop-railsでいいかな」

rubocop/rubocop-rails - GitHub

「rails_best_practicesに限らず、この種のgemは深く考えずに適用するとハマる可能性があるので注意が必要ですね: 昔のベストプラクティスが今もそうとは限らないという話も含めて」「そうですね」


後で見ると、サイトは7年間更新されていませんでした↓

flyerhzm/rails-bestpractices.com - GitHub

🔗 online_migrations: PostgreSQLマイグレーションをチェックするgem(Ruby Weeklyより)

fatkodima/online_migrations - GitHub


つっつきボイス:「これは?」「PostgreSQLのマイグレーションで危険な操作があったら警告するそうです」

「このonline_migrationsと似たようなものを以前も見た気がする」「そういえばREADMEの末尾に書いてあるstrong_migrations↓は以前取り上げたことがあります(ウォッチ20170915)」

ankane/strong_migrations - GitHub

「結局マイグレーションで同じようなミスをする人は多いということでしょうね」「まあそうですよね」「strong_migrationsと同様、使いたい人が使えばよいと思います」

🔗 マイグレーションのドキュメントとして使う

「どちらのgemでもいいんですが、READMEに書かれている問題解説と修正方法を読んで参考にするといいんじゃないかな」「なるほど、マイグレーションをチェックするときのドキュメントとして使うんですね」「必要なことはだいたいREADMEに書かれているようです👍


「これらのgemは警告のみのようですが、マイグレーションによっては自動修正しようがないものもあるんですよ: たとえばマイグレーション中にいったんデプロイをはさむとか」「あ、たしかに」「READMEにも書かれていますけど、たとえばカラムを変更するときなんかは、add_columnしてからいったんデプロイして、データ移行が終わってからremove_columnする、といった操作が必要になりますし、カラムを削除するときのignored_columnsなんかもそう」

🔗 フィーチャーフラグを1時間で実装した話(Ruby Weeklyより)

# 同記事より
  class Features
    def self.configuration
      Rails.configuration.features
    end

    def self.enabled?(feature)
      configuration.fetch(feature.to_sym, false)
    end
  end

つっつきボイス:「フィーチャーフラグの実装は実際に簡単なので、やりたい人はどうぞ👍

参考: フィーチャートグル – Wikipedia


「ところで、自分はまだ使ったことはありませんが、フィーチャーフラグをWebインターフェイスで操作できるサービスがありますよね、たしかflipperあたりかな」「flipperは以前取り上げたことがありますね(ウォッチ20210330)↓」「エンジニアの手をわずらわせずにフィーチャーフラグを切り替えられたらいいなと思うときがあるので、そういうサービスを試してみたい気持ちがあります」「わかります」

jnunemaker/flipper - GitHub

🔗 その他Rails


つっつきボイス:「Sidekiqももう10年経つんですね🎉」「お〜そんなに」「まだ課金したことないけど」「お、有料版もあるんですか」「サイトにも記載があるように↓、Pro版以上でないと使えない機能もありますし、マルチプロセスはEnterprise版が必要」「Enterprise版は月2万か…」「Enterprise版の機能が必要になるほどの大規模サービスなら十分払えると思いますよ」


前編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Rubyコンパイラの歴史動画、RubyのWebAssembly対応進む、ぼっち演算子の注意点ほか(20220126後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: Sidekiqが10歳に、BuildKiteのテストを高速化、フィーチャーフラグほか(20220131前編) first appeared on TechRacho.

週刊Railsウォッチ: Rubygems Adoptionフォームが開設、JetBrains Gateway、NGINX Unitほか(20220201後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 Thread#passQueue#popに置き換えてメソッド呼び出しを削減(Ruby Weeklyより)

queue = Queue.new
lock = Queue.new

THREADS = 10

THREADS.times do |i|
  Thread.new do
    loop do
      data, task = queue.pop
      task.call(data)
      lock << true
    end
  end
end

def wait_for_jobs_to_finish(dispatched, lock)
  dispatched.times { lock.pop }
end

def data
  Array.new(10) { rand }
end

task = ->(data) { data * 2 }

100_000.times do
  data.each { queue << [_1, task] }

  wait_for_jobs_to_finish(data.size, lock)
end

つっつきボイス:「RubyにQueueというライブラリがあるんですね」「標準のThreadにあるヤツですね↓」「Thread#passは初めて見たかも: Queue#popは名前どおりの動作という感じ」

参考 pop — class Thread::Queue (Ruby 3.1 リファレンスマニュアル)
参考: pass — class Thread (Ruby 3.1 リファレンスマニュアル)

「記事末尾のグラフで目覚ましい結果が出てますね↓」「青がThread#passの100msごとの呼び出し回数で赤がQueue#popの呼び出し、赤はグラフの下にぴったり張り付いてる」「ほぼゼロ!」「これなら呼び出しを99.9%削減できたと言えそう👍


同記事より

🔗 commontator: コメント機能を追加するRailsエンジン(Ruby Weeklyより)

lml/commontator - GitHub


つっつきボイス:「コモンテーター?」「Railsエンジンでできているようです」「votingとかmentionとかメール通知などの項目があるので、Stack OverflowやDiscussのようなコメント機能を追加できるらしい」「そんな感じですね」「ユースケースに合うなら使ってみてもいいかも」「デモサイトが見たいけど見当たらない…」

後で探すと、使ってみた記事が見つかりました↓。2014年なので随分前からあるんですね。

参考: How to Add Comments to Rails App with Commontator

🔗 ruby/debugにテキストベースUIサポートのプルリク


つっつきボイス:「今日のつっつき中にst0012さんからこのプルリクをアップしたとお知らせがありました」「TUI(Text-based UI)という名前の機能なんですね」

(プルリクに貼られた動画を見る)「入力すると下にいろいろ生えてくる」「お〜何だかすごい」「デバッガー画面だと見たい情報が押し流されることがよくあるので、これが欲しい気持ちはわかる」「コンソール画面の大きさは人によって好みが違うので、そこもうまくやってくれると嬉しいかも」「オンオフできるようなので大丈夫そう」「マージされるといいですね」「ruby/debugはこういう機能がどんどん増えてきていて素晴らしい👍

🔗 その他Ruby


つっつきボイス:「少し前にも話題にしたと思いますが、rubygems.orgが『Rubygems Adoption』と題して、オーナー不在でメンテされていないgemへのオーナーシップ移行をリクエストするフォームを開設したそうです」「名前空間を手放す手順はたしかに必要ですね👍


後でフォームを探しました。Rubygems.orgのgemエントリのサイドメニューにある[Adoption]をクリックします↓。


rubygems.orgより

フォームが表示されます↓(フォームはrubygems.orgにログインしないと表示されません)。オーナーがオーナーシップを譲りますと掲示する欄と、オーナーにオーナーシップをリクエストするフォームがあります。


rubygems.orgより

🔗DB

🔗 表示順を別テーブルに分ける


つっつきボイス:「そーだいさんのこの記事、ちょうどさっき読みました」「display_order_numberというカラムがある場合にそれを別テーブルに分けて管理するというのはよくやりそう」「SQLのORDER BY処理は重いので、1つのテーブルまるごとでやろうとするとテーブル全体がロックされてつらくなる: そこで別テーブルに分けた」「なるほど」「こういう手法はありですね👍

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

🔗 Cloudflareのワーカー(Serverless Statusより)


つっつきボイス:「Cloudflare Unboundワーカーの課金が削減されたそうです」「この図↓のようにストリームを直結してワーカーを通さないようにすることでワーカーの稼働時間が削減される、なるほど」


同記事より


「ところでCloudflareといえば、少し前にfastlyがこんな記事出してましたよね↓」「お、この記事知りませんでした」「そうそう、話題になってた」「ベンチマークを恣意的に歪めようと思えばいくらでもできることを改めて実感できますね」

参考: 嘘、大嘘、そして (Cloudflare の) 統計 : Cloudflare のパフォーマンステストの欠陥を証明 | Fastly

「『嘘、大嘘、そして統計』は英語によくある言い回しだったかな」「記事でもWikipediaにリンクされていますね↓」

参考: 嘘、大嘘、そして統計 – Wikipedia

🔗 AWS Elastic File Systemにレプリケーション機能(Publickeyより)


つっつきボイス:「EFSがレプリケーションやってくれるようになった🎉」「マルチリージョンでもレプリケーションコピーを持たせられるようになったのね」「マルチリージョンのレプリケーションやってみたい」「見た感じでは、あくまで障害復帰のためのレプリケーションで、時系列も含めて復旧するものではなさそうかな」「時間を巻き戻したりするものではないということですね」

参考: Amazon EFS(EC2 用フルマネージド型ファイルシステム)| AWS

🔗 DockerやDocker Desktopファイル共有のベストプラクティス


つっつきボイス:「自分はEvil Martians流でやっているせいか、Docker Desktop for Macを使っていて遅いと思ったことがありませんでした」「Docker Desktop for Macが遅いみたいな記事はよく見かけますけどね↓」

参考: Docker for Mac が遅い(怒) – 箱のプログラミング日記。

「最近はMacを使っていませんが、巨大なデータベースを扱ったり大量のアセットコンパイルを走らせたりすると遅いみたいな話は耳にしますね」「ふむふむ」「そうした問題を解決するためにいろいろテクニックが必要になってくる: 業務で使っている人はもうこのあたりをだいたい心得ていると思いますが」

「上の記事でも、ホスト側の変更をコンテナに同期するのに時間がかかったとありますね」「そうそう、ホストからコンテナへの同期、あるいはその逆の同期が遅かったという話は聞きますけど、それ以外で遅いという話はあんまり聞かないかな: 純粋な計算処理ならほとんどオーバーヘッドはなさそう」「扱うファイル数やサイズに依存する面はありますね」「へ〜、最近のDocker Desktop for MacはgRPC FUSEがデフォルトでオンなのか」

参考: Docker Desktop for Mac ユーザーマニュアル | Docker ドキュメント — gRPC FUSE

「Dockerブログの元記事はまさにそのあたりのベストプラクティスを解説しているようですね: WSL2でWindowsファイルシステムからマウントすると遅くて、WSL仮想マシン内のLinuxファイルシステムだと高速とか」

「Windowsではどの方法でやってます?」「後者のようにWSL内でやってますが、RubyMineの場合は前者でやってます: でもマシンの性能が高いのとSSHを多用しているせいか、特に遅いとは感じないかな」「なるほど」

「元記事でもDocker Desktopがvirtiofsを実験中とあるので、これが入ったらDocker Desktop for Macももっと速くなるかもしれませんね」「virtiofs、知らなかった…」「純粋なLinux環境でDockerを使うのが結局一番シンプルなんですけどね」

参考: virtiofsについて – Qiita

🔗 JetBrains Gatewayに期待

「ところで最近JetBrains Gatewayを試していて↓、まだβ版でバグも多いんですが、なかなかよくできています」「お〜、こんな機能が登場していたんですか」

参考: JetBrains Gateway – JetBrains IDE 向けリモート開発

「ざっくり説明すると、ターゲットホスト(ここではWSL2上のLinux)でヘッドレスのJetBrains IDEを動かして、そこにローカル(ここではWindows)のJetBrains IDEに接続できるというものです」「へ〜!」「これなら従来のRubyMineのリモートインタプリタのような設定ではなく、ローカルのRubyとして動かせるのでとても便利👍」(しばらく実演)

「何となくイメージできたかも」「こんなことができるんですか〜」「SSHで接続する感覚で使えます」

「ちょっとX Window SystemのXサーバーとXクライアントの関係に似た感じですね」「そうそう、X Window Systemと違ってサーバーは画面をレンダリングしませんけどね: JetBrains GatewayのUIはあくまでローカルOS上のクライアントが表示する」「なるほど」「Xだとサーバーがレンダリングするので、ローカルOSのフォントを引き継げないしかな漢字変換もLinux側に用意しないといけなくなるし、差分転送できないのでデータサイズも遅延も大きくなる」「あ、たしかに」

参考: X Window System – Wikipedia

「JetBrainsのコラボ開発機能である『Code With Me』については以前も話しましたが(ウォッチ20210413)、JetBrains GatewayはちょうどCode With Meと同じ感覚で使えます」「なるほど」「どちらもUIの表示に必要なデータだけをやりとりするので、帯域もそれほど使わずに済む」「まさに待ち望まれていた機能ですね」

参考: Code With Me: JetBrains が提供する共同プログラミングサービス

「要するに、WSL2のファイルシステム上にソースコード置いたからといってWSL2内でRubyMine起動する必要はないんですよ(パーミッションや同期の問題を別にすれば): JetBrains Gatewayはそうした問題をすべて解決してくれる💪

「自分は外出先で仕事することもよくあるので、その意味でJetBrains Gatewayはとてもありがたい: これなら持ち歩くノートPCは非力なものでもよくて、自宅の強力なサーバー上でJetBrains Gatewayのコアを動かしてそこに外から接続すれば快適に仕事できる👍」「たしかに外で仕事する人にとって、自宅の強力なサーバーで仕事できるのは嬉しい機能ですね」「その分回線品質が重要になってきますが、それこそクライアントはM1 MacBookでもいいくらい」

🔗 その他インフラ

編注

「NGINX Unit」および「nginx」という表記は以下の公式サイトに合わせました。

参考: About — NGINX Unit — unit.nginx.org
参考: nginx — nginx.org


つっつきボイス:「nginxの人が引退か」「インタビューも載っていますね」「MSXでプログラミングを始めたんですって」(しばし昔話)

参考: MSX – Wikipedia

🔗 NGINX Unit

「nginxといえば、最近NGINX Unitを試しに使ってみましたよ」「以前翻訳記事↓を出しましたけど、そういえばその後あまり噂を見かけない感じでしたね」

新しいRubyアプリサーバー「NGINX Unit」を調べてみた(翻訳)

「NGINX Unitはどうでした?」「使ってみるとやはり良し悪しがわかりますね: このときはサービスメッシュのバスバックエンド的なものを実現するためにNGINX Unitを使いました」「というと?」「内部サーバー同士のAPI通信をURLに応じて振り分けるのと、外部からのリクエストをURLに応じて内部サーバーにルーティングするのが両方必要だったんですよ」「これはややこしそう…」

「何を使ってもよかったんですが、従来のnginxはコンフィグがとても読みづらいのでそういう実装に向いてない」「nginxのコンフィグの読みづらさは有名ですね」「NGINX Unitだと、たとえば以下のようにJSONだけでとても簡潔にルーティングを書ける↓」「お〜、こういうふうに書けるんですか」「パターンマッチも使えるので、今回のようにURLベースでルーティングするのによいと思ってNGINX Unitを使ってみました」

参考: Configuration — NGINX Unit

// unit.nginx.orgより
{
    "match": {
        "host": "pattern",
        "method": "!pattern",
        "uri": [
            "pattern",
            "!pattern"
        ],

        "arguments": {
            "arg1": "pattern",
            "arg2": "!pattern"
        },

        "cookies": [
            {
                "cookie1": "pattern",
            },
            {
                "cookie2": "pattern",
            }
        ],

        "headers": [
            {
                "header1": "pattern",
            },
            {
                "header2": "pattern",
                "header3": "pattern"
            }
        ]
    },

    "action": {
        "pass": "..."
    }
}

「今の所NGINX Unitで大きな問題はないかな: 実際に使ってみて困ったのは、リスナーで指定する外部サービスをホスト名で指定できないこと」「IPアドレスしか設定できないのか…」「IPアドレスだとALBで直接プロキシできないので、いろいろ工夫が必要でした」

参考: Application Load Balancer とは? – Elastic Load Balancing

「それと、IPアドレスだとDocker Composeと相性が悪いんですよ: NGINX UnitをDocker Composeで起動すると起動時にIPアドレスが決まるので」「あ〜たしかに!Docker ComposeでIPを指定できないんですか?」「できるんですが、そうするとDocker環境に依存してしまうんですよ: Dockerで使うのはあくまでプライベートなIPレンジなので直接指定したくない」「それもそうですね…」

「この問題は一応解決したので、そのうち記事にしようかな: 以前銀座Rails#27で少し話したことのあるenvsubstを使うとだけ言っておきます↓」

アプリケーションコンフィグの設計パターン(銀座Rails#27)

「ちなみにDocker Hubにあるnginxの公式イメージでもenvsubstが使われています↓」「お〜知らなかった!」

参考: Nginx – Official Image | Docker Hub
参考: envsubstを使ってDockerで設定ファイルに環境変数を埋め込めこむ汎用的なパターン – Qiita

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

🔗 徳丸先生のCSRF記事


つっつきボイス:「この記事わかりやすくて助かりました」「状況が変わったことをキャッチアップするのにこういう記事は大事ですね👍

なお、つっつき後も記事に更新が入っています。

🔗言語/ツール/OS/CPU

🔗 DevToys

veler/DevToys - GitHub


つっつきボイス:「これは?」「PowerToysの開発者版みたいな便利ツールセットですね: JSON->YAML変換とか正規表現テスターとかいろいろできる」「JWTデコーダーとかよさそう」「お〜インストールしようっと」「残念ながらWindows専用です」「ありゃ」

参考: PowerToys のインストール | Microsoft Docs
参考: DevToys is PowerToys for Developers – MSPoweruser

「PowerToysは普段から便利に使っていて、特にFancyZonesは圧倒的に便利👍

参考: PowerToysFancyZonesのユーティリティWindows | Microsoft Docs


後編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Sidekiqが10歳に、BuildKiteのテストを高速化、フィーチャーフラグほか(20220131前編)

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

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

Ruby Weekly

Publickey

publickey_banner_captured

Serverless Status

serverless_status_banner

JavaScript Weekly

javascriptweekly_logo_captured

The post 週刊Railsウォッチ: Rubygems Adoptionフォームが開設、JetBrains Gateway、NGINX Unitほか(20220201後編) first appeared on TechRacho.

Rails 7: Action Text添付ファイルのHTMLタグ名がカスタマイズ可能になった(翻訳)

$
0
0

概要

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

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

Rails 7: Action Text添付ファイルのHTMLタグ名がカスタマイズ可能になった(翻訳)

Rails 7に、Action Textの添付ファイルのデフォルトHTMLタグ名をカスタム文字列に設定する機能が追加されました(#41158)。

変更前

Rails 6までは、Action TextのデフォルトHTMLタグ名はaction-text-attachmentになります。

Action Textのリッチテキストフィールドを添付ファイルでレンダリングすると以下のようなHTMLが生成されます。

<div class="trix-content">
  <div>
    <action-text-attachment sgid="BAh7CEki..." content-type="image/png" url="http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiO.../saeloun%20logo.png" filename="saeloun logo.png" filesize="9613" width="600" height="600" previewable="true" presentation="gallery">
      <figure class="attachment attachment--preview attachment--png">
          <img src="http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiO.../saeloun%20logo.png">

        <figcaption class="attachment__caption">
            <span class="attachment__name">saeloun logo.png</span>
            <span class="attachment__size">9.39 KB</span>
        </figcaption>
      </figure>
    </action-text-attachment>Description for the blog<br><br><br>
  </div>
</div>

このrich_text出力を解析してから、nokogiriでタグ名を変更することは可能です。

変更後

Rails 7では、以下の設定オプションを用いて添付ファイルのデフォルトHTMLタグ名をシンプルかつすっきりと設定できる方法が追加されました。

  # config/application.rb
  config.action_text.attachment_tag_name = "saeloun-rich-text-attachment"
<div class="trix-content">
  <div>Description for the blog<br>
    <saeloun-rich-text-attachment sgid="BAh7CEki..." content-type="image/png" url="http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiO.../saeloun%20logo.png" filename="saeloun logo.png" filesize="9613" width="600" height="600" previewable="true" presentation="gallery">
      <figure class="attachment attachment--preview attachment--png">
          <img src="http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiO.../saeloun%20logo.png">

        <figcaption class="attachment__caption">
            <span class="attachment__name">saeloun logo.png</span>
            <span class="attachment__size">9.39 KB</span>
        </figcaption>
      </figure>
    </saeloun-rich-text-attachment><br><br><br>
  </div>
</div>

原注: 既存のRailsプロジェクトで添付ファイルのHTMLタグ名を変更しても、以前の添付ファイルのHTMLタグ名がすべて更新されるわけではないので、新しい設定を導入してからレコードを更新するまではレンダリングされません。

そのようなリッチテキストのレコードをレンダリングしようとするとタグがサニタイズされるので、添付ファイルのHTMLのレンダリングはすべてスキップされます。

タグ名を以前の設定に戻すと、添付ファイルは従来どおりレンダリングされます。

関連記事

Rails 7: Active ModelからActiveModel::APIが切り出された(翻訳)

The post Rails 7: Action Text添付ファイルのHTMLタグ名がカスタマイズ可能になった(翻訳) first appeared on TechRacho.

Rails: Webpacker→jsbundling-rails+webpackアップグレード手順(翻訳)

$
0
0

概要

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

表記の大文字小文字は「Webpacker」「Shakapacker」「webpack」「jsbundling-rails」「cssbundling-rails」に統一しました。

Rails: Webpacker→jsbundling-rails+webpackアップグレード手順(翻訳)

本ガイドでは、Webpacker 5からjsbundling-rails + webpack 4への移行方法を手順を追って解説します。Webpacker/Shakapacker v6への移行方法については、Shakapackerのアップグレードガイドを参照してください。また、Webpackerとjsbundling-railsの比較についてはこちらをご覧ください。

なお、jsbundling-railsに移行すると、Webpackerの以下の機能は利用できなくなります。

上記の機能に依存している場合や、webpackのセットアップが複雑な場合は、以下のShakpackerへの移行を検討してください。

shakacode/shakapacker - GitHub

1. jsbundling-railsのセットアップ

最初にjsbundling-railsをインストールします。

# Gemfileに以下を追加する
+ gem 'jsbundling-rails'

# コマンドラインでバンドルを再ビルドする
./bin/bundle install

# コマンドラインで基本設定を作成する
./bin/rails javascript:install:webpack

このインストールスクリプトでは以下が行われます。

  • マニフェストにビルドを追加する
  • gitのビルドを無視する
  • マルチプロセス用にforemanをセットアップする
  • ./webpack.config.jsを作成する
  • package.jsonにビルドスクリプトを追加する

webpackの設定を移動する

Webpackerとjsbundling-railsの差分を最小限にとどめたい場合は、以下を実行します。

  • 1. ./webpack.config.js./config/webpack/以下に移動する
  • 2. package.jsonの設定パスを以下のように変更する
- "build": "webpack --config ./webpack.config.js"
+ "build": "webpack --config ./config/webpack/webpack.config.js"
  • 3. webpack.config.js出力パスを以下のように変更する
- path: path.resolve(__dirname, "app/assets/builds"),
+ path: path.resolve(__dirname, '..', '..', 'app/assets/builds')

2. Webpackerを削除する

1. 以下のファイルを削除し、これらのファイルで行ったカスタマイズがすべて移行されていることを確認する

  • ./bin/webpack
  • ./bin/webpack-dev-server
  • ./config/initializers/webpacker.rb
  • ./config/webpacker.yml
  • ./config/webpack/development.js
  • ./config/webpack/environment.js
  • ./config/webpack/production.js
  • ./config/webpack/test.js

2. Webpacker gemを削除する

# Remove from your Gemfile
- gem 'webpacker'

3. ./bin/bundle installを実行する

3. 依存関係をインストールする

Webpackerにはデフォルトで多数の依存関係がインストールされますが、jsbundling-railsの依存関係は開発者に任せられています。単にJavaScriptを変更せずに扱いたい場合は、webpack-cliだけをインストールします。以後のセクションについては開発者の好みに応じて適用してください。

# webpack-cliをコマンドラインで追加する
yarn add webpack-cli

# 次にWebpackerパッケージを削除する
yarn remove @rails/webpacker webpack-dev-server

オプション: Babel

Babelは、JavaScriptソースコードを古いバージョンのJavaScriptにトランスパイルするのに用いられます。

1. パッケージをインストールする

# Babelのプリセットをコマンドラインで追加する
yarn add @babel/core @babel/preset-env

2. Babelを設定する

// package.jsonに以下を追加する
+ "babel": {
+   "presets": ["@babel/env"]
+ }

3. webpackのローダーを利用する

// webpack.config.jsに以下を追加する
module.exports = {
  module: {
    rules: [
+       {
+         test: /\.(js)$/,
+         exclude: /node_modules/,
+         use: ['babel-loader'],
+       },
    ],
  },
};

例: Babel + React + TypeScript

フロントエンドのフレームワークやTypeScriptはBabelでトランスパイルできます。この例では、ReactとTypeScriptを利用するセットアップを使います。

1. パッケージをインストールする

# Babelのプリセットをコマンドラインで追加する
yarn add @babel/core @babel/preset-env @babel/react @babel/preset-typescript

2. Babelを設定する

// package.jsonに以下を追加する
+ "babel": {
+   "presets": [
+     "@babel/env",
+     "@babel/react",
+     "@babel/preset-typescript"
+   ]
+ }

3. webpackを設定する

// webpack.config.jsに以下を追加する
module.exports = {
  module: {
    rules: [
+       {
+         test: /\.(js|jsx|ts|tsx|)$/,
+         exclude: /node_modules/,
+         use: ['babel-loader'],
+       },
    ],
  },
};

オプション: CSS + SASS

webpackでは、適切なローダーを使うことでCSSファイルを扱えるようになります。このセットアップでは、ファイルを扱うときにjsbundling-rails「のみ」を利用し、cssbundling-railsを利用していません。

1. パッケージをインストールする

# ローダー、プラグイン、node sassをコマンドラインでインストールする
yarn add css-loader sass sass-loader mini-css-extract-plugin webpack-fix-style-only-entries

2. webpackを設定する

// webpack.config.jsで以下を行う

// CSSを.cssファイルに切り出す
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// CSSのみを含むエントリーから、exportされたJavaScriptファイルを削除する
// この例では、entry.customは対応する空のcustom.jsファイルを作成する
const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');

module.exports = {
  entry: {
    // 自分のcssやsassのエントリをここに追加する
    application: [
      './app/assets/javascripts/application.js',
      './app/assets/stylesheets/application.scss',
    ],
    custom: './app/assets/stylesheets/custom.scss',
  },
  module: {
    rules: [
      // ローダーを含むCSS/SASS/SCSSルールをここに追加する
      {
        test: /\.s[ac]ss$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
      },
    ],
  },
  resolve: {
    // 追加のファイル種別をここに追加する
    extensions: ['.js', '.jsx', '.scss', '.css'],
  },
  plugins: [
    // プラグインをインクルードする
    new FixStyleOnlyEntriesPlugin(),
    new MiniCssExtractPlugin(),
  ],
};

オプション: フォント、画像、SVG

webpackでは、適切なローダーを使うことでJavaScriptファイルやCSSファイル以外のファイルも扱えるようになります。このセットアップ手順は用途によって異なりますが、おおよそ以下のようになります。

1. パッケージをインストールする

yarn add file-loader

2. webpackを設定する

// webpack.config.jsに以下を追加する
module.exports = {
  module: {
    rules: [
+       {
+         test: /\.(png|jpe?g|gif|eot|woff2|woff|ttf|svg)$/i,
+         use: 'file-loader',
+       },
    ],
  },
};

4. ビルドをテストする

webpackの設定が期待どおりに動作することを確認します。以下を実行することでバンドルを再ビルドできます。

yarn build --progress --color

エントリが複数ある場合は、個別のエントリごとに確認してから、最後にバンドル全体を確認することをおすすめします。

5. Webpackerのpackタグを置き換える

Webpackerのアセットタグを以下のように検索置換します。

# Webpackerのタグ       # Sprocketsのタグ
javascript_pack_tag = javascript_include_tag
stylesheet_pack_tag = stylesheet_link_tag

タグの置き換えが完了すれば、アプリケーションはこれまでと同じように動くはずです。

オプション: development環境向けのサポートを追加する

jsbundling-railsはproductionモードにのみ同梱されています。以下のようにmode: 'development'に切り替えることで、開発中のビルド時間を劇的に短縮できます。

// webpack.config.jsを以下のように変更する
+ const mode = process.env.NODE_ENV === 'development' ? 'development' : 'production';

module.exports = {
-  mode: "production",
+  mode,
-  devtool: "source-map",
  …
+  optimization: {
+    moduleIds: 'hashed',
+  }
}

関連記事

Rails UJS・Turbolinks -> Turboアップグレードガイド(翻訳)

The post Rails: Webpacker→jsbundling-rails+webpackアップグレード手順(翻訳) first appeared on TechRacho.

Rails tips: コントローラでrescueする方法(翻訳)

$
0
0

概要

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

2018/04/23: 初版公開
2022/01/27: 更新

Rails tips: コントローラでrescueする方法(翻訳)

Railsのコントローラでエラーをrescueしなければならないことがあります。クエリをかけたがデータベースにレコードがない場合にraiseされるActiveRecord::RecordNotFound例外をrescueする必要が生じることがほとんどでしょう。

サンプルのコントローラを作りましょう。

class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound => e
    redirect_to :root, alert: 'User not found'
  end

  def edit
    @user = User.find(params[:id])
  rescue ActiveRecord::RecordNotFound => e
    redirect_to :root, alert: 'User not found'
  end
end

2つのメソッドがあり、どちらも同じ方法で例外を扱っています。このコードを使い回せるように、ActiveSupportモジュールのrescue_fromを使う方法があります。

class UsersController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound do |exception|
    redirect_to :root, alert: 'User not found'
  end

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

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

before_actionフィルタを使ってshowメソッドやeditメソッドもリファクタリングできますが、それはまたの機会に。さて、これでコントローラのコードが読みやすくなり、同じコードを2回書く必要がなくなりました。いいですね!以下のようにブロックの代わりにメソッド名を渡すこともできます。

class UsersController < ApplicationController
  rescue_from ActiveRecord::RecordNotFound, with: :redirect_to_homepage

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

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

  protected

  def redirect_to_homepage
    redirect_to :root, alert: 'User not found'
  end
end

concernを使えば、1つのメソッドを複数のコントローラで使い回すことも可能です。これは、受け取った例外からrescueするロジックに依存します。rescue_fromは、エラー時にデフォルトの退屈なエラーページではなく、もっとましなページを表示したい場合にとても便利です。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

リファクタリングRuby: サブクラスをレジストリに置き換える(翻訳)

Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)

The post Rails tips: コントローラでrescueする方法(翻訳) first appeared on TechRacho.

Rails 7: guard-livereload gemで開発中にライブリロードする

$
0
0

Rails 7の新規アプリで遊んでいると、やはりライブリロード機能(ファイル更新を監視してブラウザを自動リロードする)が欲しくなったので、guard-livereload gemでDocker環境にライブリロード機能を導入しました。Rails 7でちょっと便利になった点があります。

guard/guard-livereload - GitHub

環境

  • Docker(docker-compose)環境が前提
    • Docker Desktop for Mac: 4.4.2 (73305)
    • Engine: 20.10.12
    • Compose: v2.2.3
  • Rails 7 + Ruby 3.1
    • Rails 7サーバーを./bin/devで起動できることが前提

インストール方法

  • Gemfileに以下を追加し、bundle installを実行します。
group :development do
# (略)
+  gem "guard-livereload", require: false
+  gem "rack-livereload"
end
  • プロジェクトディレクトリで以下を実行し、設定ファイルを生成します。
guard init livereload

プロジェクトディレクトリの直下に以下のファイルが生成されます。

# Guardfile
# A sample Guardfile
# More info at https://github.com/guard/guard#readme

## Uncomment and set this to only include directories you want to watch
# directories %w(app lib config test spec features) \
#  .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")}

## Note: if you are using the `directories` clause above and you are not
## watching the project directory ('.'), then you will want to move
## the Guardfile to a watched dir and symlink it back, e.g.
#
#  $ mkdir config
#  $ mv Guardfile config/
#  $ ln -s config/Guardfile .
#
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"

guard 'livereload' do
  extensions = {
    css: :css,
    scss: :css,
    sass: :css,
    js: :js,
    coffee: :js,
    html: :html,
    png: :png,
    gif: :gif,
    jpg: :jpg,
    jpeg: :jpeg,
    # less: :less, # uncomment if you want LESS stylesheets done in browser
  }

  rails_view_exts = %w(erb haml slim)

  # file types LiveReload may optimize refresh for
  compiled_exts = extensions.values.uniq
  watch(%r{public/.+\.(#{compiled_exts * '|'})})

  extensions.each do |ext, type|
    watch(%r{
          (?:app|vendor)
          (?:/assets/\w+/(?<path>[^.]+) # path+base without extension
           (?<ext>\.#{ext})) # matching extension (must be first encountered)
          (?:\.\w+|$) # other extensions
          }x) do |m|
      path = m[1]
      "/assets/#{path}.#{type}"
    end
  end

  # file needing a full reload of the page anyway
  watch(%r{app/views/.+\.(#{rails_view_exts * '|'})$})
  watch(%r{app/helpers/.+\.rb})
  watch(%r{config/locales/.+\.yml})
end
  • Rack::LiveReloadを有効にします。
# config/environments/development.rb
Rails.application.configure do
  config.after_initialize do
 # (略)
+  config.middleware.insert_after ActionDispatch::Static, Rack::LiveReload
  end
end
  • Procfile.devに以下を追記してguardがDockerコンテナ内で起動されるようにします。./bin/devはこのProcfile.devを呼び出します。
# Procfile.dev
web: bin/rails server -p 3000
css: bin/rails tailwindcss:watch
+livereload: bundle exec guard start -i -g livereload

guardを別途起動する必要がないので、Rails 6以前よりも便利になりました😋

  • LiveReloadで使うポート35729をDockerの外部に転送します。方法はいろいろありますが、自分はdocker-compose.ymlにポート35729を追加しました。
  # (略)
  rails:
    <<: *backend
    command: bin/dev
    ports:
      - "3000:3000"
+     - "35729:35729"
  • ChromeブラウザにLiveReload拡張をインストールします。

LiveReload – Chrome ウェブストア

  • これで、./bin/devを実行し、http://localhost:3000をブラウザで開けば、ライブリロードが有効になります。
$ ./bin/dev
02:23:21 web.1        | started with pid 8
02:23:21 css.1        | started with pid 9
02:23:21 livereload.1 | started with pid 10
02:23:23 livereload.1 | 02:23:23 - INFO - LiveReload is waiting for a browser to connect.
02:23:23 web.1        | => Booting Puma
02:23:23 web.1        | => Rails 7.0.1 application starting in development
02:23:23 web.1        | => Run `bin/rails server --help` for more startup options
02:23:25 web.1        | Puma starting in single mode...
02:23:25 web.1        | * Puma version: 5.6.1 (ruby 3.1.0-p0) ("Birdie's Version")
02:23:26 web.1        | *  Min threads: 5
02:23:26 web.1        | *  Max threads: 5
02:23:26 web.1        | *  Environment: development
02:23:26 web.1        | *          PID: 8
02:23:26 web.1        | * Listening on http://0.0.0.0:3000
02:23:26 web.1        | Use Ctrl-C to stop
02:23:26 css.1        |
02:23:26 css.1        | Rebuilding...
02:23:26 livereload.1 | 02:23:26 - INFO - Guard is now watching at '/app'
02:23:27 css.1        | Done in 619ms.
02:23:27 livereload.1 | 02:23:27 - INFO - Reloading browser: /assets/tailwind.css
02:23:27 livereload.1 | 02:23:27 - INFO - Browser connected.

ctrl-cでの終了処理もforeman gemがまとめてやってくれます。

^C02:24:42 css.1        | rails aborted!
02:24:42 web.1        | - Gracefully stopping, waiting for requests to finish
02:24:42 css.1        | Interrupt:
02:24:42 livereload.1 |
02:24:42 system       | SIGINT received, starting shutdown
02:24:42 web.1        | === puma shutdown: 2022-02-04 02:24:42 +0000 ===
02:24:42 css.1        |
02:24:42 - INFO - Bye bye...
02:24:42 web.1        | - Goodbye!
02:24:42 css.1        | Tasks: TOP => tailwindcss:watch
02:24:42 web.1        | Exiting
02:24:42 css.1        | (See full trace by running task with --trace)
02:24:43 system       | sending SIGTERM to all processes
02:24:43 web.1        | exited with code 0
02:24:43 css.1        | exited with code 1
02:24:43 livereload.1 | exited with code 0
$

おまけ

Rails 7でimportmap-railsとsprockets(またはpropshaft)のみを使う場合は、./bin/devというbinstubは追加されません。

しかしguard-livereload用に自分でbinstubとProcfile.devを追加すれば、./bin/devでguard-livereloadも起動できるので楽ですね。何となく、今後は./bin/devで起動するのが主流になりそうな気がしています。

# bin/dev
#!/usr/bin/env bash

if ! command -v foreman &> /dev/null
then
  echo "Installing foreman..."
  gem install foreman
fi

foreman start -f Procfile.dev
# Procfile.dev
web: bin/rails server -p 3000
livereload: bundle exec guard start -i -g livereload

なお、foreman gemはGemfileに追記する必要はありません。見てのとおり、初期実行時にbundlerの管理の外でgem install foremanで勝手にインストールされます。

関連記事

Rails 7: data-turbo-confirmはformタグに書く

The post Rails 7: guard-livereload gemで開発中にライブリロードする first appeared on TechRacho.


Rails 7: PostgreSQLのカスタムenum型が使いやすくなった(翻訳)

$
0
0

概要

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

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

Rails 7: PostgreSQLのカスタムenum型が使いやすくなった(翻訳)

PostgreSQLに組み込まれている列挙型のサポートは、Railsでも利用可能です。しかし、PostgreSQLの列挙型をActive Recordのマイグレーションで作成するのは面倒なので、列挙型を使おうとするとつらい思いをしがちです。

変更前

従来の方法では、以下のようにSQL文を直接実行してカスタム列挙型を作成していました。

# db/migrate/*_create_articles.rb
def up
  execute <<-SQL
    CREATE TYPE status AS ENUM ('draft', 'published', 'archived', 'trashed');
  SQL

  create_table :articles do |t|
    t.column :current_status, :status
  end
end

現場ではおそらく不便でしょう。

変更後

RailsとPostgreSQLを用いる開発では、PostgreSQLのカスタム列挙型の管理を少しでもましにするために、schema.rbの代わりにstructure.sqlを使うことがよくあります。

ありがたいことに、Rails 7にPostgreSQLカスタム列挙型のサポートが追加されました(#41469)。この変更によって導入された列挙型作成用のcreate_enumメソッドとカラムへの追加用のt.enumメソッドのおかげで、そうした軋轢が取り除かれます。列挙型はActiveRecord::Enumとの相性もよいので、これまで以上に手軽に利用できます。

def up
  # 注意: 列挙型をDROPするメソッドはありません
  create_enum :status, ["draft", "published", "archived", "trashed"]

  change_table :articles do |t|
    t.enum :current_status, enum_type: "status", default: "draft", null: false
  end
end

上によって、列挙型の定義と列挙型カラムがschema.rbに追加されるので、testデータベースに読み込んで問題なく利用できます。

ActiveRecord::Schema.define(version: 2022_01_03_113555) do

  # このデータベースをサポートするために必須の拡張がある
  enable_extension "plpgsql"

  # このデータベースで定義されるカスタム型
  # 一部の型は他のデータベースエンジンで動かない可能性があることに注意
  create_enum "status", ["draft", "published", "archived", "trashed"]

  create_table "articles", force: :cascade do |t|
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.enum "current_status", default: "draft", null: false, enum_type: "status"
  end
end

これは他のデータベースアダプタと互換性がないので注意が必要です。将来別のデータベースに乗り換えることになった場合は、手動で移行する必要があるでしょう。

関連記事

The post Rails 7: PostgreSQLのカスタムenum型が使いやすくなった(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: Rails 6.1を7.0にアップグレードしてみた、PostgreSQLでジョブキューほか(20220208前編)

$
0
0

こんにちは、hachi8833です。

つっつきボイス:「このSUSEのYouTubeチャネルがめちゃくちゃ面白い↓」「お〜、英語の替え歌がどっさりですね」「映像も凝っていてクォリティが半端ない」

週刊Railsウォッチについて

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

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

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

今回は以下の更新の中からChangelogが更新されたものを中心に見繕いました。引き続きドキュメントの修正が多いようです。

🔗 TestCase#stub_constを追加

以下のようにブロック内で定数の値を変更する。

# World::List::Import::LARGE_IMPORT_THRESHOLD = 5000

stub_const(World::List::Import, :LARGE_IMPORT_THRESHOLD, 1) do
  assert_equal 1, World::List::Import::LARGE_IMPORT_THRESHOLD
end

assert_equal 5000, World::List::Import::LARGE_IMPORT_THRESHOLD = 5000

World::List::Import::LARGE_IMPORT_THRESHOLD = 5000を無理やり設定する代わりにこのメソッドを使えば、warningも出力されなくなり、そのテストが完了すれば元の値に戻る。
同PRより


つっつきボイス:「お、このstub_constは便利そう: 個人的に好き👍

「これと似たような感じで、テストコードでクラスインスタンス変数をスタブする必要が生じることがたまにあるんですが、直接スタブできないので、たとえばクラスインスタンス変数の値にアクセスするメソッドを追加してそれをスタブした覚えがあります」「あ、なるほど」

参考: クラス変数とクラスインスタンス変数を理解する – Qiita

🔗 PostgreSQL generated column関連の修正

# activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb#L666
-           default_function = extract_default_function(default_value, default)
+
+           if attgenerated.present?
+             default_function = default
+           else
+             default_function = extract_default_function(default_value, default)
+           end

つっつきボイス:「この修正はissueを見るのがわかりやすそう↓」「schema.rbでPostgreSQLのgenerated columnが常にas: nilになってしまっていたんですね」「これは修正が必要なヤツ」

参考: 5.3. 生成列 — PostgreSQL 13.1

🔗 Websocketsが接続中に閉じないようにする

これは、接続状態のWebSocketを閉じると永久に壊れてしまう問題(Safariによるバグ)を解決する。
この問題は、最近の Safari(15.1+) のNSURLSession Websockets と呼ばれる新しいWebSocketsの実装で発生した。この問題が発生するとWebSocketsが完全におかしくなり、修正するにはSafariを完全に再起動する必要がある(リロードだけでは修正できない)。

この問題はBasecampで発生したが、このパッチで問題の修正を確認できた。このパッチは単に接続状態にあるWebSocketを閉じないようにするもの。他のユーザーからも同様の問題が報告されている(これこれ)。ActionCableに限らないが、Action Cableの監視システムはstaleコネクション検出ロジックの一部で接続状態のコネクションを閉じることがあったためにこの問題が発生した。
Safari固有の問題ではあるものの、Action Cableを使うすべてのアプリに影響するため、パッチをRailsにアップストリームすることにした。この問題はトラブルシューティングが非常に難しい問題でもある (この問題を追いかけるのに数週間かかった)。他のブラウザでは問題を起こさないので、ブラウザ検出を行わずにシンプルに対応した。最悪、接続状態にあるWebsocketsコネクションが閉じていない状態で接続される可能性もあり、その場合ブラウザに2つのコネクションができてしまうが、Action Cableで監視されることも使われることもないので何もせずに勝手に死んでくれる。
Safariが近いうちにこの問題を解決して、この変更を元に戻すことを検討できるようになればと思う。
同PRより


つっつきボイス:「Safariのバグを回避する修正か」「プルリクに書かれているリンクが切れていてSafariのどのバグだかわからない…」「あらま」

🔗 非推奨化されたurlsafe_csrf_tokensを削除

関連PR: #43817
通常なら非推奨化されたコードを削除するのはもっと先になるが、#44283を進めるのに邪魔なので、今削除する方がよいと判断した。
同PRより


つっつきボイス:「お、少し前に非推奨化されたurlsafe_csrf_tokensコンフィグが早くも削除された(ウォッチ20211221)」「Shopifyからのプルリクですね」

🔗 PostgreSQLとMySQLの修正1件ずつ


つっつきボイス:「PostgreSQLにクエリをキャンセルする機能があるとは知らなかった」

# activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb#L121
        def exec_rollback_db_transaction # :nodoc:
+         @connection.cancel unless @connection.transaction_status == PG::PQTRANS_IDLE
+         @connection.block
          execute("ROLLBACK", "TRANSACTION")
        end

「ところでテストコード:<という顔文字っぽい書き方があるけど↓、Rubyでこんな書き方ができるんですね」「ちょっとぎょっとする感じ」「Rubyのシンボルでしょうか?」「どうやらシンボルですね」

# activerecord/test/cases/adapters/postgresql/transaction_test.rb#195
    assert_operator duration, :<, 5

「こちらはMySQLアダプタです」「マルチステートメントのバッチで、abandon_results!を最後に呼んでいたのをバッチごとに呼ぶように修正したのね」

# activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb#L99
          def execute_batch(statements, name = nil)
            statements = statements.map { |sql| transform_query(sql) }
            combine_multi_statements(statements).each do |statement|
              raw_execute(statement, name)
+             @connection.abandon_results!
            end
-           @connection.abandon_results!
          end

max_allowed_packet設定を超えるフィクスチャをbulk insertするとMysql2::Error: Commands out of sync; you can't run this command nowが発生していたのを修正。
同PRより

🔗 番外: Railsガイドの旧ガイドリンクにdata-turbolinks="false"を追加


つっつきボイス:「こちらはRailsガイドのテンプレートにあるリンクの修正だそうです」「過去のガイドへのリンクだから、data-turbolinks="false"を追加して完全にページ遷移する必要があるんでしょうね」「なるほど」「デフォルトでTurbolinksがオンになっていると<a>タグの挙動が変わるんだった、気をつけよう」

# guides/source/_welcome.html.erb#L18
<p>
The guides for earlier releases:
-<a href="https://guides.rubyonrails.org/v7.0/">Rails 7.0</a>,
-<a href="https://guides.rubyonrails.org/v6.1/">Rails 6.1</a>,
-<a href="https://guides.rubyonrails.org/v6.0/">Rails 6.0</a>,
-<a href="https://guides.rubyonrails.org/v5.2/">Rails 5.2</a>,
-<a href="https://guides.rubyonrails.org/v5.1/">Rails 5.1</a>,
-<a href="https://guides.rubyonrails.org/v5.0/">Rails 5.0</a>,
-<a href="https://guides.rubyonrails.org/v4.2/">Rails 4.2</a>,
-<a href="https://guides.rubyonrails.org/v4.1/">Rails 4.1</a>,
-<a href="https://guides.rubyonrails.org/v4.0/">Rails 4.0</a>,
-<a href="https://guides.rubyonrails.org/v3.2/">Rails 3.2</a>,
-<a href="https://guides.rubyonrails.org/v3.1/">Rails 3.1</a>,
-<a href="https://guides.rubyonrails.org/v3.0/">Rails 3.0</a>, and
-<a href="https://guides.rubyonrails.org/v2.3/">Rails 2.3</a>.
+<a href="https://guides.rubyonrails.org/v7.0/" data-turbolinks="false">Rails 7.0</a>,
+<a href="https://guides.rubyonrails.org/v6.1/" data-turbolinks="false">Rails 6.1</a>,
+<a href="https://guides.rubyonrails.org/v6.0/" data-turbolinks="false">Rails 6.0</a>,
+<a href="https://guides.rubyonrails.org/v5.2/" data-turbolinks="false">Rails 5.2</a>,
+<a href="https://guides.rubyonrails.org/v5.1/" data-turbolinks="false">Rails 5.1</a>,
+<a href="https://guides.rubyonrails.org/v5.0/" data-turbolinks="false">Rails 5.0</a>,
+<a href="https://guides.rubyonrails.org/v4.2/" data-turbolinks="false">Rails 4.2</a>,
+<a href="https://guides.rubyonrails.org/v4.1/" data-turbolinks="false">Rails 4.1</a>,
+<a href="https://guides.rubyonrails.org/v4.0/" data-turbolinks="false">Rails 4.0</a>,
+<a href="https://guides.rubyonrails.org/v3.2/" data-turbolinks="false">Rails 3.2</a>,
+<a href="https://guides.rubyonrails.org/v3.1/" data-turbolinks="false">Rails 3.1</a>,
+<a href="https://guides.rubyonrails.org/v3.0/" data-turbolinks="false">Rails 3.0</a>, and
+<a href="https://guides.rubyonrails.org/v2.3/" data-turbolinks="false">Rails 2.3</a>.
</p>

🔗Rails

🔗 Rails 6.1を7.0にアップグレードしてみた(Ruby Weeklyより)


つっつきボイス:「7.0へのアップグレードをやってみた記事がそろそろ出始めてますね」「Railsはアップグレードガイドが整備されているので、それに従っていれば基本的な部分は大丈夫なはず👍

参考: Rails アップグレードガイド – Railsガイド

「アップグレードで問題になるのはむしろgemでしょうね」「たしかに…」

🔗 que: PostgreSQLをジョブキューにする(Ruby Weeklyより)

que-rb/que - GitHub


つっつきボイス:「Ruby Weeklyで見かけたんですが、このプルリクにも登場していたのが気になりました↓」

「★は2000超えか」「queはケイと発音するらしいけど何語だろう?」「これはPostgreSQLをジョブキューとして使えるようにするようですね: 以前も似たようなgemを見たことがあるかも」

# 同リポジトリより
# app/jobs/charge_credit_card.rb
class ChargeCreditCard < Que::Job
  # Default settings for this job. These are optional - without them, jobs
  # will default to priority 100 and run immediately.
  self.run_at = proc { 1.minute.from_now }

  # We use the Linux priority scale - a lower number is more important.
  self.priority = 10

  def run(credit_card_id, user_id:)
    # Do stuff.
    user = User.find(user_id)
    card = CreditCard.find(credit_card_id)

    User.transaction do
      # Write any changes you'd like to the database.
      user.update charged_at: Time.now

      # It's best to destroy the job in the same transaction as any other
      # changes you make. Que will mark the job as destroyed for you after the
      # run method if you don't do it yourself, but if your job writes to the DB
      # but doesn't destroy the job in the same transaction, it's possible that
      # the job could be repeated in the event of a crash.
      destroy

      # If you'd rather leave the job record in the database to maintain a job
      # history, simply replace the `destroy` call with a `finish` call.
    end
  end
end

「たしかにぽすぐれならジョブキューもやれる」「Redisを立てたくない人が欲しくなるのかも」「新しめのPostgreSQLでないとできないでしょうけど」


以下の記事では方法こそ違うものの、PostgreSQLをジョブキューの永続化に使っています↓。

RailsのPostgreSQL上でマルチテナントのジョブキューシステムを独自構築する(翻訳)

🔗 パスワード強度をエントロピーで算出する(Ruby Weeklyより)


つっつきボイス:「パスワードの強度を根拠のある形で算出するためにstrong_password gemを使ってエントロピーベースのパスワードチェックを実装した記事、なるほど」「画面ではauto-check-elementを使っているそうです」

bdmac/strong_password - GitHub

github/auto-check-element - GitHub

# 同記事より
def create
  checker = User.password_checker
  entropy = checker.calculate_entropy(params[:value] || "")
  percentage = (entropy / STRONG_ENTROPY) * 100

  percentage = 100 if percentage > 100

  render(partial: "users/shared/password_strength_meter", locals: { strength: percentage.to_i })
end

「いつも言っていますが、セキュリティに関連する実装では専門家が推奨するライブラリを使うべき」「セキュリティコアの手作りは避けたいですね」

🔗 rpush: Rubyでプッシュ通知と連携(Ruby Weeklyより)

rpush/rpush - GitHub


つっつきボイス:「Apple Push Notification ServiceやAmazon Device Messagingなどのプッシュ通知を共通の方法で書けるgemのようですね」「なるほど」「こういうのは自分で実装するよりもよさそう👍

app = Rpush::Apnsp8::App.new
app.name = "ios_app"
app.apn_key = File.read("/path/to/sandbox.p8")
app.environment = "development" # APNs environment.
app.apn_key_id = "APN KEY ID" # This is the Encryption Key ID provided by apple
app.team_id = "TEAM ID" # the team id - e.g. ABCDE12345
app.bundle_id = "BUNDLE ID" # the unique bundle id of the app, like com.example.appname
app.connections = 1
app.save!

「プッシュ通知ってあまり好きじゃないかも…」「プッシュ通知はユーザーがオプトアウトできますし、通知を増やしすぎないようにするのがコツでしょうね」

🔗 Sidekiqのコンカレンシーを制御する(Ruby Weeklyより)


つっつきボイス:「タイトルを見たときはSidekiqのEnterprise版の機能を自前で実装するのかと思ったけど、Redisのロック周りやアトミック操作などの話をしてるな: どうやら特定のジョブがコンカレントに実行されないことを保証するようにしたということみたい」「あ、concurrency controlはそっちの意味なんですね」「特定のジョブが実行されているときに特定のジョブが同時に実行されないようにしたいというのはたしかにありそう」

🔗 その他Rails


つっつきボイス:「寿司ビール問題↓が今になってまたバズっているのはともかく、とっくにサポート終了したMySQL 5.6を今の時代に使っている方がヤバいのでは」「ですよね」「以下の記事↓を見た限りでもExtened Supportすら1年前に終了している」

参考: MySQLのEOL – 雑記帳

MySQLのencodingをutf8からutf8mb4に変更して寿司ビール問題に対応する


前編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Rubygems Adoptionフォームが開設、JetBrains Gateway、NGINX Unitほか(20220201後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: Rails 6.1を7.0にアップグレードしてみた、PostgreSQLでジョブキューほか(20220208前編) first appeared on TechRacho.

週刊Railsウォッチ: Rubygems.orgのAPIキーに権限スコープが追加、RailsのDBパフォーマンス改善ほか(20220209後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 BrakemanとRuby 2.6のEOL

gems/brakeman-5.2.0/lib/brakeman/warning.rb:251:in `sprintf': can't convert nil into Integer (TypeError)

つっつきボイス:「ruby-jp Slackで見かけました」「BrakemanがRuby 2.6のEOL(End of Life: サポート終了)のwarningでエラーになったのか」「BrakemanはCIで動かすことが多いので、いきなりCIがクラッシュしたらびっくりするでしょうね」

このissueはBrakeman 5.2.1で修正されました↓。

🔗 Rubyのメモリフットプリント


つっつきボイス:「これもruby-jp Slackで見かけました」「全般にRubyのバージョンが新しくなるとじわじわメモリフットプリントが増えてますね」

他言語のメモリフットプリントセクションも興味深い」「Pythonのフットプリントが意外に小さいですね」「一番フットプリントがでかいのはNode.js」「JavaのOpenJDKが大きいのはしょうがない感があるけど、それよりNode.jsが大きいのが謎」「こうしてみるとdashとかtcshのフットプリントは圧倒的に小さいな〜、使いたくないけど😆

🔗 Puma 5.6がリリース(Ruby Weeklyより)


つっつきボイス:「新しいPuma、もうインストールしましたよ」「早い」「Changelogをチェックして、CIが落ちなければ即インストールですね: こういうのはコツコツやっておかないと古いままになって後になるほどアップデートがつらくなるので」「そうですよね」「Changelogがたまってくるとつらいんですよ…」

🔗 Rubygems.orgにAPIキーで権限スコープが指定可能になった


つっつきボイス:「Rubygems.orgにまた新しい機能が入ったそうです」「おぉ、APIキーを生成するときに特定権限を付与できるようになったんですね: これによってリスクを下げやすくなるし、gemの権限や作業の一部を他の人に移譲しやすくなる👍


後で見てみると、以下のスコープを指定して(複数指定可)APIキーを発行できるようになっていますね。

  • Index rubygems
  • Push rubygem
  • Yank rubygem
  • Add owner
  • Remove owner
  • Access webhooks
  • Show dashboard

🔗DB

🔗 Railsのデータベースパフォーマンスのテストと最適化(Ruby Weeklyより)


つっつきボイス:「よくある記事っぽいですが、AppSignalの記事なので何か新しい知見があるかなと思って取り上げてみました」

「データベースのパフォーマンスは、既に遅くなっている場合と遅くなりそうな場合で取り組み方が違ってくるので注意が必要です」

「1.のN+1対応、2.のインデックス設定とかはクエリのExplainなどで普通にやるやつ」「3.は自社のAppSignalでRailsアプリのパフォーマンスを測定しようとありますね」「AppSignalってどんなサービスでしたっけ?」「たしかNewRelicのようなアプリケーションパフォーマンス監視(APM)などいろいろやっているはず」

参考: AppSignal for Ruby | AppSignal documentation

🔗 limit

「そうそう、3.のlimitを付けるのも大事」「limit付け忘れて結果が大量になって死ぬ、あるある」

# 同記事より
User.where(country: "Germany").limit(100)

🔗 find_each

「4.はfind_each」「find_eachは主キー以外ではソートできなくて、自分で書かないといけなかった気がする」「そういえば以前find_in_batchesでORDER BYが効くかと思ったら効かなかったことがありました」

User.where(country: "Germany").find_each(:batch_size: 5000)

つっつき後、以下のプルリク(Rails 6.1にマージ済み)でorder: :descを指定できるようになっていると教わりました↓。それ以前は昇順のみだったようです。ソートの対象は主キーのみで、バッチ単位でソートされます。

🔗 pluck

「5.はpluckを使おうという話」「SELECTするカラムを減らすのは非常に大事ですね: カラムを減らすだけで急に速くなったりする」「そうそう」「ActiveRecord::Relationに対してpluckを使うと、SQLクエリレベルでSELECTするカラムが必要なカラムのみに絞り込まれるので、SQLの結果セットサイズを小さくでき、かつActive Recordのオブジェクト生成コストを削減できます」

# 同記事より
User.pluck(:id)
# SELECT "users"."id" FROM "users"

Railsの技: pluckはActive RecordモデルでもEnumerableでも使える(翻訳)

「そういえば最近のRailsのコンフィグにenumerate_columns_in_select_statementsという設定がありますね↓」「あれはSELECT *を実際のカラムに展開するオプションですが、それはカラムを減らす話とは別ですね」

参考: Rails アプリケーションを設定する – Railsガイド

enumerate_columns_in_select_statements設定オプションは単に*をカラムに展開するだけなので、結局全部のカラムをSELECTするんですよ: pluckのようにSELECTするカラムを絞り込むものではない」「そうだったのか…」「個人的には割と好きなオプションなので、名前を復唱して覚えておくぐらいはしてもいいと思います👍

🔗 バルク操作

「6.のバルク操作は、フックを実行しなくていいときなら使っていいと思います」「そうそう、バルク系はフックが呼ばれないんですよね」「バルク系でもたとえばdestroy_allはフックが呼ばれるけどdelete_allは呼ばれないなどは注意が必要ですね」

「おや、こんな書き方↓ができる?ハッシュの配列をUser.createというクラスメソッドに渡して作成できるのか、これは知らなかった」「知りませんでした」

# 元記事より(一部修正)
users = [
  {name: "Milap", email: "milap@country.com", country: "Germany"},
  {name: "Aastha", email: "aastha@country.com", country: "Germany"}
]

User.create(users)

# INSERT INTO users (name, email, country)
# VALUES
#   ('Milap', 'milap@country.com', 'Germany'),
#   ('Aastha', 'aastha@country.com', 'Germany')

created_atupdated_atを指定しなくていいinsert_allみたいな感じですか?」「見た感じではbefore_*系フックは動きそうなので、フックを実行してからINSERT INTOすると思いたい」「お〜なるほど」「その場合1件ごとにフックが動くんでしょうか?」「仕組み上そうでないとまずいでしょうね」「それもそうですよね」「心配なら確認コードを書くのがいいと思います」


追記: ActiveRecord::Base.createは元記事でバルク操作として紹介されていますが、実際にはバルクではなく単にループでcreateを呼んでいる↓とつっつき後に教わりました。

参考: rails/persistence.rb at 76489d81ba77216271870e11fba6889088016fa5 · rails/rails

なお、手元で試したところActiveRecord::Base.createでフックは効きました。

🔗 インメモリ処理

「7.はインメモリの処理」「このあたりはデータベースチューニングでよく扱うところですね: データベースチューニングというとついSQLレベルでやろうとする人が多いんですが、以下のようにpluckで全部取ってきたものをRuby側で計算してからクエリをかけるとか、Railsのキャッシュに乗る方が速くなることもよくあります」「あ〜たしかに」

# 同記事より
existing_countries = User.distinct.pluck(:countries)
puts countries - existing_countries

# SELECT DISTINCT `users`.`countries` FROM `users`

「こういう定番のノウハウが盛り込まれている記事は何度読んでもいいもの👍」「そうですね」

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

🔗 Docker Desktop無料の猶予期間が終了(Publickeyより)


つっつきボイス:「ついにDocker Desktop無料の猶予期間が終了か」「従業員250名以上か年間売り上げ1000万ドル以上の組織が有料ということなので、自分のところはさしあたって対象外ですけどね」

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

🔗 WebVM(Publickeyより)


つっつきボイス:「これはちょっと凄い」「ついにブラウザ上でLinuxが素で動くようになった🎉」「以下のリンクをクリックすると即起動しました↓」「WebAssembly用のカーネルなのかな?」

「プロセッサ数は1だけどnprocは2になっている」「2で固定かな?」「root権限は取れなさそう」「Ctrl-Cが効かなかったのでブラウザを閉じました」「う、Safariでは動かない…他のブラウザで開けと言われてしまった😢

「/procが見当たらないな: おそらくprocファイルシステムが入っていなくて、それを参照するコマンドも動かないんでしょうね」「psコマンドが効かないのもそのせいでしょうか?」「おそらくそうでしょうね」

「触った限りでは、呼べないシステムコールやドライバが結構ありそうな環境なので、実行できないものがいろいろあるのはしょうがないかな: バーチャルドライバを作ればいろいろ動かせるようになりそう」


「最近のWebAssemblyはアツいですね」「最近はAmazon Primeの動画再生もWebAssemblyになったらしい↓」「お〜」「動画再生のようにシビアな分野でWebAssemblyを導入して、しかも高速化したのは凄い」

参考: Amazon Prime Videoが動画再生にWebAssemblyを採用。再生デバイス上にWasm VMをデプロイ、高フレームレートなど実現 - Publickey

🔗言語/ツール/OS/CPU

🔗 regexploit: 正規表現のReDoSを検出

doyensec/regexploit - GitHub


つっつきボイス:「Pythonで書かれたReDoS検出ツールだそうです」「お〜、CIとかで自分のRubyコードをRuboCop的なツールで解析するときに正規表現を抽出してこれにかけられるといいな」

# 同リポジトリより
$ regexploit
v\w*_\w*_\w*$
Pattern: v\w*_\w*_\w*$
---
Worst-case complexity: 3 ⭐⭐⭐ (cubic)
Repeated character: [5f:_]
Final character to cause backtracking: [^WORD]
Example: 'v' + '_' * 3456 + '!'

参考: ReDoS – Wikipedia

「ところでチェックされる正規表現はPythonフレーバーが前提なのかな?」「もしかするとそうかも」「Ruby(Onigmo)の正規表現はそこそこ独自性があるから、とりあえず基本的な正規表現に絞って使うのがいいのかも」「今度試してみます」

はじめての正規表現とベストプラクティス1: 基本となる8つの正規表現

🔗 libtree: 共有ライブラリの依存関係をツリー表示


つっつきボイス:「お、こういうツールがあるんですね」「今までなかったのが不思議かも」「.soや.dylibなどのローダーの挙動を制御しなければならなくなるようなことは昔に比べて随分減りましたけどね」「昔はよくやりましたね」「組み込み系でよくやってたけどめっきりやらなくなった」「機械学習系みたいに外部ライブラリを使わざるを得ない分野だとたまに必要になるかも」


後編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Rubygems Adoptionフォームが開設、JetBrains Gateway、NGINX Unitほか(20220201後編)

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

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

Ruby Weekly

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ: Rubygems.orgのAPIキーに権限スコープが追加、RailsのDBパフォーマンス改善ほか(20220209後編) first appeared on TechRacho.

Rails 7.0.2がリリースされました

$
0
0

Ruby on Rails 7.0.2がリリースされました。

英語版Changelogをまとめて見るにはGItHubのリリースタグ↓が便利です。v7.0.2タグの日付は日本時間の2022年2月9日8:17でした。

詳しくは以下のコミットリストをご覧ください。

🔗 更新の概要

リリースノートには主要な変更として以下が記載されています。

🔗 Changelogに更新が記載されている機能

以下の機能はリリースノートの記載順です。

本記事では、GitHubリリースタグに掲載されているChangelogに対応するプルリクやコミットへのリンクを取り急ぎ貼りました。

🔗 Active Suport

ActiveSupport::EncryptedConfigurationをPsych 4互換になるよう修正
Stephen Sugden

File.atomic_writeのエラーハンドリングを改善
Daniel Pepper

🔗 Active Model

現在のモデルは、メソッド名が同じでも属性のbodyが異なることがあるため競合の原因になっていた。:active_model_proxy名前空間を新たに追加することでこの問題を修正。
Chris Salzberg

🔗 Active Record

Ruby 2.7以降でPG.connectキーワード引数のdeprecation warningが出ないよう修正
Nikita Vasilevsky

暗号化済みパラメータがオートフィルタから除外されていなかったのを修正
Mark Gangl

datetimeprecisionが新しいデフォルト値6でダンプされるよう修正
Rafael Mendonça França

on_encrypted_attribute_declaredで暗号化済み属性が2回フィルタされないようを修正
Nikita Vasilevsky

データベーススキーマのダンプに現在のRailsバージョンを含めるようにした

#42297以降、datetimeカラムのデフォルトのprecisionを6で生成するようになった。
このため、Rails 6.1から7.0にアップグレードするときにデータベーススキーマを読み込むと新しいprecision値が適用されてproductionのスキーマと一致しなくなる。
これを避けるために、スキーマダンプで新しいフォーマットを生成するときに以下のようにRailsのバージョンを含めるようにした。

ActiveRecord::Schema[7.0].define

Rails 6.1 -> 7.0アップグレードでrails app:updateタスクを実行すると、スキーマバージョンが6.1に設定されるようになる。
Rafael Mendonça França

PostgreSQLのgenerated columnの式の解析方法を修正
fatkodima

max_allowed_packetの値を超えるとフィクスチャのバルクinsertでMysql2::Error: Commands out of sync; you can't run this command nowエラーが発生する問題を修正
Nikita Vasilevsky

recordという名前のリレーションを持つ関連付けを保存するとエラーになる問題を修正
Dorian Marié

datetimeカラムのprecision値のMySQL::SchemaDumperの振る舞いを修正
y0t4

リフレクションのない関連付けでのエラーを改善
Nikolai

change_tableからcheck_constraintにオプションを渡せるよう修正
Frederick Cheung

🔗 Action View

preload_link_tagでJavaScriptモジュールが正しくプリロードするよう修正
Máximo Mussini

オブジェクトのresponseメソッドでstylesheet_link_tagなどのヘルパーを利用できなかった問題を修正
dark-panda

🔗 Active Storage

7.0.0で導入された、DirectUploadsControllerservice_nameを渡せる機能を元に戻す

導入された変更によってRailsアプリケーションのアップグレードで多くの問題が発生したため、現在作業中の後方互換性の実装を進める間はこの機能を削除することにした。
Gannon McGibbon

Active StorageのJavaScriptアセットのプリコンパイルをオプトアウトできるようにした
jlestavel

🔗 Changelogに更新の記載がない機能

以下はChangelogには更新の記載がありません。


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

関連記事

Rails 7.0.1がリリースされました

Rails 7.0.0がリリースされました

The post Rails 7.0.2がリリースされました first appeared on TechRacho.

週刊Railsウォッチ: Rails 7.0.2の改修内容、receipts gemでレシートを作成ほか(20220214前編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

今回は少し趣向を変えて、7.0.2に入った改修のうち直近かつ取り上げていなかったものを中心に見繕ってみました。

Rails 7.0.2がリリースされました

🔗 Active StorageのDirectUploadsControllerservice_nameを渡す機能がいったん外された


つっつきボイス:「そういえば7.0.2で削除された機能がありましたね」「Direct Uploadのストレージエンジンを切り替えられるservice_nameオプション機能が削除されたようですね」「この機能があると6.xからのアップグレードが難しくなってしまったので外したみたい」「完全になくなったわけではなくて、再実装が終わるまでいったん外すことにしたようです」「あ、そういうことですか」

「複数のオブジェクトストレージを切り替えながら混ぜて使うことは普通あまりないと思うので、この機能を使っている人はそんなにいないんじゃないかな」「AWSとGCPのストレージを切り替えて使うとか、たしかにあまりやらなさそうですね」

🔗 schema.rbに現在のRailsバージョンも含めるようになった

#42297以降のRailsはMySQLのdatetimeカラムのprecision(精度)を6で生成するようになった。つまり、6.1から7.0にアップグレードすると、データベーススキーマを読み込んだときに新しいprecision値が使われるようになり、productionのスキーマと合わなくなってしまう。
これを回避するため、ActiveRecord::Schemaクラスを6.1の実装でフリーズし、ActiveRecord::Migrationと同様の仕組みで他のバージョンにアクセスできるようにする。
スキーマダンパーは今後以下のようにRailsのバージョンを含む新しいフォーマットを生成するようになる。

ActiveRecord::Schema[7.0].define

関連: #43934および#43909
同PRより


つっつきボイス:「こちらはDBのスキーマファイルにRailsのバージョンを含めるようにした修正」「マイグレーションファイルの[7.0]みたいな感じでschema.rbにもバージョンを含めるんですね」

「Rails 7.0では一部のカラムのデフォルト値が少し変わったんですよ」「そういえばMySQLでprecisionが6に変わったとか書かれていますね」

./bin/rails db:setupするとschema.rbからスキーマを読み込むので、そのままだとRails 6.1からアップグレードするときにスキーマのデフォルト値が変わってしまうことがある」「あ〜そういうことですか」「./bin/rails db:migrateするのであれば、マイグレーションファイルに含まれているRailsのバージョン情報を使って適切に移行できるんですが、./bin/rails db:setupだとマイグレーションファイルを参照せずにschema.rbから直接スキーマを読み込むので、schema.rbにもRailsのバージョンを付加して適切なデフォルト値でスキーマを読み込むようにしたということですね」「なるほど」

🔗 関連付けにリフレクションがない場合のエラー表示が改善


つっつきボイス:「これはエラー表示の改善ですね: 元はテーブル名しか表示されなかったのがモデル名や関連付け名を表示するようになった↓」「NoMethodErrorArgumentErrorエラーに変わったんですね」「情報が足りないと何が起こっているのかわからないことがよくある」「そうそう」「エラーメッセージが親切なのは大事👍

# 改修前
Post.where.associated(:cars).to_a # Post does not have association named `cars`

=> NoMethodError: undefined method `table_name' for nil:NilClass
# 改修後
Post.where.associated(:cars).to_a
=> ArgumentError: An association named `:cars` does not exist on the model `Post`.

🔗 recordという名前の関連付けを保存するとエラーになるのを修正

recordという名前に関連付けられているリレーションを持つモデル(ポリモーフィックなものなど)のレコードを保存すると、record_changed?belongs_to :recordで上書きされるためActive Recordでエラーが発生する。
同PRより


つっつきボイス:「recordという名前が使われているとエラーになっていたそうです」「なるほど、Active Recordが動的に生やしたrecord_changed?が誤って動いてしまうので、_record_changed?のようにアンダースコアを付けて修正したんですね↓」

# activerecord/lib/active_record/autosave_association.rb#L464
-     def record_changed?(reflection, record, key)
+     def _record_changed?(reflection, record, key)
        record.new_record? ||
          association_foreign_key_changed?(reflection, record, key) ||
          record.will_save_change_to_attribute?(reflection.foreign_key)
      end

recordという名前はいかにもダメな名前っぽいけど、現実に使われる可能性はあるかも」「たしかに」「自分もつい最近、さんざん悩んだ末に仕方なくdateというカラム名を付けました」「何とか_atにしづらい日付なんですね」

🔗Rails

🔗 GitHub CodeQLにRubyコードのサポートが追加(Ruby Weeklyより)

github/codeql - GitHub


つっつきボイス:「CodeQLはGitHubのセキュリティチェック機能だそうです」「お〜、図を見た感じでは、QL Compilerで中間言語的なものを生成して、コードから抽出したデータベースクエリをそれで評価するみたい↓」


同記事より

「Rubyコードのパーサーにはtree-sitterというものを使っている↓」「パーサーで取り出したコードをRDBに保存しているのか、これは面白い」

参考: Tree-sitter|Introduction — Ruby以外にもいろんな言語向けのラッパーがあります

tree-sitter/ruby-tree-sitter - GitHub


「記事の話と直接関係ないんですが、CodeQLを眺めているうちにノイマン型アーキテクチャvsハーバードアーキテクチャという図式をちょっと連想しました」「ハーバードアーキテクチャはデータとコードが別腹になるヤツでしたっけ」「CodeQLがやっているように、やろうと思えばノイマンアーキテクチャ的にデータストレージにコードを置いたっていいんだよなと改めて思った次第です」「すごい発想」

参考: ノイマン型 – Wikipedia
参考: ハーバード・アーキテクチャ – Wikipedia

🔗 Liquidタグで動的コンテンツ表示(Ruby Weeklyより)


つっつきボイス:「Liquidって何だろうと思ったら、何かの静的サイトジェネレータでLiquidが使われてたのを少し触った覚えがあります」「Liquidはいわゆるテンプレートエンジンで↓、Shopifyが作ったものですね」「Shopifyはこういうのも作ってるんですね」

参考: Liquid template language

Shopify/liquid - GitHub

「ちょっと懐かしい感じのテンプレートエンジン記法↓」

# 元記事より
liquid("Hi {{ missing_value }}", context: {})
#=> "Hi \{\{ missing_value }}"

liquid("Hi {{ foo", context: {})
#=> "Hi \{\{ foo"

「元記事のテンプレートを見ていてSmartyをちょっと思い出しました」「それ、今まったく同じこと思いました😆」「Smartyとは?」「相当昔からあるPHPのテンプレートエンジンですね」「PHPやってたのでSmarty懐かしいです、今も現役なのか」

参考: Smarty マニュアル | Smarty

🔗 receipts: Railsで領収書やインボイスのPDFを手軽に作成(Ruby Weeklyより)

excid3/receipts - GitHub


つっつきボイス:「領収書やインボイスを作成するからreceiptsというそのまんまの名前」「Railsエンジンではなく単なるgemなんですね」

「PDF生成にはprawnを使っているらしい↓」

prawnpdf/prawn - GitHub

「書式のカスタマイズやi18nにも対応しているようですし↓、この書式ベースでいいなら使ってみてもいいんじゃないかな」「領収書やインボイスの書式はそんなに大きく変わらなさそうですよね」「ただ国によって税制なども異なるし、特に日本の業務案件だとgemの書式そのままでOKということはまずありませんけどね」「そこなんですよね…」「PDF生成みたいな機能は同じ言語と商慣習を共有している国で作られたものが安心感ある」

# 同リポジトリより
line_items: [
  [I18n.t("receipts.date"),           created_at.to_s],
  [I18n.t("receipts.product"), "GoRails"],
  [I18n.t("receipts.transaction"), uuid]
]

🔗 paralines: スレッドをわかりやすく表示 (Ruby Weeklyより)

Inversion-des/paralines - GitHub


つっつきボイス:「パララインズ?」「お〜READMEの動画↓を見たら一発で理解できた: かっこいい👍」「見せ方がうまいですね」「1行1スレッド的に表示している感じ」


同リポジトリより

「しかもコードはたった170行ですって」「できてから1か月も経ってないからすごく新しい」
「ところでこのRubyコードはTabでインデントされていますね」「スペースのインデントじゃないのか」「これは珍しい」

「デモ画像で凄いと思えるのがいい👍

🔗 ViewComponentsとTurbo-Streamingを組み合わせる(Hacklinesより)


つっつきボイス:「thoughtbotの記事です: Turbo-StreamingとViewComponentsは割と仲がよさそうかなと思って取り上げてみました」

github/view_component - GitHub

「なるべくRubyで書きたいならViewComponentsでやってみてもいいかもしれませんね」「なるほど、ViewComponentsはどこかで試してみたい気持ちがちょっとあります」「自分は最終的にHTMLを出力するならRubyで書かなくてもいいのではという気持ちがありますけどね」「それわかります」

# 同記事より
--- a/app/controllers/messages_controller.rb
+++ b/app/controllers/messages_controller.rb

def create
- @message = Message.create(body: params[:body])
+ @message = Message.new(body: params[:body])
+
+ if @message.save
+   Broadcast::Message.append(message: @message)
+ end

  respond_to do |format|
    format.html { redirect_to messages_path }
    format.turbo_stream
  end
end

参考: ViewComponent を試してみた

🔗 Rails 6->Rails 7アップグレード

つっつきボイス:「ぼくの記事だ〜」「アップグレードお疲れさまです」「JavaScriptがなかなか動かなくて書くのに2週間ぐらいかかっちゃいました」

「turbo-railsがちゃんとpinされてる」「pinするだけでJSのコードがCDNからやってくるのってちょっと感動ですよね」「そうそう」

# 同記事より
# config/importmap.rb
pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true

# turboはhotwiredのjsにpinづけされてる。
# こうして名前と呼び出し元をマッピングすることでapplication.js内でimportするだけで呼び出しが可能になる。

「importmap-railsだとデプロイが軽くなるのがうれしいですね: Bootstrapもプリコンパイル済みのJSコードでよければimportmap-railsでpinできますし」「最初私もそれでRails 7とimportmap-railsでBootstrapのpinを試してみたらできたんですが、Bootstrapを拡張しようとしたらやっぱりjsbundling-rails経由でビルドが必要みたいです😢」「そうそう、Sassの変数が絡んでくるものがあると無理」「まさにそれだったので、最終的にBootstrapをやめてtailwindcss-railsに宗旨変えしました」「Bootstrapで変数をカスタマイズし始めるとつらくなりがちなので、それでいいんじゃないでしょうか☺」「変数カスタマイズはつらいですよね」


「ところで、importmapって表記ゆれが甚だしいですよね、import-mapsだったりImport mapsだったり」「そうそう、公式の表記も今ひとつわからないので、自分はimportmap-railsのgem名で揃えることがよくあります」「最近はimportmapと書くのが多いような気もするけどよくわからない」「やべ、自分も表記ゆれ直しておこうっと(直しました)」

WICG/import-maps - GitHub

その後で続きの記事も公開されました↓

参考: Rails7にしたあとRuby3.1 + Yjit & Rails7.0.2にアップグレードする | srockstyle


「これはjnchitoさんの言うとおりで、production環境でconfig.consider_all_requests_local = trueにするのは絶対やめるべき」「そんなことをしたらデバッグコンソールからいろいろ見えちゃう」「経験の少ない人がproductionのデバッグでそうしたくなる気持ちはわからなくもないけど、envが露出したりしたら本気で危ない」

「どのフレームワークでもデバッグコンソールは非常に強力なので、productionでさらしてはいけない」「そうそう」「慣れないうちは程度の差はあれ一度はこのようなことをやってしまいがちですが、しないに越したことはありません」


itexamplespecifyの使い分けか」「どちらかというと英語圏での利便性っぽいですね」「itsubjectがあることを前提にした代名詞的な使い方で、exampleはケースを列挙するときに書くとか、specifyは結果に名前を付けるとか、原則論的にはそういう感じかな」

「自分はRSpecのそういう項目は日本語で書いてます」「自分もそうしてますし、特に日本語で書くなら別にitでいいと思います」「ですよね、itなら短いし」「failして表示されればどれでも同じですから」

「記事を見ると、itは英語で書いてexampleは日本語で書くという流派もあるらしいけど、見たことないかも」「そんなローカルルールがあるんですか」


「ところでこんな感じの書き方↓って、昔BDD(ビヘイビア駆動開発)の流れでテストを自然言語に近い形で書くのが流行ったときにcucumberやturnipでやってたりしましたね」「そういえばturnipはちょっと使ってみたけど結局やめました」

# 同記事より
RSpec.configure do |config|
  config.alias_example_group_to :次の仕様を記述する
  config.alias_example_to :次のような振る舞いを持つこと
end

次の仕様を記述する "四則演算" do
  次のような振る舞いを持つこと "1足す1は2である" do
    expect(1 + 1).to eq 2
  end
end

参考: ビヘイビア駆動開発 – Wikipedia


前編は以上です。

バックナンバー(2022年度第1四半期)

週刊Railsウォッチ: Rubygems.orgのAPIキーに権限スコープが追加、RailsのDBパフォーマンス改善ほか(20220209後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: Rails 7.0.2の改修内容、receipts gemでレシートを作成ほか(20220214前編) first appeared on TechRacho.

Viewing all 1830 articles
Browse latest View live