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

Rails 6.0.4がリリースされました

$
0
0

Ruby on Rails 6.0.4がリリースされました。バグ修正が主で、セキュリティ修正は含まれていません。

英語版Changelogをまとめて見るにはGItHubのリリースタグ↓が便利ですv6.0.4タグの日付は日本時間の6/16 06:21でした。

詳しくは以下のコミットリストをご覧ください。

🔗 更新の概要

🔗 Changelogに更新が記載されている機能

以下の機能の順序はリリースノートの記載順に従っています。

本記事では、GitHubリリースタグに掲載されているChangelogに対応するプルリクやコミットへのリンクを取り急ぎ貼りました。

🔗 Changelogに更新の記載がない機能

以下はChangelogには更新の記載がありません。

⚓ Action Pack

Base64 strict-encoded CSRFトークンのWeb安全性は本質的ではないため、取り扱いが困難。たとえば、クライアントが読めるcookieにCSRFトークンを入れてブラウザに送信するという一般的な方法は、そのままでは正しく機能しない。値をURLエンコードおよびデコードして、転送に耐えられるようにする必要がある。
Rails 6.1ではBase64 urlsafe-encoded CSRFを生成する。このトークンの転送は本質的に安全。バリデーションでは、urlsafeトークンの他に、後方互換性のためにstrict-encodedトークンも受け付ける。
Scott Blum, Étienne Barrié
リリースノートChangelogより大意


action_dispatch.use_cookies_with_metadataを有効にしたときに、署名済みおよび暗号化済みcookieの値にfalseを保存できるように修正。
Rolandas Barysas
リリースノートChangelogより大意

🔗 Action View

SanitizeHelper.sanitized_allowed_attributesSanitizeHelper.sanitized_allowed_tagssafe_list_sanitizerのクラスメソッドを呼び出すよう修正。
Taufiq Muhammadi
リリースノートChangelogより大意

🔗 Active Record

not_で始まるenum要素のwarningを、not_なしで始まるenum要素が存在してコンフリクトする場合にのみ表示するよう修正。
Alex Ghiculescu
リリースノートChangelogより大意


autosave済みのhas_one関連付けに対応する関連付けが読み込まれていない場合に読み込むよう修正。
Steven Weber
リリースノートChangelogより大意


table_nameが変更されている場合にstatementキャッシュをリセットするよう修正。
Ryuta Kamizono
リリースノートChangelogより大意


masterブランチの型キャストはかなり向上しているので、よく使われる型ではあまり問題にならないが、厳密に言えばeager loadingは(find_by_sqlでもやっているように)データベースのカラム型を尊重すべき。
Ryuta Kamizono
同PRより大意


コレクション関連付けが複数回autosaveされないよう修正。
Eugene Kenny
リリースノートChangelogより大意


insert_all:unique_byオプションが式インデックスで使われたときの問題を修正。
ActiveRecord::Persistence.insert_alActiveRecord::Persistence.upsert_all:unique_byオプションが式インデックスの名前で使われるとエラーが発生した。:unique_byのフォーマッティング周りの振舞いをガードすることでこの問題が修正される。

# 使い方
create_table :books, id: :integer, force: true do |t|
  t.column :name, :string
  t.index "lower(name)", unique: true
end

Book.insert_all [{ name: "MyTest" }], unique_by: :index_books_on_lower_name

Austen Madden
リリースノートChangelogより大意


カスタムスコープを用いるポリモーフィック関連付けのプリロードを修正。
Ryuta Kamizono
リリースノートChangelogより大意


orメソッドにSQLコメント付きのリレーションを渡せるようになった。
Takumi Shotoku
リリースノートChangelogより大意


カウンタキャッシュと楽観的ロックのコンフリクトを解消。
Active Recordインスタンスのロックバージョンの更新は、カウンタキャッシュの更新後に行うこと。こうすることで、対応するデータベースレコードのlock_versionカラムとのパリティが以後のトランアクションで維持され、不要なActiveRecord::StaleObjectErrorを回避できる。
Aaron Lipman
リリースノートChangelogより大意


source/throughのスコープでjoinsが使われる場合のthrough関連付けの問題を修正。
Ryuta Kamizono
リリースノートChangelogより大意


through関連付けがincludespreloadでsourceスコープを扱うよう修正。
Ryuta Kamizono
リリースノートChangelogより大意


joinsの元の順序が維持されるようArelのjoinsのeager loadingを修正。
Ryuta Kamizono
リリースノートChangelogより大意


eager loadingとorderとlimit(またはoffset)したときのcountのGROUP BYを修正。
Ryuta Kamizono
リリースノートChangelogより大意


異なる関連付け間にまたがる複数のleft_joinsmergeしたときのleft_joinsの順序を修正。
Ryuta Kamizono
リリースノートChangelogより大意


MySQLのインデックス作成でテーブルのbulk変更時のインデックスコメントを維持するよう修正。
Ryuta Kamizono
リリースノートChangelogより大意


データベースで機能がサポートされていない場合はremove_foreign_key:validateオプションをチェックしないよう変更。
Ryuta Kamizono
リリースノートChangelogより大意


重複した”group by”フィールドを維持するよう集約の結果を修正。
Ryuta Kamizono
リリースノートChangelogより大意


プリロード使用時に重複したレコードを返さないよう修正。
Bogdan Gusiev
リリースノートChangelogより大意

🔗 Active Storage

Poppler PDFプレビューアがプレビュー画像をレンダリングするとき、元のドキュメントのmediaボックスではなくcropboxを使っていたため、印刷のマージンが隠されていた。この修正によって、MuPDFプレビューアと振舞いが一致する。
Vincent Robert
リリースノートChangelogより大意

🔗 Active Support

