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

週刊Railsウォッチ: Active Storageのvariantsをeager loadingするメソッドが追加、Hotwire専用Discuss、AnyCable Proほか(20210621前編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

今回は公式更新情報とコミットリストのChangelogから見繕いました。Changelogに記載された変更が多めです。

「公式更新情報のうち最後の1つ以外は既にウォッチで取り上げていました」

🔗 Active RecordのBaseやCoreのクラス変数をActiveRecordクラスに移動して高速化


つっつきボイス:「cattr_accessormattr_accessorがなくなるのかと思ったら、ActiveRecord::BaseActiveRecord::Coreで使われているcattr_accessormattr_accessorによるクラス変数を親のActiveRecordクラスに移動するリファクタリングを行ったようですね」「mattrのmは何でしたっけ?」「モジュールです」

参考: Rails API cattr_accessorModule
参考: Rails API mattr_accessorModule


詳しくは#42442を参照。
クラス変数は端的に言って遅い。ancestor(先祖)が多いクラスでは特にそうで、ActiveRecord::Baseには60ものancestorがある。
これらのうちパフォーマンスに影響するものは一握りしかないが、一貫性を保つためにも、今後コントリビュータが新しくcattrmattrを追加せずに新しいパターンに沿って進めるためにも、クラス変数をすべて移行することを考えている。
ActiveRecord::Baseに残っていたのはこのプルリクで最後のはず。Active Recordの他のクラスにもあるが、それらのancestorチェインは長くないのでさほど影響しない。
ancestorチェインが長そうな他のクラスも見てみる(ActionController::Baseあたりか?)。
#42451より大意

🔗 Active Supportのvariantsでeager loadingをサポート


つっつきボイス:「variantは、100×100みたいな添付画像のサイズバリエーションでしたね」「これはわかりやすい改修: with_all_variant_recordsを使えばeager loadingできる」「いいねが21個もついてますね」「ものによってはパフォーマンスに結構影響しますし、これまで自力でN+1を回避していた人たちもいると思うので、これは欲しい機能ですね👍」

現在のActive Storageではvariantトラッキング(#37901)で添付ファイルごとにvariantが存在するかどうかをチェックするクエリが走る。通常のRailsのN+1防止策(includes)ではこれを防止できない。
このプルリクはwith_all_variant_recordsメソッドを追加し、かつincludesがActive Storageの添付ファイルで期待どおり動作するようになる。

user.vlogs.with_all_variant_records.map do |vlog|
  vlog.representation(resize: "100x100").processed
end

また、ビルトインのhas_manyスコープも更新されてvariantレコードも読み込めるようになった。したがって、現在N+1が発生している以下のようなコードはこのプルリクによってN+1が発生しなくなる。

User.where(id: user.id).with_attached_vlogs.map do |user|
  user.vlogs.map do |vlog|
    vlog.representation(resize: "100x100").processed
  end
end

#37901の多くのコメントが修正される。これを実装するにあたり、#39397のスタイルを大いに参考にした。
同PRより大意

🔗 replicaへの書き込み自動保護を無効に


つっつきボイス:「replicaなのでマルチデータベース関連の改修ですね」「今まではRails側でreplicaをデフォルトで書き込み禁止にしていたけど、replicaの書き込み禁止はデータベース側でやるべきなので削除した: これはそのとおりですね」「あ、そういうことですか」「想像ですけど、Rails側のロジックによる書き込み禁止は、gemと絡んだりすると迂回できてしまうことがあるんじゃないかな: replicaを使う場合はデータベース側でread専用のユーザーを作ることでリードオンリーにするのが普通ですし、書き込み禁止はデータベース側でやるべきだと思います」「なるほど」

# activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L139
      def preventing_writes?
-       return true if replica?
        return ActiveRecord::Base.connection_handler.prevent_writes if ActiveRecord.legacy_connection_handling
        return false if connection_klass.nil?

        connection_klass.current_preventing_writes
      end

この機能を使いたければ一応書き込み保護はできるが、今後は万全な保護としては扱われない。この書き込み保護はすべてのケースを正しく分類できるほど正確ではない。ユーザーは自分でreplicaを設定して書き込みを禁止し、許可されてないクエリの場合はデータベースのエラーに依存すべき。
別の解決方法: #42432
修正されるissue: #42432
同PRより大意

🔗 MySQLアダプタのクエリパラメータのセキュリティを向上

  • MySQLアダプタが、文字列の?で渡される数値やbooleanのパラメータを安全上の理由で文字列にキャストするようになった。

あるクエリ内で文字列と数値を比較する場合、MySQLは文字列を数値に変換する。つまり、たとえば"foo" = 0は暗黙で"foo"0にキャストしてTRUEと評価する。これはセキュリティ上の脆弱性につながる可能性がある。
Active Recordには、比較されるカラムの型を認識している場合の脆弱性に対する保護は既にあるが、?で渡す場合は引き続き脆弱だった。

User.where("login_token = ?", 0).first

上は以下を実行してしまう。

SELECT * FROM `users` WHERE `login_token` = 0 LIMIT 1;

修正後は以下を実行するようになる。

SELECT * FROM `users` WHERE `login_token` = '0' LIMIT 1;

Jean Boussier
同Changelogより大意


つっつきボイス:「MySQLアダプタでquote_bound_valueを追加して数値やブーリアン値を文字列にするようにしたんですね↓」「なるほど」「これまでもUser.where(login_token: 0).firstのようにハッシュの形で渡せば正しく文字列に変換されていたんですが、User.where("login_token = ?", 0).firstのように文字列とプレースホルダ?で値を渡すときはそうなっていなくてMySQLのキャストが効いていたらしい: 従来の挙動は普通にバグっぽいですね」

# activerecord/lib/active_record/connection_adapters/mysql/quoting.rb#L6
  module ConnectionAdapters
    module MySQL
      module Quoting # :nodoc:
+       def quote_bound_value(value)
+         case value
+         when Numeric
+           _quote(value.to_s)
+         when BigDecimal
+           _quote(value.to_s("F"))
+         when true
+           "'1'"
+         when false
+           "'0'"
+         else
+           _quote(value)
+         end
+       end
+

「ちなみにPostgreSQLだとこのようにカラムを文字列で指定するとデフォルトではクエリパーサーの段階でエラーになるんですよ(自動で型キャストする設定を付ければ変えられます)」「なるほど、ところで文中でbound variableとあるのはどういう意味なんでしょう?」「ここではプレースホルダ?で変数の値を渡すことを指していると思います」

🔗 Model.update!が追加


つっつきボイス:「Model.updatesave!は前からあったけど、エラーをraiseするModel.update!も追加されたんですね」

# activerecord/lib/active_record/persistence.rb#L336
+     def update!(id = :all, attributes)
+       if id.is_a?(Array)
+         if id.any?(ActiveRecord::Base)
+           raise ArgumentError,
+             "You are passing an array of ActiveRecord::Base instances to `update`. " \
+             "Please pass the ids of the objects by calling `pluck(:id)` or `map(&:id)`."
+         end
+         id.map { |one_id| find(one_id) }.each_with_index { |object, idx|
+           object.update!(attributes[idx])
+         }
+       elsif id == :all
+         all.each { |record| record.update!(attributes) }
+       else
+         if ActiveRecord::Base === id
+           raise ArgumentError,
+             "You are passing an instance of ActiveRecord::Base to `update`. " \
+             "Please pass the id of the object by calling `.id`."
+         end
+         object = find(id)
+         object.update!(attributes)
+         object
+       end
+     end
+

参考: Rails API updateActiveRecord::Persistence

🔗Rails

🔗 GitLab RunnerパッケージのGPGキーローテーション


つっつきボイス:「GitLabのセキュリティポリシーに基づいてGPGキーをローテーションしたそうです」「こういう暗号化キーのローテーションは定期的にも行われますね: GitLab Runnerのパッケージが対象なので、GitLab RunnerをパッケージアップデートしようとしたときにGPG検証エラーが出るようになると思います」「ふむふむ」

「GPG keyの有効期限が来ると新しいパッケージリストのアップデートが失敗するようになるので、新しいGPG keyで署名したパッケージをインストールする際には何らかの形で事前に新しいGPG keyをインストールしておく必要があります: そうしないとインストール時にGPG署名エラーでインストールできなくなってしまう」「なるほど」

GitLabでは、GitLab Runnerの公式パッケージへの署名にGPGキーを使っています。最近、この鍵や、GitLab Runnerの公式パッケージやバイナリを配布するための他のトークンが、GitLabのセキュリティポリシーに基づいて保護されていない事例があることが判明しました。
これまで弊社は、パッケージの不正な変更や、パッケージを保存しているサービスへのアクセスの証拠を発見していません。弊社チームの調査では、整合性ハッシュ、バケットのログとバージョン管理、パイプラインの履歴などを監査した結果、パッケージが不正に変更された可能性は極めて低いと結論づけました。
慎重を期して、リリースの署名や検証に用いられていたGPGキーは、不適切に保護されていた他のすべてのトークンとともにローテーションされました。
同記事冒頭より大意

🔗 AnyCableのPro版登場

anycable/anycable - GitHub


つっつきボイス:「AnyCableのPro版が出た🎉」「Evil Martiansの記事でもプロダクションレベルのRailsサンプルアプリとしてよく使われていますね↓」

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

「ところでAnyCableやSidekiqのようなソフトウェアの有償版ってどのぐらい使われているのかな? AnyCableはまずEvil Martians自身が使う製品という感じもするので、売れ行きはそれほど気にしないスタンスなのかもしれませんが」「AnyCableの場合はサポートを有料で買えるみたいですね」「少なくともSidekiqのエンタープライズ版を使ってる人は見たことないですね」

参考: Sidekiq — Simple, efficient background jobs for Ruby.

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

🔗 Hotwire専用Discuss

今レスが一番多いのは以下のスレッドでした。


つっつきボイス:「ruby-jp Slackで知りました」「discuss.hotwire.devという専用ドメインでDiscussアプリを立ち上げたのは、HotwireがRailsに限定しないフレームワークだからかもしれませんね」「そういえばDjangoの話題(PythonのWebフレームワーク)も出ていました」

参考: Django ドキュメント | Django ドキュメント | Django

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

🔗 Rack::SendfileミドルウェアにリクエストヘッダーからのRegexインジェクション問題

参考: rack/sendfile.rb at v2.2.2 · rack/rack
参考: Rails API send_fileActionController::DataStreaming


つっつきボイス:「記事にもある、Rack::SendFileミドルウェアをRailsのデフォルトから削除しようというissue↓は少し前につっつきでも話題にしましたね」「あのときは情報が少なかったので記事にしませんでしたが、その後で上の情報がhackerone.comで正式に公開されました」

参考: Remove Rack::SendFile from default middleware. · Issue #41148 · rails/rails

「こういうふうにX-Sendfile-typeX-Accel-Mappingで正規表現を注入してReDoS(正規表現DoS)したり、読み出してはいけないファイルを読み出したりできてしまうらしい↓」

# hackerone.comより
curl -i -H 'X-Sendfile-type:X-Accel-Redirect' -H 'X-Accel-Mapping:(([^\r])+.)+[^\r]([\r])+=/www/' http://localhost:3000/files
# hackerone.comより
curl -i -H 'X-Sendfile-type:X-Accel-Redirect' -H 'X-Accel-Mapping:/.*=/secret_internal' http://localhost:80/rails/files

「tenderloveさんのコメントには、Rack::SendFileは本来アプリケーションをプロキシの向こうに置いて使うべきもので、Railsのデフォルトミドルウェアから削除すべきだろうと書かれていました」「たしかRack::SendFileはもともと、ファイルのダウンロードをnginxなどに中継させるのに使ったりするミドルウェアだったと思います」

Sendfileミドルウェアは、bodyがファイルから提供されるレスポンスをインターセプトして、サーバー固有のX-Sendfileヘッダーで置き換えます。Webサーバはファイルの内容をクライアントに書き込む役割を果たします。これによりRubyのバックエンドで必要な負荷を大きく減らすことができ、最適化されたファイル配信コードをWebサーバで利用できるようになります。
このミドルウェアを利用するには、レスポンスのbodyがto_pathに応答し、リクエストにX-Sendfile-Typeヘッダが含まれている必要があります。Rack::Filesや他のコンポーネントはto_pathを実装しているので、アプリケーション内で何かをする必要はほとんどありません。通常、X-Sendfile-Typeヘッダの設定はウェブサーバで設定します。
Rack::Sendfileドキュメントより大意

Rack::SendFileのようにファイルダウンロードをミドルウェアでリダイレクトする方がたしかに効率はいいんですが、RailsとWebサーバーの間でロジックをやりとりするとこのような問題が生じる可能性もありそうですね」「なるほど」


ひとまず自分のRailsアプリからはRack::SendFileを削除しました。

$ .bin/rails middleware
use Rack::MiniProfiler
use Sqreen::ShrinkWrap
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Sqreen::Middleware
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use BetterErrors::Middleware
use Sqreen::ErrorHandlingMiddleware
use Sqreen::RailsMiddleware
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CacheStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Rack::Attack
use Rack::Attack
use Bullet::Rack
run Enno::Application.routes

🔗 その他Rails


つっつきボイス:「Parameters.newにcontextを渡せるようにして、パラメータがunpermittedの場合にどのコントローラのどのアクションでunpermittedなパラメータが渡されたかをInstrumentationのログから取れるようになったんですね↓👍」「今まではunpermittedだったときのキーしか取れなかったそうです」

# 同記事より
context = { controller: self.class.name, action: action_name }
request_params = { user: { name: "Francesco", email: "fransceso@example.com", role: "admin" } }

params = ActionController::Parameters.new(request_params, context)
params.permit(user: [:name, :email])

# Unpermitted parameter: :role. Context: { controller: UsersController, action: create }

参考: Active Support の Instrumentation 機能 - Railsガイド


前編は以上です。

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

週刊Railsウォッチ(20210615後編)RubyのRBSを理解する、シンボルがGCされないとき、Terraform 1.0リリースほか

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: Active Storageのvariantsをeager loadingするメソッドが追加、Hotwire専用Discuss、AnyCable Proほか(20210621前編) first appeared on TechRacho.


週刊Railsウォッチ: childprocess gemで子プロセスを制御、Ruby 2.6〜3.0で動くdelegationほか(20210623後編)

$
0
0

こんにちは、hachi8833です。RubyKaigi Takeout 2021のプロポーザル提出とスポンサー募集は今月いっぱいで締め切りだそうです。どうぞお早めに。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 childprocess: Rubyで子プロセスを制御(Ruby Weeklyより)

enkessler/childprocess - GitHub


つっつきボイス:「お〜、Rubyで手軽に子プロセスの管理(Ruby側からのプロセス終了待ちやexitコードの取得)やシグナルの送信を行えるのか、なるほど」

# 同リポジトリより
process = ChildProcess.build("ruby", "-e", "sleep")

# 親プロセスからstdout/stderrを継承
process.io.inherit!

# またはIOを渡す
process.io.stdout = Tempfile.new("child-output")

# 子のenvを変更する
process.environment["a"] = "b"
process.environment["c"] = nil

# 子のワーキングディレクトリを設定
process.cwd = '/some/path'

# プロセススタート
process.start

# プロセスステータスのチェック
process.alive?    #=> true
process.exited?   #=> false

# プロセスが終了するまで待つ
process.wait
process.exited?   #=> true

# exitコードを取得
process.exit_code #=> 0

# またはexit + 強制終了をポーリング
begin
  process.poll_for_exit(10)
rescue ChildProcess::TimeoutError
  process.stop # プロセスkillの方法をだんだん荒っぽくしてゆく
end

process = ChildProcess.build("ruby", "-e", "sleep")でビルドしてから各種設定を行い、それからprocess.startでプロセスを開始するという流れのようですね」「プロセスの終了方法もいろいろあるんですね」「RubyにもOpen3.#popen3というライブラリが以前からありますけど、これはPOSIXのpopenライクなインターフェースなので、制御インターフェースがRubyっぽくない部分もあったりします」「なるほど」「このchildprocess gemは、process.waitなどのようにRubyらしく直感的に書けるのがよさそう👍

参考: Open3.#popen3 (Ruby 3.0.0 リファレンスマニュアル)
参考: Man page of POPEN

🔗 Ruby 2.6〜3.0のどれでも動くdelegationとは


つっつきボイス:「最新の話題ではありませんが、Ruby 2.6〜3.0のどのバージョンでもdelegationを動かすにはruby2_keywordsrespond_toで確認する必要があるという記事でした」「Railsでもruby2_keywordsを使って互換性を保つようにしていますね」

# 同記事より: 2.6〜3.0で動く
def foo(*args, &block)
  target(*args, &block)
end
ruby2_keywords :foo if respond_to?(:ruby2_keywords, true)

参考: Module#ruby2_keywords (Ruby 3.0.0 リファレンスマニュアル)

「こういう書き方だと動かないバージョンがあるのね↓」

# 同記事より
# 2.6と2.7で動かない
def foo(*args, **kwargs, &block)
  target(*args, **kwargs, &block)
end

# Ruby 3以降で動かずRuby 2.7でwarning
def foo(*args, &block)
  target(*args, &block)
end

「トリプルドット...記法を使ったdelegationなら複数バージョンのRubyで使えるかなと思ったけど、記事によると使えるのはRuby 2.7からか」「そういえばRuby 3.0で(arg, ...)のように...の前に1個以上引数を置けるように拡張されていましたね」「その拡張記法も後からRuby 2.7.3にバックポートされたと記事に書かれてた(#16378): Ruby 2.7.3以降でよければ(...)(arg, ...)でdelegationできますね」

参考: 全引数を別のメソッドに引き渡す...引数が導入された — サンプルコードでわかる!Ruby 2.7の主な新機能と変更点 Part 3 - 新機能と変更点の総まとめ - Qiita

参考: Feature #16378: Support leading arguments together with ... - Ruby master - Ruby Issue Tracking System

🔗 JRubyが9.2.19.0にアップデートしてRuby 2.5.xに対応


つっつきボイス:「JRuby頑張ってますね」「JRubyを自分で使ったことはないな〜」「RubyKaigiが現地開催だった頃にJRubyのセッションをよく見に行ってたのを思い出して懐かしくなってきました」

「JRubyはどういう層で使われているんでしょう?」「推測ですが、主にエンタープライズ分野かなと思います: TomcatのようなJavaのWebコンテナを動かすインフラ環境が共通基盤として既に整備されている企業だと、Rubyのプロセスを動かせるサーバー環境を新たに導入するより、RailsをWARのWebコンテナにパッケージングして普通のJavaアプリと同じようにデプロイおよび管理できるJRubyの方が社内手続き的にも運用体制的にも導入しやすいかもしれませんね」「なるほど」

参考: Java Servlet - Wikipedia

🔗 RBSとGitHub linguist


つっつきボイス:「@st0012さんが上げたissueで、RBSがGitHubでシンタックスハイライトされるようにGitHub linguist↓に登録してはどうかと提案していました」「なるほど、RBSもシンタックスハイライトされるといいですね」

github/linguist - GitHub

「以下のドキュメントによると、GitHub linguistに言語を追加するには自分以外のリポジトリでその言語が使われていることが望ましいそうです」「拡張子には限りがあるので、レビューを通さないと収拾がつかなくなりますよね😆」「たしかに」「言語を追加できるかどうかは使われている行数とかで決めるのかなと思ったら、リポジトリの数なのね」

参考: linguist/CONTRIBUTING.md at master · github/linguist

言語の追加は、GitHubである程度使われるようになってから行われます。ほとんどの場合、新しい拡張子につきユニークなリポジトリ(:user/:repo)200個以上でその言語が使われていることが望ましいと考えています。
同ドキュメントより大意

「linguistに追加された言語は、GitHubのサイドバーにこういう感じで表示されるようになるはず↓」「あ、むしろそっちがメインかも」「言語検出の設定とシンタックスハイライトの設定は別物ですね」


ruby/rbsより

「GitHubに言語を追加する手順がこうやって公開されているのは好感が持てます👍


つっつき後、上のissue #682に@soutaroさんもレスを付けていました。なお、以下のdependency graphを見ると本記事公開時点では149リポジトリでRBSが使われています。6日前は139リポジトリだったので順調に伸びてますね。

参考: Network Dependents · ruby/rbs

🔗 その他Ruby

つっつきボイス:「選択肢がもうちょっと欲しいかな〜」「私もprintデバッグですが”その他”で回答しました」「ruby_jardは使ったことありませんが以前ウォッチで取り上げましたね(ウォッチ20200818)」



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

🔗 AWS Proton


つっつきボイス:「またAWSの新しいサービスが出たそうです」「AWSには似たようなサービスがいろいろあるけど、Protonはコンテナのデプロイ周りまで面倒を見てくれるようですね」


aws.amazon.comより

参考: AWS Proton (コンテナとサーバーレスデプロイメントのための自動管理) | AWS

「GitHubにある公式のサンプルテンプレート↓に沿って構築できる分、プレビュー版では細かなことは多少やりにくいらしいという話も耳にしています」

aws-samples/aws-proton-sample-templates - GitHub

参考: AWS Protonのサンプルプロジェクトを試す - Qiita

「サービスの構築はデプロイ体制を整えるまでの作業にインフラ周りの経験値を要求されるので、Protonはそういう部分をテンプレでやれるのはよさそうかな」

🔗 Krustlet(Publickeyより)

deislabs/krustlet - GitHub

参考: WebAssembly | MDN
参考: Rustで書かれたKubernetesのためのWASM実行環境Krustletとは? | Think IT(シンクイット)


つっつきボイス:「KubernetesのノードとしてDockerコンテナの代わりにWebAssemblyのランタイムを用使える、つまりWebAssemblyアプリをKubernetesのPodとして動かせるということか」

参考: Podとノードについて | Kubernetes

「WebAssemblyは、どことなくかつてJavaが夢見たものを思わせますね: JavaがJVMを舞台としていたのが、WebAssemblyの場合はブラウザでそれをやろうとしているノリで」「Javaの”write once, run anywhere”ですね」「もちろんWebAssemblyはブラウザだけに限定されないと思いますが、WebAssemblyはサンドボックス内で動くから、普通のDockerコンテナならできるけどWebAssemblyではできないこともまだあるでしょうね」「そうですね」

参考: Write once, run anywhere - Wikipedia

「Krustletは純粋な計算処理を少ないリソースで高速実行するにはよさそうかな: ファイルアクセスとかミドルウェアを使おうと思ったらDockerコンテナの方がいいと思いますが、WebAssemblyが普及したら今後パッケージ管理や入出力周りが拡張されていくかもしれませんね」「なるほど」「WebAssemblyがこういうふうにクラウド化されていけば、Dockerコンテナよりも多重化の度合いを上げやすそうなので、より安くサービスを提供されるようになるかも」

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

🔗 target="_blank"


つっつきボイス:「たぶんもう知られていると思いますが、最近のブラウザはtarget="_blank"を指定して開いた別ウィンドウではデフォルトで親タブを操作できないようになっていたことを今頃知りました」「target="_blank"で開いたウィンドウがデフォルトでrel="noopener"になったのは割と前からですね」

参考: rel=noopener | Can I use... Support tables for HTML5, CSS3, etc
参考: 新規タブリンクの恐ろしい仕様、Chrome 88で変更へ ~Safari/Firefoxに合わせた安全な仕様に - やじうまの杜 - 窓の杜

🔗言語/ツール/OS/CPU

🔗 Mathpix


つっつきボイス:「Mathpixは画像の数式や手書きの数式をキャプチャして整形してくれるアプリか」「これはマジ凄いですね」「TeXのコードも生成できるとは賢い」

「そうそう、LaTeXはこういうふうに分子の構造式も描けますね↓」「言われてみたらなるほどですが知りませんでした!」「構造式を記述するLaTeXパッケージの追加が必要だったと思います」

参考: XyMTeX - Wikipedia

「数式であるとわかっているパターンなら認識しやすいのかな: それでもkとギリシャ文字のκとか、xとギリシャ文字のχあたりは区別が大変そうですが」「人間でも間違えそうですね」「LaTeXはかなり書き慣れてこないと描画を想像しにくいので、こうやって画像認識で生成するのはのは理にかなってそう👍


後編は以上です。

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

週刊Railsウォッチ: Active Storageのvariantsをeager loadingするメソッドが追加、Hotwire専用Discuss、AnyCable Proほか(20210621前編)

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

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

Ruby Weekly

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ: childprocess gemで子プロセスを制御、Ruby 2.6〜3.0で動くdelegationほか(20210623後編) first appeared on TechRacho.

週刊Railsウォッチ: GitLab 14.0のbreaking changes、Railsのセキュリティ脅威解説シリーズ記事ほか(20210628前編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

今回は、以下の公式更新情報から見繕いました。今回載せきれないほど更新がありましたので、来週も追ってみたいと思います。


つっつきボイス:「今回の公式更新情報、すごく多いですね」「Railsウォッチの先週の改修とのかぶりも少なかったので、もしかして追いつかれないように気合い入れてたりして」「ないない😆

🔗 set_pool_configpool_configがnilの場合にエラーを出すようにした


つっつきボイス:「コネクションプールでマルチプルデータベースのロールやシャーディングなどを設定するときに、pool_configがnilならエラーをraiseするようにしたようですね」

# activerecord/lib/active_record/connection_adapters/pool_manager.rb#L38
      def set_pool_config(role, shard, pool_config)
-       @name_to_role_mapping[role][shard] = pool_config
+       if pool_config
+         @name_to_role_mapping[role][shard] = pool_config
+       else
+         raise ArgumentError, "The `pool_config` for the :#{role} role and :#{shard} shard was `nil`. Please check your configuration. If you want your writing role to be something other than `:writing` set `config.active_record.writing_role` in your application configuration. The same setting should be applied for the `reading_role` if applicable."
+       end
      end

参考: Shard (database architecture) - Wikipedia


#41549で、pool_confignilだったのでall_connection_poolsメソッドがエラーになったユーザーがいた。再現用アプリを入手すると、アプリケーションのコンフィグをミスったときにこれが発生することがわかった。たとえば、アプリケーションでwritingロールに:allを使ってもconfig.active_record.writing_role = :allが設定されず、setup_shared_connection_poolwriting_pool_configの値がnilになり、それがset_pool_configに設定されていた。
setup_shared_connection_poolを直接修正してエラーを出すようにすることも検討したが、外部gemやアプリケーションがprivate APIを使っているとこのエラーが発生する可能性がある。現実には、Railsであるかどうかに関わらず、どのコードもプールのproolコンフィグにnilを設定して欲しくない。

注: テストでは別途コネクションハンドラを作成して、テスト対象に別のプールを持たせるようにした。そうでないと既存のプールがテストされてしまうので、そちらに影響を与えたくない。
同PRより大意

🔗 forced_encoding_for_deterministic_encryptionオプションの追加など


つっつきボイス:「deterministic?」「”決定論的な”と訳されることが多いですね」「元が同じ文字列なら暗号化した結果も常に同じになる暗号化をdeterministic encryptionと呼びますね: この場合、暗号化済み文字列同士で同値かどうかを比較できます」「なるほど、そういう意味ですか」「deterministic encryptionでも元の文字列のエンコードが違えば同じにならなくなるので、同じになるはずのものがならないことがある問題を修正したようですね: テストでもエンコードをUS-ASCIIとUTF-8にして比較している↓」

# activerecord/test/cases/encryption/encryptable_record_api_test.rb#94
  test "encrypt will honor forced encoding for deterministic attributes" do
    ActiveRecord::Encryption.config.forced_encoding_for_deterministic_encryption = Encoding::UTF_8

    book = ActiveRecord::Encryption.without_encryption { EncryptedBook.create!(name: "Dune".encode("US-ASCII")) }
    book.encrypt
    assert Encoding::UTF_8, book.reload.name.encoding
  end

「いつも暗号化済みで同値比較できる方がよさそうですけど?」「カーディナリティの低いデータ(年齢や性別、誕生日など)を想定したときに、Aさんの暗号化前データを知っていればAさんと同じ情報を持つ人を特定できることになります」「あ、それはマズそう」「いえ、それだけでマズいというものではなく、そういう方式の暗号化もあるということです: 暗号化方式を選ぶときにはそうした使い勝手や運用も含めて検討する必要があります」「なるほど」

参考: Deterministic encryption - Wikipedia

ActiveRecord::Encryptionで”決定論的”暗号化を使う場合には値をUTF-8でエンコードするようになった。このエンコードは暗号化済みペイロードの一部なので、値によってエンコード方式が変わると暗号文も異なってしまう。これによってunique制約やクエリが壊れる可能性がある。
新しい振舞いはactive_record.encryption.forced_encoding_for_deterministic_encryptionでコンフィグ可能。デフォルトはEncoding::UTF_8で、nilを設定すると無効にできる。
Jorge Manrubia
Changelogより大意

他にも以下が追加されています。

  • 暗号化済み属性でexists?をサポート。
EncryptedBook.exists?(name: "Dune")
  • ignore_case: trueオプションを指定しても再暗号化で大文字小文字を区別するようになった。

🔗 strict_loadingがthrough関連付けの中間レコードへカスケードするようになった


つっつきボイス:「ここで言うカスケードって何だろう?」「このテスト↓を見るとDeveloper.strict_loading.includes(:firms)strict_loadingしたものがdev.firms.first.contracts.firstなどのメソッドチェーンでも効くようにしたということみたい」「なるほど」「明示的にstrict_loadingするならこういうふうに動いて欲しいでしょうね👍

# activerecord/test/cases/strict_loading_test.rb#256
  def test_strict_loading_with_has_many_through_cascade_down_to_middle_records
    dev = Developer.first
    firm = Firm.create!(name: "NASA")
    contract = Contract.create!(developer: dev, firm: firm)
    dev.contracts << contract
    dev = Developer.strict_loading.includes(:firms).first

    assert_predicate dev, :strict_loading?

    [
      proc { dev.firms.first.contracts.first },
      proc { dev.contracts.first },
      proc { dev.ship }
    ].each do |block|
      assert_raises ActiveRecord::StrictLoadingViolationError do
        block.call
      end
    end
  end

🔗 package.jsonでカレントのRails->npm_versionを使うようになった


つっつきボイス:「ちょうどさっきセマンティックバージョニングの話をしましたけど(後述)、まさにそれに通じる改修かも」「5.0.0.rc15.0.0.beta1.1というバージョン表記だとnpmのバージョニングシステムに合致しないのか」「Rubygemは5.0.1.1のような4桁表示を認識できますけど、npmだと認識できないから5.0.1-1のような表記に置き換えるようですね」

参考: Semantic Versioningの闇 - knqyf263’s blog

# railties/lib/rails/generators/app_base.rb#L301
+     # This "npm-ifies" the current version number
+     # With npm, versions such as "5.0.0.rc1" or "5.0.0.beta1.1" are not compliant with its
+     # versioning system, so they must be transformed to "5.0.0-rc1" and "5.0.0-beta1-1" respectively.
+     #
+     # "5.0.1"     --> "5.0.1"
+     # "5.0.1.1"   --> "5.0.1-1" *
+     # "5.0.0.rc1" --> "5.0.0-rc1"
+     #
+     # * This makes it a prerelease. That's bad, but we haven't come up with
+     # a better solution at the moment.
+     def npm_version
+       # TODO: support `options.dev?`
+
+       if options.edge? || options.main?
+         # TODO: ideally this would read from Github
+         # https://github.com/rails/rails/blob/main/actioncable/app/assets/javascripts/action_cable.js
+         # https://github.com/rails/rails/blob/main/activestorage/app/assets/javascripts/activestorage.js
+         # https://github.com/rails/rails/tree/main/actionview/app/assets/javascripts -> not clear where the output file is
+         "latest"
+       else
+         Rails.version.gsub(/\./).with_index { |s, i| i >= 2 ? "-" : s }
+       end
+     end
+
+     def turbolinks_npm_version
+       # since Turbolinks is deprecated, let's just always point to main.
+       # expect this to be replaced with Hotwire at some point soon.
+       if options.main? || options.edge?
+         "git://github.com/turbolinks/turbolinks.git#main"
+       else
+         "^5.2.0"
+       end
+     end

「ついでにドキュメントも更新されてる↓」「Rails 7という文字を見てちょっとゾクゾクしました」

# guides/source/upgrading_ruby_on_rails.md#19
### Ruby Versions

Rails generally stays close to the latest released Ruby version when it's released:

* Rails 7 requires Ruby 2.7.0 or newer.
* Rails 6 requires Ruby 2.5.0 or newer.
* Rails 5 requires Ruby 2.2.2 or newer.

It's a good idea to upgrade Ruby and Rails separately. Upgrade to the latest Ruby you can first, and then upgrade Rails.

🔗 セマンティック バージョニングよもやま

「半年前の記事ですが、RubyやNode.jsを例に出していました」「記事冒頭の要約に大事なことは書かれていますね: バージョンの比較とバージョン制約は別の話」

  • Semantic Versioning 2.0.0にはバージョン”比較”の定義はあるが、バージョン”制約”(>= 2.1.3みたいなやつ)の定義がない
  • その結果、同じsemver準拠ライブラリでも制約の解釈が異なり結果が真逆になる
  • というかそもそもsemver使ってるエコシステムが少なすぎる
    Semantic Versioningの闇 - knqyf263’s blogより

「Semantic Versioningはひと頃かなりメジャーになりましたね↓」「お〜、こんなガイドラインもあるんですか」「こうしたルールを何らかの形で決めておかないとRubyのbundlerのようなものが作れません」「それもそうか」

参考: セマンティック バージョニング 2.0.0 | Semantic Versioning

v1.2.3みたいにvを付けるのはセマンティックバージョンではないそうです↓」「え、vダメなのか」「この2.0ドキュメントではBNFまで使ってバージョンの書き方決めてる」「そうしないと壊れるからでしょうね」

『v1.2.3』はセマンティック バージョンでしょうか?
 いいえ、『v1.2.3』はセマンティック バージョンではありません。しかしながら、セマンティック バージョンに接頭辞の『v』を付けるのは英語ではバージョン番号であることを示す一般的な方法です。バージョン管理では、『バージョン』を『v』と略すことがよくあります。たとえば git tag v1.2.3 -m” Release version 1.2.3 ” では『v1.2.3』はタグ名であり、セマンティック バージョンは『1.2.3』です。
semver.orgより

参考: バッカス・ナウア記法 - Wikipedia

「Semantic Versioningに沿っていると謳っているソフトウェアでも実際に厳密に沿っているとは限らないことが割とありますよ」「へ〜」「X.Y.Z(Xがメジャー、Yがマイナー、Zがパッチ)の3桁形式は取り入れていても、細かい点が違っていたりするのも見かけます: Railsもここで言うSemantic Versioning (SemVer)2.0.0には従っていないと言えますが『意味付けをしたバージョニング』という意味ではバージョンの付け方はちゃんと管理されているので、広義ではセマンティクスのあるバージョニング、という言い方もできると思います」「なるほど」

参考: Maintenance Policy for Ruby on Rails — Ruby on Rails Guides

Rails follows a shifted version of semver:
edgeguides.rubyonrails.orgより

「Railsのバージョンアップのインパクトとしては、Semantic Versioningで言うYのバージョンアップが事実上メジャーバージョンアップに近いものを感じますね」「まあたしかに😁」「Railsではセキュリティパッチのリリースに4桁目も付けますけど、こちらの方がパッチバージョンに近い気がしています」

「記事にもRubygemsのバージョニングは独特とありますね」「Rubygemのバージョニングとnpmパッケージのバージョニングなどもそうですけど、単に表記が違うだけでなく意味づけも違ったりすることがあるんですよ」「バージョニングって大変…」「固有名詞としての『Semantic Versioning(SemVer)』は厳密に定義を決めたものであるのに対し、世の中ではSemVerを参考にした『セマンティクスのあるバージョニングポリシー』の方言が色々あって、それらが混在してしまっているのが現状ですね」

参考: Representational State Transfer - Wikipedia


後で仕様のリポジトリを見つけました。

semver/semver - GitHub

オンラインのsemverバリデータも見つけました(公式ではないようです)。

🔗 Active StorageでGCSのcache_control:にデフォルト値の設定がサポートされた


つっつきボイス:「これはわかりやすいですね: Google Cloud Storage(GCS)のcache_controlにデフォルト値を書けるようになった」

gcs:
  service: GCS
  ...
  cache_control: "public, max-age=3600"

参考: Cloud Storage  |  Google Cloud

🔗Rails

🔗 GitLab 14.0のbreaking changes


つっつきボイス:「お、GitLabのメジャーバージョンアップきた: 近々にアップグレードしようかな」「GitLabのバージョンアップはmorimorihogeさんがやってるんですか?」「1〜2か月に1回ぐらいのペースで気が向いたときにやってます: GitLabのOmnibusパッケージで上げるだけなので随分楽になりましたよ」「へ〜、どんなふうにやってます?」「そんなに大変ではないですね、Ubuntuのパッケージがあるので基本的にはapt-get upgradeしますが、公式のアップグレードガイドにも推奨手順が書いてある↓のでそれに沿って進めます: 注意すべきはアップグレードパスで、基本的にマイナーバージョンを1つずつアップグレードします」

参考: Upgrading GitLab | GitLab

「GitLab 14.0にはbreaking changesがあるようなのでチェックするか: GraphQLフィールドの一部がdeprecatedになるのね」

「初期ブランチがmasterからmainに変わるんですね」「ついにGitLabもか」「最初のうちmainって打つときに違和感ありましたけど最近慣れてきました」

「”WIP merge request”の呼び方が”draft merge request”に変わるのは、GitHubの命名に寄せた感じかな」

「タイトルがWIPや[WIP]で始まるとマージボタンが押せなくなるGitLabの機能: ちなみにこの機能自体はGitLabの方がGitHubよりも前から搭載していて、後から追いかけたGitHubではdraft pull requestという名称なんですよ」「へぇ〜!」

参考: Draft Pull Requestをリリースしました - GitHubブログ

「WIPという略語よりdraftの方が非英語話者とかにもわかりやすいからとも書かれてますね」「WIPとかLGTMって何の略かもあまり考えずに使ってたかも」「たしかに略語だと通じる範囲が狭まるので、ちょっとわかる」

「GitLab OAuthのimplicit grantも非推奨化: 今は明示的にやるのが普通なのであまりやらなさそう」「CI_PROJECT_CONFIG_PATHCI_CONFIG_PATHに変わる」

参考: GitLab as an OAuth2 provider | GitLab

「期限切れのsshキーを追加するとデフォルトで無効にするようになった」「GitLab 13.9でsshキーを管理者が強制的に期限切れにするオプションが入っていたのね」「うっかりするとCIが止まったりして」

参考: Optional enforcement of SSH key expiration (#250480) · Issues · GitLab.org / GitLab · GitLab

「Code Quality?」「あぁ、Code QualityはGitLabの機能名で↓、そこでサポートするデフォルトのRuboCopバージョンを変更したのね」「Ruby 2.4〜3.0をサポートして2.1〜2.3のサポートは終了するけど、コンフィグ引き続きでサポート可能なところがさすがのGitLabですね👍

参考: Code Quality | GitLab

「最近のGitLabはメジャーバージョンアップを以前よりも頻繁にするポリシーになっているんですけど、今回のGitLab 14.0はbreaking changesが割とあるので、後でじっくりチェックしておこうっと」

🔗 fx: Railsで使うPostgreSQLの関数やトリガーを管理

teoljungberg/fx - GitHub

以下の記事で知りました。

参考: Logidze 1.0: Active Record, Postgres, Rails, and time travel — Martian Chronicles, Evil Martians’ team blog


つっつきボイス:「関数だからfxなのかな」「PostgreSQLの関数やトリガーを別ファイルに書いてマイグレーションで管理するようですね」「マイグレーションにdrop_functiondrop_triggerも書けるらしい」

# 同リポジトリより
% rails generate fx:function uppercase_users_name
      create  db/functions/uppercase_users_name_v01.sql
      create  db/migrate/[TIMESTAMP]_create_function_uppercase_users_name.rb
# 同リポジトリより
def change
  drop_function :uppercase_users_name, revert_to_version: 2
end

「この書き方どこかで見たな、たしかデータベースVIEWを扱えるgem…scenicだ」「あ、たしかに」「scenicはSQLファイルを別途作ってそこに生SQLを書いて使うんですけど、このあたりとかfxととても似てる↓」「ホントだ」

# scenic-views/scenicより
$ rails generate scenic:view search_results
      create  db/views/search_results_v01.sql
      create  db/migrate/[TIMESTAMP]_create_search_results.rb

scenic-views/scenic - GitHub

「インターフェイスがこんなに似てるということは、fxとscenicは同じ人が作ってるのかな?: コントリビュータを見ると、fxの作者もscenicにコミットしてる↓」「なるほど納得」「どちらのgemもやっていることは似ているので、fxの機能がscenicに入ったらいいかも」

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

🔗 DatabaseCleaner設定の見直し


つっつきボイス:「truncationdeletionに変えて速くなる場合がある、なるほど」

DatabaseCleaner/database_cleaner - GitHub

「DatabaseCleanerは使いこなしが大変」「システムテストだとDatabaseCleanerがなくてもよくなったんでしたっけ?」「最初からDatabaseCleanerなしで動くようにテストが書かれていればいいんですけど、データベース書き込みをテストするようになると何らかの形でrewinder的なものが必要になって、気をつけないとテストの熟考順序で結果が変わったりすることもあるんですよ」「あ〜」「テスト数が多くなると原因を特定するだけでも時間がかかるので、DatabaseCleanerを入れて様子を見たりしましたよ」

Rails 5.1以降のシステムテストをRSpecで実行する(翻訳)

🔗 Railsのセキュリティ脅威を学ぶ: 認証編


つっつきボイス:「ざっと見た感じでは、項目ごとに具体的なコードもあって丁寧に書かれていそうですね👍」「お〜!」「この記事には他のシリーズもあるみたいですね↓: OWASPトップテンと同じ見出しなので今後もトップテンを順に追いかけて記事にしていくみたい」「なるほど」「これ全部追いかけたら凄い」「この記事翻訳したいです」

参考: Rails Security Threats: Injections - Honeybadger Developer Blog

  • Injection
  • Broken authentication(上の記事)
  • Sensitive data exposure
  • XML external entities (XXE)
  • Broken access control
  • Security misconfigurations
  • Cross-site scripting (XSS)
  • Insecure deserialization
  • Using components with known vulnerabilities
  • Insufficient logging and monitoring
    シリーズ見出しより

「ちなみに記事の冒頭にあるOWASP(Open Web Application Security Project)はこういうセキュリティ上の脅威トップテンみたいなものを定期的に発表しています↓」


前編は以上です。

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

週刊Railsウォッチ: childprocess gemで子プロセスを制御、Ruby 2.6〜3.0で動くdelegationほか(20210623後編)

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

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

Rails公式ニュース

The post 週刊Railsウォッチ: GitLab 14.0のbreaking changes、Railsのセキュリティ脅威解説シリーズ記事ほか(20210628前編) first appeared on TechRacho.

週刊Railsウォッチ:書籍『Polished Ruby Programming』、DragonRuby、ES2021の新機能ほか(20210629後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 VSCodeのRubyデバッガextension「VSCode rdbg Ruby Debugger」


つっつきボイス:「@ko1さんのRubyデバッガがリリースされました🎉」「早くもやってみた記事が出てますね」「binding_pryで頑張らなくてもいいのか:後で入れようっと」「ツイートでちょくちょく進捗を横目で見ていました」「私もです」「自分はVSCode使っていないけど、もっといろんなやってみた記事が出て使用感がわかるといいですね」

「以下も見つけました」「ivarはインスタンス変数」「Rubyで参考実装を作ってみるとこういうこともわかってきたりしますよね: パフォーマンスは後からが基本」

🔗 DragonRuby


dragonruby.orgより


つっつきボイス:「おー、DragonRubyはRubyMotionと同じ人たちがやってるのね」「mrubyベースのクロスプラットフォームなRubyランタイムか」「AndroidやラズパイやWasm(WebAssembly)やNintendo Switchでも動くのか」「名前がかっこいい」「ドラゴンのロゴもイケてますね🐉

「DragonRuby Game Toolkitも出してるのね↓」「これでRubyのゲームが書けるのか〜」「最近のゲームのクレジットでRubyのコードが使われているのをたまに見かけますね」「そうそう、あります」「これもそういう感じで使えそう」「Toolkitは有料なんですね」「こういう組み込みのニーズは確実にあるので、必要な人は買ってみてもいいでしょうね」

「このあたりの話題をRubyKaigiで見たような記憶があったな🤔」「RubyでSWITCHのゲーム作る話していたのを見ました〜↓」

参考: Building a game for the Nintendo Switch using Ruby - RubyKaigi 2019


後で知りましたが、以下によると「月収1000ドル以下」「学生」「熱狂的ラズパイ使い」のいずれかであれば申請のうえで無料ライセンスを発行してもらえるそうです。

DragonRuby/dragonruby-game-toolkit-contrib - GitHub

🔗 DWARF

以下のツイートで気になりました。


つっつきボイス:「DWARFはデバッグ情報保存方式の標準で、ものすごく昔からあります」「サイトが昔っぽいのでそんな気がしてましたが、そうでしたか」「DWARFの規格そのものはよく知りませんが、CやC++などのデバッガでこの用語が頻出していたのを覚えてます」「DWARFについて調べていて以下のサイトを見つけました↓」「@k0kubunさんだ」

参考: Rubyist Hotlinks 【第 38 回】国分崇志 さん — DWARFについての記述あり
参考: デバッグ情報の歩き方 - Qiita

🔗 書籍『Polished Ruby Programming』予約受付中


つっつきボイス:「7/9に発売予定の書籍だそうです」「著者はあのJeremy Evansさんか!」「Jeremyさんといえば、RubyKaigiのクロージングキーノートで超濃い話をしていたのが忘れられません↓」

参考: Optimization Techniques Used by the Benchmark Winners - RubyKaigi 2019

「ググったらamazon.comの方が出てきたけど、amazon.co.jpで検索し直したら出てきた↓」「amazon.comで買うとKindleアカウントが別になっちゃうから不便ですよね」「そうそう」

「Jeremyの本なら相当期待できそう」「日本語版ないのかな〜」「まだpreorder中ですから😆」「お、こんないいものを発見↓」「@kakutaniさんが依頼を受けてこの本を査読した記録なんですね」「これはありがたい」「kakutaniさんが翻訳するのかな?」「この分量でこの内容だとある程度時間はかかりそうですね」

参考: Polished Ruby Programming翻訳査読書(のようなもの)

「おぉ、この査読書を見た感じではかなりよさそう👍」「しかもRuby 3.0対応ですって」「”現状で入手しやすい類書は特にない”、自分もそう感じますね」「なるほど」「”適切な変数の使い方”みたいな視点のある本はあまり見かけたことがありませんし、Rubyの本は書き味の話からメタプロの話に進むことがよくあるんですが、この本はどちらかというとRubyのしくみにより近いところを追っているように思えました」「書籍のコード例のリポジトリまで公開されてるんですね↓」

PacktPublishing/Polished-Ruby-Programming - GitHub


@kakutaniさんは以下の書籍の共訳も手掛けました。

🔗DB

🔗 スライド『SQL Training 2021』


つっつきボイス:「BPS社内Slackに貼っていただいたスライドです」「枚数が凄いですね」「157ページってもう技術書並かも」「スライドなので書籍のようなまとめ方とは違いますが、大事なことはひととおり盛り込まれているようなので、データベースをまったく知らない人向けの資料としてよさそう👍

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

🔗 GitHub Issuesの強化


つっつきボイス:「GitHub issueの強化、今日見かけたのでとりあえずベータ登録してみました」「私も登録しました」「これは何が変わるんでしょうか?」「まだ情報があまりなさそうですが、issue tracker機能が拡張されてJiraのような感じでリッチに管理できるようですね」「へ〜」「GitHub Codespacesもベータ登録したけどまだ結果出てないんですよね: GitHub issueの強化はいつお試しできるかな?」

参考: Jira | 課題 & プロジェクト追跡ソフトウェア | Atlassian

「こういう高度な機能はGitLabが先行していることが多くて、”GitLabですべてできる世界”を目指しているところがありますが、GitHub Codespacesやこれを見ているとGitHubもそういう世界を目指すことにしたのかもしれませんね」「なるほど」

「ちなみに弊社では、開発者向けのissue管理と、顧客と行う上流工程向けのissue管理を分けていることがよくあります: 開発者向けはGitHub Issuesでいいと思うんですが、上流工程向けのissue管理は取り扱いの敷居がもう少し低いものが望ましいので、Backlogを使うこともよくあります↓」「Backlog使ってますね」

参考: タスク管理、ファイル共有もできるプロジェクト管理ツールBacklog

🔗JavaScript

🔗 ES2021が正式な仕様に


つっつきボイス:「そうそう、ES2021がapproveされましたね」「何か変わるんでしょうか?」「ブラウザには既に新機能が実装されているので何かが急に変わるわけではなくて、仕様として正式なものになったということですね」「あ、なるほど」「すぐ消える心配なしに安心して新機能を使えるようになったのはいい👍

参考: Ecma International approves new standards - Ecma International

_で桁区切りできる機能はRubyでお馴染みですね」「見覚えあります」「replaceAll()もどことなくRubyっぽいかも」「文字列操作メソッドはいくらあっても足りないぐらいですよね」

// 同記事より
100_000_000;  // 1億(100,000,000)

Promise.anyもよさそう」

// 同記事より
Promise.any([
  promise1, promise2, promise3
]).then(first => {
  // 3つのpromiseのうち、最初に解決したpromiseが出力される
  console.log(first)
});

「演算子の||=&&=はRubyにもありますけど、??=はなかったかな」「??=は値がnullundefinedのときに代入: undefinedはJavaScriptでよく登場するのでいかにもJavaScriptらしい演算子ですね」

// 同記事より
let a = null;
a ??= "🐈";
console.log(a); // 結果: "🐈"

let b = "🐷";
b ??= "🐈";
console.log(b); // 結果: "🐷"

「弱参照(WeakRef)という概念があるのか」「GCしていいものをこれで指定するんですね」「”可能であれば使用を避けるのがよい”、たしかにGCは複雑なので使いこなしも複雑そう」「どういうときに使うのかあまり想像できないかも」

// 同記事より
const myObject = { name: "田中" };
// myObjectへの弱参照が作られる
const ref = new WeakRef(myObject);

「最近JavaScriptを書かないといけないことが増えてきたんですけど、Rubyに慣れているとJavaScriptの書き方の違いに戸惑うこともちょくちょくあるんですよ」「わかります」「letを避けて極力constにするために何かと使い捨ての関数を作ってはmap()するとか、文化が違うといえばそれまでなんですが、ちょっと消化不良というかもやもやした気持ちになるときがあります」

「Rubyだと変数をなるべくイミュータブルにするような書き方はあまりしないんですけど、JavaScriptだと何とかしてconstに落とし込もうとする傾向がどこかにあって、実際letで書いてみると何となく今風のJavaScriptではないのかなと思うこともあります」「constにするのが有効なシチュエーションはたしかにありますけど、ちょっとした文字列操作ぐらいなら普通に変数に再代入してもよさそうですし、何となくやりすぎ注意な気もしますけどね」「Reactのように関数に渡すものをなるべくイミュータブルに保つのはとても納得できます」

参考: React – ユーザインターフェース構築のための JavaScript ライブラリ


つっつき後に以下の記事も出ていました。

参考: ES2021/ES2022を知ろう | フューチャー技術ブログ
参考: ES2021に対応したJavaScript Primer 3.0を公開しました - JavaScript入門 | Web Scratch


後編は以上です。

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

週刊Railsウォッチ: GitLab 14.0のbreaking changes、Railsのセキュリティ脅威解説シリーズ記事ほか(20210628前編)

The post 週刊Railsウォッチ:書籍『Polished Ruby Programming』、DragonRuby、ES2021の新機能ほか(20210629後編) first appeared on TechRacho.

Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)

$
0
0

概要

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


  • 初版公開: 2018/01/11
  • 訳文更新: 2021/06/24

Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)

2005年にRailsのActiveRecordを初めて見たときに、稲妻のような天啓を感じたことが忘れられません。当時はPHPアプリで生SQLクエリを書いていましたが、それまで面倒で退屈でたまらなかったデータベースの扱いが、そのときを境に突如として簡単で楽しいものに変身したのです。そう、楽しくなったのです。

…やがて、ActiveRecordのパフォーマンスの問題に気づくようになりました。

ActiveRecordそのものが遅いわけではありませんでした。ちょうどその頃、実際に実行されるクエリに注意を払わなくなっていたのです。やがて、Rails CRUDアプリで用いられる最も定番のデータベースクエリの中に、データセットが巨大化したときのデフォルトのパフォーマンスがかなり低下するものがあることがわかってきました。

本記事では、パフォーマンスを損なう3つの主要な要因について解説します。しかし最初に、データベースクエリが正常にスケールしているかどうかを調べる方法について説明しましょう。

🔗 パフォーマンスの測定

データセットが十分小さければ、どんなデータベースクエリでも十分パフォーマンスを発揮できます。したがって、本当のパフォーマンスを実感するには本番のサイズでデータベースのベンチマークを取る必要があります。ここでは22,000レコードを持つfaultsというテーブルを用いることにします。

データベースはPostgreSQLです。PostgreSQLでパフォーマンスを測定するには次のようにexplainを使います。

# explain (analyze) select * from faults where id = 1;
                                     QUERY PLAN
--------------------------------------------------------------------------------------------------
 Index Scan using faults_pkey on faults  (cost=0.29..8.30 rows=1 width=1855) (actual time=0.556..0.556 rows=0 loops=1)
   Index Cond: (id = 1)
 Total runtime: 0.626 ms

クエリ実行の見積もりコスト(cost=0.29..8.30 rows=1 width=1855)と、実際の実行にかかった時間(actual time=0.556..0.556 rows=0 loops=1)の両方が表示されます。

もう少し読みやすくしたければ、次のようにYAML形式で出力することもできます。

# explain (analyze, format yaml) select * from faults where id = 1;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Index Scan"         +
     Scan Direction: "Forward"       +
     Index Name: "faults_pkey"       +
     Relation Name: "faults"         +
     Alias: "faults"                 +
     Startup Cost: 0.29              +
     Total Cost: 8.30                +
     Plan Rows: 1                    +
     Plan Width: 1855                +
     Actual Startup Time: 0.008      +
     Actual Total Time: 0.008        +
     Actual Rows: 0                  +
     Actual Loops: 1                 +
     Index Cond: "(id = 1)"          +
     Rows Removed by Index Recheck: 0+
   Triggers:                         +
   Total Runtime: 0.036
(1 row)

本記事では、「Plain Rows」と「Actual Rows」の2つだけに注目することにします。

  • Plan Rows: クエリに応答するときに、DBがループを最悪で何行回すかという予測を示します。
  • Actual Rows: クエリの実行時にDBが実際にループを何行回したかを示します。

上のように「Plain Rows」が1の場合、このクエリは正常にスケールすると見込まれます。「Plain Rows」がデータベースの行数と等しい場合、クエリが「フルテーブルスキャン」を行っていることが示されます。この場合クエリはうまくスケールできないでしょう。

クエリパフォーマンスの測定方法の説明が終わりましたので、Railsのいくつかの定番コードでどんな問題が起きているかを見てみましょう。

🔗 count

以下のコードはRailsビューで非常によく見かけます。

Total Faults <%= Fault.count %>

このコードから生成されるSQLは次のような感じになります。

select count(*) from faults;

explainで調べてみましょう。

# explain (analyze, format yaml) select count(*) from faults;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Aggregate"          +
     Strategy: "Plain"               +
     Startup Cost: 1840.31           +
     Total Cost: 1840.32             +
     Plan Rows: 1                    +
     Plan Width: 0                   +
     Actual Startup Time: 24.477     +
     Actual Total Time: 24.477       +
     Actual Rows: 1                  +
     Actual Loops: 1                 +
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Parent Relationship: "Outer"+
         Relation Name: "faults"     +
         Alias: "faults"             +
         Startup Cost: 0.00          +
         Total Cost: 1784.65         +
         Plan Rows: 22265            +
         Plan Width: 0               +
         Actual Startup Time: 0.311  +
         Actual Total Time: 22.839   +
         Actual Rows: 22265          +
         Actual Loops: 1             +
   Triggers:                         +
   Total Runtime: 24.555
(1 row)

うわわ!シンプルなcountクエリが22,265回もループしているではありませんか。これはテーブルの全行数です。PostgreSQLでは、countは常に全レコードセットをループします。

このレコードセットのサイズを減らすには、クエリにwhere条件を追加します。要件によっては、パフォーマンスが十分受け入れられる程度にサイズを減らすことができるでしょう。

それ以外にこの問題を回避する唯一の方法は、count値をキャッシュする方法です。Railsにはそのための仕組みがあるので、以下のように設定できます。

belongs_to :project, :counter_cache => true

クエリが何らかのレコードを返すかどうかのチェックにも別の方法があります。Users.count > 0をやめて、代わりにUsers.exists?をお試しください。こちらの方がずっとパフォーマンスは上です(情報提供いただいたGerry Shawに感謝いたします)。

訳注

より高度なcounter_culture gemを使う方法もあります。

Rails向け高機能カウンタキャッシュ gem ‘counter_culture’ README(翻訳)

🔗 ソート

indexページは、ほぼどんなアプリにも1つや2つあるでしょう。indexページでは、データベースから最新の20レコードを取り出して表示します。これをもっとシンプルにするにはどうすればよいでしょう?

レコード読み出し部分はおおよそ以下のような感じになっていると思います。

@faults = Fault.order(created_at: :desc)

このときのSQLは次のような感じになります。

select * from faults order by created_at desc;

分析してみましょう。

# explain (analyze, format yaml) select * from faults order by created_at desc;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Sort"               +
     Startup Cost: 39162.46          +
     Total Cost: 39218.12            +
     Plan Rows: 22265                +
     Plan Width: 1855                +
     Actual Startup Time: 75.928     +
     Actual Total Time: 86.460       +
     Actual Rows: 22265              +
     Actual Loops: 1                 +
     Sort Key:                       +
       - "created_at"                +
     Sort Method: "external merge"   +
     Sort Space Used: 10752          +
     Sort Space Type: "Disk"         +
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Parent Relationship: "Outer"+
         Relation Name: "faults"     +
         Alias: "faults"             +
         Startup Cost: 0.00          +
         Total Cost: 1784.65         +
         Plan Rows: 22265            +
         Plan Width: 1855            +
         Actual Startup Time: 0.004  +
         Actual Total Time: 4.653    +
         Actual Rows: 22265          +
         Actual Loops: 1             +
   Triggers:                         +
   Total Runtime: 102.288
(1 row)

このクエリを実行するたびに、データベースが22,265行をソートしていることがわかります。これは問題です。

SQLのORDER BY句は、デフォルトで毎回レコードセットをその場でソートします。これにはキャッシュも効きませんし、うまいマジックもありません。

解決法は、インデックスを用いることです。この例のようにシンプルであれば、created_atカラムにソート済みインデックスを追加するだけでクエリはかなり高速になります。

Railsのマイグレーションに以下を追加します。

class AddIndexToFaultCreatedAt < ActiveRecord::Migration
  def change
    add_index(:faults, :created_at)
  end
end

このマイグレーションで、以下のSQLが実行されます。

CREATE INDEX index_faults_on_created_at ON faults USING btree (created_at);

末尾の(created_at)はソート順を指定しています。デフォルトは昇順です。

これでソートのクエリを再度実行してみると、ソートが行われなくなることがわかります。インデックスからソート済みのデータを読み出すだけで済むようになりました。

# explain (analyze, format yaml) select * from faults order by created_at desc;
                  QUERY PLAN
----------------------------------------------
 - Plan:                                     +
     Node Type: "Index Scan"                 +
     Scan Direction: "Backward"              +
     Index Name: "index_faults_on_created_at"+
     Relation Name: "faults"                 +
     Alias: "faults"                         +
     Startup Cost: 0.29                      +
     Total Cost: 5288.04                     +
     Plan Rows: 22265                        +
     Plan Width: 1855                        +
     Actual Startup Time: 0.023              +
     Actual Total Time: 8.778                +
     Actual Rows: 22265                      +
     Actual Loops: 1                         +
   Triggers:                                 +
   Total Runtime: 10.080
(1 row)

複数のカラムでソートする場合は、複数カラムでソートしたインデックスを作成する必要があります。Railsマイグレーションでは以下のような感じで記述します。

add_index(:faults, [:priority, :created_at], order: {priority: :asc, created_at: :desc)

より複雑なクエリに対処する場合は、explainで確認するとよいでしょう。なるべく早い段階でまめに行うのがポイントです。クエリによっては、わずかな変更をかけただけでPostgreSQLでソートのインデックスが効かなくなることに気づくかもしれません。

🔗 limitoffset

データベースの全項目をindexページに表示することはめったにありません。ページネーション機能を使って一度に10件、30件、50件ぐらいずつを表示するのが普通です。このときに最もよく使われるのがlimitoffsetの組み合わせです。Railsでは次のような感じになります。

Fault.limit(10).offset(100)

生成されるSQLは次のようになります。

select * from faults limit 10 offset 100;

ここでexplainを実行してみると奇妙なことに気づきます。スキャンされた行数は110件で、ちょうどlimitoffsetを足したのと同じです。

# explain (analyze, format yaml) select * from faults limit 10 offset 100;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Limit"              +
     ...
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Actual Rows: 110            +
         ...

ここでoffsetを10,000に増やしてみると、スキャンされた行数も一気に10,010件に増加し、クエリの実行時間も64倍に増えます。

# explain (analyze, format yaml) select * from faults limit 10 offset 10000;
              QUERY PLAN
--------------------------------------
 - Plan:                             +
     Node Type: "Limit"              +
     ...
     Plans:                          +
       - Node Type: "Seq Scan"       +
         Actual Rows: 10010          +
         ...

ここから残念な結論が得られます。ページネーションでは後のページになるほど速度が低下します。上の例では1ページあたり100件を表示するので、100ページ目になると1ページ目より13倍も時間がかかってしまいます。

どうしたらよいでしょうか?

正直に申し上げると、これについて完璧なソリューションをまだ見つけられていません。私なら、ページ数が100ページや1000ページにならないよう、まずはデータセットのサイズを減らせないか検討するかもしれません。

レコードセットのサイズを減らすのが無理であれば、offsetlimitwhereに置き換える方法が一番有望でしょう。

# データの範囲を指定
Fault.where("created_at > ? and created_at < ?", 100.days.ago, 101.days.ago)

# またはidの範囲を指定
Fault.where("id > ? and id < ?", 100, 200)

まとめ

本記事が、PostgreSQLのexplain関数を利用してデータベースクエリに潜むパフォーマンスの問題を検出するのにお役に立てば幸いです。どんなシンプルなクエリであってもパフォーマンス上の大きな問題の原因となる可能性があるのですから、チェックする値打ちは十分あると思います :)

関連記事

Rails:「Pagy」gemでRailsアプリを高速ページネーション(翻訳)

The post Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: DI的な書き方が必要なとき、脆弱性学習用アプリRailsGoat、brakemanは優秀ほか(20210705前編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

今回も以下の公式更新情報の続きを追います。次の更新情報も出ましたね。

🔗 association extensionでのpurgepurge_later呼び出しを非推奨化

#42383コメントのフォローアップ。
association extensionからpurgeやpurge_later`を呼び出すとdeprecation warningを出すようになる。これらのメソッドは7.1リリース時に削除される。
同PRより大意


つっつきボイス:「purgepurge_laterの呼び出しが非推奨になるのはassociation extensionでの話のようですね」「完全に消されるのかと思ったら違った」「7.1で消されるそうです」

「今後はどうしたらいいんだろう?」「deprecation warningに書かれてた↓」「メッセージのアクション名がpurgepurge_laterに相当するんですね」

# activestorage/lib/active_storage/attached/model.rb#L157
          def purge
+           deprecate(:purge)
            each(&:purge)
            reset
          end

          def purge_later
+           deprecate(:purge_later)
            each(&:purge_later)
            reset
          end
+
+         private
+         def deprecate(action)
+           reflection_name = proxy_association.reflection.name
+           attached_name = reflection_name.to_s.partition("_").first
+           ActiveSupport::Deprecation.warn(<<-MSG.squish)
+             Calling `#{action}` from `#{reflection_name}` is deprecated and will be removed in Rails 7.1.
+             To migrate to Rails 7.1's behavior call `#{action}` from `#{attached_name}` instead: `#{attached_name}.#{action}`.
+           MSG
+         end

リフレクション名でのアクション名呼び出しは非推奨化され、Rails 7.1で削除される。この振舞いをRails 7.1に移行するには代わりにアタッチされた名.アクション名を呼び出すこと。
同メッセージより大意

「このテストコード↓で言うと、highlights_attachments.purgeではなくhighlights.purgeを呼ぶように変えるということですね: _attachmentsの部分が”association extension”」「あ〜なるほど」「association extensionを付けて呼び出すのが非推奨になるだけで、purgepurge_laterはなくならない」

# activestorage/test/models/attached/many_test.rb#459
 test "purging from the attachments relation" do
    [ create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg") ].tap do |blobs|
      @user.highlights.attach blobs
      assert @user.highlights.attached?

      message = <<-MSG.squish
        Calling `purge` from `highlights_attachments` is deprecated and will be removed in Rails 7.1.
        To migrate to Rails 7.1's behavior call `purge` from `highlights` instead: `highlights.purge`.
      MSG
      assert_deprecated(message) do
        assert_changes -> { @user.updated_at } do
          @user.highlights_attachments.purge
        end
      end
      assert_not @user.highlights.attached?
      assert_not ActiveStorage::Blob.exists?(blobs.first.id)
      assert_not ActiveStorage::Blob.exists?(blobs.second.id)
      assert_not ActiveStorage::Blob.service.exist?(blobs.first.key)
      assert_not ActiveStorage::Blob.service.exist?(blobs.second.key)
    end
  end

参考: Association extensions — ActiveRecord::Associations::ClassMethods
参考: Rails6 のちょい足しな新機能を試す99(association extension編) - Qiita

🔗 CollectionAssocation#buildのパフォーマンスリグレッション修正

#40379によって、自分たちのアプリのひとつをRails 6.1にアップグレードしたときに大きなリグレッションが発生した。レコードを多数持つ関連付けでbuildを呼び出すと、targetの重複チェックのためにあらゆるオブジェクトをイテレートしなければならなくなって実行に長時間かかる。
has_many_inversingの仕組み上これは必要だが、もっと高速に実行する実装は可能。このプルリクでは、has_many_inversingでターゲットに追加されたレコードを別キャッシュに保持し、マッチするレコードをそこのみで検索することで解決を試みた。
このバグの再現スクリプトは、Rails 6.1とmainブランチでは失敗し、このブランチでは成功する。

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  gem "rails", "~> 6.1.0"
  gem "sqlite3"
end

require "active_record"
require "minitest/autorun"
require "logger"

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :authors, force: true do |t|
  end
  create_table :posts, force: true do |t|
    t.references :author
  end
end

class Author < ActiveRecord::Base
  has_many :posts
end

class Post < ActiveRecord::Base
  belongs_to :author
end

class BugTest < Minitest::Test
  def test_association_stuff
    author = Author.create!

    posts = 100_000.times.map { |n| { author_id: author.id } }
    Post.insert_all(posts)

    author = Author.find(author.id)
    author.posts.load_target

    5000.times do |n|
      time = Benchmark.ms { author.posts.build }
      assert time < 30, "iteration #{n}: #{time}" # takes about 200ms in Rails 6.1
    end
  end
end

同PRより大意


つっつきボイス:「プルリクメッセージにもあるようにRails 6.1でレコードを多数持つ関連付けでbuildを呼び出すと遅くなっていたのを修正したようですね」

「この@replaced_targetsに結果をキャッシュして、同じレコードセットを毎回検索しなくて済むようにしたっぽい↓」

# activerecord/lib/active_record/associations/collection_association.rb#L79
      def reset
        super
        @target = []
+       @replaced_targets = Set.new
        @association_ids = nil
      end
# activerecord/lib/active_record/associations/collection_association.rb#L271
      def add_to_target(record, skip_callbacks: false, replace: false, &block)
-       if replace || association_scope.distinct_value
-         index = @target.index(record)
-       end
-       replace_on_target(record, index, skip_callbacks, &block)
+       replace_on_target(record, skip_callbacks, replace: replace || association_scope.distinct_value, &block)
      end

      def target=(record)
        return super unless reflection.klass.has_many_inversing
        case record
        when Array
          super
        else
-         add_to_target(record, skip_callbacks: true, replace: true)
+         replace_on_target(record, true, replace: true, inversing: true)
        end
      end

「今Rails 6.0のプロジェクトがあるんですが、早く6.1にアップグレードしたい」「あ〜」「プロジェクト開始時点で6.1は出ていたんですが当時としては時期尚早だったんですよ」「それももっともですね」

🔗 TurboのフォームにUJSフォーム送信ハンドラをアタッチしないようになった


つっつきボイス:「DHH自らのHotwire関連プルリクです」「TurboとUJS(Unobtrusive JavaScript)を共存できるように、両方が読み込まれた場合にUJSを無効化してTurboだけが有効になるようにしたみたい: これは地味にありがたい👍」「なるほど」「'form:not([data-turbo=true])'data-turboがtrueのときはUJSを動かさないということか」「not trueってややこしいですね😅

# actionview/app/assets/javascripts/rails-ujs.coffee#L20
  # Form elements bound by rails-ujs
- formSubmitSelector: 'form'
+ formSubmitSelector: 'form:not([data-turbo=true])',

  # Form input elements bound by rails-ujs
- formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])'
+ formInputClickSelector: 'form:not([data-turbo=true]) input[type=submit], form:not([data-turbo=true]) input[type=image], form:not([data-turbo=true]) button[type=submit], form:not([data-turbo=true]) button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])',

Rails UJSで書かれたアプリをHotwireの新しいTurboフレームワークに移行するなら、移行中は(あるいは永遠に!)TurboとUJSを共存させたいこともあるだろう。そのためにはフォーム送信の処理方法を区別する方法が必要。更新されたセレクタを使うことで、Rails UJSはdata-turbo=true付きのフォームだけを無視し、Turboで処理できるようになる。
(rails/jquery-ujs#521のミラープルリク)
同PRより大意

🔗 zoneプロパティが設定されている場合にTime#changezoneプロパティを渡すよう修正


つっつきボイス:「テストコード↓を見てて気づいたんですけど、Time.newActiveSupport::TimeZone["Moscow"]という形でタイムゾーンを渡せるみたい」「へ〜、これいいですね」「日本だとTokyo以外のタイムゾーンはあまり使いませんけど、"+03:00"みたいな書き方よりわかりやすい: 」

# activesupport/test/core_ext/time_ext_test.rb#486
+   assert_equal Time.new(2021, 5, 29, 0, 0, 0, "+03:00"), Time.new(2021, 5, 29, 0, 0, 0, ActiveSupport::TimeZone["Moscow"])
+   assert_equal Time.new(2021, 5, 29, 0, 0, 0, "+03:00").advance(seconds: 60), Time.new(2021, 5, 29, 0, 0, 0, ActiveSupport::TimeZone["Moscow"]).advance(seconds: 60)
+   assert_equal Time.new(2021, 5, 29, 0, 0, 0, "+03:00").advance(days: 3), Time.new(2021, 5, 29, 0, 0, 0, ActiveSupport::TimeZone["Moscow"]).advance(days: 3)

+   assert_equal Time.new(2021, 5, 29, 0, 0, 0, "+03:00"), ActiveSupport::TimeZone["Moscow"].local(2021, 5, 29, 0, 0, 0)
+   assert_equal Time.new(2021, 5, 29, 0, 0, 0, "+03:00").advance(seconds: 60), ActiveSupport::TimeZone["Moscow"].local(2021, 5, 29, 0, 0, 0).advance(seconds: 60)
+   assert_equal Time.new(2021, 5, 29, 0, 0, 0, "+03:00").advance(days: 3), ActiveSupport::TimeZone["Moscow"].local(2021, 5, 29, 0, 0, 0).advance(days: 3)
  end

Time#changeおよびこれを呼び出すメソッド(Time#advanceなど)が、呼び出し元がタイムゾーン引数で初期化されていた場合は指定のタイムゾーン引数を持つTimeを返すようになった。
修正: #42467
Alex Ghiculescu
同Changelogより

追いかけボイス:「つっつき会の後でちょうど話題になった呪いの書↓的にはtzdb ID形式で書くべきなんだろうなあ: MoscowならEurope/Moscow が正しそう」

参考: タイムゾーン呪いの書 (知識編)
参考: タイムゾーン呪いの書 (実装編)
参考: List of tz database time zones - Wikipedia

🔗 number_to_currencyがゼロをマイナス表示することがあるのを修正


つっつきボイス:「なるほど、-0.00456789という数値をprecision: 2で丸めると-0.00になってたのか」「あら〜」「これは修正が必要なヤツ」

# 同PRより
assert_equal("$0.00", number_helper.number_to_currency(-0.00456789, precision: 2))

🔗Rails

🔗 DI的な書き方が必要なとき


つっつきボイス:「DHHによる元記事↓は2013年のだそうです」

「記事をざっと見た限りでは、自分がこのpublish!のコード↓をレビューするなら、Time.nowをハードコードしないでオプショナル引数などでDI的に渡せるようにすべきと指摘するでしょうね」「ふむふむ」「今はtimecop gemを使わなくてもRailsの時刻を変えられるので、わざわざDIっぽく書かなくてもテストできますけどね」

# 同記事より
def publish!
  self.update published_at: Time.now
end

「ちなみに上のコードをDIっぽく書き直すとたとえばこういう感じになります↓: あとRailsアプリなら理由がない限りRubyのTime.now(システムTZを参照する)よりもRailsのTime.current(RailsのTimeWithZoneになり、かつRailsの設定に従ったZoneになる)を使うべきでしょうね」「なるほど」「どうしても使いたければTime.zone.nowにすべき」

def publish!(time = Time.current)
  self.update published_at: time
end

「自分としては、publish!を呼んでいるのが1箇所だけならDI的に書くことでテストもしやすくなりますし別に構わないと思います: DI的に書くことの問題は、他のいろんな場所でも呼ばれていると、以下のようにそれらも全部Time.currentを渡す形に書き換えないといけなくなることでしょうね↓」「あ、そうか」

def use_publish(a, b, c)
  # 何かする
  publish!
end

# ↓

def use_publish(a, b, c, time = Time.current)
  # 何かする
  publish!(time)
end

「さらにその場所で別のTime.currentがハードコードされていれば、そこも以下のような感じでhogehoge_time = Time.current - 1.dayを引数に追加したりすることになるでしょうね」「うう、昔こんなコード書いたような覚えが😅」「DIを追求していくとこういうふうになっていくんですよ」

def use_publish(a, b, c, time = Time.current)
  # 何かする
  hogehoge(Time.current - 1.day)
  publish!(time)
end

# ↓

def use_publish(a, b, c, hogehoge_time = Time.current - 1.day, time = Time.current)
  # 何かする
  hogehoge(hogehoge_time)
  publish!(time)
end

「Javaのような言語ではpublish!use_publishが相互依存しないようにするためにhogehoge_time = Time.current - 1.dayなどを別の小さなクラスに分離したりするんですが、DIをやり始めるとこういうふうになるからだと思います」「なるほど」「RubyならTimecop gemを使えば済むような話ですね: 記事の要約↓にもあるように、RubyではテストのためだけにDIを使う意味はないと思います」

  • DHHはDI自体を否定しているのではなく、テストのためだけにDIを使うのはRubyにおいては無駄であると言っている
    • 上のコードで言うと、Timeを外から注入させたい理由がテストのためだけなら「ヤメロ」と
      同記事より

「ただ、自分がさっきのpublish!のようなコードではTime.currentをオプショナル引数でDI的に渡すべきと言ったのは、テストのためではなく、publish!のようなロジックは時刻を変更できる機能をビジネス上求められる可能性が非常に高いからなんですよ」「あ、それもそうか」

「たとえばさっきのpublish!Time.currentがハードコードされていると、それを呼ぶバッチが失敗したときに時刻を変更してやり直せなくなってしまいますよね: そういうふうに時刻を変えて呼び出したいというニーズが実際にありうるので、ここはDI的に書くべきという話」「なるほど、そういうDIならちゃんと意味がありますね」「経験した範囲では、Time.nowTime.currentがハードコードされている箇所は、想定外で失敗した日次バッチのやり直しや障害調査のために”この日時に実行したのと同じ挙動をさせたい”という使い方を要求される可能性がとても高いですよ」

「その意味では、DHHの元記事にあるpublish!のコードを別のコードに変えた方が、テストのためだけにDIを使う意味がないということを納得しやすいかも」「それもそうですね」

「ちなみに自分ならDI的なものはこういう感じで書くと思います↓」「あ、Rubyのキーワード引数ですね!」「そう、キーワード引数の方が明示的になります: キーワードもtime:よりnow:にする方が現在時刻という意図が伝わりやすいので好み」

def use_publish(a, b, c, now: Time.current)
  # 何かする
  publish!(now)
end

「DHHの記事はだいぶ昔のものなので、今もDIについて同じ考えかどうかはわかりませんが」「それもそうですね」

Rails: Timecopを使わなくても時間を止められた話

🔗 load_asyncRuby Weeklyより)


つっつきボイス:「Railsに追加されたload_asyncウォッチ20210222)の解説記事が出たんですね: ちなみに銀座Rails#33でもload_asyncの話をしました↓」「あ、そうでしたか」「load_asyncよさそう」「load_asyncはクエリが複数のデータベースコネクションにまたがるので更新系は要注意ですね: 参照系だけなら比較的使いやすそう」

🔗 RailsGoat: 脆弱性を仕込んだ教育用Railsアプリ

OWASP/railsgoat - GitHub


つっつきボイス:「RailsGoat?」「先週取り上げたRailsセキュリティ脅威解説記事の第1回↓でこのRailsアプリが紹介されていました」「どこかで見た名前」

「RailsGoatはOWASPが提供していて、セキュリティ教育用にOWASPトップテン入りした脆弱性が仕込まれているそうです」「なるほど、脆弱性のサンプルアプリですか」「脆弱性をゼロから作り込むと大変なので記事ではこれを使っていました」

参考: OWASP Japan | OWASP Foundation

「ちなみにRailsGoatはまだ最新のRailsには対応していないそうです」「Railsのバージョンが変わると脆弱性も変わるので、すぐに作れないのは仕方ないでしょうね: Railsセキュリティの勉強用にはよさそう👍

参考: Rails セキュリティガイド - Railsガイド

🔗 brakeman gemは優秀

「ところで、この間Rails 4アプリの脆弱性を調べるためにbrakemanのコードを大量に読みましたよ」「お〜それは大変そうですけど、brakemanならCVE IDの情報も付いているのでいいアプローチだと思います」

presidentbeef/brakeman - GitHub

「brakemanの素晴らしい点は、動いてないRailsコードでもチェックできること👍」「なるほど、静的にチェックするんですね」「静的解析なので、APIキーがないとか、Rubyのバージョンが古すぎるとか、Railsコンソールも動かないようなRailsアプリでもとりあえずチェックできるのがホントありがたい」「マジ優秀です」

「brakemanはDockerに入れる必要もないので、これだけgem installでインストールしてます」「なるほど」「Dockerコマンドを打たなくていいのが便利」

「brakemanはCVEのURLやサンプルの脆弱性コードなんかも表示してくれてすごく勉強になりますね: lib/brakeman/checksの下にあるファイルを読んでいてめちゃめちゃ楽しかった😋

参考: 共通脆弱性識別子CVE概説:IPA 独立行政法人 情報処理推進機構

🔗 activerecord_json_validator:バリデーション条件をJSONスキーマで書けるgem(Ruby Weeklyより)

mirego/activerecord_json_validator - GitHub


つっつきボイス:「お〜、JSONスキーマを使ってActive Recordのバリデーションができるんですね↓」

{
  "type": "object",
  "$schema": "http://json-schema.org/draft-04/schema#",
  "properties": {
    "city": { "type": "string" },
    "country": { "type": "string" }
  },
  "required": ["country"]
}
# 同リポジトリより
create_table "users" do |t|
  t.string "name"
  t.json "profile" # First-class JSON with PostgreSQL, yo.
end

class User < ActiveRecord::Base
  # Constants
  PROFILE_JSON_SCHEMA = Pathname.new(Rails.root.join('config', 'schemas', 'profile.json'))

  # Validations
  validates :name, presence: true
  validates :profile, presence: true, json: { schema: PROFILE_JSON_SCHEMA }
end

user = User.new(name: 'Samuel Garneau', profile: { city: 'Quebec City' })
user.valid? # => false

user = User.new(name: 'Samuel Garneau', profile: { city: 'Quebec City', country: 'Canada' })
user.valid? # => true

user = User.new(name: 'Samuel Garneau', profile: '{invalid JSON":}')
user.valid? # => false
user.profile_invalid_json # => '{invalid JSON":}'

「JSONスキーマのバリデーションってこうやって書くのか〜」「JSONスキーマ自体が簡単な制約を含んでいるのでバリデーションに使えるでしょうね」「あ、なるほど」「JSONスキーマがどのぐらい使われているかはわかりませんが」

「制約条件の記載されたJSONスキーマが既にあるプロジェクトならこのgemを使うとよさそう👍


前編は以上です。

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

週刊Railsウォッチ:書籍『Polished Ruby Programming』、DragonRuby、ES2021の新機能ほか(20210629後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: DI的な書き方が必要なとき、脆弱性学習用アプリRailsGoat、brakemanは優秀ほか(20210705前編) first appeared on TechRacho.

週刊Railsウォッチ: GitHub CopilotのAI補完、Pure Ruby実装のRuby JIT rhizome、PostgreSQLのPG-Strom拡張ほか(20210706後編)

$
0
0

こんにちは、hachi8833です。Kaigi on Rails 2021のプロポーザル募集中です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 rhizome: Pure Rubyで実装されたRuby JIT(Ruby Weeklyより)

chrisseaton/rhizome - GitHub


つっつきボイス:「RubyのJITをPure Rubyで実装ですか」「すごいものを作る人がいるんですね」「rhizomeって生物学用語だったかな?」「植物の”地下茎”の意味で、哲学用語にもなってるみたい↓」

参考: リゾーム - Wikipedia

英語の発音だと「ライゾーム」が近いようです。

「READMEには”You’re supposed to read it, not use it!”ってある」「使っちゃだめよ読むだけよ、ですね」「パーサーやバイトコードなど、JITに必要そうな要素のドキュメントはひととおりあるみたい」「ドキュメントそのものもかなりみっちり書かれてる感じですね、すごい👍」「壮大なプロジェクト」

🔗 Rails Girls Japan


つっつきボイス:「Rails Girlsは日本でもワールドワイドでも活躍していますね」「Rubyでこういうコミュニティ活動が盛んなのはいい👍

参考: Rails Girls - Japanese


後で他の言語にも同じような活動があるかどうか少しだけ探してみました。

参考: Python Girls
参考: js-girls
参考: GoLang Girls (Pune, インド) | Meetup
参考: GTUG Girls - connpass — さまざまな言語やフレームワークが対象

🔗 記法の名前


つっつきボイス:「そうそう、クラス名#インスタンスメソッド名クラス名.クラスメソッド名みたいな記法に名前欲しい」「インスタンスメソッドのときは#で、クラスメソッドのときは.で書く記法、たしかに名前がありませんね」「レビューのときにこの記法を名前で呼びたくなることがよくあります」

「最初にRubyを学んだときに#は実際のコードに出てこないので不思議に思った覚えがあります」「Rubyの公式ドキュメント↓でこの記法が使われているので、そういうものだと思ってました」「Rubyを使っている人にとってはもう常識でしょうね」「知らないとドキュメントを読むのが大変」

参考: オブジェクト指向スクリプト言語 Ruby リファレンスマニュアル (Ruby 3.0.0 リファレンスマニュアル)

# https://docs.ruby-lang.org/ja/3.0.0/class/IO.html より
IO.foreach
IO.readlines
IO#each_line
IO#gets
IO#getc
IO#ungetc
IO#read
IO#readchar
IO#readline
IO#readlines

🔗 その他Ruby

「Rubyベストブログ集だそうですけど、Evil Martiansのブログがエントリにないのが意外ですね」「そうなんですよ、TechRachoでは露出度高いのに(お世話になってます🙇)」

参考: Martian Chronicles, Evil Martians’ team blog

🔗DB

🔗 『決済システムの残高管理周りのDB設計と戦略』


つっつきボイス:「決済システムを含む、いわゆる口座系のシステムはこの記事のような設計にするのが一般的ですね」「なるほど」「応答速度を上げるために現在の残高を保存することもないわけではありませんが、基本的に口座系のシステムでは入金と出金を積み上げていく設計になります」「そうそう、履歴テーブル的に作りますよね」「そのように作っておかないと、たとえば特定日時の残高を求められなくなってしまいます」

「履歴テーブルだからUPDATEは禁止ですよね」「当然禁止: UPDATEしたらエビデンス改ざんになってしまいます」

「記事を見ていても、口座系システムの基本部分は昔からあまり変わりませんね: ポイントやソシャゲの”石”を扱うシステムだと現金と関連する”有償石”と”無償石”を分けて管理する必要があるとか、速度面や設計上の戦略は変わってくるところもありますが」「ふむふむ」

「こういう業務に寄り添ったDB設計記事をあまり見かけたことがなかったかも」「この種の記事は昔からいろいろありますよ: Web系をやっているとあまり見えてこないかもしれませんが」「あぁ、探すところが違ってたのか」「普段B2Cなシステムばかり触っているWeb系ソフトウェアエンジニアだと、こういうビジネストランザクションを扱うチャンスがなかなかないこともあるので、こうした「堅い」システムの設計も勉強しておくと学べることがあると思います」「そうですね」

「こういう定番のシステム設計を、より現代的な技術を使って改めて丁寧に説明してくれる記事はありがたい👍」「たしかに」

🔗 PG-Strom: GPUでPostgreSQLのSQLワークロードを高速化

heterodb/pg-strom - GitHub

Stormかと思ったらシュトローム(Strom)だそうです。

PG-StromはPostgreSQL v11および以降のバージョン向けに設計された拡張モジュールで、チップあたり数千個のコアを持つGPU(Graphic Processor Unit)デバイスを利用する事で、大規模なデータセットに対する集計・解析処理やバッチ処理向けのSQLワークロードを高速化するために設計されています。
同ドキュメントより


つっつきボイス:「ここに書かれている↓ようなSCANやJOINやGROUP BYは実行計画的にもパラレル処理しやすいので、PG-StromはそういうものをGPUで処理するようですね」

PG-Stromの中核となる機能は、SQL命令から自動的にGPUプログラムを生成するコードジェネレータと、SQLワークロードをGPU上で非同期かつ並列に実行する実行エンジンです。現バージョンではSCAN(WHERE句の評価)、JOINおよびGROUP BYのワークロードに対応しており、GPU処理にアドバンテージがある場合にはPostgreSQL標準の実装を置き換える事で、ユーザやアプリケーションからは透過的に動作します。
同ドキュメントより

「PG-StromはApache Arrow関連でググって見つけました」「なるほど、Apache Arrowとも相性よさそう」

参考: Apache Arrow - PG-Strom Manual
参考: Apache Arrow | Apache Arrow

「GPUダイレクトSQL実行という機能は、CPUバスを通さずにSSDからPCIe(PCI Express)バス経由でダイレクトにGPUに接続するんですね」「名前が強そう」「メモリにも読み込まずにGPUに転送するからメモリも節約できるのか: こういう構成だから、NVMe(NVM Express)経由で直接SSDを接続して、NVIDIAのGPUDirect Storageモジュールなどを使う必要があるのね」


同ドキュメントより

本機能は、内部的にNVIDIA GPUDirect Storageモジュール、またはHeteroDB社の独自Linux kernelモジュールであるNVME-Stromモジュール(RHEL7/CentOS7)を使用して、GPUデバイスメモリとNVMEストレージとの間でP2Pのデータ転送を行います。したがって、本機能を利用するには、PostgreSQLの拡張モジュールであるPG-Stromだけではなく、上記のどちらかのLinux kernel拡張モジュールが必要です。

また、本機能が対応しているのはNVME仕様のSSDや、NVME-oFで接続されたリモートデバイスのみです。SASやSATAといったインターフェースで接続された旧式のストレージには対応していません。今までに動作実績のあるNVME-SSDについては 002: HW Validation List が参考になるでしょう。
同ドキュメントより

参考: PCI Express - Wikipedia
参考: NVM Express - Wikipedia

「PG-Stromって何だかとてもハードウェア寄りのシステムですね」「ぽすぐれは昔からこういうハードウェアの性能を限界まで引き出すような機能を研究したり実現したりしていますね: いかにL1キャッシュに乗せるかとか」「お〜」

参考: キャッシュメモリ - Wikipedia

「こんなすごいシステムを実際に使うのかな?使うんでしょうね」「有効なクエリにはものすごくよく効くと思います」「なるほど」「一種のクエリ計算機的なものをたくさん生成して、GROUP BYなどのパラレル化しやすい処理を分散するといった処理は昔から行われていますね」

「こういうシステム構成なら、必ずしも最新のGPUでなくても速度を出せそう: たとえばビットコインを掘るにはもう力不足になった安いGPUをたくさん手に入れて、その上でぶん回すみたいなこともできるかも」「へ〜!」「今はGPUコア数も増えましたよね: 昔のGPUはコア数はあっても機能が少なかったんですが、最近のGPUはたいていのことができるようになっていますし、メモリもメインのものより高速だったりしますね」「PG-Strom、なかなか面白い👍

参考: Graphics Processing Unit - Wikipedia


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

🔗言語/ツール/OS/CPU

🔗 GitHub CopilotのAI補完機能


つっつきボイス:「ぼくのツイートが載ってる〜」「GitHub Copilot、早くもあちこちでネタにされていますね」「この間ウォッチで取り上げたAWS Copilot(ウォッチ20210601)ではない😆

「GitHub Copilotってどうなんでしょうね?」「Copilot自体はいいと思いますよ: ツイートにも書きましたけど、AIで使われたコードのライセンス周りはしばらく問題になるかもしれませんが」「あ、GPLか」

参考: GNU General Public License - Wikipedia

「個人的にこれがウケました↓」「ベストテキストエディターはVim?」

「@mametterさんもその後クワインを補完だけで書くのに成功してますね」「当分遊べそう」

🔗 AI補完よもやま

「GitHub Copilotで思ったんですが、AI補完が使えれば、自分が書いたことのないプログラミング言語でも書けるようになるんじゃないかな」「あ、それいいですね!」「他の言語の経験者なら、たとえばRubyの構文を詳しく知らなくてもRubyを書けるかも」「正しい構文を補完してくれれば、関数の引数のおかしいところを自分でちょっと修正するぐらいはできそう」「夢が膨らみますね」

「GitHub CopilotのようなAI補完でプログラマーが職を失うかどうかについても、詳細設計書どおりにコーディングするだけのプログラマーならともかく、AIで補完したコードをちゃんと吟味して評価できるのは結局プログラマーなのであまり心配していませんし、便利なものなら使えばいいくらいの気持ちです」「ですよね」「機械翻訳と人間の翻訳者の関係にも似ていますね」

「AI補完が今後何か問題になるとすれば、プログラミングの授業で出す課題を採点するときとか、採用面接で成果物を評価するときなんかに、どこまで本人が作ったのかの判断が難しくなるかもしれない、とかかな(面接の場でコーディングするところを見せてもらうならいいんですが)」「たしかに」「論文のパクリチェッカーのようにはいかなさそうですね」「補完では書けないような問題を工夫する必要があるかもしれない」

🔗 TabNine: VSCodeのAI補完拡張

codota/TabNine - GitHub


つっつきボイス:「自分はまだGitHub Copilotが招待待ちですが、こちらのAI補完はVSCode拡張なのですぐ動かせます」「MicrosoftはVSCodeを持っているから、AI補完もVSCodeでやれる強みがある」「JetBrains IDEもAIまでは使っていないけどコード補完や表示の優先順位とかは相当賢くできてますね」


「ところで今使いたいのはむしろGitHub Codespacesかな: 自分もまだ招待待ち」「WebだけでできるIDEですね」「Codespacesが使えれば本当にブラウザだけで開発できるようになるのが大きい」「環境構築の手間から解放されたいですね」

参考: Codespaces

🔗 その他

🔗 howtheytest: 有名企業のテスト方法集


つっつきボイス:「これもぼくのツイート〜」「大手企業のソフトウェアテスト方法の記事や動画へのリンク集だそうです」「さっき眺めてたら、PayPalとかいくつかのエントリは中身が空でした😆」「軽くずっこけましたね」

「少し前ならAwesomeなんとかみたいな名前になりそうなリストですけど、使われすぎて陳腐化してきた気もするので違う名前にしたのかな」「いっときGitHubリポジトリで山ほど見かけましたけど、たしかにAwesomeは手垢ついちゃいましたね」

🔗 fishとzshとbash


つっつきボイス:「fishはまだ使っていないけど、そろそろシェルを変えてみてもいいかな: WSL2上のUbuntu 20.04の標準bashでコマンド補完がイマイチうまく動かなくて」「あ〜」

fish-shell/fish-shell - GitHub

「fishもよさそうだけど、zshもいいかも」「ぜひぜひ!ぜとしぇはいいですよ〜」「お、zshお使いなんですね」「基本zshです」「zshなら何でもリッチに動くし、凝ったコンフィグにしなければ基本的にbashと同じ挙動なんですよね」「そうそう」

zsh-users/zsh - GitHub

「今のbash環境だと、ハイフン抜きのdocker compose(Docker Compose CLI)で-fオプションの補完が効かないのがつらい」「あ〜なるほど」「従来のハイフンありdocker-composeなら補完が効くので、こっちで補完してからハイフンを消したりしているんですけど、さすがに何度もやっていると地球環境に優しくないなと」「ですよね」

docker/compose-cli - GitHub

「原因不明なんですが、ハイフンありのdocker-composeだとWSL2のUbuntu環境でたまにペアレントディレクトリを見失うことがあって、ハイフンなしのdocker composeなら問題なくやれるんですよ」「う〜む」

「そういえば最近ハイフンありのdocker-compose upを使っていると”docker compose upがありますよ”みたいなメッセージが出るようになりましたね」「へ〜、今後はDocker Compose CLIがメインになるのかな、既に使ってますけど」

参考: 新しい docker compose


後編は以上です。

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

週刊Railsウォッチ: DI的な書き方が必要なとき、脆弱性学習用アプリRailsGoat、brakemanは優秀ほか(20210705前編)

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

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

Ruby Weekly

The post 週刊Railsウォッチ: GitHub CopilotのAI補完、Pure Ruby実装のRuby JIT rhizome、PostgreSQLのPG-Strom拡張ほか(20210706後編) first appeared on TechRacho.

Ruby 3.0.2/2.7.4/2.6.8セキュリティ修正がリリースされました

$
0
0

Ruby 3.0.2/2.7.4/2.6.8セキュリティ修正がリリースされました。

詳しくは上記リリース情報をご覧ください。

🔗 修正の概要

3.0.2/2.7.4/2.6.8で以下の3つの脆弱性が修正されました。

🔗 CVE-2021-31810: Trusting FTP PASV responses vulnerability in Net::FTP

Net::FTPで、FTP PASVレスポンスを信頼してしまう脆弱性が発見されました。この脆弱性に割り当てられたCVE IDはCVE-2021-31810です。Rubyのアップグレードを強く推奨します。
net-ftpはRuby 3.0.1のデフォルトgemですが、パッケージングに問題があるため、Ruby自体をアップグレードしてください。
同記事本文より

ruby/net-ftp - GitHub

  • 影響を受けるRubyバージョン: Ruby 3.0(3.0.1以前)
  • 影響を受けるRubyバージョン: Ruby 2.7(2.7.3以前)
  • 影響を受けるRubyバージョン: Ruby 2.6(2.6.7以前)
  • 修正されたRubyバージョン: Ruby 3.0.2、Ruby 2.7.4、Ruby 2.6.8

🔗 CVE-2021-32066: A StartTLS stripping vulnerability in Net::IMAP

Net::IMAPにはStartTLS strippingの脆弱性が存在します。この脆弱性に割り当てられたCVE IDはCVE-2021-32066です。Rubyのアップグレードを強く推奨します。
net-imapはRuby 3.0.1のデフォルトgemですが、パッケージングに問題があるため、Ruby自体をアップグレードしてください。
同記事本文より

ruby/net-imap - GitHub

  • 影響を受けるRubyバージョン: Ruby 3.0(3.0.1以前)
  • 影響を受けるRubyバージョン: Ruby 2.7(2.7.3以前)
  • 影響を受けるRubyバージョン: Ruby 2.6(2.6.7以前)
  • 修正されたRubyバージョン: Ruby 3.0.2、Ruby 2.7.4、Ruby 2.6.8

🔗 CVE-2021-31799: A command injection vulnerability in RDoc

このCVE-2021-31799については既に2021年5月2日に修正済みのRDoc 6.3.1がリリースされています。本記事公開時点の最新バージョンはRDoc 6.3.2です。

Rubyに同梱されているRDocにはコマンドインジェクションに関連する脆弱性があります。
同記事本文より


この脆弱性の影響を受けるRDocバージョンを使っている場合は、最新のRDocに更新してください。
アップデート方法
脆弱性を修正するには、gem install rdocコマンドを実行してRDocを最新バージョン(6.3.1以降)にアップデートします。
bundlerを使っている場合は、Gemfileにgem "rdoc", ">= 6.3.1"を追加してください。
同記事アップデート方法より

  • 影響を受けるRDocバージョン: 3.11〜6.3.0の全バージョン
  • 修正されたRDocバージョン: RDoc 6.3.1以降

参考: ruby-buildとDocker Hub

rbenvで使われるruby-buildでは、既にRuby 3.0.2/2.7.4/2.6.8が利用可能になっています。

rbenv/ruby-build - GitHub

Docker Hubでも各種ディストリビューション向けのRuby 3.0.2/2.7.4/2.6.8が利用可能になっています。

The post Ruby 3.0.2/2.7.4/2.6.8セキュリティ修正がリリースされました first appeared on TechRacho.


Rails向け高機能カウンタキャッシュ gem「counter_culture」README(翻訳)

$
0
0

概要

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

magnusvk/counter_culture - GitHub


  • 初版公開: 2017/08/03(counter_culture v1.7.0
  • 訳文更新: 2021/07/01

counter_culture README(翻訳)

Railsアプリ向けの、ターボの効いたカウンタキャッシュです。Rails標準のカウンタキャッシュと比べて多くの点が改善されています。

  • カウンタキャッシュの更新を、値の作成や破棄のほか、値の変更時にも行える
  • 「多階層カウンタキャッシュ」をサポート(訳注: リレーション階層が離れていてもカウンタキャッシュの更新を直接指定できる)
  • 動的なカラム名をサポート: オブジェクトの種類ごとにカウンタキャッシュを分離
  • カウントの他に合計も出せる

Ruby 2.5.8、2.6.6、2.7.2、3.0.0、およびRails 3.2、4.0、4.1、4.2、5.0、5.1、6.0、6.1の最新パッチリリースでテストされています。

注意: Rails組み込みのカウンタキャッシュと異なり、counter_cultureはActive Record関連付けの.sizeの振舞いを変更しません。データベースへのクエリ発生を避けてキャッシュ値を読み込みたい場合は、カウンタキャッシュを含む属性名を直接お使いください。

product.categories.size  # => SELECT COUNT(*)クエリが発生
product.categories_count # => クエリを発生せずにカウンタキャッシュを使う

🔗 インストール

Gemfileにcounter_cultureを追加します。

gem 'counter_culture', '~> 2.0'

次にbundle installを実行します。

🔗 データベーススキーマ

必要なカラムをすべてのカウンタキャッシュについて作成しなければなりません。counter_cultureのジェネレータで、マイグレーション用のスケルトンを作成できます。

rails generate counter_culture Category products_count

上を実行すると、以下のようなコードを含むマイグレーションが生成されます。

add_column :categories, :products_count, :integer, null: false, default: 0

注意: gemが正常に機能するには、カラムは必ずNOT NULLに設定し、ゼロ値のデフォルトを設定する必要があります。

既存のデータにカウンタキャッシュを追加する場合は、生成されたマイグレーションに手動で値を設定する必要があります。

🔗 利用法

🔗 シンプルなカウンタキャッシュ

  • has_many関連付け
class Product < ActiveRecord::Base
  belongs_to :category
  counter_culture :category
end

class Category < ActiveRecord::Base
  has_many :products
end

Productモデルにcounter_culture :categoryと書くことで、Categoryモデルのcategoriesテーブルのproducts_countカラムのカウンタキャッシュが最新に保たれます。

  • 🔗 多対多の関連付け
class User < ActiveRecord::Base
  has_many :group_memberships
  has_many :groups, through: :group_memberships
end

class Group < ActiveRecord::Base
  has_many :group_memberships
  has_many :members, through: :group_memberships, class: "User"
end

class Membership < ActiveRecord::Base
  belongs_to :group
  belongs_to :member, class: "User"
  counter_culture :group, column_name: "members_count"
  # `members_count`の更新時にgroupでtouchも指定したい場合:
  # counter_culture :group, column_name: "members_count", touch: true
end

これで、Groupモデルのmembers_countカラムに最新のメンバー数が表示されます。

  • 🔗 多階層カウンタキャッシュ
class Product < ActiveRecord::Base
  belongs_to :sub_category
  counter_culture [:sub_category, :category]
end

class SubCategory < ActiveRecord::Base
  has_many :products
  belongs_to :category
end

class Category < ActiveRecord::Base
  has_many :sub_categories
end

Productモデルにcounter_culture [:sub_category, :category]と書くことで、リレーション階層が離れたCategoryモデルのcategoriesテーブルのproducts_countのカウンタキャッシュを最新に保ちます。カウントキャッシュを指定できる階層レベル数に制限はありません。

カウンタキャッシュは、リレーションの階層レベルごとに指定する必要があります。上のコード例で、CategorySubCategoryのそれぞれにproductのカウントが必要な場合は、Productクラスを次のように変更します。

class Product < ActiveRecord::Base
  belongs_to :sub_category
  counter_culture [:sub_category, :category]
  counter_culture [:sub_category]
end

🔗 カラム名のカスタマイズ

class Product < ActiveRecord::Base
  belongs_to :category
  counter_culture :category, column_name: "products_counter_cache"
end

class Category < ActiveRecord::Base
  has_many :products
end

Productモデルにcounter_culture :category, column_name: "products_counter_cache"と書くことで、Categoryモデルのcategoriesテーブルのproducts_counter_cacheカラムのカウンタキャッシュが最新に保たれます。カウントキャッシュを指定できる階層レベル数に制限はありません。

🔗 動的なカラム名

class Product < ActiveRecord::Base
  belongs_to :category
  counter_culture :category, column_name: proc {|model| "#{model.product_type}_count" }
  # product_type属性は ['awesome', 'sucky'] のいずれか
end

class Category < ActiveRecord::Base
  has_many :products
end

🔗 増分(delta magnitude)の指定

class Product < ActiveRecord::Base
  belongs_to :category
  counter_culture :category, column_name: :weight, delta_magnitude: proc {|model| model.product_type == 'awesome' ? 2 : 1 }
end

class Category < ActiveRecord::Base
  has_many :products
end

Productモデルに上のように書くことで、Categoryモデルのweightカラムのカウンタキャッシュが最新に保たれます。productがawesomeなら増分は2、それ以外なら増分は1になります。

次のように、delta_magnitudeに固定の増分を指定することもできます。

class Product < ActiveRecord::Base
  belongs_to :category
  counter_culture :category, column_name: :weight, delta_magnitude: 3
end

class Category < ActiveRecord::Base
  has_many :products
end

Productに追加が1件あると、Categoryweightカラムが3増え、Productで削除が1件あると3減ります。

🔗 条件付きカウンタキャッシュ

class Product < ActiveRecord::Base
  belongs_to :category
  counter_culture :category, column_name: proc {|model| model.special? ? 'special_count' : nil }
end

class Category < ActiveRecord::Base
  has_many :products
end

Productモデルに上のように書くことで、Categoryモデルのspecial_countのカウンタキャッシュが最新に保たれます。productのspecial?trueの場合にのみspecial_countを更新します。

これをcounter_culture_fix_countsと併用したい場合は、column_namesの設定も指定してください。

🔗 カウントの代わりに合計を出す

カウントを実行する代わりに、合計を自動更新することもできます。
この場合、対象のカウンタを1ずつ増やす代わりに、フィールド値の合計で更新します。

カウントを行うオブジェクトの特定のフィールド値をカウンタの増分に使いたい場合は、:delta_columnオプションを使います。

たとえば、Productモデルのテーブルにweight_ouncesフィールドがあり、Categoryモデルのproduct_weight_ouncesにあるすべてのproductについてweightの合計を最新に保つ場合は、次のようにします。

class Product < ActiveRecord::Base
  belongs_to :category
  counter_culture :category, column_name: 'product_weight_ounces', delta_column: 'weight_ounces'
end

class Category < ActiveRecord::Base
  has_many :products
end

Productモデルに上のように書くことで、Categoryモデルのproduct_weight_ouncesのカウンタキャッシュが最新に保たれます。
このカウンタキャッシュの値は、Categoryに関連付けられているProductの各レコードのweight_ouncesを合計した値になります。

delta_columnオプションでは、:integerを含むすべての数値型カラムをサポートします。特に、:floatもサポート対象かつテスト済みです。

🔗 foreign_key_valuesによる外部キーの動的上書き

class Product < ActiveRecord::Base
  belongs_to :category
  counter_culture :category, foreign_key_values:
      proc {|category_id| [category_id, Category.find_by_id(category_id).try(:parent_category).try(:id)] }
end

class Category < ActiveRecord::Base
  belongs_to :parent_category, class_name: 'Category', foreign_key: 'parent_id'
  has_many :children, class_name: 'Category', foreign_key: 'parent_id'

  has_many :products
end

上のコードによって、Categoryモデルのcategoriesテーブルのproducts_countカラムのカウントキャッシュが最新に保たれます。各productは、直接のcategoryのカウンタと、そのcategoryの親のカウンタの両方に影響します。カウントキャッシュを指定できる階層レベル数に制限はありません。

🔗 カウンタ変更時にタイムスタンプを更新する

counter_culture gemは、カウンタキャッシュ更新時にモデルのタイムスタンプをデフォルトでは更新しません。カウンタキャッシュカラムの更新時にタイムスタンプも更新したい場合は、touchオプションにtrueを指定します。

  counter_culture :category, touch: true

このオプションは、カウンタキャッシュ変更時にキャッシュを無効にする必要がある場合に便利です。

🔗 カスタムのタイムスタンプカラム

特定のカウンタキャッシュが変更された場合にのみ更新されるタイムスタンプカラムを独自に指定することもできます。

  counter_culture :category, touch: 'category_count_changed'

上のようにオプションを指定すると、category_counter_cacheの更新時にcategory_count_changedカラムとupdated_atカラムが常に両方とも更新されます。

🔗 デッドロックの回避と、commit後のカウンタキャッシュ更新

アプリケーションによっては、このgemを使うとカウンタキャッシュの更新に伴ってデッドロックの問題が発生することがあります。この問題を回避するための情報や有用なリンクについては#263を参照してください。

もうひとつの方法は、単にカウンタキャッシュの更新をトランザクションの外に延期することです。これにより、カウンタキャッシュ更新のトランザクションが保証されなくなる代わりにデッドロックが解消されるはずです。この振舞いはデフォルトでは無効であり、以下のように影響を受けるカウンタキャッシュごとに有効にしてください。

  counter_culture :category, execute_after_commit: true

🔗 カウンタキャッシュ値を手動で流用する

主要なデータのカウンタキャッシュ値を他の場所で使いたい場合があります。これは、たとえばカウンタキャッシュを既存のデータに追加する場合に必要になります。カウンタキャッシュに含まれる無効な値を検出するために、カウンタキャッシュは定期的に実行することをおすすめします(BestVendor社の場合、週に1度実行しています)。

Product.counter_culture_fix_counts
# Productで定義済みの全カウントを自動で修正する

Product.counter_culture_fix_counts exclude: :category
# Productで定義済みの全カウントを自動で修正する
# ただし:categoryのリレーションについては除く

Product.counter_culture_fix_counts only: :category
# Productの:categoryリレーションについてのみカウントを自動で修正する
# :excludeと:onlyには、同じ階層レベルにあるリレーションの配列も指定できる
# カウントの自動修正を多階層にわたって行う場合は、これではなく、その次の[[ ]]書式が必要

Product.counter_culture_fix_counts only: [[:subcategory, :category]]
# Productの2つの階層レベルのリレーション([:subcategory, :category])についてのみカウントを自動で修正する

Product.counter_culture_fix_counts column_name: :reviews_count
# Productの:reviews_count columnでのみカウントを自動で修正する
# これにより、処理済みのカラムをスキップできる
# 1個のカウンタキャッシュカラムにのみ影響する大規模なDB変更で有用

# :exceptと:onlyには配列も指定できる

Product.counter_culture_fix_counts verbose: true
# ログをSTDOUTに出力する

Product.counter_culture_fix_counts only: :category, where: { categories: { id: 1 } }
# Productの「id 1リレーション」を持つ:categoryでのみカウントを自動で修正

カウント用のcounter_culture_fix_countsメソッドでは、レコードをバッチ処理することでメモリ消費を抑えています。デフォルトのバッチサイズは1000ですが、以下の方法で設定することもできます。

# initializerに追加
CounterCulture.config.batch_size = 100

メソッド呼び出しでも:batch_sizeオプションでサイズを指定できます。

Product.counter_culture_fix_counts batch_size: 100

counter_culture_fix_countsはデバッグ用に、すべての無効な値をハッシュの配列として返します。ハッシュの形式は次のとおりです。

{ entity: カウントを修正するモデル,
  id: カウントが誤っているモデルのid,
  what: 誤ったカウントがあるカラム名,
  wrong: 前回保存されている誤ったカウント,
  right: 修正された正しいカウント }

counter_culture_fix_countsの動作は高速で、クエリ数を最小限に抑えるよう最適化されています。

counter_cultureと同様に、カウント修正時にレコードのタイムスタンプを更新できます。デフォルトのタイムスタンプフィールドを更新したい場合は以下のようにtouch: trueオプションを渡します。

Product.counter_culture_fix_counts touch: true

カスタムのタイムスタンプカラムを指定している場合は、その名前を touch オプションの値として渡します。

Product.counter_culture_fix_counts touch: 'category_count_changed'
  • 🔗 複数ワーカーでカウンタキャッシュをパラレルに修正する

start:オプションとfinish:オプションは、特に複数ワーカーで同じ処理キューを扱いたい場合に有用です。ワーカーごとにstart:finish:を設定することで、たとえばワーカー1ではid 1〜9999までの全レコードを処理し、ワーカー2ではid 10000以上のレコードを処理できるようになります。

Product.counter_culture_fix_counts start: 10_000
# Productで定義されたid 10000以上のレコードで全カウンタキャッシュを修正する

Product.counter_culture_fix_counts finish: 10_000
# レコード数10,000件まで処理する

Product.counter_culture_fix_counts start: 1000, finish: 2000
# ワーカー1では1000〜2000まで処理する

Product.counter_culture_fix_counts start: 2001, finish: 3000
# ワーカー2では2001〜3000まで処理する
  • 🔗 動的なカラム名を扱う

動的なカラム名が使われているカウンタキャッシュを手動で流用する場合、以下の追加設定が必要です。

class Product < ActiveRecord::Base
  belongs_to :category
  counter_culture :category,
      column_name: proc {|model| "#{model.product_type}_count" },
      column_names: {
          ["products.product_type = ?", 'awesome'] => 'awesome_count',
          ["products.product_type = ?", 'sucky'] => 'sucky_count'
      }
  # product_type属性は ['awesome', 'sucky'] のいずれか
end

column_namesでは、条件文字列の代わりにスコープも指定できます。

class Product < ActiveRecord::Base
  belongs_to :category
  scope :awesomes, ->{ where "products.product_type = ?", 'awesome' }
  scope :suckys, ->{ where "products.product_type = ?", 'sucky' }

  counter_culture :category,
      column_name: proc {|model| "#{model.product_type}_count" },
      column_names: {
          Product.awesomes => :awesome_count,
          Product.suckys => :sucky_count
      }
end

この設定を避けて、動的なカラム名を持つカウンタキャッシュを単にスキップし、動的でないモデルのカウンタを修正したい場合は、以下のようにskip_unsupportedオプションを渡せます。

Product.counter_culture_fix_counts skip_unsupported: true
  • 🔗 外部キー動的上書きの制限事項

:foreign_key_valuesオプションを使っている場合、「カウンタキャッシュ値を手動で流用する」に記載されている方法はサポートされません。独自のコードを書く必要があります。

🔗 paranoiadiscardによる論理削除

本gemは、論理削除(soft-delete)をサポートするparanoia gemやdiscardgemをRails 4.2以降で使う場合にカウンタを正しく更新します。ただし、リストア後にカウンタが正しく増加するには、モデル内でcounter_cultureを呼び出す前に論理削除を設定(acts_as_paranoidまたはinclude Discard::Model)する必要があります。

🔗 Paranoia

class SoftDelete < ActiveRecord::Base
  acts_as_paranoid

  belongs_to :company
  counter_culture :company
end

🔗 Discard

class SoftDelete < ActiveRecord::Base
  include Discard::Model

  belongs_to :company
  counter_culture :company
end

🔗 PaperTrailとの統合

paper_trail gemを利用していて、counter_cultureによってカウンタキャッシュカラムが変更されたときに新しいバージョンを作成したい場合は、以下のようにwith_papertrailオプションを指定できます。

class Review < ActiveRecord::Base
  counter_culture :product, with_papertrail: true
end

class Product < ActiveRecord::Base
  has_paper_trail
end
  • 🔗 ポリモーフィック関連付け

counter_culture gemは、1階層レベルの限定的なポリモーフィック関連付けをサポートするようになりました。

🔗 counter_culture gemに貢献するときの手順

  1. 常に最新のmasterブランチをチェックアウトし、機能が実装されていないかどうか、バグが修正されていることを確認します。

* GitHubのissue trackerで、同じissueがリクエスト済みかどうか、既に貢献済みかどうかを確認します。
* プロジェクトをforkします。
* feature/bugfixブランチを立てます。
* コードに修正や改良を加えたらcommit、pushします。
* 貢献の際は必ずテストコードも追加してください。貢献した機能が将来不意に動かなくならないようにするために重要です。
* Rakefileのバージョンや履歴で問題が発生しないようご注意ください。独自のバージョンを利用したい場合や必要な場合でも貢献は可能ですが、こちらでcherry-pickできるようにコミットを分けておくようお願いします。

🔗 Copyright

Copyright (c) 2012-2021 BestVendor, Magnus von Koeller. See LICENSE.txt for further details.

The post Rails向け高機能カウンタキャッシュ gem「counter_culture」README(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: AR::Relation#destroy_allがバッチ分割に変更、Active Record暗号化解説、sidekiq-unique-jobsほか(20210712前編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

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

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

🔗 ActiveRecord::Relation#destroy_allの処理をデフォルトでバッチに分割

ActiveRecord::Relation#destroy_allが処理をバッチに分割するようになった。
destroy_allは実際には全リレーションを読み込んでからレコードのdestroyを1件ずつ繰り返すのでメモリがすぐ吹っ飛びがち。これを正しく行うために、デフォルトでは100件ずつのバッチに小分けするようにし、#destroy_all(batch_size: 100)のようにバッチサイズも指定できるようにする。
アプリを7.0にアップグレードするときにdeprecation warningが表示される。Rails 7.1までにdestroy_allはdestroy対象のオブジェクトのコレクションを返さなくなる。
新しい振舞いに移行するには以下のイニシャライザを設定する。

config.active_record.destroy_all_in_batches = true

このオプションは、今後新規作成するRailsアプリではデフォルトでオンになる。イニシャライザで設定しておくことで環境ごとの違いを生じないようにできる。
Genadi Samokovarov, Roberto Miranda
同Changelogより


つっつきボイス:「find_in_batches的なバッチ分割処理がRails 7.1のdestroy_allに取り入れられるようですね」「今後はdestroy_allがデフォルトでバッチ分割されるようになるのか: 割と大きな変更かも」「コンフィグでオフにできるんですね」

find_in_batchesのようなバッチ分割は、途中に別のトランザクションがはさまると結果が変わる可能性があるんですよ: destroy_allもその点は同じだと思いますが、心配な人はdestroy_allをトランザクションで囲むだろうし、destroy_allが遅いと思う人はActiveRecord::Relationdelete_allを使うと思うので、気にする人は少ないのかも」「なるほど」

destroy_allが途中でコケたらどうするんでしょう?」「destroy_allはデフォルトでorder: :ascが付いているので、コケた場合には順序を頼りに追うことになるでしょうね」

🔗 ActiveSupport::TimeZone.iso8601でordinal date値をサポート

Date._iso8601'21087'のようなordinal dateの文字列の値をパースしようとすると「28th March 2021」になる。
Rubyの標準ライブラリでDate._iso8601をサポートしているように、ActiveSupport::TimeZone.iso8601でもordinal date値をサポートすべき。
「年」と「年日数」({ year: 2021, yday: 87 }など)の値のパースをサポートし、Date.ordinalで有効となる日付の生成を試みる。このとき、パースされた値のうち、対応する年の:ydayがサニタイズされる。
同PR Summaryより

参考: Date._iso8601 (Ruby 3.0.0 リファレンスマニュアル)


つっつきボイス:「ordinal dateって初めて聞きました」「'21087'が2021年の87日目を表す、つまりその年の日付を3桁の日数で表せるのか、へ〜!」「あ、1月1日が001みたいな感じでカウントするんですね」「YYYY-DDD形式って…」

参考: ISO 8601 - Wikipedia

ordinal dateが「年間通算日(年日付)」と訳されているケースを見つけましたが、正式な名前かどうかは不明です。

「ordinal dateって日本だと見かけないかも」「普通あんまり使わなさそう」「ordinal dayの表を見つけた↓」「面白いけど使いたくないな〜😅」「うるう年は数字がズレるんですって」

# https://www.atmos.anl.gov/ANLMET/OrdinalDay.txtより
   TABLE OF ORDINAL DAY NUMBER FOR VARIOUS CALENDAR DATES.
          (After February, add 1 on leap years).

    JAN  FEB  MAR  APR  MAY  JUN  JUL  AUG  SEP  OCT  NOV  DEC

 1    1   32   60   91  121  152  182  213  244  274  305  335
 2    2   33   61   92  122  153  183  214  245  275  306  336
 3    3   34   62   93  123  154  184  215  246  276  307  337
 4    4   35   63   94  124  155  185  216  247  277  308  338
 5    5   36   64   95  125  156  186  217  248  278  309  339

 6    6   37   65   96  126  157  187  218  249  279  310  340
 7    7   38   66   97  127  158  188  219  250  280  311  341
 8    8   39   67   98  128  159  189  220  251  281  312  342
 9    9   40   68   99  129  160  190  221  252  282  313  343
10   10   41   69  100  130  161  191  222  253  283  314  344

11   11   42   70  101  131  162  192  223  254  284  315  345
12   12   43   71  102  132  163  193  224  255  285  316  346
13   13   44   72  103  133  164  194  225  256  286  317  347
14   14   45   73  104  134  165  195  226  257  287  318  348
15   15   46   74  105  135  166  196  227  258  288  319  349

16   16   47   75  106  136  167  197  228  259  289  320  350
17   17   48   76  107  137  168  198  229  260  290  321  351
18   18   49   77  108  138  169  199  230  261  291  322  352
19   19   50   78  109  139  170  200  231  262  292  323  353
20   20   51   79  110  140  171  201  232  263  293  324  354

21   21   52   80  111  141  172  202  233  264  294  325  355
22   22   53   81  112  142  173  203  234  265  295  326  356
23   23   54   82  113  143  174  204  235  266  296  327  357
24   24   55   83  114  144  175  205  236  267  297  328  358
25   25   56   84  115  145  176  206  237  268  298  329  359

26   26   57   85  116  146  177  207  238  269  299  330  360
27   27   58   86  117  147  178  208  239  270  300  331  361
28   28   59   87  118  148  179  209  240  271  301  332  362
29   29  *60   88  119  149  180  210  241  272  302  333  363
30   30        89  120  150  181  211  242  273  303  334  364

31   31        90       151       212  243       304       365

* Feb 29 exists only on a leap year.

「ordinal dateはISO 8601でも定義されていますし、この機能がActive Supportに入るということは使いたい人がいるということでしょうね」「ordinal dateは月替りを考えたくないときに使うのかな?」「週を数字にして2021年の何週目みたいに表すのは英語圏のアプリとかでたまに見かけますけど」

「ordinal dateを使うことがあるとすれば、組み込みのように少しでも桁数を減らしたい分野かもしれませんね」「あ〜たしかに」「後何日で保証が切れるみたいなタイマーが作りやすそう」


Ordinal dates
異なるカレンダーの日付を比較するなど、週や月の定義が任意だと障害になりやすい場合のためのシンプルなフォーマットです。(中略)このフォーマットは、日付システムを必要とするが完全なカレンダー計算ソフトウェアを含めるのが難しい単純なハードウェアシステムで使われます。
ISO 8601 - Wikipedia(英語版)より

🔗 remove_foreign_keyadd_foreign_keyif_exists:if_not_exists:オプションをサポート

remove_foreign_key/add_foreign_keyif_exists:if_not_exists:オプションをサポートする。以下のようにアプリケーションのマイグレーション中に、既に存在する外部キーを追加したときの例外や、存在しない外部キーを削除したときの例外を無視できるようになる。

class AddAuthorsForeignKeyToArticles < ActiveRecord::Migration[7.0]
  def change
    add_foreign_key :articles, :authors, if_not_exists: true
  end
end
class RemoveAuthorsForeignKeyFromArticles < ActiveRecord::Migration[7.0]
  def change
    remove_foreign_key :articles, :authors, if_exists: true
  end
end

同Changelogより


つっつきボイス:「Railsのマイグレーションで使うことのあるremove_foreign_keyadd_foreign_keyif_exists:オプションとif_not_exists:オプションがサポートされたんですね: 個人的にはマイグレーションにこういうオプションをあまり付けたくない気はしますけど」

参考: Active Record マイグレーション - Railsガイド

「たしかschema.rbには最終的にDDLの形で抽出したスキーマが反映されるはずだと思うので、このオプション部分で成功しても失敗してもschema.rbの一貫性は保てそうかな」「自分もたしかそうだったと認識してます」「マイグレーションの蓄積を直接schema.rbに反映していたらschema.rbが不定になる可能性があるので、そういう方法ではやっていないはず」

参考: データ定義言語 - Wikipedia — DDL

「普段は使わないと思いますが、たとえば外部キーを追加した後に何らかの理由でマイグレーションに失敗したときのリトライにはこういうオプションがあるといいかも」「あ〜たしかに」「マイグレーションが途中で失敗すると再実行でもコケるので、それをリカバリーしたいというニーズに応えるための改修だとしたら理解できる👍」「そういう状況はあって欲しくないけど、そうなったときには欲しいかも」「外部キーのマイグレーションにif_exists:が書かれているのを見かけたら不安な気持ちになりそうですけどね」

🔗 Action Mailboxで使うデフォルトのActive Storageサービスをカスタマイズ可能に

生メールソースの保存に使うActiveStorageサービスをコンフィグできる機能を追加。

# config/storage.yml
incoming_emails:
  service: Disk
  root: /secure/dir/for/emails/only
config.action_mailbox.storage_service = :incoming_emails

Yurii Rashkovskii
同Changelogより


つっつきボイス:「今まではAction Mailboxの保存先のActive Storageサービスを選べなかったのがyamlに記述することで選べるようになった: これは必要ですね👍」「Action Mailboxまだ使ったことなかったな〜」「そもそもメールを普段使っていません😆

参考: Action Mailbox の基礎 - Railsガイド

🔗 Active Storageのコントローラでstrict_loading_by_defaultコンフィグをサポート


つっつきボイス:「これもコンフィグ追加ですね」「今までActive Recordでstrict loadingをデフォルトでオンにするとActive Storageのコントローラでエラーになっていたのを修正したらしい」「ActiveStorage::Representationsというコントローラがあったとは知りませんでした」「ActiveStorage::RepresentationsActiveStorage::Previewと関連しているみたい: そのコントローラがビューの中でstrict loading違反していたんでしょうね」

新しくedgeで生成したアプリケーションでactive_record.strict_loading_by_default = trueを設定すると、ActiveStorage::Representationsコントローラでstrict loadingエラーが発生する。このプルリクではその問題を修正し、Active Storageのモデルでstrict loadingを無効にせずに済むようにした。
同PR Summaryより

🔗 uglify-jsをterserに置き換え

terser/terser - GitHub


つっつきボイス:「ターサー?」「terserが一瞬teaser(いじめっ子)に見えてしまいましたが、terserはterse(簡潔な)の比較級だそうです」「JSコードのminifyや難読化などに使うuglify-jsを新しいterserライブラリに置き換えたんですね: 改修の差分を見てもgemの差し替えぐらいしかやっていない↓」「ホントだ」「こんなにキレイに移行できるとは」「後発なだけにインターフェースも互換性があるんでしょうね」

# Gemfile#L26
gem "uglifier", ">= 1.3.0", require: false
gem "terser", ">= 1.1.4", require: false

参考: 難読化コード - Wikipedia

「uglifier、そんなgemもありましたね(遠い目)」「この記事は2019年だけどterserが伸びているらしい↓」「JSのライブラリは移り変わりが激しいので、こういう置き換えもケアしているんですね」

参考: 2019年のJavaScript minifier “terser” - Qiita

後で現在の比較を見るとterserが上回っています↓。

参考: terser vs uglify-js | npm trends

🔗Rails

🔗 Active Recordの暗号化機能解説(RubyFlowより)


つっつきボイス:「Rails 7に標準で搭載されるActive Record暗号化(ウォッチ20210412)の解説記事が出たんですね」

extend_queriesコンフィグをオンにすると以下ができるらしい: uniquenessバリデーションは、まさにこの間話したdeterministic encryptionに関連するヤツ(ウォッチ20210628)」「なるほど」「暗号化方式がdeterministicでない場合は、暗号化データを復号化しないとuniquenessバリデーションができなくなります」

  • 暗号化カラムで暗号化なしの平文データもクエリできる(config.active_record.encryption.support_unencrypted_dataもオンにする必要あり)
  • 暗号化スキームを複数利用可能になる
  • uniquenessバリデーションのサポートが有効になる
    同記事より

「検索はこういう感じになるのね↓」

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

「記事末尾のlimitation解説もよさそう」「以下のmultiple keysはたぶんキーのローテーションを指していると思います」「なるほど」

Deterministic searching does not support multiple keys – Something good to be aware of going in – if using deterministic encryption/searching, we won’t have the ability to use more than one key at a time. If we need to change keys, we’ll likely need to do something fancy.
同記事より

「Railsコンソールだと生データが見えるそうです」「当然そうなりますね」「deterministic encryptionだとセキュリティが下がる、これもごもっとも」

「コード例も載っていて要点を押さえた記事、よさそう👍」「この記事翻訳してくださ〜い」「はい、聞いてみます」


その後Honyebadgerより翻訳を許可いただきました🙇

🔗 Railsアンチパターンシリーズ記事最終回


つっつきボイス:「AppSignalブログのRailsアンチパターンシリーズ記事の最終回だそうです」

「最初はデメテルの法則↓: このコード例は、song下のlabelに直接アクセスさせるとlabelで何でもできてしまうので、Railsのdelegateヘルパーを使って隠蔽しましょうという話のようですね」「なるほど」「コンポジションしたオブジェクトを直接公開するべきではない、たしかに」

# 同記事より
# Bad
song.label.address

# Good
song.label_address

参考: Module#delegate
参考: デメテルの法則 - Wikipedia

「次は”そのGem、本当に必要?”的なトピック」「あまりに簡単な機能ならgemより直接実装する方が早いしgemのバージョンアップとかも気にしなくて済むので、自分もよくそう思います」「たしかに」

「このグラフのnpmのモジュールの増え方がヤバい↓」「自動生成してそうな勢い」「大半は使われていないモジュールでしょうけど、勢いがあるのは言語としては望ましいんですよね」「そうですね」「欲しいモジュールを検索するのは大変ですけど」


同記事より

「次は”例外を握りつぶすな”トピック」「以下のコード例を見ていて思ったんですが、returnで最後に返す値をあまり考慮していないrescueはたまに問題になりますね」「あ〜」「Rubyは最後に評価したものを値として返す仕様なので、rescueをメソッドの最後に書くときはちょっと気をつけておかないと、どこからreturnするかで想定外の値が返される可能性もあります」「なるほど」

# 同記事より
begin
  song.upload_lyrics
rescue
  puts 'Lyrics upload failed'
end

「なかなかよさそうな記事👍」「これも翻訳してみたいです」

🔗 sidekiq-unique-jobs(Ruby Weeklyより)

mhenrixon/sidekiq-unique-jobs - GitHub


つっつきボイス:「Sidekiqに重複したジョブが入らないように一意性を確保できるgemだそうです」「Sidekiqで全く同じパラメータのジョブが複数実行されるのを排除できるようですね」「ふむふむ」「割と欲しい機能に見えるので後でチェックしてみようかな」

mperham/sidekiq - GitHub

lock: :until_executeを指定できる↓:、実行完了するまでは同じジョブを投入できないけど実行が終われば投入できるということか、へ〜!」

# 同リポジトリより
class UntilExecuted
  include Sidekiq::Workers

  sidekiq_options lock: :until_executed

  def perform(id)
    # Do work
  end
end

lock: :until_expiredの場合はジョブが期限切れになれば同じジョブを再投入できる: これ賢い!」「おぉ〜」

# 同リポジトリより
class UntilExpired
  include Sidekiq::Workers

  sidekiq_options lock: :until_expired, lock_ttl: 1.day

  def perform
    # Do work
  end
end

「Webアプリの注文ボタンにたとえると、注文ボタンを押してもすぐに反応がないとつい何回も連打してしまうことがありますよね」「そうそう」「この場合なら:until_executeを指定すれば、実行中は同じジョブをキューに入れられないようにできる」「なるほど!」「READMEをさっと見た限りではロックやexpireなどをいろいろ制御できるみたい」

「このgemを使えば、ジョブの重複制御を自分でやらなくても投機的にジョブを投入できるということになりますね」「それすごいじゃないですか」

「思いついた範囲だと、たとえば時間課金の外部コンピューティングリソースを使いたいときにこのgemが合いそう: 投機的にジョブを積んでおくだけでCPU時間を効果的に使い切れるようになる」「あ〜なるほど」

「ジョブのステータスをチェックして重複を排除したりリトライしたりする機能は自分で実装すると複雑になりがちなので、今度機会があったら使ってみよう👍」「意識することが減るのはありがたい🙏

🔗 Hanami Mastery: Hanami Frameworkのブログ

Hanami公式かどうかは確かめきれませんでした。


つっつきボイス:「Hanamiフレームワークのブログサイトを立ち上げたそうです」「Hanami Masteryというタイトルがなかなかポイント高いですね」「花見名人的な」

🔗 その他Rails


つっつきボイス:「Gmailなどで見かける2文字のアバターSVGをERBで生成する記事です」「SlackのWorkspaceもデフォルトでこういう2文字アバターが表示されますね」「たしかにアバターがないと識別が面倒」


前編は以上です。

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

週刊Railsウォッチ: GitHub CopilotのAI補完、Pure Ruby実装のRuby JIT rhizome、PostgreSQLのPG-Strom拡張ほか(20210706後編)

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

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

Rails公式ニュース

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

The post 週刊Railsウォッチ: AR::Relation#destroy_allがバッチ分割に変更、Active Record暗号化解説、sidekiq-unique-jobsほか(20210712前編) first appeared on TechRacho.

週刊Railsウォッチ: ruby-spacyで自然言語処理、Ruby製x86-64アセンブラ、『タイムゾーン呪いの書』ほか(20210713後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 ruby-spacy: 自然言語処理ライブラリspaCyのRuby版

yohasebe/ruby-spacy - GitHub


つっつきボイス:「自然言語処理に惹かれて拾ってみました」「PythonのspaCyという新し目の自然言語処理ライブラリ↓をRubyで使えるようにしたようですね」

explosion/spaCy - GitHub

参考: 自然言語処理 - Wikipedia

「お〜、ちゃんとRubyライクに書ける↓」

# 同リポジトリより
require "ruby-spacy"
require "terminal-table"

nlp = Spacy::Language.new("ja_core_news_lg")
doc = nlp.read("任天堂は1983年にファミコンを14,800円で発売した。")

headings = ["text", "lemma", "pos", "tag", "dep"]
rows = []

doc.each do |token|
  rows << [token.text, token.lemma, token.pos, token.tag, token.dep]
end

table = Terminal::Table.new rows: rows, headings: headings
puts table

「ruby-spacyのGemfileを見ると@mrknさんのPyCall(RubyからPythonの関数を呼び出せるgem)を使ってる: この分ならおそらく元のspaCyと完全互換でしょうね」「お〜!」「ツイートにもspaCyとPyCallありがとうとありますね」

mrkn/pycall.rb - GitHub

🔗『Polished Ruby Programming』を読んでみた(1)


つっつきボイス:「この間のウォッチで取り上げたJeremy Evansさんの『Polished Ruby Programming』(ウォッチ20210629)を、TechRachoの翻訳記事でもお世話になっているBrandon Weaverさんが早速読んでみたそうです」「お、発売日は7/9なのにもう?(注: つっつきの日は7/8)」「それもそうですね」「時差の分早く出たのかも」

つっつきの翌日に無事Kindleで配信されました🎉。夢中で読んでます。

🔗 fisk: Ruby製x86-64アセンブラ(Ruby Weeklyより)

tenderlove/fisk - GitHub


つっつきボイス:「x86-64アセンブラをRubyで書けるとは、RubyとRailsのコミッターである@tenderlove(Aaron Patterson)さんがまたすごいのを作ったな〜」「x86のアセンブラがRubyのブロックに生で書かれているのがちょっと不思議な感じ」

# 同リポジトリより
fisk = Fisk.new

binary = fisk.asm do
  push rbp
  mov rbp, rsp
  int lit(3)
  pop rbp
  ret
end

参考: アセンブリ言語 - Wikipedia

「実行結果もあるので、本当にバイナリを出力して実行できるみたい↓」「すげ〜」

# 同リポジトリより
[aaron@tc-lan-adapter ~/g/fisk (master)]$ lldb ~/git/ruby/ruby -- -I lib fun.rb
error: module importing failed: invalid pathname
(lldb) target create "/Users/aaron/git/ruby/ruby"
procCurrent executable set to '/Users/aaron/git/ruby/ruby' (x86_64).
(lldb) settings set -- target.run-args  "-I" "lib" "fun.rb"
(lldb) process launch
Process 33042 launched: '/Users/aaron/git/ruby/ruby' (x86_64)
Process 33042 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=EXC_I386_BPT, subcode=0x0)
    frame #0: 0x00000001007f4005
->  0x1007f4005: popq   %rbp
    0x1007f4006: retq
    0x1007f4007: addb   %al, (%rax)
    0x1007f4009: addb   %al, (%rax)
Target 0: (ruby) stopped.
(lldb) bt
# 略

「fiskは64ビット版だけど、別の人が作った32ビット版も昔からあったんですね↓」

seattlerb/wilson - GitHub

lib/fisk/instructions/ディレクトリにインストラクションの数だけRubyのファイルがありますね」「ファイル数がめちゃ多い」「これはさすがに何らかのスクリプトで自動生成したんじゃないかと思います」「言われてみればファイルの日付が揃っていますね」

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

🔗 RubyでPunycode

HoneyryderChuck/idnx - GitHub


つっつきボイス:「何だろうと思ったら、Punycodeを生成するgemなんですね」「bücher.deみたいなウムラウト混じりのドメイン名をxn--bcher-kva.deのようなPunycodeに変換するのか」

# 同記事より
require "idnx"

Idnx.to_punycode("bücher.de") #=> "xn--bcher-kva.de"

参考: Punycode - Wikipedia

Punycode(ピュニコード、プニコード)とは、国際化ドメイン名で使われる文字符号化方式で、RFC 3492 で定義されている。Unicode で書かれた文字列をDNSで使用可能な、アルファベット(大文字小文字を区別しない)、数字、ハイフンのみの文字列に変換する。
Wikipediaより

「Punycodeといえば、日本語ドメイン名はさっぱり定着していませんね」「使われているの見たことないかも」

🔗 その他Ruby

つっつきボイス:「7/10だからあさって土曜発売🎉(注: つっつきは7/8でした)」「絵がかわいい❤」「五十嵐さんの学習ガイドは常に更新されているのが本当にいいですよね👍」「新しいの大事」

「ところで、自分がRailsを始めた頃のRailsガイドは頑張れば1日で読めるぐらいのボリュームでしたけど、大幅な書き換えを繰り返して今はものすごいボリューム」「もう1日で読むのは無理でしょうね」

参考: Ruby on Rails ガイド:体系的に Rails を学ぼう


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

🔗設計

🔗『タイムゾーン呪いの書』シリーズ


つっつきボイス:「先週のウォッチでも少しだけ取り上げたタイムゾーン呪いの書シリーズです(ウォッチ20210705)」「元になったQiita記事も読んだ覚えがあります」「お〜、かなり長い記事みたいですね」「長くてヘトヘトになりますけど、すごく学びありました」

「タイムゾーンに関する説明もすごく丁寧で事例も豊富、とてもいい記事だと思います👍」「同意です」「知識編の、誰もが知っていそうな話から始めて徐々に難易度を上げていく進め方も見事」「最初は時差や標準時ぐらいから始めて、次は夏時間という感じでだんだん濃くなっていますね」

「Unix timeを使ったことのあるエンジニアはそこそこいると思いますけど、Unix timeで小数点以下の時刻を扱うとうるう秒挿入で時刻が巻き戻る可能性があるという話はなるほどと思いました」「読んでて衝撃でした」

参考: UNIX時間 - Wikipedia

「これまで使っていたtzdbにtz databaseという名前が付いているのもこの記事で知りました」

参考: tz database - Wikipedia

JSTのような3文字のみのタイムゾーン略称が非推奨という話も、言われてみればたしかに」「ST(Standard Time)で2文字使うところがほとんどなので、国を指定するのに使えるのは事実上1文字ですね」「あ、そうか」「この記事ではTZ=Asia/Tokyoのように地域名も含めたタイムゾーン名を書くことを推奨しています」

「全部読むのは大変そうだけど、知っておいて損のない記事ですね」「最初の知識編はまとまりがいいので、知識編だけでも一度は最後まで読んでおくことをおすすめします」「なるほど」「知識編にはRubyの話も登場していますよ」「そうそう、ありました」「読んですぐ理解できなくてもいいので、1年ぐらい後にもう一回読んでもいいくらい: 仕様書を読解するスキルを養うのにもいいんじゃないかな」

「次の実装編は少し難易度が上がりますけどやはり大事なことが書かれているので、できれば実装編まで読んで欲しい」「お〜」「Java編はまだ流し見た程度ですが、命名周りの話が興味深いですね: さまざまな概念にいかに適切な名前を付けるかとか」

「Rubyでもタイムゾーン周りの詳しい記事を誰か書かないかな〜」「RailsだとたまにActive Supportのタイムゾーン関連に更新が入っていますね」

🔗 タイムゾーンよもやま

「この記事を読んでいて、たとえばプログラマーやエンジニアがどんな仕事をしているのかを一般の人や学生に説明するときに、この記事の”タイムゾーン”というお題はとても向いているんじゃないかなと思いました」「あ〜なるほど」「自分が子どものときに、社会人が小学校や中学校に赴いて自分の職業について講演するというカリキュラムがあったんですけど、そういうときのテーマによさそう」

「理由としては、時計を見たこともない人はまずいないのと、地球上には時差というものがあるという前提にほぼ説明抜きで同意してもらえるから」「たしかに」「プログラマーやエンジニアがどれだけ大変な仕事をしているかを一般向けに説明するようなときに、前提を長々と説明しなくて済むという点が重要」

「逆に、この間のウォッチで話したような口座系システムの話(ウォッチ20210706)をお題にしたとすると、銀行の通帳を見たことぐらいしかない人や会計の知識がない人に前提条件を説明するだけで大変じゃないですか」「たしかに、”口座データはUPDATE禁止”みたいな話をいきなり説明するわけにもいかないでしょうね」「タイムゾーンはその点うってつけだと思います」

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

🔗 TablesNG


つっつきボイス:「TablesNGは、Googleが再実装を進めている次世代のHTML tableの内部実装だそうです」「今までtableにposition: stickyを付けられなくてJavaScriptやCSSのflexなどで対応していたのをCSSでできるみたい」

参考: position - CSS: カスケーディングスタイルシート | MDN
参考: flex - CSS: カスケーディングスタイルシート | MDN

「HTMLのtableと言えば長年の歴史がありますよね」「tableがないとレイアウトを組めなかった時代、ありましたね」

「ちょうどWebデザインの歴史をたどる記事があった↓」「いろいろ懐かしい」「あの頃は何でもtableでこんなレイアウト組んでたな〜」「今のgridレイアウトも概念的にはほぼtableだと思いますけど」

参考: A Brief History of Trends in Web Design | by Shannon Draper | codeburst


codeburst.ioより

参考: grid - CSS: カスケーディングスタイルシート | MDN

🔗言語/ツール/OS/CPU

🔗 GitHubでRSSリーダー


つっつきボイス:「へ〜、GitHub PagesでJekyllなどに使うファイルの生成機能を使ってRSSリーダー相当のHTMLを生成しているのか、面白い」「お〜」「最近のGitHub Pagesならページに認証もかけられるようになっているので、自分用の認証も実装しようと思えばできる感じですね」

参考: GitHub Pages について - GitHub Docs
参考: GitHub Pages でパスワードによる認証をつける | hgrs’s Blog


後編は以上です。

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

週刊Railsウォッチ: AR::Relation#destroy_allがバッチ分割に変更、Active Record暗号化解説、sidekiq-unique-jobsほか(20210712前編)

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

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

Ruby Weekly

The post 週刊Railsウォッチ: ruby-spacyで自然言語処理、Ruby製x86-64アセンブラ、『タイムゾーン呪いの書』ほか(20210713後編) first appeared on TechRacho.

rbenv: Ruby 3.0.xのビルドがbinutilsでエラーになった

$
0
0

しょうもないところで詰まりました。

現象

macOS Big Sur環境でrbenv install 3.0.2rbenv install 3.0.1を実行すると以下のエラーが発生しました。rbenv install 2.7.4rbenv install 2.6.8ではなぜか発生しませんでした。

$ rbenv install 3.0.2
Downloading ruby-3.0.2.tar.gz...
-> https://cache.ruby-lang.org/pub/ruby/3.0/ruby-3.0.2.tar.gz
Installing ruby-3.0.2...
ruby-build: using readline from homebrew

BUILD FAILED (macOS 11.4 using ruby-build 20210707-4-gebdcf0c)

Inspect or clean up the working tree at /var/folders/3x/sfj972cx6vnddfqpk5yv36m00000gn/T/ruby-build.20210708152635.35557.WWeFzy
Results logged to /var/folders/3x/sfj972cx6vnddfqpk5yv36m00000gn/T/ruby-build.20210708152635.35557.log

Last 10 log lines:
compiling rb_str_dup.c
installing default date_core libraries
installing digest libraries
compiling set_len.c
linking shared-object digest.bundle
linking shared-object etc.bundle
linking shared-object dbm.bundle
linking shared-object -test-/string.bundle
linking shared-object date_core.bundle
make: *** [build-ext] Error 2

メッセージに表示されたログファイルを開くと以下のようにbigdecimal.cのビルドがコケていました。

# 略
In file included from bigdecimal.c:13:
./bigdecimal.h:122:1: error: redefinition of 'rb_array_const_ptr'
rb_array_const_ptr(VALUE a)
^
../.././include/ruby/internal/core/rarray.h:184:1: note: previous definition is here
rb_array_const_ptr(VALUE a)
^
In file included from bigdecimal.c:13:
./bigdecimal.h:139:1: error: static declaration of 'rb_sym2str' follows non-static declaration
rb_sym2str(VALUE sym)
^
../.././include/ruby/internal/symbol.h:63:7: note: previous declaration is here
VALUE rb_sym2str(VALUE);
      ^
bigdecimal.c:108:1: error: static declaration of 'rb_rational_num' follows non-static declaration
rb_rational_num(VALUE rat)
^
../.././include/ruby/internal/intern/rational.h:39:7: note: previous declaration is here
VALUE rb_rational_num(VALUE rat);
      ^
bigdecimal.c:120:1: error: static declaration of 'rb_rational_den' follows non-static declaration
rb_rational_den(VALUE rat)
^
../.././include/ruby/internal/intern/rational.h:40:7: note: previous declaration is here
VALUE rb_rational_den(VALUE rat);
      ^
bigdecimal.c:132:1: error: static declaration of 'rb_complex_real' follows non-static declaration
rb_complex_real(VALUE cmp)
^
../.././include/ruby/internal/intern/complex.h:38:7: note: previous declaration is here
VALUE rb_complex_real(VALUE z);
      ^
bigdecimal.c:144:1: error: static declaration of 'rb_complex_imag' follows non-static declaration
linking shared-object -test-/tracepoint.bundle
rb_complex_imag(VALUE cmp)
^
../.././include/ruby/internal/intern/complex.h:39:7: note: previous declaration is here
VALUE rb_complex_imag(VALUE z);
      ^
# 略

1. Command Line Toolsが最新であることを確認する

よくある原因は、MacのCommand Line Toolsが古くなっているというものだそうです。Command Line Toolsのインストール/アンインストール方法はちょくちょく変わっていましたが、現在は以下のようにコマンドラインでできるようになっています。

参考: 3.0.0-preview2 install failed (Mac OS X 10.15.7 using ruby-build 20201210) · Issue #1505 · rbenv/ruby-build

# issue #1505 #issuecomment-752274233より
$ sudo rm -rf /Library/Developer/CommandLineTools
$ xcode-select --install
$ sudo xcode-select -s /Library/Developer/CommandLineTools

しかし自分の場合は、これを行っても引き続きエラーになりました。

2. binutilsとcoreutilsをチェックする

さらに調べてみると、少し前に何かの都合で自分がHomebrewでインストールしたbinutilsとcoreutilsが怪しそうです。

.profileで以下をコメントアウトしてからbashに再ログインしました。

# ~/.profile
export PATH="/usr/local/opt/coreutils/libexec/gnubin:$PATH"
export MANPATH="/usr/local/opt/coreutils/libexec/gnuman:$MANPATH"

export LDFLAGS="-L/usr/local/opt/binutils/lib"
export CPPFLAGS="-I/usr/local/opt/binutils/include"
export PATH="/usr/local/opt/binutils/bin:$PATH"

今度はrbenv install 3.0.2で3.0.2のインストールに成功しました!

$ rbenv install 3.0.2
Downloading ruby-3.0.2.tar.gz...
-> https://cache.ruby-lang.org/pub/ruby/3.0/ruby-3.0.2.tar.gz
Installing ruby-3.0.2...
ruby-build: using readline from homebrew
Installed ruby-3.0.2 to /Users/hachi8833/.anyenv/envs/rbenv/versions/3.0.2

後で切り分けてみると、原因はbinutilsの方でした。

さしあたってどちらも使っていないので、brew uninstall binutils coreutils; brew cleanupを実行してbinutilsとcoreutilsを削除しました。

関連記事

Bash: .bashrcと.bash_profileの違いを今度こそ理解する

The post rbenv: Ruby 3.0.xのビルドがbinutilsでエラーになった first appeared on TechRacho.

週刊Railsウォッチ: GitHubによるdisable_joins解説、MemoWise gemでメモ化、RailsのDDoS攻撃対策ほか(20210719前編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

お知らせ: 今週木金は祝日のため、来週は週刊Railsウォッチの代わりに通常記事を公開します。

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

以下の公式更新情報から見繕いました。今回は小粒でわかりやすい改修が多い印象です。

🔗 config_forでyamlのsharedルート要素が配列を受け取れるよう修正

# railties/test/application/configuration_test.rb#2060
    test "config_for works with only a shared root array" do
      set_custom_config <<~RUBY
        shared:
          - foo
          - bar
      RUBY

      app "development"

      assert_equal %w( foo bar ), Rails.application.config.my_custom_config
    end

    test "config_for returns only the env array when shared is an array" do
      set_custom_config <<~RUBY
        development:
          - baz
        shared:
          - foo
          - bar
      RUBY

      app "development"

      assert_equal %w( baz ), Rails.application.config.my_custom_config
    end

つっつきボイス:「yamlファイル内の各環境設定のルート要素にハッシュではなく配列を指定するとエラーになる問題が修正されたようですね」「なるほど」「通常はハッシュを渡すと思うので、これを踏むことは少なそうかな」

# railties/lib/rails/application.rb#L249
        if shared
-         config = {} if config.nil?
-         if config.is_a?(Hash)
+         config = {} if config.nil? && shared.is_a?(Hash)
+         if config.is_a?(Hash) && shared.is_a?(Hash)
            config = shared.deep_merge(config)
+         elsif config.nil?
+           config = shared
          end
        end

参考: プログラマーのための YAML 入門 (初級編)

🔗 filepathにファイルがない場合のエラー出力を改善


つっつきボイス:「ActionView::TemplateRendererで出すエラーの種類を増やして、絶対パスでない場合に適切なエラーを出すようにしたんですね」

# actionview/lib/action_view/renderer/template_renderer.rb#L14
    private
      # Determine the template to be rendered using the given options.
      def determine_template(options)
        keys = options.has_key?(:locals) ? options[:locals].keys : []
        if options.key?(:body)
          Template::Text.new(options[:body])
        elsif options.key?(:plain)
          Template::Text.new(options[:plain])
        elsif options.key?(:html)
          Template::HTML.new(options[:html], formats.first)
        elsif options.key?(:file)
          if File.exist?(options[:file])
            Template::RawFile.new(options[:file])
          else
-           raise ArgumentError, "`render file:` should be given the absolute path to a file. '#{options[:file]}' was given instead"
+           if Pathname.new(options[:file]).absolute?
+             raise ArgumentError, "File #{options[:file]} does not exist"
+           else
+             raise ArgumentError, "`render file:` should be given the absolute path to a file. '#{options[:file]}' was given instead"
+           end
          end
        elsif options.key?(:inline)
          handler = Template.handler_for_extension(options[:type] || "erb")
          format = if handler.respond_to?(:default_format)
            handler.default_format
          else
            @lookup_context.formats.first
          end
          Template::Inline.new(options[:inline], "inline template", handler, locals: keys, format: format)
        elsif options.key?(:renderable)
          Template::Renderable.new(options[:renderable])
        elsif options.key?(:template)
          if options[:template].respond_to?(:render)
            options[:template]
          else
            @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
          end
        else
          raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option."
        end
      end

🔗 credentialエディタ呼び出し時にファイルパスに含まれるスペースを正しく扱えるよう修正


つっつきボイス:「credentialへのパスがエスケープされていなかったのか: これは修正すべきでしょうね」「修正で追加されたShellwordsは、そういえば以前も取り上げましたね(ウォッチ20200225)」「ShellwordsはRubyのデフォルトgemで、Rubyで実行するsystemコマンドに渡す文字列に変数を埋め込む必要がある場合は必ずShellwordsでエスケープしないといけません: 今回はたまたまcredentialのパスにスペースが含まれているとcredentialエディタの起動でエラーになったことで見つかったんでしょうね」

# railties/lib/rails/commands/credentials/credentials_command.rb#L95
        def change_credentials_in_system_editor
          credentials.change do |tmp_path|
-           system("#{ENV["EDITOR"]} #{tmp_path}")
+           system("#{ENV["EDITOR"]} #{Shellwords.escape(tmp_path)}"
          end
        end

参考: Shellwords.#shellescape (Ruby 3.0.0 リファレンスマニュアル)
参考: Rubyから外部コマンドを実行するときはShellwordsモジュールが便利 - ブログのおんがえし

🔗 Action Cableのbroadcastでログ出力を300文字までに変更


つっつきボイス:「なるほど、ログが溢れないようにtruncate(300)を追加したんですね」「気持ちわかります」「Action Cableのやりとりに画像のような大きなファイルが含まれているとログが大量に発生するのはあるある」

# actioncable/lib/action_cable/server/broadcasting.rb#L42
          def broadcast(message)
-           server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" }
+           server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect.truncate(300)}" }

            payload = { broadcasting: broadcasting, message: message, coder: coder }
            ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
              encoded = coder ? coder.encode(message) : message
              server.pubsub.broadcast broadcasting, encoded
            end
          end

🔗 fixtureの読み込み後に外部キーをverifyするよう変更


つっつきボイス:「Railsのfixture機能では参照の外部キーチェックを行わないんですよ: 以下で言うとpirate: redbeardはpirates.ymlに存在しないんですがバリデーションされないので通る」「ふむふむ」「この改修では外部キーのバリデーションを行うようにしたようですね」

# 同PRより
# test/fixtures/parrots.yml
george:
  name: "Curious George"
  pirate: redbeard

# test/fixtures/pirates.yml
blackbeard:
  name: "Blackbeard"

「修正後はバリデーションに失敗するとエラーをraiseするようになっている↓」

# activerecord/lib/active_record/fixtures.rb#L640
            if ActiveRecord.verify_foreign_keys_for_fixtures && !conn.all_foreign_keys_valid?
              raise "Foreign key violations found in your fixture data. Ensure you aren't referring to labels that don't exist on associations."
            end

Rails API: `ActiveRecord::FixtureSet`(翻訳)

🔗 fixtureとfactory_bot

「ただfixtureが外部キーでバリデーションエラーを出すようになると少し面倒になるんですよ: 複雑な依存関係を持つデータをfixtureで扱う場合、バリデーションエラーにならないためにfixtureの適切な読み込み順序なども考えないといけなくなってしまう」「あ〜そうか!」

「おそらくfixtureの使い所は基本的に単体モデルのデータのようなあまり複雑でないものだと思うので、それもあって従来はfixtureで外部キーをバリデーションしていなかったんじゃないかなと想像しています」「ふ〜む」「自分もこれまでfixtureは比較的シンプルなデータのセットアップに使うことが多くて、リレーションが複雑になってきたらfixtureをやめてfactory_botにしていましたね」

thoughtbot/factory_bot - GitHub

「このバリデーション機能はfixtureで基本的にやりたい人にはありがたいと思います: 個人的には上のように外部キーを使うデータはあまりfixture向きではないかなという気もしていますが」「なるほど」

🔗 存在しないミドルウェアをdeleteした場合にエラーを出すよう修正


つっつきボイス:「config.middleware.deleteで削除するミドルウェアが存在していない場合にエラーを出すようにしたんですね: 何も出さないよりは正しそう」

# actionpack/lib/action_dispatch/middleware/stack.rb# 132
    def delete(target)
-     middlewares.delete_if { |m| m.name == target.name }
+     middlewares.reject! { |m| m.name == target.name } || (raise "No such middleware to delete: #{target.inspect}")
    end

🔗 rails gで指定するインデックス種別が無効な場合にエラーを出すようにした


つっつきボイス:「従来は以下の:indxeみたいなスペルミスが無視されていたのを、エラーを出すようにしたそうです」

# 同PRより
bin/rails g model post title:string:indxe

「Railsのジェネレータを普段使っていないから、post title:string:indexと書くとインデックスを付けられるとは知らなかった」「そういえばRailsを長くやっている人はscaffoldをあまり使わない印象がありますね」

参考: Rails ジェネレータとテンプレート入門 - Railsガイド

🔗Rails

🔗 GitHubがRails 7に追加したdisable_joinsの解説記事


つっつきボイス:「GitHub主席ソフトウェアエンジニアのEileenさんの記事です」「お、どこかで見たと思ったらRails 7に最近入ったdisable_joinsですね(ウォッチ20210426): マルチプルデータベース間であたかもjoinsしているかのように関連付けを取り出そうとするとSQLではJOINできないので、Active Recordが代わりに分割してクエリを発行してくれる機能」「あ、そうでした」「EileenさんがこうやってRailsコミット記事を出すことで、Railsに貢献する意義をGitHub社内にアピールできるという側面もありそうですね: よさそうな記事👍

# 同記事より
class Dog < AnimalsRecord
  has_many: treats, through: :humans, disable_joins: true
  has_many :humans
end
-- 同記事より
SELECT "humans"."id" FROM "humans" WHERE "humans"."dog_id" = ?  [["dog_id", 1]]
SELECT "treats".* FROM "treats" WHERE "treats"."human_id" IN (?, ?, ?)  [["human_id", 1], ["human_id", 2], ["human_id", 3]]

Rails 7: has_many :through関連付けにdisable_joins: trueオプションが追加(翻訳)

🔗 Rails 7のモデル暗号化導入の経緯


つっつきボイス:「BasecampがやっているHEYの中の人が書いた、Rails 7のモデル暗号化機能のいきさつ記事だそうです」「そういえば暗号化機能はもともとHEYで使っていたものを切り出したという話がありましたね」「HEYの中で第三者によるセキュリティ監査も受けたそうです」「この記事もなかなかよさそう👍

「モデル属性の暗号化はRails 7の新機能の中では比較的大きな位置を占めそうなので、Action Mailboxとかよりも使う人は多いんじゃないかな」「そんな気がしますね」

🔗 MemoWise gem: メモ化支援gem(Ruby Weeklyより)

panorama-ed/memo_wise - GitHub


つっつきボイス:「MemoWiseは、Rubyのメモ化(memoisation)を以下のようにmemo_wiseなどで書けるgemなんですね: どこかで見たかも」

# 同記事より: 通常のメモ化
class Example
  def slow_value
    @slow_value ||= begin
      ...
    end
  end
end
# 同記事より: MemoWiseの場合
class Example
  prepend MemoWise

  def slow_value
    ...
  end
  memo_wise :slow_value
end

参考: メモ化 - Wikipedia

「インスタンス変数とメソッド名を取り違えそうになると記事に書かれていました」「そうそう、アクセサメソッドをインスタンス変数と同じ名前にするとそうなりがちなので注意が必要ですね」

「一見メモ化のためだけのgemを作るほどでもなさそうにも見えますが、単なる利便性よりも、ここでメモ化が行われることをmemo_wiseで明示的に示すことでコードの可読性を高める効果が期待できそうかなと思いました👍」「なるほど」

つっつきの後で以下の記事も見つけました。

参考: Optimizing MemoWise Performance @ ja.cob.land

Ruby: インスタンス変数初期化のメモ化`||=`はほとんどの場合不要

🔗 RailsへのDDoS攻撃の影響を最小化する(Hacklinesより)

参考: DoS攻撃 - Wikipedia


つっつきボイス:「記事ではrack-attackミドルウェアによるスロットリングが取り上げられていますね: ちょうど最近使いました」「私も使ってます」

rack/rack-attack - GitHub

「記事ではCloudflareなどのサービスを用いたDNSレベルの防御の話もしている」「ふむふむ」

「DDoS対策は、まずRailsサーバーにいかにDDoSを届かせないかが重要だと思います: rack-attackのスロットリングについても、記事にもあるようにfail2banと組み合わせられればより防御を固められるでしょうね」

参考: Fail2ban
参考: 不正アクセスからサーバを守るfail2ban。さくらのクラウド、VPSで使ってみよう! | さくらのナレッジ

「fail2banって初めて知りました」「fail2banはrack-attackと別にかなり昔からある不正アクセス遮断用のソフトウェアで、iptablesと統合されることもよくありますし、いろんなところで使われています」「なるほど」「たしかにfail2banのページのつくりが昔っぽいかも」

参考: iptables - Wikipedia

「たとえばiptablesにfail2banを組み合わせると、一定時間内に何リクエスト以上来ると指定の秒数だけ遮断するといった設定を書けます: ただしfail2banはシステムに入っていないと使えません」「なるほど」「もちろんrack-attackでもスロットリングはできますが、DDoSがRackに届くことはRailsに届くことでもあるので、ガチのDDoS相手だときつい」「あ〜」「元記事にも書いてあるように、可能ならいわゆるWAF(Web Application Firewall)的なものも使いたいですね」

参考: Web Application Firewall - Wikipedia

同記事によると、rack-attackはfail2banなどのシステムレベルのツールが利用できない環境(Herokuなど)で便利だそうです。


前編は以上です。

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

週刊Railsウォッチ: ruby-spacyで自然言語処理、Ruby製x86-64アセンブラ、『タイムゾーン呪いの書』ほか(20210713後編)

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

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

Rails公式ニュース

Ruby Weekly

Hacklines

Hacklines

The post 週刊Railsウォッチ: GitHubによるdisable_joins解説、MemoWise gemでメモ化、RailsのDDoS攻撃対策ほか(20210719前編) first appeared on TechRacho.

週刊Railsウォッチ: ruby-gitでGit操作、最近のruby/debug、stdgems.org、Windows 365 Cloud PCほか(20210720後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

お知らせ: 今週木金は祝日のため、来週は週刊Railsウォッチの代わりに通常記事を公開します。

🔗Ruby

🔗 ruby-git(Ruby Weeklyより)

ruby-git/ruby-git - GitHub


つっつきボイス:「RubyからGitを操作できるgemがあるんですね」

# 同リポジトリより
g = Git.open(working_dir, :log => Logger.new(STDOUT))

g.index
g.index.readable?
g.index.writable?
g.repo
g.dir

g.log   # returns array of Git::Commit objects
g.log.since('2 weeks ago')
g.log.between('v2.5', 'v2.6')
g.log.each {|l| puts l.sha }
g.gblob('v2.5:Makefile').log.since('2 weeks ago')

g.object('HEAD^').to_s  # git show / git rev-parse
g.object('HEAD^').contents
g.object('v2.5:Makefile').size
g.object('v2.5:Makefile').sha

g.gtree(treeish)
g.gblob(treeish)
g.gcommit(treeish)
# (略)

「インストールコマンドがsudo gem install gitなの、わかるけどちょっと面白かった」「内部でGitコマンドを使っているみたい」

「Gitのコマンドはひととおり使えそうかな」「★も1,500超えですね」「たしかにRubyでGitコマンドをシェル経由で呼び出して標準入力を自分で取ったりするより使い勝手はいいでしょうね: こういうGitバインディングがRubyで使えるのはよさそう👍

🔗 dry-transformer

dry-rb/dry-transformer - GitHub


つっつきボイス:「dry-transformerは、dry-rbシリーズの割と新しいgemだそうです」「新しいといっても2年前ぐらいかな」

「こんなふうに変換のパイプラインを記述できるらしい: mapperやreducerを思わせますね」

# dry-rb.orgより
class MyMapper < Dry::Transformer::Pipe
  import Dry::Transformer::ArrayTransformations
  import Dry::Transformer::HashTransformations

  define! do
    map_array do
      symbolize_keys
      rename_keys user_name: :name
      nest :address, [:city, :street, :zipcode]
    end
  end
end

mapper = MyMapper.new

mapper.(
  [
    { 'user_name' => 'Jane',
      'city' => 'NYC',
      'street' => 'Street 1',
      'zipcode' => '123'
    }
  ]
)
# => [{:name=>"Jane", :address=>{:city=>"NYC", :street=>"Street 1", :zipcode=>"123"}}]

参考: MapReduce - Wikipedia

「dry-transformerは以下の記事で知りました↓」「CSVのエクスポート/インポートやRSSのパース、コードハイライトなどに使えるとある: たしかにパーサーにはいいかも」

Dry::Transformer::HashTransformationsをインポートするとハッシュを受けるパイプを作れるみたい」

# 同記事より
class HanamiMasteryAdapter < Dry::Transformer::Pipe
  import Dry::Transformer::HashTransformations

  define! do
    deep_symbolize_keys
    nest :address, %i[city zip]
    rename_keys login: :name
  end
end

hash = {
  'id' => 1
  'login' => 'John',
  'city' => 'NY',
  'zip' => { 'number' => 1234 }
}
adapter.call(hash)
# => { id: name: 'John', address: { city: 'NY', zip: { number: 1234 } } }

「できればdry-transformerがうまくハマるような、もうちょっと複雑な例をコードで見たいですね: MapReduce的な処理は複数プロセス間でのデータのやりとりがメインになる印象が強いんですが、この記事だと1個のプロセス内でやるシンプルなコード例しか載っていない」「たしかに変換を1個作るだけだとありがたみがわかりにくいかも」「さまざまな変換処理をパイプでつなぎ替えて出力を変えられるとか、そういう実例が欲しい」

🔗 最近のruby/debug

つっつきボイス:「ruby/debugのドキュメントが全面的にリライトされたそうです🎉」「_ko1さん精力的に活動していてスゴい」

「1.0.0のリリースを目指しているようです↓」「今はv1.0.0.beta8なのね」

🔗 stdgems.org

同サイトは、以下のブログサイトにあった情報を独自ドメインに切り出したもののようです。

参考: Idiosyncratic Ruby

つっつきボイス:「ruby-jp Slackでこのstdgems.orgを見かけました」「おぉ、Rubyバージョンごとのdefault gemsとbundled gemsのリストをここで見られるんですね、これは地味にありがたい!」


stdgems.orgより

「こういうリストはそんなに頻繁に使わないんですけど、たまに探し回るんですよ」「たしかに」「gemのバージョン番号もちゃんと書かれているのがいいですね: Gemfile.lockで競合が発生したときにデフォルトでインストールされるgemのバージョンを調べることがあるんですが、こういう情報があると助かります: いいサイト👍

🔗 その他Ruby

つっつきボイス:「mrubyではなくmruby/cの変更ですね」

mrubyc/mrubyc - GitHub

「CRubyのFixnumとBignumがIntegerに統合されたのはいつだったかな…Ruby 2.4か↓」「もうだいぶ前なんですね」

参考: Ruby 2.4 unifies Fixnum and Bignum into Integer | BigBinary Blog

後でやってみると、Ruby 2.5.9やRuby 2.6.8では以下のwarningを出してIntegerが返り、Ruby 2.7.4とRuby 3.0.2ではwarningなしでIntegerが返りました。

irb(main):001:0> Fixnum
(irb):1: warning: constant ::Fixnum is deprecated
=> Integer

🔗 セキュリティ

🔗 Open Source Vulnerabilitiesデータベース


つっつきボイス:「Open Source VulnerabilitiesはGoogleがやっているプロジェクトのようですね」「現在の脆弱性交換スキーマ定義はこんな感じらしい↓」

# 同記事より
{
        "id": string,
        "modified": string,
        "published": string,
        "withdrawn": string,
        "aliases": [ string ],
        "related": [ string ],
        "package": {
                "ecosystem": string,
                "name": string,
                "purl": string,
        },
        "summary": string,
        "details": string,
        "affects": [ {
                "ranges": [ {
                        "type": string,
                        "repo": string,
                        "introduced": string,
                        "fixed": string
                } ],
                "versions": [ string ]
        } ],
        "references": [ {
                "type": string,
                "url": string
        } ],
        "ecosystem_specific": { see spec },
        "database_specific": { see spec },
}

「あるとありがたい情報ですしGoogleがこういうプロジェクトをやるのも理解できるけど、他の企業とかも巻き込んでアライアンス主体の運用になればプロジェクトの継続性についてより安心できるかなと思いました」「CVEはIDの付与と基本情報やリンクの提供にとどまっていますけど、ちゃんと継続的に運用されていますよね」「スキーマも欲しいけど継続的運用の方が欲しい」

参考: CVE - CVE

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

🔗 大きなGitリポジトリの操作


つっつきボイス:「はてブでバズっていた記事です」「シャロークローンはよく使われていて、パーシャルクローンも見た覚えがあったけど、リファレンスリポジトリ周りの話は初めて見た」


同記事より: リファレンスリポジトリの活用例

「こうした個別の手法や概念は昔からGitにありますが、ゲームなどのように画像などのアセットが多い大規模なサービスを扱うようになると、この記事で解説されているようなことが関連してくるでしょうね: 通常の開発だとなかなかそこまではいかないかもしれませんが」「なるほど」「GitHubのような大規模リポジトリサービスを構築する場合に必要になりそうですね」

「このgit fetch --unshallow知らなかった↓: シャロークローンで最初はスキップして取り込まなかったコミットを改めて全部取り直せると記事にもある」「へ〜!」

🔗言語/ツール/OS/CPU

🔗 Windows 365 Cloud PC


つっつきボイス:「Windows界隈がこれで盛り上がっていますね」「Twitterにも書きましたけど、企業のインターネット接続が果たして持つだろうかと思いましたね」「ああ、そこか!」「今はリモートワーク主体なので企業のネット回線にはそれほど負荷はかかっていませんけど、それが終わって全員出社するようになったときに負荷が一気に高まって詰まりそうな予感」「う〜む」

「多くの企業のインターネット接続が1Gbpsぐらいだとすると、社員がそこで普通にネット接続をシェアしている分には、社員全員がNetflixを見るみたいな無茶をしない限り問題ないんですが、Windows 365 Cloud PCのような感じで全員がリモートデスクトップ経由でVDIに常時接続すると回線が詰まるんじゃないかな」「それもそうですよね…」

参考: デスクトップ仮想化(VDI)の基本について学ぶ | IT価値創造塾

「逆に、社員同士での巨大ファイルのやりとりが非常に多い場合とかならWindows 365 Cloud PCの方がファイルのやりとりがクラウド内に閉じ込められる分軽くなるかもしれませんね」「なるほど」「シンクライアントの夢ふたたび感ある」「遅延がつらくなりそうなので、自分は使わないかな〜」

参考: シンクライアント - Wikipedia

🔗 Windowsリモートデスクトップ接続よもやま話

「最近調べたんですが、Windows 10リモートデスクトップのホスト機能はHomeエディションのみ利用不可で、たとえばProエディションとEnterpriseエディションでは同時に1台まで接続可能」「ふむふむ」「同時接続数はレジストリを変更すれば増やせるんですが、デスクトップ画面を表示できるのはあくまで1ユーザーのみで、他の同時接続はその画面をシェアする形になるので、いわゆるUnix系OSのマルチログイン的なことができないんですよ(別ユーザーで接続すると既存ユーザーはログアウトされる)」

「Windows Serverなら同時ログインデスクトップ数が2まで緩和されるので、たとえばAdministratorとoperatorが同時に異なるデスクトップを利用できるそうです」

参考: Windows10 - リモートデスクトップを有効にする方法 | PC設定のカルマ

「個人的にはHomeエディションでリモートデスクトップ接続が許可されて同時ログインデスクトップ数2まで使える方がうれしいかな: それが可能になればリモート接続でゲームができるから」「それはたしかに憧れの機能😆」「リモートPCにゲーム専用のユーザーを作っておけば、手元のPCのフォアグラウンドを落とさずにリモートでゲームを起動できるかなと思ってそのあたりを一生懸命調べたんですけど、どうやらWindows Serverじゃないとできないみたい」「残念」

「その一方で、Azureで動いているWindowsはマルチセッションが使えることが判明しました: このAzure Virtual Desktopがそうです↓」「へ〜!」「Azure環境ならマルチセッションで複数ユーザーが同時接続できるのに、通常のWindowsだと許されてなくて悲しい結論になってしまいました」

参考: Azure Virtual Desktop | リモート デスクトップ | Microsoft Azure

🔗 その他ツール(StatusCode Weeklyより)


つっつきボイス:「rfc.fyiはRFCファイルを爆速検索できるサービスですね」

「slowfil.esは何だろうと思ったら、https://slowfil.es/file?type=js&delay=2500みたいなURLをscriptタグとかで読み込ませると、遅延をかけたりステータスコードを404や500にしたレスポンスを返してくれるということらしい」「なるほど」「ごくたまに、開発中に遅いWebサイトを読み込ませるとどうなるかを手軽に試すときに使う感じかな」


後編は以上です。

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

週刊Railsウォッチ: GitHubによるdisable_joins解説、MemoWise gemでメモ化、RailsのDDoS攻撃対策ほか(20210719前編)

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

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

Ruby Weekly

StatusCode Weekly

statuscode_weekly_banner

The post 週刊Railsウォッチ: ruby-gitでGit操作、最近のruby/debug、stdgems.org、Windows 365 Cloud PCほか(20210720後編) first appeared on TechRacho.

Ruby: メソッド引数のデフォルト値で遊んでみた(翻訳)

$
0
0

概要

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

Ruby: メソッド引数のデフォルト値で遊んでみた(翻訳)

ちょうど昨日、突如ちょっとした小技を思いつきました。メソッドのパラメータがない場合に親切なエラーメッセージを表示する方法です。

# デフォルトのやり方
def read_data(file)
  # ...
end

read_data('1.txt') # => works
read_data
# wrong number of arguments (given 0, expected 1) (ArgumentError)
# ^^ ちょっとそっけない

def read_data(file:)
  # ...
end

read_data
# missing keyword: file (ArgumentError)
# ^^ 何が欲しいのか、少しはヒントが欲しい

# ではこれならどう?
def read_data(file = raise(ArgumentError, '#read_data requires path to .txt file with data in proper format'))
  # ...
end

read_data
# #read_data requires path to .txt file with data in proper format (ArgumentError)
# ^^ いい感じになった

もちろん、引数が1つしかない単純なメソッドでは便利というほどでもありませんが、キーワード引数がいくつもある複雑なAPIならそれなりに使いみちがあるかもしれません。しかし自分にとって嬉しい驚きは、この方法が実にシンプルかつ実際に動いたことでした。

しくみ

引数 = raise(...)という独立した機能がRubyにあるのではなく、以下の2つから自然に導かれます。

  1. 引数のデフォルト値には任意のRuby式を書ける。メソッド呼び出しのたびに、メソッド本体と同じコンテキストで評価される(引数が渡されない場合)。
  2. raiseは特別な構文ではなく、単なるメソッドに過ぎない。他のメソッド呼び出しと同様に「式」なので、引数のデフォルト値として使える。

任意の式?信じていいの?

もちろんです。

その気になれば以下のようにだって書けます(たぶんやらない方がよいと思いますが)。

def read_data(file = begin
                       puts "Using default argument"
                       if Time.now.hour < 12
                        'morning.txt'
                       else
                        'evening.txt'
                       end
                     end)
  puts "Reading #{file}"
end

read_data
# Prints:
#  Using default argument
#  Reading evening.txt

既に述べたように、これが可能なのは(たぶんすべきではありませんが)評価のコンテキストがメソッド本体と同じであり、すべてのデフォルト値が順に評価されるからです。

class ArgsTracker
  attr_reader :args

  def initialize
    @args = []
  end

  def track(
    a: begin; args << :a; 100 end,
    b: begin; puts "a was #{a}"; args << :b end)
  end
end

tracker = ArgsTracker.new

tracker.track
# "a was 100"を出力して[:a, :b]をtrackerに追加する

tracker.track(a: 5)
# "a was 5"を出力し、引数に渡されなかった[:b]だけをtrackerに追加する

tracker.args # => [:a, :b, :b]

クールかつひどいとも言えますが、でもクールですよね。

これに使いみちはあるの?

「デフォルト値が呼び出しのたびに、しかも呼び出されたクラスのコンテキストで算出される」という性質から、シンプルかつ便利な利用法がいくつか編み出せます。中には、おそらく皆さんが既に見たり使ったりしたものもあるでしょう。

# logを呼び出すたびに、at:が渡されなければ算出する
def log(something, at: Time.now) 
  #...
end

# warnのデフォルト出力先デバイスは常に`out`と同じになる
def setup_output(out: $stdout, err: $stderr, warn: out)

  # ...
end

class A
  # デフォルト値を算出するために同じオブジェクトのメソッドを呼び出す
  def process(order: default_order) 
  end

  private

  def default_order
    # オブジェクトのステートに応じて何か複雑な計算を行う
  end
end

さらに高度な利用法

上で紹介した例以外にも、ある程度「常識」を保ちつつ、より複雑なオンザフライ計算方法が考えられそうです。たとえば、デフォルト値の利用状況のトラッキングなどです(レガシーコードのリファクタリングで、デフォルト値が使われているかどうかが不明だが、コードベースを壊すわけにいかないような場合などに便利でしょう)。

def log_default(name, value)
  # or logger.debug
  puts "#{caller.first}: default value for #{name} was invoked from #{caller[2]}"
  value
end

def some_method(factor: 100)
end
# 上を以下のように変える
def some_method(factor: log_default(:factor, 100))
end

# そして実行
some_method
# すると以下がログ出力される
#   ...in `some_method': default value for factor was invoked from `some_other_method'

また、本記事の最初の例(failを使ったもの)をfun(arg: friendly_fail(:arg))のような「非常にフレンドリーな」APIに拡張することも考えられそうです。このAPIでは、定数またはi18n設定から長い説明文を取得し、呼び出しコンテキストで補足して(あるメソッドがcallerに含まれていたら「このメソッドはXXフレームワークから呼び出すべきではない」など)、非常にフレンドリーな例外を発生します。

「今すぐそうすべき」ということではなく、単に「こんなことが可能なのが面白い」「そのうち試せるといいよね」ということです。

楽しみましょう!

関連記事

Ruby 2.5の`yield_self`が想像以上に何だかスゴい件について(翻訳)

The post Ruby: メソッド引数のデフォルト値で遊んでみた(翻訳) first appeared on TechRacho.


Ruby: 配列のモードとメジアンを算出する方法(翻訳)

$
0
0

概要

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

Ruby: 配列のモードとメジアンを算出する方法(翻訳)

Rubyで配列の平均値を求める方法については以前も別記事『Calculate a mean average from a Ruby array』で取り上げました。しかし、普段あまり目にしませんがそれでも有用な平均が他にも2種類あります。

メジアン(中央値)は次のように求める

a = [1, 3, 2, 4, 6, 5, 7, 8]

sorted = a.sort # required
#=> [1, 2, 3, 4, 5, 6, 7, 8]
midpoint = a.length / 2 # integer division
#=> 4
if a.length.even?
  # メジアンは中間の前後にある2つの値の平均
  sorted[midpoint-1, 2].sum / 2.0
else
  sorted[midpoint]
end
#=> 4.5

モード(最頻値)は次のように求める

Array#tallyを使い、それからソートする。

a = [1, 3, 3, 4, 6, 5, 7, 8]

tallied = a.tally
#=> {1=>1, 3=>2, 4=>1, 6=>1, 5=>1, 7=>1, 8=>1}
top_pair = tallied.sort_by { |_,v| v }.last(2)
#=> [[8, 1], [3, 2]]
if top_pair.size == 1
  top_pair[0][0] # 要素が1個だけなら、それがモード
elsif top_pair[0][1] == top_pair[1][1]
  nil # 個数が同じならモードは存在しない
else
  top_pair[1][0]
end
#=> 3

そうする理由

#tallyメソッドはRuby 2.7でEnumerableに追加されました。injectを用いる実装を見たことがあるかもしれませんが、こちらはパフォーマンスが落ちます。

以前平均値の計算について記事を書いたときに、Rubyのネイティブメソッドを使った場合と、自分で実装した場合のパフォーマンスを比較しました。

他には?

これらの計算をメソッドにまとめておくと良いと思います。

この種の計算を頻繁に行う場合や、パフォーマンスが重要な場合には、enumerable-statistics gemをチェックしてみましょう。これはArrayとEnumerableにミックスインされたいくつかの統計的なサマリー算出のネイティブ実装版です。

これらのメソッドはC言語で実装されていて、Rubyでプログラムされたアルゴリズムよりもはるかに高速です。

関連記事

RubyでISO国名コード2文字を絵文字の国旗に変換する(翻訳)

The post Ruby: 配列のモードとメジアンを算出する方法(翻訳) first appeared on TechRacho.

週刊Railsウォッチ: SorbetでRailsアプリの型シグネチャを書く、activerecord-cte gemとanycable-client gem(20210803前編)

$
0
0

こんにちは、hachi8833です。RubyKaigi Takeout 2021のチケット販売が開始されました。

週刊Railsウォッチについて

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

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

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

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

🔗 Active SupportのNilClass#tryNilClass#try!がRuby 2.7以降で遅かった問題を修正


つっつきボイス:「以前#34068でRuby 2.5向けにNilClass#tryNilClass#try!を高速化したけど、Ruby 2.7以降だと逆に遅くなることがわかったので元に戻したんですね」

# activesupport/lib/active_support/core_ext/object/try.rb#L5
module ActiveSupport
  module Tryable #:nodoc:
-   def try(method_name = nil, *args, &block)
-     if method_name.nil? && block_given?
+   def try(*args, &block)
+     if args.empty? && block_given?
        if block.arity == 0
          instance_eval(&block)
        else
          yield self
        end
-     elsif respond_to?(method_name)
-       public_send(method_name, *args, &block)
+     elsif respond_to?(args.first)
+       public_send(*args, &block)
      end
    end
    ruby2_keywords(:try)

-   def try!(method_name = nil, *args, &block)
-     if method_name.nil? && block_given?
+   def try!(*args, &block)
+     if args.empty? && block_given?
        if block.arity == 0
          instance_eval(&block)
        else
          yield self
        end
      else
-       public_send(method_name, *args, &block)
+       public_send(*args, &block)
      end
    end
    ruby2_keywords(:try!)
  end
end
...

class NilClass
...
- def try(_method_name = nil, *)
+ def try(*)
    nil
  end

- def try!(_method_name = nil, *)
+ def try!(*)
    nil
  end
end

🔗 ハッシュ構文でorderしたときのeager_loading?を修正

ハッシュ構文でorderしたときのeager_loading?を修正。

外側テーブルをハッシュ構文でorderしたときにeager_loading?が正しく動くようになった。

Post.includes(:comments).order({ "comments.label": :ASC }).eager_loading?
#=> true

Jacopo Beschi
同PRより


つっつきボイス:「eager_loading?はeager loadingされているかどうかをtrue/falseで返すだけのメソッドなのに、これでエラーになったらびっくりする」「よくぞ見つけた感」「修正を見ると以前はStringしか想定されていなかったんですね↓」

# activerecord/lib/active_record/relation/query_methods.rb#L1550
      def column_references(order_args)
-       references = order_args.grep(String)
+       references = order_args.flat_map do |arg|
+         case arg
+         when String
+           arg
+         when Hash
+           arg.keys
+         end
+       end
        references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact!
        references
      end

🔗 パラレルテストの最小数を設定可能になった


つっつきボイス:「test_parallelization_minimum_number_of_testsコンフィグが追加されたんですね」「デフォルトの最小パラレルテスト数は50か」「環境によって使えるメモリ量も違うので、これはたしかにコンフィグ可能であって欲しい👍

# Changelogより
config.active_support.test_parallelization_minimum_number_of_tests = 100

参考: 3 並列テスト — Rails テスティングガイド - Railsガイド

🔗 データベースごとにスキーマダンプを無効にできるようになった


つっつきボイス:「マルチプルデータベース向けの機能っぽいですね」「schema_dump: falseでデータベースごとにスキーマダンプをオフにできる」「細かいですけど見出しのdumbはdumpのつもりだったんでしょうね」

# 同Changelogより
# config/database.yml
production:
  schema_dump: false

「ところでスキーマダンプをデータベースごとにオフにしたいシチュエーションって何だろう?🤔」「言われてみるとこの機能が欲しい理由がプルリクに書かれていませんね」

追いかけボイス:「もしかするとRailsのmigrationで管理していないDBが接続先にある場合に使いたいのかもしれませんね: 手動で作ったschema dumpを読み込ませたいといったケースがありえるのかも」

🔗 belongs_to関連付けにトラッキング変更メソッドが2つ追加

belongs_to関連付けが直前のsaveで新しいレコードを指しているかどうかと、次のsaveで新しいレコードを指しているかどうかを調べられるようになる。

post.category # => #<Category id: 1, name: "Ruby">

post.category = Category.second   # => #<Category id: 2, name: "Programming">
post.category_changed?            # => true
post.category_previously_changed? # => false

post.save!

post.category_changed?            # => false
post.category_previously_changed? # => true

利用例: Hotwireで、ブログ記事のカテゴリが変更されたら直前のカテゴリから削除するようブロードキャストする。関連付けの直前のターゲットのアクセサが必要だが、これは別途追加可能。
同PRより


つっつきボイス:「belongs_to関連付けに関連付け名_changed?関連付け名_previously_changed?が生えてくるようになったみたい」「今まではbelongs_to関連付けにDirty的な機能がなかったのか」「早くも記事が出ていました↓」「使おうと思ったことはなかったけど、あれば使うかも」

参考: Rails 7 adds change tracking methods for belongs_to associations | Saeloun Blog

🔗Rails

🔗 SorbetでRailsアプリの型シグネチャを書く方法(Ruby Weeklyより)

# 同記事より: Sorbetを使うRubyコードサンプル
# typed: true
class Foo
  extend T::Sig

  sig { params(num: Integer).returns(Integer) }
  def self.double(num)
    num * 2
  end
end

Foo.double('bar') #=> Expected Integer but found String("bar") for argument num
T.reveal_type(Foo.double(10)) #=> Revealed type: Integer

つっつきボイス:「記事にあるTapiocaは、たしかSorbet用のRBI(Ruby interface)を生成するShopifyのgem↓」「sorbet-rails gemなども含めてSorbet関連ツールはひととおり揃ってきたようですね」

Shopify/tapioca - GitHub

chanzuckerberg/sorbet-rails - GitHub

sorbet/sorbet-typed - GitHub

「このあたりを見極めるには、Sorbet環境の整ったRailsプロジェクトで数か月ぐらい開発を体験してから、自分でもスクラッチでいくつかアプリを作ってみたりする必要があるかも」「自分もそんな気がします」「Sorbetで開発が順調に回っているところを実際に体験してみないとなかなかわからなそう」「これができるとどのあたりが嬉しいんでしょうか?」「少数精鋭のプロジェクトだと実感しにくいですが、特に人数が多いプロジェクトがSorbetなどで型チェックがうまく回るようになれば、メンバーの出入りがあったときの安心感が違いますし、他にも嬉しいことがいろいろあると思います」「お〜」

🔗 activerecord-cte(Ruby Weeklyより)

vlado/activerecord-cte - GitHub


つっつきボイス:「activerecord-cte、このgem名だけでおぉっという気持ちになりますね: .withでCTE(Common Table Expression: 共通テーブル式)をActive Recordで書ける」「しかもMySQLとPostgreSQLのどちらでも使えるんですって」「お〜マジですか」

# 同リポジトリより
Post.with(
  posts_with_comments: Post.where("comments_count > ?", 0),
  posts_with_tags: Post.where("tags_count > ?", 0)
)
# 同リポジトリより
posts = Arel::Table.new(:posts)
top_posts = Arel::Table.new(:top_posts)

anchor_term = posts.project(posts[:id]).where(posts[:comments_count].gt(1))
recursive_term = posts.project(posts[:id]).join(top_posts).on(posts[:id].eq(top_posts[:id]))

Post.with(:recursive, top_posts: anchor_term.union(recursive_term)).from("top_posts AS posts")
# WITH RECURSIVE "popular_posts" AS (
#   SELECT "posts"."id" FROM "posts" WHERE "posts"."comments_count" > 0 UNION SELECT "posts"."id" FROM "posts" INNER JOIN "popular_posts" ON "posts"."id" = "popular_posts"."id" ) SELECT "posts".* FROM popular_posts AS posts
-- 同リポジトリより
WITH posts_with_comments AS (
  SELECT * FROM posts WHERE (comments_count > 0)
), posts_with_tags AS (
  SELECT * FROM posts WHERE (tags_count > 0)
)
SELECT * FROM posts

「recursive CTEもこうやって書けるのね↓」

# 同リポジトリより: recursive CTE
posts = Arel::Table.new(:posts)
top_posts = Arel::Table.new(:top_posts)

anchor_term = posts.project(posts[:id]).where(posts[:comments_count].gt(1))
recursive_term = posts.project(posts[:id]).join(top_posts).on(posts[:id].eq(top_posts[:id]))

Post.with(:recursive, top_posts: anchor_term.union(recursive_term)).from("top_posts AS posts")
# WITH RECURSIVE "popular_posts" AS (
#   SELECT "posts"."id" FROM "posts" WHERE "posts"."comments_count" > 0 UNION SELECT "posts"."id" FROM "posts" INNER JOIN "popular_posts" ON "posts"."id" = "popular_posts"."id" ) SELECT "posts".* FROM popular_posts AS posts

参考: Understanding SQL Server Recursive CTE By Practical Examples

「複雑なクエリでCTEを使うのは考えものですが、ちょっとしたサブクエリなどでWITHを少しだけ使いたいときならこのgemを使うとよさそう👍」「おぉ」「CTEは複雑になったときにActive Recordが解釈できるかどうかが問題なんですが、CTE自体は通常のSQLでも多用される強力な機能なので、ちゃんと動くならActive RecordでもシンプルなCTEが標準でサポートされてもいいなという気持ちです」「ちゃんと動くならですね😆

🔗 AnyCable用JavaScriptクライアント


同記事より

anycable/anycable-client - GitHub


つっつきボイス:「Evil MartiansのVladimirさんがAnyCable↓用のJSクライアントも作ったそうです」「AnyCableはRailsのAction Cableの高速版的なgemでしたね」「元記事によると、このanycable-clientはAction CableとJSONプロトコルレベルで完全互換とある」「TypeScriptでクライアントを書けるのはよさそう👍

AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)

🔗 SeleniumとCupriteとPlaywright


つっつきボイス:「ruby-jp Slackで見かけた上の記事にTechRachoの翻訳記事が引用されていたのでピックアップしました」「あ〜、こういう問題は実際にやってみないとわからないヤツでしょうね」「最初からCupriteで書いていたらもっとスムーズだったのかな?」「既に動いているE2Eテストはさんざん試行錯誤して書かれることが多そうなので、差し替えたときにデフォルトの挙動が細かく違ったりするのかも」

「これで思い出しましたけど、ちょっと前の銀座Railsの発表で、playwright-ruby-clientというgemを作った方がSeleniumやCupriteなどのWebドライバ周りも含めて解説していましたね」「お、後で探してみます」

参考: Playwright

以下のスライドとリポジトリです。なおplaywrightは「劇作家」という意味だそうです。

YusukeIwaki/playwright-ruby-client - GitHub

🔗 その他Rails


つっつきボイス:「RubyMineの新バージョンが出た」「今回もRBS周りが改良されているようですね」「手元のRubyMineは2021.1.3でした」

「ところで、最近自分のWindows環境でDocker for Windowsをアップデートしたらなぜか急に速くなったんですよ」「へ〜!」「まだ調べたわけではありませんがストレージアクセスが速くなった感じがする: これならボリュームマウントしてもいいかなと思ったけど環境構築で1日ぐらいつぶれそうなので、そのうちに試してみようかな」

参考: Windows に Docker Desktop をインストール — Docker-docs-ja 19.03 ドキュメント


前編は以上です。

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

週刊Railsウォッチ: ruby-gitでGit操作、最近のruby/debug、stdgems.org、Windows 365 Cloud PCほか(20210720後編)

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

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

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: SorbetでRailsアプリの型シグネチャを書く、activerecord-cte gemとanycable-client gem(20210803前編) first appeared on TechRacho.

週刊Railsウォッチ: Rubyの可変長アロケーションプロジェクト、サーキットブレーカーgem、EC2-Classicが終了へほか(20210804後編)

$
0
0

こんにちは、hachi8833です。

週刊Railsウォッチについて

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

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

🔗Ruby

🔗 Rubyの可変長アロケーションプロジェクトがフェーズIIに


#4680より


つっつきボイス:「Rubyのアロケーターをリニューアルして高速化するプロジェクトが進行中なんですね」「スゴい」「これはRubyKaigiに向いていそうなお題」「そのうち解説記事が出るといいですね」

サマリー:

  • 現在はアロケーション高速化のために、スロットサイズの異なるページをプールで使っている。要求されたサイズを収納できる最小のスロットサイズでオブジェクトが割り当てられる。これによって、フリーリストベースのアロケーションアルゴリズムに立ち返り、アロケーションのパフォーマンスが大幅に向上した。
  • サイズプールの成長パターンに合わせてヒープの成長アルゴリズムが変更された。ヒープのアロケーションと解放を繰り返すと「成長ヒープ」とみなされ、サイズの安定したヒープと異なる扱いになる。
    同issueより

🔗 time_up gemでRubyコードを計測する(Ruby Weeklyより)

testdouble/time_up - GitHub


つっつきボイス:「time_upというgemを作って測定してみた記事だそうです」

「測定したいコードにTimeUp.startでタイマー名を:processing:queryingのように複数指定できる」

# 同記事より
transactions = TimeUp.start(:querying) {
  Transaction.where(user: @user)
}

process_timer = TimeUp.start(:processing)
categorized_transactions = group_by_category(transactions)
monthly_spending = arrange_by_month(categorized_transactions)
process_timer.stop

payments = TimeUp.start(:querying) {
  PaymentFinder.find(@user, monthly_spending)
}

render json: TimeUp.start(:presenting) {
  present_summary(
    spending: monthly_spending,
    payments: payments
  )
}

「するとTimeUp.timerでタイマー名ごとのカウントや最大最小平均が取れるようになる」

# 同記事より
> TimeUp.elapsed(:presenting)
=> 1.0050820000469685
> query_timer = TimeUp.timer(:querying)
> query_timer.count
=> 2
> query_timer.timings
=> [0.5051469999598339, 0.8050760000478476]
> query_timer.min
=> 0.5051469999598339
> query_timer.max
=> 0.8050760000478476
> query_timer.mean
=> 0.6551115000038408

TimeUp.all_statsすると全タイマー名の測定結果を取れる」

# 同記事より
> TimeUp.total_elapsed
=> 2.822688000043854
> TimeUp.all_stats
=> {
  :querying=>{:elapsed=>1.3102230000076815, :count=>2, :min=>0.5051469999598339, :max=>0.8050760000478476, :mean=>0.6551115000038408},
  :processing=>{:elapsed=>0.5073829999892041, :count=>1, :min=>0.5073829999892041, :max=>0.5073829999892041, :mean=>0.5073829999892041},
  :presenting=>{:elapsed=>1.0050820000469685, :count=>1, :min=>1.0050820000469685, :max=>1.0050820000469685, :mean=>1.0050820000469685}
}

TimeUp.print_detailed_summaryで整形できる」

# 同記事より
> TimeUp.print_detailed_summary

=================================================================================
   Name     | Elapsed | Count |   Min   |   Max   |  Mean   |  Median  |  95th %
---------------------------------------------------------------------------------
:querying   | 1.31028 | 2     | 0.50520 | 0.80507 | 0.65514 | 0.65514  | 0.78221
:processing | 0.50314 | 1     | 0.50314 | 0.50314 | 0.50314 | 0.50314  | 0.50314
:presenting | 1.00508 | 1     | 1.00508 | 1.00508 | 1.00508 | 1.00508  | 1.00508

「測定ポイントごとのデータを一括で管理しつつ簡単な集計が行えるようですね: 記事タイトルにはベンチマークとあるけど計測という方が近いかな」「なるほど」「通常だと自分でグローバルな場所に値を保存することになるでしょうけど、time_up gemを使うと一括で管理できる: 知っていたら使うかも👍

🔗 メモリ上のRuby文字列を見る


つっつきボイス:「お〜、 /procファイルシステムからプロセスの生ユーザーメモリ空間を解析して、その中からRStringとして使われている部分を参照して復元したんですね」「これは面白い❤」「RStringなら比較的やりやすいでしょうね: これがRObjectだと難易度が上がりそう」「こういう作業は理解が深まりますね: Ruby内部でどのようにメモリをマッピングして使っているかを学ぶのによさそう👍

🔗 サーキットブレーカーgem 2種類

yammer/circuitbox - GitHub

wsargent/circuit_breaker - GitHub


つっつきボイス:「Jeremy Evansさんの近著『Polished Ruby Programming』↓を読んでいて、サーキットブレーカーはgemでやる方が楽という記述があったので探してみました」

「今さらですけどサーキットブレーカーって何でしょう?」「サーキットブレーカーはKubernetesやマイクロサービスなどでサービス同士がチェインする部分で使われるパターンですね」「お〜」「以下のIstioドキュメントに簡単な説明があった↓」

参考: Istio / Circuit Breaking

サーキットブレーキングは、レジリエントな(回復力のある)マイクロサービスアプリケーション構築で重要なパターンです。サーキットブレーキングを使うと、障害やレイテンシの急増、その他のネットワークの特異性による望ましくない影響を制限するアプリケーションを書けます。
istio.ioより

「たとえば、あるサービスがリクエストを返すために別のサービスに問い合わせる必要があってみたいなチェインが繰り返されると、途中で1箇所でも障害が起きたときにさんざん待たされた末に全部コケたりしますけど、そういうのをうまく扱うためにサーキットブレーカーを挟む感じですか?」「だいたいそんな感じ: タイムアウトなども細かく管理できます」「お〜」「サーキットブレーカーはKubernetesやIstioのようなサービスには必ずあって、マイクロサービスのコントロールプレーンの話題にもよく登場します」「なるほど」

参考: コントロールプレーン | TED用語集 | 東京エレクトロンデバイス

参考: 分散サービス環境へのCircuit Breakerの適用 - LINE ENGINEERING



engineering.linecorp.comより

「サーキットブレーカーでは、たとえばチェインしているサービスの手前の段の処理が軽くて奥の段の処理が重いとすると、軽い手前のサービスがリクエストをたくさん投げると奥の処理が詰まってしまうので、奥の段の処理が詰まっていることをサーキットブレーカーが何らかの形で検知すると、手前の段で早期に失敗させる、というような処理を行います」「なるほどそういう感じですか」

「でgemの方を見てみると、Circuitboxは文字どおり失敗時に転送している↓」

Circuitbox.circuit(:your_service, exceptions: [Net::ReadTimeout]) do
  Net::HTTP.get URI('http://example.com/api/messages')
end

「以下のコンフィグでタイムアウトやスリープやスレッショルドなどを指定できる↓」

# github.com/yammer/circuitboxより
class ExampleServiceClient
  def circuit
    Circuitbox.circuit(:your_service, {
      # exceptions circuitbox tracks for counting failures (required)
      exceptions:       [YourCustomException],

      # seconds the circuit stays open once it has passed the error threshold
      sleep_window:     300,

      # length of interval (in seconds) over which it calculates the error rate
      time_window:      60,

      # number of requests within `time_window` seconds before it calculates error rates (checked on failures)
      volume_threshold: 10,

      # the store you want to use to save the circuit state so it can be
      # tracked, this needs to be Moneta compatible, and support increment
      # this overrides what is set in the global configuration
      cache: Circuitbox::MemoryStore.new,

      # exceeding this rate will open the circuit (checked on failures)
      error_threshold:  50,

      # Logger to use
      # This overrides what is set in the global configuration
      logger: Logger.new(STDOUT),

      # Customized notifier
      # overrides the default
      # this overrides what is set in the global configuration
      notifier: Notifier.new
    })
  end
end

「もうひとつのcircuit_breaker gemの方はincludeして使う感じで、できることは上と似ているかな」「なるほど」

# github.com/wsargent/circuit_breakerより
require 'circuit_breaker'
class TestService
  include CircuitBreaker

  def call_remote_service() ...

  circuit_method :call_remote_service

  # Optional
  circuit_handler do |handler|
    handler.logger = Logger.new(STDOUT)
    handler.failure_threshold = 5
    handler.failure_timeout = 5
    handler.invocation_timeout = 10
    handler.excluded_exceptions = [NotConsideredFailureException]
  end

  # Optional
  circuit_handler_class MyCustomCircuitHandler
end

「サーキットブレーカーをちゃんと使ったことはまだありませんが、サービスが大規模に連携するようになるとこういうgemが欲しくなるのかも」

🔗 サーキットブレーカー自作は避けたい

「そういえばJeremy Evansさんが『サーキットブレーカーを自分で作るとつらいよ』みたいなことを書いていました」「はい、この種の機能を自作するのは避けた方がよいと思います」

「たとえば自分の実装で排他制御周りをしくじってリソースが永遠に開放されなくなってしまうバグを踏むと、サーキットブレーカーが誤動作してすべてのリクエストが止まってしまったりします」「ありゃ〜それはつらい」「かといって逆に排他制御の設定を厳しくしすぎると、今度はサーキットブレーカーがボトルネックになってしまったりします」「う〜む」「どちらもサーキットブレーカー的なものを自分でちょろっと作ったときにありがちなバグなので、自前実装は避けたい」

参考: 排他制御 - Wikipedia


Azureのドキュメントによると、以下の『Release It!』という書籍でサーキットブレーカーパターンが広まったそうです。

🔗 その他Ruby


つっつきボイス:「GSoCって何だろうと思ったらGoogle Summer of Codeのようですね」「オープンソースソフトウェアをテーマに毎年開催されている学生向けのイベント」「そういえばGoogle Summer of CodeでRubyのプロジェクトも見たことあります↓」

参考: Ruby | Google Summer of Code
参考: Google Summer of Code - Wikipedia

🔗DB

🔗 rails-pg-extras(Ruby Weeklyより)

pawurb/rails-pg-extras - GitHub


つっつきボイス:「RailsのPostgreSQL関連機能を拡張するのかな」「heroku-pg-extrasをRailsに移植したgemのようです↓」「Herokuと無関係に使えそうですね」

heroku/heroku-pg-extras - GitHub

「何ができるんでしょう?」「MySQLで言うINFORMATION_SCHEMA的な情報をRubyオブジェクトとして取得できるみたい↓」「お〜、Railsコンソールでも取れるのかな?」「サンプル出力を見るとRubyのコードからメトリクス情報がオブジェクトとして取得できているようなので、できるということでしょうね」

# 同リポジトリより
RailsPGExtras.index_cache_hit

$ rake pg_extras:index_cache_hit

| name                  | buffer_hits | block_reads | total_read | ratio             |
+-----------------------+-------------+-------------+------------+-------------------+
| teams                 | 187665      | 109         | 187774     | 0.999419514948821 |
| subscriptions         | 5160        | 6           | 5166       | 0.99883855981417  |
| plans                 | 5718        | 9           | 5727       | 0.998428496595076 |
(truncated results for brevity)

「この種のデータベースメトリクスをRubyのコードから参照したいと思ったことはなかったけど、こういうふうにRubyのコンテキストで使えるなら、たとえばメトリクスをrakeタスクで処理したりRailsコンソールでデバッグしながら見たりできそう」「おぉ」「他にも、最近だとWebアプリのステータスを返す専用のURLを用意して各種情報を見られるようにすることがありますけど、そこにこういう情報を追加してメトリクスを継続的に収集できるようにするといった使い方も考えられますね👍

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

🔗 EC2-Classicが終了に向かう


つっつきボイス:「お〜我のツイート、morimorihogeさんのツイートを見て反応したヤツです」「EC2-Classicを使っていた人はただでさえ相当少数派だと思うので、EC2-Classic終了の影響をすぐに理解できる人が果たしてどれだけいるのかなと思ったりしました」「自分はEC2-Classicから移行した経験あるのでわかりますけど、EC2-Classicって随分昔の話ですよね」「そうそう」

「自分もツイートしましたけど、EC2-Classicが終わるとPVインスタンスが動かなくなるのが問題」「あ〜わかります」「PVがなくなるとHVMだけになるので、PV->HVMの移行はそれなりに大変そう」

参考: インスタンスタイプマトリックス | AWS

「まだPVで動いていたものが世の中にあったとは」「AWSはそう簡単に古いサービスを終了しないのがよい点ですが、長期間サポートにも限度はあるので終了は仕方がないでしょうね」「これはもう終了やむなし」

「それにPVインスタンスだった頃のAWSの常識的な運用は、現代のAWS運用から見るとかなりかけ離れているんですよ」「たしかに」「たとえば当時はRDSのようなサービスが高い割にメリットがあまり感じられなくて、コストを考えるとRDSを使わずに自分でEC2にMySQLサーバーを立てることが多かった」「そうそう、当時はよくそれでやってました」「今は?」「今はRDSを使うメリットが圧倒的に大きくなりましたね」

参考: Amazon RDS(マネージドリレーショナルデータベース)| AWS

「現代のAWSの常識で組まれたシステムなら比較的移行しやすいんですが、EC2-Classicという時点で昔の常識でシステムが組まれている可能性が高いので、PVからHVMに移行したりDBを移行したりするのは大変そうだなと思いました」

「今もEC2-Classic使ってるサービスってどのぐらいあるんだろう?」「HVMインスタンスへの移行が終わってさえいればそれほど大変ではないんですが、うんと古くからあるサービスだと移行する機会のないままEC2-Classicを使い続けているところもあるでしょうね」「つらそう…」「PV->HVM移行ではたとえばプライマリパーティションが変わるのでMBR(Master Boot Record)も変わるんですよ」「え〜!」「だからddコマンドでコピーして終わりというわけにいかない」

参考: マスターブートレコード - Wikipedia
参考: dd (UNIX) - Wikipedia

「調べればPV->HVM移行のノウハウは見つかりますけど、何しろ古いのでやってみないとわからない部分も多いし、そもそもEC2-Classicをやっていた人数が少ない」「EC2-Classic自体を知らない人も多そうですよね」「Classic-ELBのことだと思う人がいたりして」「ありそう」「Classic-ELBは今も問題なく使えますけど、EC2-Classicの古さはレベルが違う」

参考: What is a Classic Load Balancer? - Elastic Load Balancing

「EC2-Classic終了は大きいニュースなんですね」「古くからやっているところにとってはかなり大きいニュースだと思います」「リリース記事を見ると2021/10/30には動いていないEC2-Classicのインスタンスが起動できなくなって、翌年の2022/08/15にはすべて終了するとありますね」「割とすぐなのか〜」「AWSの終了スケジュールは少なくともまず前倒しになることはないと思います: 逆に何やかやで延期する可能性の方があるかも😆

「EC2-Classicがサービスインしたのが2006年か: 当時はCore 2ぐらいの時代だから、サービス止めたいのもわかる」「あの当時はAWSコンソールもなければシンガポールリージョンもなかった」「USのリージョンだけだったんですね」「自分がAWSを使い始めたのはEIPが始まった頃だから2008年ぐらいかな」

参考: Intel Core 2 - Wikipedia
参考: Elastic IP アドレス - Amazon Elastic Compute Cloud

🔗言語/ツール/OS/CPU

🔗 PHPチュートリアルのSQLインジェクション


つっつきボイス:「ググって見つかったPHPチュートリアルで30件中16件にSQLインジェクションが見つかったという記事でした」「mysqli_queryを直接使っている時点でヤバい」「これ直接使わないヤツですよね」

// 同記事より
// Don't do this!
mysqli_query("SELECT * FROM user WHERE id = '" . $_POST["user'] . "'");

「考えてみれば、RailsはいわゆるCGIも経験せずにいきなりWeb開発ができるわけで、むしろ特異なのかも」「まあたしかに」「他の言語だと、昔ながらのCGIから始めてHTML埋め込みでWebページにhello worldや日付を表示するところから学んだものですが、Railsはrails newから始まりますから」「まるで違いますよね」

参考: Common Gateway Interface - Wikipedia

「Railsだけやってきた人から見るとわかりにくいかもしれませんが、昔は他の言語のチュートリアルの一環としてこういうふうに生SQLを動かしてみるという教え方が普通でしたね」「そうそう」「インターネット昔話になってきた」

「脆弱性うんぬんを別にすれば、チュートリアルなどでこういうふうに1行1行何が行われているかを確かめながら学ぶことは初学者にとって有用な面もあると思うんですよ」「わかります」「業務で使わなければOK」

「逆にRailsは初学者にとって便利すぎて裏で何が行われているのかさっぱりわからないですよね」「ブレークポイント置いても追いきれない😆」「Web開発をRailsから始めた人の中にはCGI時代のWebアプリの仕組みを知らずに普通に開発している人もいると思うので、昔と今とどちらの学び方がいいのか自分には断定しきれないかなという気持ちです」


後編は以上です。

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

週刊Railsウォッチ: SorbetでRailsアプリの型シグネチャを書く、activerecord-cte gemとanycable-client gem(20210803前編)

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

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

Ruby Weekly

Publickey

publickey_banner_captured

The post 週刊Railsウォッチ: Rubyの可変長アロケーションプロジェクト、サーキットブレーカーgem、EC2-Classicが終了へほか(20210804後編) first appeared on TechRacho.

Rubyの文字列連結に「#+」ではなく式展開「#{}」を使うべき理由

$
0
0

更新情報

  • 初版: 2016/09/05
  • 更新: 2021/07/29

こんにちは、hachi8833です。

rubystrconcat

Slackでmorimorihogeさんが投げてくれたアドバイスのメモを残します。上のスクショはGitLabのmerge requestにmorimorihogeさんがコメントしたものです。

Rubyでの文字列連結メソッド#+#{}は同じではない!

Rubyの文字列連結メソッド#+#{}は、通常それぞれ次のように使われます。

str2 = "おいしい"

str1 = str2 + "店"  #=> 「#+」は変数同士や変数と文字列リテラルを連結する
str1 = "#{str2}店"  #=> 「#{}」は" "の中で変数内の文字列を取り出す(式展開)
                    #上のどちらも出力は「おいしい店」になる

後者の式展開(interpolation)は変数展開や文字列補間などと呼ばれることもあり、RubyやPythonなどのモダンな言語で文字列内に変数の値を置くときに使われる定番の手法です(追伸: JavaScriptではtemplate literalと呼ばれています)。

参考: 文字列補間 - Wikipedia

Rubyの文字列連結は上の例のどちらの方法を使っても同じになりそうな気がしますが、実は同じになるとは限りません

次に挙げる理由から、Rubyでは基本的に後者の式展開「#{}」を使うことをおすすめします。

理由1: 式展開なら自動で#to_sが効く

式展開#{}の変数は、出力時に自動的に#to_sが行われます。#to_sは文字列でない値を文字列に変換するメソッドであり、しかも値がnilの場合には空文字""を出力してくれるので、余分なエラーが発生しません。

理由2: 式展開の#to_sはほとんどのオブジェクトで使える

#to_sメソッドはObjectクラスにあります。言うまでもなくRubyではすべてのものがオブジェクトなので、#to_sメソッドはほぼどんなオブジェクトに対してもエラーなしで確実に実行できます。

「ほぼ」と書いたのは、Ruby 1.9以降の継承リストのルートにあるのはObjectクラスではなくBasicObjectだからです(クラスの継承リスト: Object < Kernel < BasicObject)。BasicObjectクラスにはほとんどメソッドがなく、さすがに#to_sはエラーになります(使う人はいないと思いますが)。

理由3:#+で連結すると順序によって結果が異なることがある

文字列連結であればどんな順序であろうと挙動が変わらないように思えますが、変数と文字列を#+で連結すると、変数の内容によって以下のような問題が生じる可能性があります

変数がもしFixnumだったら:

[1] pry(main)> 1 + 'hoge'
TypeError: String can't be coerced into Fixnum from (pry):1:in `+'
[2] pry(main)> 'hoge' + 1
TypeError: no implicit conversion of Fixnum into String from (pry):2:in `+'

変数がもしnilだったら:

[3] pry(main)> 'hoge' + nil
TypeError: no implicit conversion of nil into String from (pry):3:in `+'
[4] pry(main)> nil + 'hoge'
NoMethodError: undefined method `+' for nil:NilClass from (pry):4:in `<main>'

Rubyの#+で文字列と変数を結合すると、当初は正常に動作したとしても後で思わぬトラップを踏むかもしれません。

#+による文字列連結を使うなら、少なくとも変数を先行させないようにし、変数に文字列以外の値が使われないよう注意する必要があります。

しかし式展開#{}ならそうした気遣いはまったく不要になります。実際、前述の#+の問題はすべて式展開#{}で正常に動作します。

まとめ

変数を含む文字列を出力する場合は、基本的に+よりも式展開#{}を使うことをおすすめします。

元レビューの補足(2016/09/06 13時ごろ追記)

元のMRコメントを書いたmorimorihogeです。
当該コメントは全体としては以下な感じでもうちょっと詳しく書いてました。

Screenshot 2016-09-06 13.23.05_2

↑スクショにもある通り、このコメントをした事例においては変数オブジェクトがStringであることが保証されていたので「直す必要はない」としたわけですね。
そこそこRubyを書いていると式展開なんかは当たり前のことではあるのですが、新人さんやRuby自体にまだ慣れていないメンバもいる中ではあえて常識と思っていることもしっかり解説するのが大事かな、と思って普段レビューしています。

元レビューも単に「式展開を使って下さい」でも良かったのですが、なぜ式展開を使うのか、どうして式展開がbetterなのかを知っておくことで今後思わぬ地雷を踏まなくなるといいなと思った次第です。Railsはビューの中でエラーが出るとデフォルトでは問答無用でアプリケーションエラーページが出てしまいますので、#try#digなど、多少想定外のデータが来ても良い書き方をするクセを付けておくのが良いかと思います。

関連記事

Rubyの式展開(string interpolation)についてまとめ: `#{}`、`%`、Railsの`?`

The post Rubyの文字列連結に「#+」ではなく式展開「#{}」を使うべき理由 first appeared on TechRacho.

週刊Railsウォッチ: システムテスト用headlessドライバにCupriteが追加、rails-mini-profiler、Jeremy Evansインタビューほか(20210810)

$
0
0

こんにちは、hachi8833です。今回は短縮版でお送りします。

週刊Railsウォッチについて

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

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

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

前回の公式情報の続きです。

🔗 redirect_toallow_other_hostオプションが追加

許可されていないオープンリダイレクトでエラーをraiseする。
redirect_toallow_other_hostオプションを追加。ActionController::Base.raise_on_open_redirects = trueでこの振舞いをオプトインできる。
Gannon McGibbon
同Changelogより


つっつきボイス:「許可されていないオープンリダイレクトをraise_on_open_redirectsコンフィグでエラーをraiseできるようになった」「ここで例外を出してくれるのは優しいですね」「基本的にはraise_on_open_redirects = trueにしたうえで、オープンリダイレクトを許す場合にのみredirect_toreirect_back_or_toの引数にallow_other_host: trueを追加する感じで使うんでしょうね」

「URL直書きでよそのサイトにリダイレクトするのは基本的にはイレギュラーですが、その必要が生じることもありうるので、許可されてないサイトへのリダイレクトをraiseするかどうかを選べるのはいい👍

参考: オープンリダイレクトとは(Open-Redirect) | 【情報処理 用語集】

後でガイドの更新を見ると、現行はデフォルトがfalseで、7.0になったらデフォルトでtrueにするようです。

# guides/source/configuring.md#1098
#### For '7.0', defaults from previous versions below and:

+- `config.action_controller.raise_on_open_redirects`: `true`
...

#### Baseline defaults:

+- `config.action_controller.default_protect_from_forgery`: `false`

🔗 システムテスト用に登録可能なheadlessドライバリストのpoltergeistとcapybara-webkitが非推奨化されてcupriteが追加

Poltergeistとcapybara-webkitは既にメンテされなくなっている。
* https://github.com/teampoltergeist/poltergeist
* https://github.com/thoughtbot/capybara-webkit

たまたま以下の利用法を見かけた。

https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/system_testing/driver.rb

近年はもっぱらSeleniumとHeadless Chromeが使われており、poltergeistやcapybara-webkitの利用は推奨されない。
CupriteはPoltergeistに代わるよい選択肢(参考)。
ガイドのPoltergeist関連部分はCupriteに置き換えるのがよいだろう。
同PRより


つっつきボイス:「お〜poltergeistとcapybara-webkitがついに非推奨化」「poltergeistはさすがに非推奨化でもいいかな」「今後はCupriteですか」「poltergeistとcapybara-webkitもしばらくは使えますね」「プルリクメッセージで引用されているEvil MartiansのCuprite記事は翻訳版が以下にあります↓」

2020年のRailsでブラウザテストを「正しく」行う方法(翻訳)

「お、このプルリクを出したYusukeIwakiさんは、まさにこの間の銀座Rails#34でPlaywrightやCupriteなどのドライバについて発表していた人ですね↓(ウォッチ20210803)」「あ、ホントだ」

参考: なんだかんだで1年半くらいRuby向けにブラウザの自動操作ライブラリを作っている - YusukeIwakiのブログ

🔗 Float::NaNBigDecimal::NaNのダーティーチェックを修正

RubyのFloat::NaNBigDecimal::NaN特殊な値であり、==で比較できない。
Marcelo Lauxen
同PRより


つっつきボイス:「Float::NaNBigDecimal::NaNがゼロ除算などで生成されたときの問題が修正されたんですね」「#41663のバグを踏んで見つけたのか」

      def test_changed?
        type = Decimal.new
        assert type.changed?(0.0, 0, "wibble")
        assert type.changed?(5.0, 0, "wibble")
        assert_not type.changed?(5.0, 5.0, "5.0wibble")
        assert_not type.changed?(5.0, 5.0, "5.0")
        assert_not type.changed?(-5.0, -5.0, "-5.0")
        assert_not type.changed?(5.0, 5.0, "0.5e+1")
+       assert_not type.changed?(BigDecimal("0.0") / 0, BigDecimal("0.0") / 0, BigDecimal("0.0") / 0)
+       assert type.changed?(BigDecimal("0.0") / 0, 0.0 / 0.0, 0.0 / 0.0)
      end

🔗 ActiveModel::Errors#inspectのメッセージをスリム化

# 同PRより: 従来
#<ActiveModel::Errors:0x00007ff68cda24f8 @base=#<Foo id: 6, created_at: "2021-07-09 04:28:48.056662000 +0000",
updated_at: "2021-07-09 04:28:48.168576000 +0000", email: "user@example.com", name: "Foo Bar", company: "Foo",
activated_at: "2021-07-09 04:28:39.039853000 +0000">, @errors=[]>

# 修正後: Errorsが空の場合
#<ActiveModel::Errors []>

# 修正後: Errorsが空でない場合
#<ActiveModel::Errors [#<ActiveModel::Error attribute=base, type=invalid, options={}>]>

つっつきボイス:「ActiveModel::Errors#inspect@errorsがある場合はその配列を出力して、@baseを出力しないように変更したのか」「通常ActiveModel::Errorsのオブジェクトをinspectするときは@errorsをチェックしたいはずなので、inspectではActiveModel::Errors@errorsに限定して配列を出力したいということかなと思いました」

🔗 Middleware#removeを追加

削除するミドルウェアが存在しない場合にraiseするMiddleware#removeを追加。
Middleware#removeの動作はMiddleware#deleteと同様だが、ミドルウェアが存在しない場合はエラーをraiseする。
Alex Ghiculescu, Petrik de Heus
同PRより


つっつきボイス:「以前は存在しないミドルウェアを削除しようとしても何も起きなかったのをエラーをraiseするMiddleware#removeが追加された」「割とシンプルな改修ですね」

「ところでMiddleware#removeは最近見たような気がする🤔」「あ、以前の#42655↓はMiddleware#deleteを修正して存在しないミドルウェアでエラーをraiseするようにしたけど破壊的変更の影響範囲が大きかったので、#42821ではMiddleware#deleteを元に戻してからMiddleware#removeを追加してこちらでエラーをraiseするようにしたのか」

🔗Rails

🔗 rails-mini-profiler(Ruby Weeklyより)

hschne/rails-mini-profiler - GitHub


つっつきボイス:「rack-mini-profilerはよく使われていますけど、それとは違うんでしょうか?」「READMEによるとrack-mini-profilerやScount APMなどのAPMツールに強くインスパイアされてこのrails-mini-profilerを作ったそうです」

MiniProfiler/rack-mini-profiler - GitHub

「お〜、なかなかいい感じにビジュアライズしている」「どのAPIが何秒かかるみたいなのが一覧できるんですね」


rack-mini-profiler READMEより

# rack-mini-profiler READMEより
# routes.rb
Rails.application.routes.draw do
  ...

  mount RailsMiniProfiler::Engine => '/rails_mini_profiler'
end

「APIのみのRailsアプリでも使えるみたい」「さすがにproduction環境は想定してないようです」「でしょうね」「ローカル開発で便利そうだし、プロファイリングするリクエスト数を絞り込めるみたいなのでproductionでも使いようがあるかも: とりあえず入れてみてもよさそう👍」「後で入れてみようっと」「ハリネズミがかわいい❤

# rack-mini-profiler READMEより
RailsMiniProfiler.configure do |config|
  config.enabled = proc { |env| env.headers['RMP_ENABLED'].present? }
end

🔗 RSpecでbullet gemを無視する方法(Ruby Weeklyより)

flyerhzm/bullet - GitHub


つっつきボイス:「この記事は、bullet gemに限らず、RSpecのテストで特定の機能を動かしたくないときに使える簡単なベストプラクティスという感じかな」「そうそう、こうやってテストヘルパーで無効にしたりしますね」

# 同記事より
config.before(:each, bullet: :skip) do
  Bullet.enable = false
end

config.after(:each, bullet: :skip) do
  Bullet.enable = true
end

🔗Ruby

🔗 Rubyの新しいデバッガを紹介

ruby/debug - GitHub


つっつきボイス:「先週紹介しそびれましたが、最近ruby/debugで頻繁にコントリビュートしているst0012さんの記事です」「お〜スタンくんが中の人になってる」「ruby/debugが触れるレベルになってきたところなのでこういう記事はありがたいですね👍

🔗 Ruby 3でHTTPサーバーをスクラッチから作った(Ruby Weeklyより)


つっつきボイス:「RubyのFiberやRactorなども使ってHTTPサーバーをスクラッチから書く記事か」「久々のマルチスレッドサーバー系記事ですね」「こういうシンプルなコード例を手元で動かして試せるのがよさそう」

# 同記事より(コメントは省略)
def start
  queue = Ractor.new do
    loop do
      conn = Ractor.receive
      Ractor.yield(conn, move: true)
    end
  end

  WORKERS_COUNT.times.map do
    Ractor.new(queue, self) do |queue, server|
      loop do
        # this method blocks until the queue yields a connection
        conn = queue.take
        request = RequestParser.call(conn)
        status, headers, body = server.app.call(request)
        HttpResponder.call(conn, status, headers, body)
      rescue => e
        puts e.message
      ensure
        conn&.close
      end
    end
  end

  listener = Ractor.new(queue) do |queue|
    socket = TCPServer.new(HOST, PORT)
    socket.listen(SOCKET_READ_BACKLOG)
    loop do
      conn, _addr_info = socket.accept
      queue.send(conn, move: true)
    end
  end
  Ractor.select(listener)
end

「これで思い出したんですが、最近のPHP 8.1にもついにfiberが入ったという記事を見かけました」「お、このPHPのRFCですね↓」「fiberってRuby独自の概念かと思ってたけどコンピュータサイエンスの用語だったのか〜」

参考: PHP: rfc:fibers
参考: ファイバー (コンピュータ) - Wikipedia

🔗 Jeremy Evansインタビュー


つっつきボイス:「少し前の記事ですが、『Polished Ruby Programming』の著者であるJeremy Evansさんのインタビューです」「お顔初めて見ました」「Jeremy EvansさんはSequelのメンテナーやっているのが有名かも」

「記事の方は、どうやらJeremy Evansさんが昨年のRubyPrize 2020で受賞したタイミングでのインタビューみたいですね」「へ〜、Jeremy EvansさんはOpenBSD版のRubyもメンテしているのか」「OpenBSD版のRubyはこの人にかかっているんですね」「この記事訳してみたいです」

参考: OpenBSD - Wikipedia

🔗『Polished Ruby Programming』

「『Polished Ruby Programming』は自分も読んでいるところですが、Ruby中級者がスキルアップするのにいい本だと思いました」「自分も読んでみて同感です」

「同書の特徴は、Rubyに特化していて他の言語にはなかなか応用できなさそうなところ: Java的なオブジェクト指向やデザインパターンの流儀をRubyに持ち込んだ形で書かれた本はよくありますけど、この本はそれをいったん忘れてRubyならではの最適化された書き方を再定義しているところがありますね」「あ〜そういう感じですか」「この本でのRubyの書き方はたとえばJavaにはなかなか持ち込めないんじゃないかなと思ったりしました」「そうかも」「内容はとてもいいです👍」「今ポチりました〜」

「同書はRubyKaigiについてこられる中級者上級者なら問題なく読めると思います」「そうですね」「逆にRuby初心者にはハードでしょうね」「この本の書き方が当たり前だと思うと他の言語がやりにくくなったりして」

🔗 その他Ruby(Ruby公式ニュースより)


つっつきボイス:「Ruby Award 2022の募集が始まった🎉」「毎年12月に福岡県で開催されているRubyのイベントですね」「自社でRubyやRailsを使った試みをやっていれば応募できます」

🔗 その他

🔗 n月刊ラムダノート


つっつきボイス:「Rubyコミッターのmametterさんがこの『n月刊ラムダノート』に寄稿していたのでさっきポチりました」「計算機好きのための技術解説情報誌ですか」「紙でしか買えないのかと思ったらPDF版も買えることにやっと気づきました」「PDF版の方がちょっと安い」「ページが多すぎなくて気軽に読めるのがよさそう」「まとめ版もあるのでポチってみた」


今回は以上です。

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

週刊Railsウォッチ: Rubyの可変長アロケーションプロジェクト、サーキットブレーカーgem、EC2-Classicが終了へほか(20210804後編)

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

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

Ruby 公式ニュース

Rails公式ニュース

Ruby Weekly

The post 週刊Railsウォッチ: システムテスト用headlessドライバにCupriteが追加、rails-mini-profiler、Jeremy Evansインタビューほか(20210810) first appeared on TechRacho.

Viewing all 1080 articles
Browse latest View live