ActiveSupport::Cache::RedisCacheStoreがオプションをread_multiに渡さず、fetch_multi`が正しく動かなくなった問題を修正。
Rajesh Sharma
リリースノートChangelogより大意


with_optionsのミューテーションのリーク回避のため、オプションハッシュをコピーするよう修正。
Eugene Kenny
リリースノートChangelogより大意

🔗 Railties

rails testに渡す相対パスの末尾にスラッシュを付けられるようになった。
Eugene Kenny
リリースノートChangelogより大意


リクエストで未知のHTTPメソッドが使われた場合に405 Method Not Allowedを返すよう修正。
Loren Norman
リリースノートChangelogより大意


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

関連記事

速報: Rails 6.1.1がリリースされました

The post Rails 6.0.4がリリースされました first appeared on TechRacho.


Rails 7のenumに新しい構文が導入(翻訳)

$
0
0

概要

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

Rails 7のenumに新しい構文が導入(翻訳)

Rails 7の最新の変更で、enum定義に新しい構文が導入されました(#41328)。

変更前

従来の構文では、以下のように「enumの名前」「enumの値」「enumのオプション」を渡せます。

class Post < ActiveRecord::Base
  enum status: [ :draft, :published, :archived ], _prefix: true, _scopes: false
  enum category: [ :free, :premium ], _suffix: true, _default: :free
end

変更後

新しい構文では「enumの名前」と「enumの値」が別の引数に分けられ、以下のようにenumのオプションをキーバリューペアとして渡せるようになりました。

class Post < ActiveRecord::Base
  enum :status, [ :draft, :published, :archived ], prefix: true, scopes: false
  enum :category, [ :free, :premium ], suffix: true, default: :free
end

この変更は、ハッシュ構文でも有効です。

class Post < ActiveRecord::Base
  enum :status, { draft: 0, published: 1, archived: 2 }, prefix: true, scopes: false
  enum :category, { free: 0, premium: 1 }, suffix: true, default: :free
end

上のコードによって以下のインスタンスメソッドがこれまでどおりRailsによって生成され、動作も同じです。

  • status_draft?
  • status_draft!
  • status_published?
  • status_published!
  • status_archived?
  • status_archived!
  • free_category?
  • free_category!
  • premium_category?
  • premium_category!

以下のスコープも作成されます。

  • free_category
  • premium_category

なお、ここではscopes: falseを指定したのでstatus用のスコープは生成されません。

オプションのキー名も以下のように_なしに変更されました。

従来のオプション 新しいオプション
_default default
_prefix prefix
_suffix suffix
_scopes scopes

原注: 従来の構文は、今後非推奨化されるまで利用できます。

関連記事

Rails 7: ERBでRubyのハッシュをHTML属性に変換する機能(翻訳)

The post Rails 7のenumに新しい構文が導入(翻訳) first appeared on TechRacho.

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 後編(翻訳)

$
0
0

概要

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

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


更新情報

  • 2020/01/17: 初版公開
  • 2021/06/10: 更新

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 後編(翻訳)

(前編からの続き)

Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 前編(翻訳)

今もJavaScriptコードをSprocketsで扱える

Webpackerドキュメントには以下のように書かれています。

(略)Webpackの主要な目的は「アプリのようなJavaScript」であり、画像やCSSのためではなく、ましてやJavaScript Sprinklesのためではない(これらは今後もapp/assetsの下に置ける)。
同ドキュメントより

つまり、ビューで何かJavaScriptを使いたいときや、使う必要に迫られたときは、これまでどおりSprocketsを使えるのです。

  • app/assets/javascriptsディレクトリを作成する(複数形の”javascripts”になっていることに注意)
  • app/assets/config/manifest.jsを上に合わせて更新する(//= link_directory ../javascripts .js
  • ビューにjavascript_include_tagを書いて、Sprockets用JavaScriptファイルをインクルードする(以下の違いに注意)
    • Sprockets用はjavascript_include_tag
    • Webpacker用はjavascript_pack_tag
  • 後は好きにやる

個人的にはこの手段をできる限り避けるようにしていますが、このことは知っておく価値があります。


原注: manifest.jsconfig.assets.precompileの配列は、どちらもコンパイル対象のトップレベルに露出させるという目的は同じなのに、どうしてファイルが2つもあるのかが気になる方もいらっしゃると思います。その目的は後方互換性のためです。Sprocketsのアップグレード手順では、後者をおすすめしていません。

Rails 6アプリケーションでbootstrap 4とfont-awesome 5を追加する手順

本記事をより深く理解いただくために、ここにある手順をそのまま適用することをおすすめします。JavaScriptの知見を深めるのに大いに役に立つでしょう。

1. Rails 6アプリケーションを新規作成する

rails new bloggy

以下にリストしたファイルを見てみましょう。目的は、皆さんにこれらを隅々まで理解してもらうことではなく、こういうファイルが存在するということを皆さんに知ってもらい、そこに何が含まれているのかというぼんやりとしたメンタルイメージを頭の隅に置いて、必要に応じていつでもそこに立ち返ることができるようにすることです。

Yarnのファイル:

  • package.json

Webpackerのファイル:

  • config/webpacker.yml
  • app/javascript/packs/application.js
  • app/views/layouts/application.html.erb

Sprocketsのファイル:

  • app/assets/config/manifest.json

2. ルートページを追加する

rails generate controller welcome index

ついでにconfig/routes.rbroot to: 'welcome#index'を追記します。

rails serverを実行し、問題なく動作することを確認します。

3. 必要なyarnパッケージを追加する

ここではBootstrap 4(jQueryとpopper.jsが必要です)とfont-awesome 5を追加したいと思います。

訳注(2021/06/10)

元記事はBootstrap 4を前提としていますが、その後Bootstrap 5がリリースされました。Bootstrap 5の場合は以下の手順と異なる部分がありますのでご注意ください。特にjQueryはBootstrap 5で必須でなくなっています

Yarnパッケージの検索エンジンで、自分に必要なパッケージを検索し(各パッケージのダウンロード数の多さに気づくことでしょう)、このチュートリアルを続行します。

yarn add bootstrap jquery popper.js @fortawesome/fontawesome-free

パッケージがyarnによって./bloggy/node_modules/にキャッシュされ、package.jsonも更新されます。しかしこのままではアプリケーションから利用できませんので対応しましょう。まずはJavaScript部分をインクルードすることにし、CSS部分は後でやることにします。

4. bootstrapとfont-awesomeのJS部分をインクルードする

アプリのレイアウトには既にjavascript_pack_tag 'application'が置かれています。これは、Webpackにapp/javascript/packs/application.jsのコンパイルを指示してその出力をレイアウトにインクルードします。bootstrapを追加するには、bootstrapをインクルードする専用のpackを別途作成するか、application.js packを使います。本物のアプリを作るわけではありませんので、ここでは後者をやってみましょう。

以下をapp/javascript/packs/application.jsに追加します。

require("bootstrap");
require("@fortawesome/fontawesome-free");

原注: ここでbootstrap/dist/js/bootstrap.minではなくbootstrapをrequireしていることにご注意ください。その理由は、ファイルパスを指定しない場合はどのファイルをインクルードすべきかという必要な情報をモジュールのpackage.json(つまりbloggy/node_modules/bootstrap/package.json)が提供するからです。bootstrap/dist/js/bootstrap.minをrequireすれば、それはそれで問題なく動くでしょう。


bootstrapとfont-awesomeの設定を続けましょう。Railsサーバーを起動してJavaScriptコンソールを開いてみると、application.jsでjQueryをrequireしていないにもかかわらず、問題なく動作していることを確認できます。

Webpackerを用いてbootstrapをインクルードする方法を他のチュートリアルで学んだ方の中には、他のチュートリアルのほとんどが最初にjQueryをrequireし、次にbootstrapをrequireしていることに気づいた方もいるかもしれません。これは実際には無意味です。

その理由がわかりますか?私たちはjQueryをyarnでインストールしているので、bootstrap自身がjQueryを自動でrequireできるのです。jQueryはapplication.jsの中で利用可能な状態になるので、jQueryをapplication.jsでrequireする必要はありません。すなわち、jQueryをapplication.jsの中で直接使う必要がない限り、実際にはjQueryをapplication.jsでrequireする必要はありません。

5. bootstrapとfont-awesomeの(S)CSS部分をインクルードする

私はSCSSを使うのが好きなので、bootstrapやfont-awesomeをインクルードする前にapplication.cssをapplication.scssにリネームし、コメントやSprockets用の指示を全部空にします。

続いて以下のコードを貼り付けます。

$fa-font-path: '@fortawesome/fontawesome-free/webfonts';
@import '@fortawesome/fontawesome-free/scss/fontawesome';
@import '@fortawesome/fontawesome-free/scss/regular';
@import '@fortawesome/fontawesome-free/scss/solid';
@import '@fortawesome/fontawesome-free/scss/brands';

@import 'bootstrap/scss/bootstrap';

原注: SprocketsはWebpackと異なり、インクルードするファイルを決定するためにnpmモジュールのpackage.jsonファイルを読み込みません。つまり、名前だけを指定してモジュールをインポートできません。実際にインポートしたいファイル名とそのパスを指定する必要があります(拡張子はあってもなくても構いませんが)。


ついに準備が整いました。

ビューに何かボタンとアイコンを追加して、問題なく動作しているかどうかを確認しましょう。

app/views/welcome/index.html.erbファイルに<a href="#" class="btn btn-primary">Yeah <i class="far fa-thumbs-up"></i></a>を追加してRailsサーバーを実行し、primaryボタンとアイコンがbootstrapらしく表示されていることを確認します。

jQueryをあらゆるpackで利用する

jQueryのような依存関係を多くのpackで使う必要が生じた場合、それらをpackごとにいちいちrequireするのは面倒です。私好みのソリューションは、設定を変えて全packで利用可能にすることです(繰り返しますが、あくまでpack内での話であり、ビューでは使えません)。

これを行うには、config/webpack/environment.jsに以下をコピペします。

const { environment } = require('@rails/webpacker')
var webpack = require('webpack');

environment.plugins.append(
  'Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
  })
)

module.exports = environment

このスニペットによって、WebpackはjQueryモジュールを$という名前を介して全packに「提供」します。これは、各packの冒頭に以下を追加するのと同等です。

import $ from 'jquery';

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

関連記事

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

Rails 5: Webpacker公式README — Webpack v4対応版(翻訳)

The post Rails 6: Webpacker+Yarn+Sprocketsを十分理解してJavaScriptを書く: 後編(翻訳) first appeared on TechRacho.

週刊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の技: パンくずリストをgemなしで実装する(翻訳)

$
0
0

概要

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

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

Railsの技: パンくずリストをgemなしで実装する(翻訳)

パンくずリスト(breadcrumbs)は、多くのアプリケーションでよく用いられるUIパターンのひとつです。Railsにはパンくずリストのためのツールは特に組み込まれておらず、以下のように既存の便利なパンくずリスト用gemもいろいろあります。

fnando/breadcrumbs - GitHub

piotrmurach/loaf - GitHub

lassebunk/gretel - GitHub

zachinglis/crummy - GitHub

weppos/breadcrumbs_on_rails - GitHub

このぐらいの機能なら、わずか数行のコードを手書きすれば自分のアプリでも簡単に実装できると思います。

おそらく最終的にはパンくずリストの表示方法を完全に制御したくなるでしょうから、この機能を自分で実装してコードを掌握しておく方がよいでしょう。

方法

最初にBreadcrumbモデルをapp/modelsディレクトリに追加します。ただしデータベースへの永続化は不要なので、Active Recordモデルにする必要はありません。

class Breadcrumb
  attr_reader :name, :path

  def initialize(name, path)
    @name = name
    @path = path
  end

  def link?
    @path.present?
  end
end

次に、パンくずリストを保存および追加するメソッドをApplicationControllerに追加します。

class ApplicationController < ActionController::Base
  ...

  helper_method :breadcrumbs

  def breadcrumbs
    @breadcrumbs ||= []
  end

  def add_breadcrumb(name, path = nil)
    breadcrumbs << Breadcrumb.new(name, path)
  end
end

続いて、アプリケーションのビューレイアウトでパンくずリストを好みの方法でレンダリングします。私の場合は、パンくずリストを<head>タグの<title>タグとページヘッダーの両方に表示しています。

<head>
  <title>
    <%= breadcrumbs.map(&:name).reverse.append("My App").join(" | ") %>
  </title>
</head>

<nav>
  <ol class="breadcrumbs">
    <% breadcrumbs.each do |crumb| %>
     <li>
      <% if crumb.link? %>
        <%= link_to crumb.name, crumb.path, class: "breadcrumb-link" %>
      <% else %>
        <span class="breadcrumb-page">
          <%= crumb.name %>
        </span>
      <% end %>

      <% unless crumb == breadcrumbs.last %>
        <span class="breadcrumb-separator">/</span>
      <% end %>
     </li>
    <% end %>
  </ol>
</nav>

このAPIはシンプルですが、このモデルを使うことでどのコントローラにもパンくずリストを追加できます。パンくずリストをリンク化したい場合は、pathも指定できます。

パンくずリストはbefore_actionsや各アクションにセットアップできます。パンくずリストを表示する条件も普通のRubyコードで追加できます。

class PostsController < ApplicationController
  before_action :set_breadcrumbs

  def index
    @posts = Post.all
  end

  def show
    @post = Post.find(params[:id])

    add_breadcrumb(@post.title, @post)
  end

  def new
    @post = Post.new

    add_breadcrumb("New Post")
  end

  private

  def set_breadcrumbs
    add_breadcrumb("Admin", admin_home_path) if Current.user.admin?
    add_breadcrumb("Posts", posts_path)
  end
end

「セクシーなコードでしょうか?」いいえ。「退屈なコードでしょうか?」そうですね。「うまく動きますか?」もちろん!

関連記事

Railsの技: Active Recordバリデーションをコンテキストに応じて実行する(翻訳)

The post Railsの技: パンくずリストをgemなしで実装する(翻訳) 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 6.1.4がリリースされました

$
0
0

Ruby on Rails 6.1.4がリリースされました。

英語版Changelogをまとめて見るにはGItHubのリリースタグ↓が便利です。v6.1.4タグの日付は日本時間の2021/06/25 05:23でした。

詳しくは以下のコミットリストをご覧ください。

更新の概要

Changelogに更新が記載されている機能

以下の機能の順序はリリースノートの記載順に従っています。

本記事では、GitHubリリースタグに掲載されているChangelogに対応するプルリクやコミットへのリンクを取り急ぎ貼りました。

Changelogに更新の記載がない機能

以下はChangelogには更新の記載がありません。

Action Cable

commit: RemoteConnection#disconnect: fix ArgumentError on ruby 3.0 · rails/rails@27ac308

Ruby 3.0でRemoteConnection#disconnectで発生するArgumentErrorを修正。
Vladislav
リリースノートChangelogより大意

Action Pack

PR: Ignore file fixtures on `db:fixtures:load` by kevinsjoberg · Pull Request #42153 · rails/rails

rake db:fixtures:loadでファイルのfixtureを無視するようにした。
Kevin Sjöberg
リリースノートChangelogより大意

PR: actionpack: Use an infinite sized queue in testing ActionController::Live by dylanahsmith · Pull Request #41609 · rails/rails

テストのbody buffer size limitを削除するとActionController::Live controllerがデッドロックする問題を修正。
Dylan Thacker-Smith
リリースノートChangelogより大意

PR: cause rails to correctly place optional path parameter booleans by HParker · Pull Request #42283 · rails/rails

オプションのパスパラメータのブーリアン値が適切に配置されるようにした。
従来は、パスのある部分でURLパラメータにfalseを指定すると、パスのその部分まで以下のようにインクルードされていた。

get "(/optional/:optional_id)/things" => "foo#foo", as: :things
things_path(optional_id: false) # => /things?optional_id=false

この修正によって、truefalseはオプショナルパスパラメータとして使われた場合にも同様に扱われるようになる。つまり以下のようになる。

get '(this/:my_bool)/that' as: :that

that_path(my_bool: true) # => `/this/true/that`
that_path(my_bool: false) # => `/this/false/that`

Adam Hess
リリースノートChangelogより大意

PR: Backport of “Allow ‘private, no-store’ Cache-Control header” by pixeltrix · Pull Request #42115 · rails/rails

private, no-store Cache-Controlヘッダーをサポート。
従来は’no-store’を指定すると他のディレクティブを指定できなかった。
Alex Smith
リリースノートChangelogより大意

Action Text

commit: Always render attachment partials as HTML with :html format inside tr… · rails/rails@d8ff47f

trixエディタ内では添付ファイルのパーシャルを常に:htmlでHTMLとしてレンダリングするようにした。
James Brooks
リリースノートChangelogより大意

Action View

PR: Pass default values for `translate` through I18n by jonathanhefner · Pull Request #40691 · rails/rails

translateヘルパーが、I18n.translateで訳文キーにならないdefault値を展開時に渡すようになった。
Jonathan Hefner
リリースノートChangelogより大意

PR: Don’t attach UJS form submission handlers to Turbo forms by dhh · Pull Request #42476 · rails/rails

TurboのフォームにUJS形式の送信ハンドラを渡さないようになった。
David Heinemeier Hansson
リリースノートChangelogより大意

PR: Allow both `current_page?(url_hash)` and `current_page?(**url_hash)` on Ruby 2.7 · rails/rails@789c988

current_page?(url_hash)current_page?(**url_hash)が両方とも使えるように
Ryuta Kamizono
リリースノートChangelogより大意

Active Model

PR: Fixes #41521, ActiveModel::Dirty fails on to_json by anilmaurya · Pull Request #41677 · rails/rails

ActiveModel::Dirtyto_jsonを修正。
再帰につながるmutations_from_databaseをjsonから除外。
Anil Maurya
リリースノートChangelogより大意

Active Record

PR: Rollbacks for deadlocks by jamiemccarthy · Pull Request #30922 · rails/rails

ただしマージは以下で行われました。

ActiveRecord::TransactionRollbackErrorで失敗したトランザクションのロールバックを試行しないようにした。
Eileen M. Uchitelle
リリースノートChangelogより大意

PR: Fix nil pool_config in legacy connection handling by eileencodes · Pull Request #42579 · rails/rails

set_pool_configpool_configがnilの場合はエラーになるようにした。
Eileen M. Uchitelle
リリースノートChangelogより大意

commit: Fix compatibility with psych 4.x · rails/rails@255b5ff

psych >= 4との互換性を修正。
Psych 4.0.0からYAML.loadYAML.safe_loadのように振る舞うようになった。互換性を維持するため、Rails.application.config_forで可能な場合はYAML.unsafe_loadを使うようになった。
Jean Boussier
リリースノートChangelogより大意

PR: Support using replicas when using `rails dbconsole` by cthornton · Pull Request #42285 · rails/rails

rails dbconsole--include-replicasフラグをサポート。
Christopher Thornton
リリースノートChangelogより大意

PR: Restore connection pools after transactional tests by eugeneius · Pull Request #40384 · rails/rails

トランザクショナルなテストの後でコネクションプールをリストアするようになった。
Eugene Kenny
リリースノートChangelogより大意

PR:upsert_all fails cleanly for MySQL by coding-chimp · Pull Request #41628 · rails/rails

:unique_byを使った場合にMySQLでupsert_allがきれいに失敗するようになった。
Bastian Bartmann
リリースノートChangelogより大意

PR: Fix user-defined `self.default_scope` to respect table alias by kamipo · Pull Request #41892 · rails/rails

ユーザー定義のself.default_scopeがテーブルのエイリアスを扱うよう修正。
Ryuta Kamizono
リリースノートChangelogより大意

commit: Clear `@cache_keys` cache even when eager loading · rails/rails@7261f90

update_alldelete_alldestroy_allの後で@cache_keysキャッシュをクリアするようにした。
Ryuta Kamizono
リリースノートChangelogより大意

PR: Quote the arguments passed to the Contains/Overlaps Arel nodes by bradleypriest · Pull Request #41640 · rails/rails

Arel::Nodes::ContainsArel::Nodes::Overlapsquoted_nodeを使うよう変更して、Arel::PredicationsでPostgreSQLのarrayが正しく引用符で囲まれるようにした。
Bradley Priest
リリースノートChangelogより大意

PR: Fix `WhereClause#extract_attributes` to work it with a string where clause · rails/rails@dc3b116

where句にstringコンテンツがある場合のmergeを修正。
Ryuta Kamizono
リリースノートChangelogより大意

PR: Fix rollback of parent destruction with nested dependent:destroy by intrip · Pull Request #41602 · rails/rails

ネステッドなdependent: :destroyで親をdestroyしてもロールバックしていたのを修正。
Jacopo Beschi
リリースノートChangelogより大意

PR: Fix binds logging for “WHERE ... IN ...” statements by ricardotk002 · Pull Request #41068 · rails/rails

"WHERE ... IN ..."bindsログ出力を修正。
Ricardo Díaz
リリースノートChangelogより大意

PR: Handle false in relation strict loading checks by eileencodes · Pull Request #41688 · rails/rails

リレーションでのstrict loadingチェックでfalseを扱えるようにした。
従来は、モデルでstrict loadingがtrueに設定されていてかつリレーションでstrict_loadingがfalseに設定されていると、strict loadingでraiseまたはwarningを出力するかどうかを決定するときにこのfalseが反映されなかった。

class Dog < ActiveRecord::Base
  self.strict_loading_by_default = true

  has_many :treats, strict_loading: false
end

上の例では、strict_loadingをfalseにしても引き続きdog.treatsがraiseされる。これはActive Storageへの影響の方が大きいバグなので、#41461に代わるこのプルリクを作った。この挙動には少々驚かされたので、すべてのアプリケーションでこの問題を修正する必要がある。テストは#41461のものを使い、#41453を参考に少し手を加えた。
Eileen M. Uchitelle, Radamés Roriz
リリースノートChangelogより大意

PR: Fix numericality validator without precision in Active Record by kamipo · Pull Request #41672 · rails/rails

precisionがない場合のnumericalityバリデーションを修正。
Ryuta Kamizono
リリースノートChangelogより大意

PR: Fix aggregate attribute on Enum types by kamipo · Pull Request #41613 · rails/rails

Enum型の属性aggregationを修正。
Ryuta Kamizono
リリースノートChangelogより大意

PR: Fix CREATE INDEX statement generation for PostgreSQL by eltongo · Pull Request #41490 · rails/rails

PostgreSQLでのCREATE INDEXステートメント生成を修正。
eltongo
リリースノートChangelogより大意

PR: Should not change serializable value for the enum attribute by kamipo · Pull Request #41486 · rails/rails

enum属性に文字列の値を配列で渡したときのwhere句の挙動を修正。
Ryuta Kamizono
リリースノートChangelogより大意

PR: Fix `unprepared_statement` to work it when nesting by kamipo · Pull Request #41423 · rails/rails

unprepared_statementがネストで動くよう修正。
Ryuta Kamizono
リリースノートChangelogより大意

Active Storage

PR: ActiveStorage: Allow the parameters sent to ffmpeg to be configurable by brendon · Pull Request #42472 · rails/rails

動画プレビュー画像生成用にffmpegに渡されるパラメータがconfig.active_storage.video_preview_argumentsでコンフィグ可能になった。
Brendon Muir
リリースノートChangelogより大意

PR: Fix Active Storage update task when running in an engine by jmalcic · Pull Request #41199 · rails/rails

Active Storageがエンジン内で実行されているときの更新タスクを修正。
Justin Malčić
リリースノートChangelogより大意

PR: Active Storage: don’t crash if mini_mime doesn’t recognize content_type by ghiculescu · Pull Request #42225 · rails/rails

MIMEタイプが認識されない場合にエラーをraiseしないようになった。
Alex Ghiculescu
リリースノートChangelogより大意

commit: Active Storage: raise PreviewError when a preview cannot be generated · rails/rails@547645a

プレビューアでプレビュー画像を生成できない場合にActiveStorage::PreviewErrorをraiseするようになった。
Alex Robbin
リリースノートChangelogより大意

commit: Active Storage representations: respond with 404 given invalid variation key · rails/rails@7bcbb1c

representationvariation_keyが無効の場合にレスポンス404を返すようになった。
George Claghorn
リリースノートChangelogより大意

commit: Active Storage: `Blob` creation shouldn’t crash if no service selected · rails/rails@27bee74

サービスが未選択の場合にblob作成がクラッシュしないようになった。
Alex Ghiculescu
リリースノートChangelogより大意

Active Support

PR: MemCacheStore: always convert underlying values into an `Entry` by ghiculescu · Pull Request #42559 · rails/rails

MemCacheStore:で(falseを含む)任意の背後の値をEntryに変換するようになった。
#42559も参照。
Alex Ghiculescu
リリースノートChangelogより大意

PR: Use BigDecimal compatible operation in NumberToRoundedConverter by federicoaldunate · Pull Request #42316 · rails/rails

number_with_precisionBigDecimal値が巨大な場合のバグを修正。
Federico Aldunate, Zachary Scott
リリースノートChangelogより大意

commit: secure_compare: Check byte size instead of length · rails/rails@a215e47

secure_comparelengthではなくbytesizeを比較するよう修正。
Tietew
リリースノートChangelogより大意

PR: Fix `Time.at` to not lose `in` option by kamipo · Pull Request #41592 · rails/rails

Time.at:inオプションを失わないよう修正。
Ryuta Kamizono
リリースノートChangelogより大意

PR: Require a path for `config.cache_store = :file_store` by ghiculescu · Pull Request #41522 · rails/rails

config.cache_store = :file_storeへのパスを必須にした。
Alex Ghiculescu
リリースノートChangelogより大意

commit: Avoid having to store complex object in the default translation file · rails/rails@3eb546e

デフォルトの訳文ファイルに複雑なオブジェクトを保存しなくてもいいようにした。
Rafael Mendonça França
リリースノートChangelogより大意

Railties

commit: Fix compatibility with psych 4.x · rails/rails@255b5ff

psych >= 4との互換性を修正。
Psych 4.0.0からYAML.loadYAML.safe_loadのように振る舞うようになった。互換性を維持するため、Rails.application.config_forで可能な場合はYAML.unsafe_loadを使うようになった。
Jean Boussier
リリースノートChangelogより大意

commit: Ensure `Rails.application.config_for` always cast hashes to `ActiveSu… · rails/rails@e4bf943

Rails.application.config_forが常にハッシュをActiveSupport::OrderedOptionsにキャストするようにした。
Jean Boussier
リリースノートChangelogより大意

PR: Fix create migration generator with `--pretend` option by euxx · Pull Request #41255 · rails/rails

マイグレーションに--pretendオプションを付けたときに正しく生成されるよう修正。
euxx
リリースノートChangelogより大意


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

関連記事

Rails 6.0.4がリリースされました

The post Rails 6.1.4がリリースされました 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の技: Active Recordオブジェクトはチェイン可能にして返そう(翻訳)

$
0
0

概要

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

Railsの技: Active Recordオブジェクトはチェイン可能にして返そう(翻訳)

Active Recordで優秀な点のひとつは、クエリインターフェイスが以下のようにチェイン可能(chainable)である点です。

Post.includes(:comments)
  .where(published: true)
  .where(author: Current.user)
  .order(:name)

この強みを活用してコードを柔軟にするために、データにクエリをかけるときは常にチェイン可能なオブジェクトを返すようにしましょう。

使い方

アプリケーションが育つに連れて、複雑なクエリからデータを切り出すようになりがちです。

class SpecialOffer

  def self.find_eligible_products(store, shopper)
    return [] if store.restricted?

    store.products
      .where('price >= ?', 100)
      .select{ |p| shopper.can_order?(p) }
  end
end

@products = SpecialOffer.find_eligible_products(store, shopper)
#=> [ #<Product:0x00007fb1719b7ec0>, #<Product:0x00007fb174744de8>, ... ]

上のコードはとりあえず動くかもしれませんが、今後@productsを何らかの方法でorderする必要が生じたらどうなるでしょうか?ロジックを追加したらどうなるでしょうか?何らかの関連付けをlazy loadingするとどうなるでしょうか?

この場合、SpecialOfferのメソッドが返しているのは配列型です。これではRubyのsortselectといった配列メソッドに切り替えなければならず、より多くのデータが必要になるとN+1クエリバグにつながる可能性もあります。

このコードを以下のようにリファクタリングして、チェイン可能なオブジェクトを返すようにしましょう。

class SpecialOffer

  def self.find_eligible_products(store, shopper)
    return Product.none if store.restricted?

    product_ids = store.products
      .where('price >= ?', 100)
      .select{ |p| shopper.can_order?(p) }
      .map(&:id)

    Product.where(id: product_ids)
  end
end

@products = SpecialOffer.find_eligible_products(store, shopper)
#=> Product::ActiveRecord_Relation

最初にnoneというクエリメソッドを用いています。noneは空の結果を返しますが、この結果はチェイン可能です。この空のリレーションに対してorderincludeswhereなどのActive Recordメソッドを呼び出すと、単に空の結果を返します。

次に、productの複雑なクエリを直接返す代わりに、該当するproductをコレクションし、「改めて」productのidに対応する結果を返します。この場合データベースへのクエリが追加で発生しますが、必要に応じて結果を自由に操作することもできます。

結果をソートしたい場合や関連付けを読み込みたい場合は、そうした操作をデータベース内で行えるようになり、処理の一部として実行された既存の条件を気にする必要がなくなります。

@products = SpecialOffer.find_eligible_products(store, shopper)
  .includes(:variants)
  .order(:price)

@products = SpecialOffer.find_eligible_products(store, shopper)
  .joins(:sales)
  .where("sales.count > 15")
  .order(:sku)

このパターンは、データを適切な形に加工するための柔軟性を損なわずに複雑なクエリを取り出せるので、非常に重宝することに気が付きました。

参考資料

関連記事

Railsの技: パンくずリストをgemなしで実装する(翻訳)

The post Railsの技: Active Recordオブジェクトはチェイン可能にして返そう(翻訳) first appeared on TechRacho.

Rails 7でunpermitted_parametersのログ出力にcontextも含められる(翻訳)

$
0
0

概要

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

Rails 7でunpermitted_parametersのログ出力にcontextも含められる(翻訳)

Rails 7でunpermitted_parameters.action_controller1のペイロードが拡大され、許可されないパラメータがどのコントローラのどのアクションで受信されたかを開発者が調べられるようになりました(#41809)。

変更前

従来のRailsでは、許可されないパラメータがリクエストで見つかると、許可されていないキーに関する情報しかログ出力されず、許可されないパラメータがどのコントローラのどのアクションで受信されたのかを開発者が知るための情報が不十分でした。

以下のコードで考えてみましょう。Userにはname属性、email属性、role属性があり、このうちname属性とemail属性のみ許可されています。

request_params = { user: { name: "Francesco", email: "fransceso@example.com", role: "admin" } }

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

# Unpermitted parameter: :role

ログでわかるのは、許可されないキーの情報のみであり、許可されないパラメータを受信したコントローラとアクションに関する情報を取れませんでした。

変更後

Rails 7では、呼び出し元がcontextcontrollerキー、actionキー、paramsキー、requestキーを渡すと、このcontextもログ出力のペイロードに含まれるようになります。

これにより、ActionController::Parameterscontextをパラメータとして受け取れるようになります。

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 }

許可されないパラメータに加えて、コントローラやアクションもログ出力されていることがわかります。contextが渡されない場合は、空のコンテキストがペイロードに含まれます。

request_params = { user: { name: "Francesco", email: "fransceso@example.com", role: "admin" } }

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

# Unpermitted parameter: :role. Context: { }

原注: この変更は、ログ出力のコンテキストを呼び出し元が提供することを期待しています。

関連記事

Rails 7のenumに新しい構文が導入(翻訳)


  1. 訳注: これはActive Support Instrumentationのフックです。 

The post Rails 7でunpermitted_parametersのログ出力にcontextも含められる(翻訳) 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.

APIの命名規則はフロントエンド・バックエンドどちらに合わせるべきか?

$
0
0

morimorihogeです。ちょっと色々忙しくて死んでますが、深夜の勢いで書いてみます。

ことの起こり

Twitterにてこんな発言を見かけました

元記事(翻訳)はこちら

Rails: 日付や時刻のカラム名を命名規則に合わせよう(翻訳)

本件について、Twitterではreplyしてみたのですが、文字数の都合で詳細に書きづらいということもあり、一度自分の意見をまとめてみようということで記事に考えをまとめてみました。

※本記事の内容はあくまで僕の意見であり、社内エンジニア陣の共通見解というわけではありません。この手の設計思想問題はちょいちょい社内でも衝突することがありますし、その際は都度議論しながらプロジェクトごとにコンセンサスを取っています。

そもそも「バックエンド」の中でも命名規則は衝突している

さて、ここで皆さん大好きGitHub GraphQL APIを見てみましょう。
GitHubは言わずと知れたRails backendでいい感じに複雑なこういったケースを議論するのには最適な見本の一つだと思います。

基本的な命名規則については日本語でざっと解説してくれている記事がありましたので詳細はそちらを参考にしてみてください。

さて、では実際の実装としてはどうなっているでしょうか?ここでは分かりやすいということでDateTime型なフィールドに着目してみましょう。


あれあれ?天下のGitHubでも命名が統一されていませんね 🤔

果たしてこのCommit.xxxDateなどのdate suffixはRailsの命名規約違反で良くない仕様なのでしょうか?あるいはCommitComment.publishedAtなどのat suffixはフロントエンド向けの名前が表記揺れしていて良くない仕様なのでしょうか?考察してみましょう。

※以降の考察は僕が勝手に推察したもので、GitHubの中の人に聞いた情報や公式情報ではありません。鵜呑みにしないで下さい🙏

その名前はどのシステムが命名したものなのか?

そもそもGitHubは今ではソフトウェア開発に関する何でもサービスみたいになっていますが、名前の通り裏ではGitが使われているのは周知の事実かと思います。そして、Gitにはcommitに対してauthor dateとcommit dateという概念があります

ここではGitの用語としてこれらのDateがあるということが重要なので、この二つがそれぞれ何を意味するのかという話については以下の記事を参照してみてください。

ここで重要なのはこれら二つのDateはGitが命名・作成したもので、GitHub側のバックエンドRailsアプリが命名・作成したものではないということです。

Gitが命名・作成した用語については基本そのままの用語でAPIに出す方が、システムを一気通貫して同じ用語を使うことができるという点で設計上のメリットがあります。なのでここはdateという名前がそのままAPIに出ているのですね(あくまで推測です)。

それでも意味を分かりやすくするために少し命名を弄ることもある

それならそのままのGit用語をAPIにも出しているのかというと、実はそうでもありません(ここがやや複雑)。
もう一度よく見てみましょう。

  • Git用語:author date, commiter date
  • GitHub API: authoredDate, committedDate

はい。author -> authored、commiter -> committedの名前置き換えがありますね。これらはGit用語ではなくGitHub用語に変更されています

これは完全な推察になりますが、GitオブジェクトのデータをGitHub側のシステムに取り込む際にGit用語そのままだとやや意味が不明確なので、より明確な命名にしようとして一捻りしたのではないかと思います。

こうしたシステム間結合時の再命名は、意味的には利用システムにとって分かりやすくなりますが、その分システムを跨いだときに一つのものが複数の名前を持ってしまうというシステム横断の表記揺れ問題を引き起こします。カオス。

ここまでの内容で各システム用語対応の現状をまとめてみましょう。

Git用語 GitHubシステム内部用語(憶測) GitHub GraphQL API
author date authored_date authoredDate
committer date committed_date committedDate
重箱の隅
さらに細かい話をするならば、commiter dateについてはGitの中でも微妙に表記揺れがあり、commit.hなどのソースコード上は**commit** dateですが、環境変数で上書きする際にはGIT_COMMITTER_DATEだったりします。ここではGitが外部向けに公開している(と推察される)名前のcommitter dateの方を正として採用しています

at 系フィールドも合わせて見てみる

さて、では次にatで終わる系フィールドも同じ表にまとめてみます。CommitComment.publishedAtについてはそもそも(既に存在するコミットに後付するという意味での)コミットコメントという仕様自体がGitにはないので、GitHubが命名したものだと推測できます。

Git用語 GitHubシステム内部用語(推測) GitHub GraphQL API
author date authored_date authoredDate
committer date committed_date committedDate
published_at publishedAt
last_edited_at lastEditedAt

さて、これで現在の命名について整理ができました。

もし外部APIフィールド名をdate suffixに統一すると?

こうしてみてみると、この対応表は以下のようにまとめられるのではないかと思います。

  • GraphQL Schemaだけ見てみると、dateとatの表記揺れがあってちょっと気持ち悪い
  • GitHubのシステム内部用語がそのままGraphQL APIに出ているので、そこの対応は分かりやすい
  • Git用語が微妙にGitHub用語に変換されているが、意味的に分かりやすくするレベルの差に留まる

では、もしこれを元Tweetにあったように「フロント側から分かりやすいようにdate suffixに統一」してみるとどうなるでしょうか?

Git用語 GitHubシステム内部用語(推測) GitHub GraphQL API
author date authored_date authoredDate
committer date committed_date committedDate
published_at publishedDate
last_edited_at lastEditedDate

ちょっとわかりにくいので、全部snake_caseに直して単語レベルだけで比較してみます。

現行のGitHub APIでは以下の通りです。

Git用語 GitHubシステム内部用語(推測) GitHub GraphQL API
author_date authored_date (←と同じ)
committer_date committed_date (←と同じ)
published_at (←と同じ)
last_edited_at (←と同じ)

date suffix変換を噛ますとこうなります

Git用語 GitHubシステム内部用語(推測) GitHub GraphQL API
author_date authored_date (←と同じ)
committer_date committed_date (←と同じ)
published_at published_date
last_edited_at last_edited_date

はい。GraphQL APIを見るフロントエンド側からは分かりやすくなりましたが、システム全体での用語統一という観点では名前が増えました

このようにフロントエンドとバックエンドで違う名前を使うようになると、お互いの使う用語が異なるので仕様のやり取りをする際常に用語管理テーブルを挟まねばならず、脳のリソースを持って行かれてしまいます。

GitHub APIの仕様から推測した命名規則

というわけで、ここまでのことから推測できるGitHub(及びGitHub API)の命名規約をまとめてみると、システム全体として、

  • GitHubバックエンドアプリが作成し管理するフィールドについてはRailsの命名規約に従う
  • 外部のシステム(Git等)が扱うデータを取り込んで利用するフィールドについては外部のシステム側の命名規約に従う。ただし、意味的に分かりにくいものを多少修正することはある
  • (GraphQL)APIにはGitHubバックエンドアプリから見える名前をそのまま利用する

というポリシで命名しているのではないかという推測が立てられました。
また、ここまでの中では挙げてきませんでしたが、そもそも今回のGraphQLのようなスキーマを持つAPIの場合、命名のうちデータ型を想起させる部分の単語については型の定義情報を合わせて見ればDateTimeと分かるんだからいいだろうという事情もありそうです。

となっているであろうことが分かります。僕は割と良い落とし所ではないかと感じました。自分が設計するときも参考にしたい。

まとめ的なもの

結局の所この辺りは設計上のトレードオフで、唯一の正しい正解がない中でbetterなものをプロジェクトごとに探す、ということなのではないかと思います。
こういった複数システム横断するものを設計するときは、一部だけを切り出して見たときの整合性に着目するのではなく、全体として見たときに設計思想が統一されているかを重視するのが大事です。

今回、フロント側だけの視点では「suffixがdateに統一されていないのは使いにくい・不整合だ」と感じたのかもしれませんが、システム全体としては date suffixとat suffixが混じっている方が統一されているという見方もできるわけです。

もしどうしても使っているフレームワークなどの事情から自分達に合わせた命名を使いたいということであれば、それはそのシステムの中で変換を噛ますという手もあります。RailsならActiveSupport#alias_attributeがありますし、他の言語・フレームワークでもこの手の外部APIと内部APIの命名をbridgeするような機能は大抵備えているでしょう(最悪setter/getterを作れば良い)。

そもそもAPIというもの自体異なるシステム間を繋ぐものなので、違う言語やフレームワーク同士を繋ぐ以上ある程度規則上の不整合はどうしても発生します。こういったときに「ここがこうなってるのはこういう事情によるものです」と納得のいく説明ができるかが設計上大事な部分であり、こういったものを積み重ねたものが「設計思想」になっていくのだと僕は思います。

ここからが本当の地獄だ(ゴゴゴゴ・・・・・

うん、きれいに説明できたな、ヨシ!で終わっても良かったのですが、さらなるカオスをチラ見せします。
GitHub APIのCommitオブジェクトにはpushedDateというものが存在します。

これまでの解説に従うと「GitHub側で付けた命名ならat suffixのはずだから、これはGitのプログラム内用語かな?」という推測になるのですが、Git自体にpushed_dateという概念はありません。ナゼダー 😇😇😇😇😇

morimorihogeの解釈
そもそもGitにおけるpushはリポジトリ間でコミットやブランチを転送するコマンドであり、いつ転送したかという情報はGitの興味の対象ではない、という事情からだと思われます。

元々Gitそのものは分散型リポジトリであり単一のoriginサーバーを前提としていないこともありますし、pushed_dateをもしGitデータ構造の中に持ってしまった場合、各所のリポジトリによってpushed_dateが違うとバイナリレベルでの互換性が保てない(保持しているpushed dateだけが違うリポジトリ間でpushしたときにconflictしてしまう)などの問題が発生することが予想されるため、Gitそのものがpushed dateを保持しないのは設計思想上そういうものだと思って良いでしょう。

ちなみにこの問題に説明を付ける候補は僕の中に数個あるのですが、どれも憶測でしかないこともあり、ここではあえて解説しないことにします。
こうした「答えがない中でbetterな正解を探す」のは良い頭の訓練になったり、チーム内の価値観すり合わせに繋がるので皆さんの開発チームなどでZoom飲み会の話題にしてみてもらってもいいかもしれません。

ではでは、色々書きたいことが他にもあるのですがとりあえず今は仕事に戻ることとします。

The post APIの命名規則はフロントエンド・バックエンドどちらに合わせるべきか? 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.


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: Logidze gemでActive Record背後のPostgreSQL DB更新をトラッキング(翻訳)

$
0
0

概要

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

日本語タイトルは内容に即したものにしました。また、元記事で同サイト別記事へのリンク切れを含む文は原著者の了解を得て削除しました。

なおLogidzeの最新バージョンは1.2.0です。

palkan/logidze - GitHub

  • Logidze 1.2.0の要件
    • PostgreSQL >=9.6
    • Ruby ~> 2.5
    • Rails >= 5.0(Rails 4.2では0.12.0以下を使うこと)

Rails: Logidze gemでActive Record背後のPostgreSQL DB更新をトラッキング(翻訳)

はじめに

Logidzeは、Active Recordの変更をトラッキングするRubyライブラリです。背後のPostgreSQLデータベースでレコードが更新されるたびにLogidzeが新旧ステートの差分を保存し、レコードの履歴で任意の時点にタイムトラベルできるようにします。Logidzeはこの5年間のRailsエコシステム内のモデル監査系gemとしては最速を誇り、このたびバージョン1.0になったことでさらに使いやすくなりました。

さて問題です。「ジョージア国のレモネード(#731」「Rails」「タイムトラベル」の共通点といえば何でしょうか?それがLogidze gemです。私が2016年に作ったこのライブラリは、最初のメジャーバージョンアップを迎えてめでたく大人の仲間入りを果たしました。本記事では、Logidzeの主要な機能を紹介するほか、Logidzeの完全な利用例として、弊社顧客向けの成熟した商用プロダクトで私たちが行った作業も紹介します。準備はよろしいですか?

Rails開発者にとってのLogidze導入は、gemをインストールしてジェネレータをいくつか実行し、以下のようにモデルに1行追加するだけで終わる、いたってシンプルな作業です。

class Post < ApplicationRecord
  has_logidze
end

post = Post.find(id)

yesterday_post = post.at(time: 1.day.ago)

Logidzeは以下のたった2つのアイデアで構成されています。

  • 変更のトラッキングにデータベーストリガーを利用: 混乱しがちでメンテの困難なActive Recordのコールバックも、速いとは言えないRubyやRailsのコードも使わず、高速かつ堅牢な昔ながらのPostgreSQLトリガーをシンプルに用いています。MySQLは「まだ」サポートされていませんが、ちゃんとしたプルリクがひとつ投げられれば可能です。
  • changelogはレコードデータのすぐ隣に保存(PostgreSQLのlog_data JSONBカラム)

このアプローチを PaperTrailAuditedといったその他の著名な監査系gemと比較したときの速度については、私が2016年にLogidzeをリリースしたときの過去記事をご覧ください。

私はかれこれ5年ほどLogidzeの開発を続けてきましたが、最近になってやっと巨大なRailsコードベース内で正しくドッグフーディングする機会に恵まれ、おかげで荒削りな部分を見つけて削ぎ落とすことができました。変更点についてはこの先をぜひ読み進めてください。

目次

🔗 Logidze 1.0: 構成からスキーマまで

最新リリースの大きな変更点は、デフォルトのRails wayである「schema.rbへのデータベーススキーマ保存」のサポートです。従来のLogidzeはSQL形式のstructure.sqlスキーマでしか動きませんでした。プレーンSQL形式のスキーマファイルが必要になったことのない方や、プレーンSQL形式のスキーマファイルを見たことのない方は、2種類のスキーマ形式について余すことなく解説されているAppSignalの良記事『Pros and Cons of Using structure.sql in Your Ruby on Rails Application』をご覧ください

変更点の完全なリストについては1.0.0リリースノートをご覧ください。

これまで私は常々、schema.rbではなく structure.sqlを選ぶのは「必要悪」だと考えていました。たしかにSQLをマージしたときのコンフリクトを解決するのはつらい作業ですし、schema.rbの方がずっと簡潔なのですが、schema.rbでは「トリガー」「シーケンス」「ストアドプロシージャ」「チェック制約(Rails 6.1でのみ追加)」といったPostgreSQLの一部の機能がサポートされていません。トリガーがなければ、高速なモデルトラッキングもLogidzeも存在しようがありません(昔はそうだったのですが)。

私がF(x)という「本物のgem」を発見したことで、世界が変わりました。このライブラリはRails専用で、SQLの関数やトリガーを別ファイル(.sql)内で宣言できるようにし、それらをRubyマイグレーションおよびschema.rbに「読み込む」APIを提供します。素晴らしいと思いませんか?

teoljungberg/fx - GitHub

なお、schema.rbをPostgreSQLのenum互換にできるactiverecord-postgres_enumや、同じくPostgreSQLのVIEW(データベースビュー)互換にできるScenicといったgemもあります。


そういうわけでLogidzeにF(x)を統合することを決め、その結果主要な問題を解決できました。Logidzeがschema.rbstructure.sqlのどちらもカバーできるようになったのです!


既にプロジェクトでfx gemがインストールされていれば、LogidzeがF(x)を用いて関数やトリガーを自動生成してくれます。どうぞお試しください。

Logidze ❤ F(x)

🔗 実際のActive Recordタイムトラベル

そろそろ業務に即した話に移りましょう。コードを書くのが好きだからというだけではなく、生きていくためにも。

問題点

この数か月、私と同僚でRetail Ziplineという小売業向けコミュニケーションプラットフォームの顧客向けの主要機能の強化や新機能の構築、全体のパフォーマンス向上、開発エクスペリエンス向上について同社のコア技術チームを支援してきました。

私の人生にLogidzeが再び舞い戻ってきたのは、同プラットフォームで重要な機能のひとつである「アンケート(survey)」機能でした。

簡潔にまとめると、アンケート機能では以下を行えます。

  • 管理者は、さまざまな種類(単一回答、複数選択回答、yes/no、記述式など)のアンケートを作成する
  • ユーザーは回答を送信する
  • 管理者はレポートを生成して履歴データを分析する

アンケートのデータモデル

アンケートのデータモデル

ユーザーが複数選択回答を送信すると、Responseのレコードを作成し、選択した回答のIDを含む#valueフィールド(背後のPostgreSQLはarray型)を設定します。後でこの回答を表示するには、以下のようなコードを使います。

# 近似的コード
module Surveys
  class Response < ApplicationRecord
    def display_value
      #  {answer.id => answer.value}形式のハッシュをビルド
      id_to_val = question.answers.index_by(&:id).transform_values!(&:value)

      # IDごとに値を抽出して文字列を形成する
      value.map(&id_to_val).compact!.join(", ")
    end
  end
end

どこが問題かおわかりでしょうか?

ここで問題なのは「回答がイミュータブルではない」ことです。アンケート作者は回答の削除も変更も可能になっています。実際のユーザーは、古い回答を捨てて新しく作るよりも、作成済みの回答を完全に変更することを好みます。つまり「ID=1」「値=”非常に重要”」という回答があるとすると、やがて値が”まったく重要でない”にすぐ変わってしまう可能性があります。これは問題です。

このため、履歴データがまったく信用できません。今表示されている回答の意味が当時も同じだったかどうかがわからなくなってしまいます。

私が今から何をしようとしているかは、もうおわかりですね。

ソリューション

私たちはこの問題解決のために、さまざまなアプローチを検討しました。

最も素朴で単刀直入な方法は「IDではなくナマの値を保存する」ことです。しかしこの方法はデータの整合性が損なわれるので、ほぼ一瞬で却下されました。もう少し手を加えた「IDと一緒にナマの値も保存する」方法もうまくいきませんでした。アンケートには多言語の訳文を持たせることができるので、回答をカレントユーザーのロケールで表示する必要がありました。この方法だとすべての訳文を保存する必要があり、ストレージのオーバーヘッドががかなり大きくなります。

さらに別の方法は「イミュータビリティ」を復活させることです。回答が更新されるたびに同じ#reference_idを持つレコードを作成し、古い方を論理削除する(レコードを削除せずにデータベース内で非表示にする)というものです。このアプローチでは、基本的にAnswerモデルをスナップショットベースで実装することになります。このソリューションの擬似コードは以下のような感じになります。

# app/models/surveys/answer.rb
module Surveys
  class Answer < ApplicationRecord
    #paranoia gemを利用した
    acts_as_paranoid

    before_create :assign_reference_id

    after_create :discard_previous_version

    private

    def assign_reference_id
      # 一意のid生成にはNanoid gemを使うのが個人的に好み
      self.reference_id ||= Nanoid.generate(size: 6)
    end

    def discard_previous_version
      self.class
          .where(reference_id: reference_id)
          .where.not(id: id)
          .update_all(deleted_at: Time.current) # 削除フラグは付くがDBには残る!
    end
  end
end
# app/controllers/surveys/answers_controller.rb
module Surveys
  class AnswersController < ApplicationController
    # updateは古い回答を更新せず新規回答を作成すべき
    def update
      answer = Answer.find(params[:id])

      #  重複には古いreference_idが含まれる
      new_answer = answer.dup
      new_answer.assign_attributes(answer_params)
      new_answer.save!

      redirect_to new_answer
    end
  end
end

これは完全にまっとうなソリューションですが、欠点もあります。Answerで変更されるのはテキスト値だけではなく、パラメータ集計などに用いるユーティリティフィールドもいくつかあり、実はこれらの方が頻繁に更新されます。テキスト値の更新とは対象的に、ユーティリティフィールドの更新はビジネスロジック上まったく問題ありません。これはつまり、モデル内で論理削除のためのチェックが増加し、同一の回答がコンカレントに更新されると競合する可能性があります(データベースロックで修正可能ですが)。これ以外にもエッジケースが潜んでいることは確かなので、より強固なバージョントラッキング手段が必要でした。

ここでLogidzeの登場です!以下は実装のサンプルコードです。

module Surveys
  class Question < ApplicationRecord
    has_many :answers

    # 削除済みも含めてすべての回答をLogidzeデータと一緒に読み込む
    # 特定の関連付けを追加する(デフォルトでは無視する)
    has_many :answers_with_deleted, -> { with_deleted.with_log_data },
             class_name: "Surveys::Answer",
             inverse_of: :question

    # ...
  end

  class Response < ApplicationRecord
    # Adds .with_log_data scope
    has_logidze

    def display_value
      # 論理削除された回答を扱うためにwith_deleted_answersスコープを追加した
      # これで、必要な回答だけをHash#sliceで返せるようになる
      # ActiveSupport:Enumerable#index_byについては以下を参照
      #  https://api.rubyonrails.org/classes/Enumerable.html#method-i-index_by
      answers = question.with_deleted_answers.index_by(&:id).slice(*value).values

      # 上によって、各回答で使われた全テキスト値を持つハッシュが
      # IDでグループ化されていい感じに取れるようになる

      answers.map do |answer|
        # ログを追加する前にレスポンスが作成されたら現在のステートにフォールバック
        answer = answer.at(time: created_at) || answer
        answer.value
      end.join(", ")
    end
  end
end

以上でおしまいです!これ以外の変更は一切不要です。コードにほんの数行を追加するだけでできます(もちろんLogidzeマイグレーションの実行もお忘れなく)。

なお、Logidze gemとApartment gem(データベースマルチテナンシー)を仲良くさせる作業も多少行い、その結果LogidzeドキュメントのTroubleshootingに新しいセクションを追加しました。

かくして、Logidzeを用いた実装はRetail Ziplineチームからゴーサインを頂戴し、めでたくproduction環境にリリースされました。

🔗 過去、現在、そして未来もまた

このgemを使い始めてから4年を経て、ようやくこのgemに依存したproduction向け機能をリリースできました。


自分で作ったgemをそれまで真面目に使わなかった理由ですか?AnyCableのときと異なり、Logidzeはオープンソースの実験として作ったものではなく、私自身が文字どおり「悪い火星人」になるためのテスト課題だったからです(私たちが自らを”evil”と呼ぶ意味がこれでおわかりでしょう🙂


Logidzeは、主にバグレポートやフィーチャーリクエストによって進化を遂げてきました。バージョン1.0になったことで安定性を獲得し、隅々まで仕上がりました。Logidzeが独自の設計で完成したのです。

今後についてですか?

今回のリリース作業では、PostgreSQLに関連するコードやテストの改善にかなり力を入れました。Logidzeは実際には70%がPL/pgSQL、30%がRubyとRailsでできています。バージョン2.0への最初のステップは、Railsへの依存を完全に消し去る作業であることは明らかです(Ruby != Railsですよ)。

それが終われば、さらに「LogidzeをPostgreSQLライブラリ化する(extensionやヘルパーなど)」作業に進めるようになります。SQLをRubyから切り離すことに加えて、差分計算も以下のようにデータベースに移動してみたいと思います。

SELECT posts.*,
       logidze_diff_from(
         '2020-10-09 12:43:49.487157+00'::timestamp,
         posts.log_data
       ) as logidze_diff
FROM posts

また、ログデータにクエリを書けるヘルパー関数もいくつか追加できます。よくあるユースケースのひとつとして、特定の時点に特定のフィールドに特定の値を持っていたレコードをすべて取得することが考えられます。

SELECT posts.*
FROM posts
WHERE logidze_log_exists(
  posts.log_data,
  'moderation_status',
  'suspicious'
)

最近のPostgreSQLではバージョン12でSQL/JSON Path Languageが追加されました。これは単に以下のシンタックスシュガーです。

SELECT posts.*
FROM posts
WHERE jsonb_path_exists(
  posts.log_data,
  '$.h[*].c ? (@.moderation_status == "suspicious")'
)

責務をアプリケーションコードからデータベースに移動するなんて、まるで道路を逆走するような話に思えるかもしれませんが、そんなことはありません。私見では、製品の機能をデータベースレベルで実装するのはよくないと思いますが、ある種の数値計算やユーティリティーコードをデータベースに移動するのは間違いなくOKです👌

ここまでお読みいただいた皆さんに感謝いたします。モデルトラッキングとPostgreSQLトリガーを(皆さんの責任のもとで)お楽しみください。本記事で解説したような課題に直面している方は、どうぞお気軽にEvil Martiansのフォームにてご相談ください。お客様の製品に私たちが力添えする方法を検討いたします。

本記事の翻訳や転載についてのご相談は、まずメールにてお願いします。

関連記事

AnyCable 1.0: RubyとGoによるリアルタイムWebの4年間(翻訳)


  1. 訳注: Logidzeはジョージア語(グルジア語)で、同issueでリンクされている動画ではカタカナだと「ラギーゼ」と発音しているように聞こえました。 

The post Rails: Logidze gemでActive Record背後のPostgreSQL DB更新をトラッキング(翻訳) 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.

Webのバッチ処理とオンライン処理のポイントとシステムの応答性能を学ぶ#3(社内勉強会)

$
0
0

バッチ処理を設計するうえでの注意点

バッチ処理を設計する際は、処理時間がどの程度になるのかを事前に計測・見積し、その結果を元に実行計画を立てるのが重要です。少なくとも、所要時間の見通しもなくバッチ処理を作成するのは危険です。

一般的にはデータ量が増えれば処理時間も増えるので、データ量が増えたときに処理時間がどのように延びるかは予め見積もっておきます。たとえば「データが1万件のときは20分、件数が倍になると処理時間が4倍増える」といったように、データ増加に応じた処理時間を予測できるように計測やベンチマークを事前に実施しておく必要があります。

データ量と処理時間の関係
データ量と処理時間の関係は一概に比例とは限らず、指数的に増加するケースもあれば、量が増えても誤差レベルに収束する場合もあります。

特に注意したいのは指数的に増加するケースです。開発時の数件~数十件程度のデータでは全く問題がなかったのが、本番稼働後に数千件程度データ投入した辺りで著しく性能低下するようなケースは設計の甘いケースで割と目にします。

パフォーマンスチューニングは開発段階ではYAGNI(You ain’t gonna need it: 必要になるまでは手を付けるべきでない)と見なされやすい部分ではありますが、開発中でも当初想定データの10倍くらいまでは「ここのデータ量が増えたらこの処理ってまともに動くのだろうか?」程度の想像を働かせておかないと、リリース直後の障害に繋がってしまう可能性すらあるので注意です。

特に、RubyやJavaのようなオブジェクト指向言語の場合、大量のレコードを一括処理する際にオブジェクト生成が絡むとメモリ不足で一気にパフォーマンスが低下したり、最悪プロセスが落ちてしまったりするといった問題に繋がる可能性があります。

実行時間計画とは別に、バッチが失敗したときにどのようにリカバリするかも考えておく必要があります。

バッチ処理は溜めておいたデータを一括処理するという性質上、バッチ処理の時点になってはじめて想定外データが見つかってエラーでバッチが止まってしまう、というのはよくある話です。

毎回バッチが一回で成功するはず、という甘い前提で見積もってしまうと、バッチに失敗した場合にリトライする時間を確保できなくなり、いわゆる「突き抜け」と呼ばれる事態に陥る可能性があります。

システム上ミッションクリティカルなバッチ処理については十分に余裕のある実行計画を立て、失敗した場合も想定してリスク管理しておくことが肝要です。

バッチ処理設計のポイント

一度実行したバッチは簡単にやり直すことができないので、慎重に設計しなければなりません。特に外部API(決済サービスなど)と連携したデータがある場合、対向システムの仕様によってはやり直しが不可能なこともあります。

バッチ処理を設計するうえで、一歩引いて考えておきたいポイントがいくつかあります。

1.「システムを停止しないと実行できない作業かどうか」に着目する

トランザクション管理などの難易度は上がりますが、システムを停止しなくても処理を完了できる道があるならば、原則としてオンラインバッチを検討します。マスタデータ洗い替えや、ロック時間がとても長いトランザクション処理など、オンライン処理と並行するのが現実的ではない場合には、システムの計画停止を伴うオフラインバッチで実装することになるでしょう。

2.「処理結果がatomicでなければならないか」に着目する

処理結果がatomicである必要があるということは「一連の処理をフェーズ分割することも、実行単位を小分けすることもできず、一つのトランザクションとして実行する必要がある」ということです。こうした処理の途中でエラーが発生した場合は、最初から実行し直す必要があります。

実行結果がatomicでなければならない場合は、処理が失敗した場合に正常にrollbackされるように実装します。実装の際は、処理失敗のやり直し時に同様のエラーで再失敗してしまう可能性を下げるため、できるだけログに詳しい情報を出力し、1回のやり直しで成功させられるように情報を残しておくことも重要です。

「重い」バッチ設計のコツ
再実行が完全なやり直ししかないような「重い」バッチを実装する場合のコツとして、元データのエラーチェックをメイン処理の中で逐次に行うのではなく、処理の前半にまとめて行っておくことで早めにエラーを発生させる、という方法があります。

1時間かかるバッチ処理の最初の10分でエラーが発生するのと最後の10分でエラーが発生するのでは、リトライのためにかけられる時間が大きく変わってきます(早く失敗するということはその分無駄になる時間が短いということです)。

データフォーマットチェックやNULLチェックなどの単純にチェックできるようなエラー処理はバッチ処理の前半に持ってきておくことで、バッチ実行失敗時のリスクを小さくすることが可能です。

他テーブルに跨がったデータ整合性チェックなどの複雑な条件は前処理でチェックが難しいですが、CSVやJSON/XMLなどに対する項目数や文字数チェック、フォーマット違反チェックなどは単純なコードで実装できる割に効果の高いものなので、実行時間の長いバッチを実装する場合には検討するとよいでしょう

3. データを小刻みに確定させる

処理したいデータ全体としてatomicにする必要がない場合には、atomicにしないといけない処理単位を小分けにして細かい単位で完了させていくという方法が有効です。

たとえば、ある締め処理が、特定の1ユーザーの異常データが原因で失敗した場合を考えてみましょう。全ユーザーをまとめたトランザクションとして設計すると、一人分でも異常なデータがあれば処理にかけた時間は全て無駄になりますが、1ユーザーごとのトランザクションで処理するようにしておけば、異常データのあるユーザー以外は正常に確定させることができます。

再実行の場合も、既に処理の終わったユーザーはスキップし、未処理のユーザーだけ処理するようなロジックを実装しておけば、全件処理し直す場合に比べて再実行が圧倒的に早く終わります。

バッチ処理の分割

バッチ処理をフェーズに分けてフェーズ単位で可能な形で適切に分割することで、個別の処理を確実に実行しやすくなります。
例えばリモートから大規模なCSVデータを取ってきて内容を解析しつつDBに取り込んでいくような処理の場合、

  • データ取得フェーズ(SCPやS3 Getなど)
  • 取得したデータのvalidationフェーズ(カラム数チェックやフィールド長チェック)
  • DB取り込みフェーズ

の3段階に分けることでやり直しがしやすくなります。

また、失敗した場合もバッチが分かれていることで「どのフェーズで失敗したか」がすぐに分かるというメリットがあるため、運用上も利点があります。

1. 個別のバッチが行う処理を明確化する

※ここで扱う用語はシステムや文化によって微妙に異なる名前になることがあります(タスク、ジョブネットなど)。適宜読み替えて下さい。

ジョブ(あるいはバッチジョブ)とは、連続または並列で動作するいくつかのバッチのまとまりを指す単位です。上の図で言うと、青が個別のバッチであり、それを取りまとめる「注文データ連携」がジョブになります。

バッチジョブは開発者とは別の運用側のエンジニアが監視・保守することも多いので、設計時に適度な粒度のバッチに分割するなどして、個別のバッチが何を行うのかを明快にしておきます

運用を見越して考える場合は、失敗した場合や想定より長く時間がかかってしまっている場合に「このバッチが失敗・未完了ならシステムにはこういう問題が発生するはず」というのが明確であることが望ましいです。

2. バッチの分割を検討する

第2のポイントは、時間のかかる処理をなるべく複数のバッチに分割することです。「データ取り込みのバッチ」「バリデーションのバッチ」「データ処理のバッチ」「データ保存のバッチ」というように、処理の内容に応じた分割はもちろん、アトミックでない処理ならたとえば1000件ずつに水平分割することも検討します。

繰り返しますが「バリデーション(単純データチェック)のバッチ」を分けておくのは非常に有用なので、優先的に分割しましょう。バリデーションのバッチは他でも使えることが多いので、独立したバッチにしておくと後々助かります。

3. バッチは再実行可能にしておく

最も重要なのは第3のポイントで、バッチをできる限り個別に再実行可能な設計にすることです。個別に再実行可能な設計になっていないと、原状復帰したうえでジョブを最初からやり直さなければならなくなり、大きな時間ロスになります。

目安として、たとえば1時間かかる処理を「20分ごとの再実行可能な処理」3つに分割しておくと、やり直しも20分単位で済みます。分割されていないと、最後の1分で失敗したらまた1時間かけてやり直さなければならなくなります。

あまりに細かい単位(秒単位で終わる処理など)に分割しても煩雑になりすぎるので、作業のしやすい単位での分割を心がけましょう。

また、互いに副作用を及ぼさない処理は同時並行での実行が可能です。たとえば「独立したマスタデータの取り込み」などは互いに影響を及ぼさないため同時に実行することで並列度を上げることが可能です。

一方で、並列度を上げる際にはリソース競合や枯渇が生じないよう注意が必要です。並列化については次で少し詳しく説明します。

並列化(同時実行)の注意点

処理の並列化は可能であれば実施することが望ましいのですが、安易に並列化すると逆効果になることもあります。

複雑なトランザクションはロジックそのものがテーブルロックなどに依存しているケースがあります。このような処理を並列化するとマルチスレッドで動作させてもリソース競合が発生し、実質シングルスレッドでしか処理できないので並列化の効果が薄くなってしまいます。

こうした場合は並列度を上げても処理の完了までの時間は短くならないので、高速化が必須ならボトルネック調査を行いCPUなりDBのスループットを上げるなどの対応が必要になるでしょう。

また、並列化を行うと何らかの理由で複数のバッチが同時に異常終了してしまった場合のリカバリは複雑になります(例:DBサーバーがメモリ不足で落ちてしまった、など)。

一般に処理は高速な程良いですが、メンテナンスや障害時のことを考えると要件に対して十分な速度が確保できている範囲では速度よりも設計のシンプルさを取るという選択肢もあるということは抑えておきたいです。

並列化の効きやすい処理

一般に並列化がうまく機能するケースを挙げてみましょう。

1. 巨大データのブロック処理

大量のデータをDBからフェッチして1000件ずつなどの「ブロック単位」に分け、分けられたブロックを並列化するという方法です。処理が「読み込み」や「計算」などのフェーズに分かれている場合は、うまく時間をずらして実行すると効率の向上が期待できます。

並列化及び1ブロック単位の実行件数は10であったり100であったりとさまざまですが、「このぐらいの個数に分ければよい」というような一般法則はありませんので、いい感じのポイントはベンチマークによって調べるのが良いでしょう。

なお、並列化の単位が細かすぎると(1件ずつなど)リソース競合の都合でかえって遅くなることもあるため、そういった意味でもベンチマークによる計測と並行して行うのが大事になります。

なお、実装する際には最低限「並列度」「1ブロックの件数」の二つは引数などで渡せるように作っておくと、ベンチマークが簡単にできるのでお勧めです。

2. 処理キューから処理を取り出して逐次処理する(Producer-Consumerパターン)

Sidekiqなどの処理キューに積まれたジョブの処理は、処理がジョブ単位にまとまっているので、ワーカーを増やすだけで並列化ができます。

代表的なユースケースは動画や画像の変換処理などで、1つ1つのジョブが他のジョブに影響を与えないジョブ同士の独立性がポイントになります。

ジョブを詰んで別のワーカーで処理するという非同期実行は昨今のリッチなフレームワークでは大抵備えているため(例:RailsのActiveJob)、基本はそうしたフレームワークの推奨利用方法に従って実装するのが良いでしょう。


関連記事

Webアプリのセッション管理とデータ保存を学ぶ#1(社内勉強会)

The post Webのバッチ処理とオンライン処理のポイントとシステムの応答性能を学ぶ#3(社内勉強会) first appeared on TechRacho.

Viewing all 1384 articles
Browse latest View live