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

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

$
0
0

更新情報:

  • 2016/08/25: 初版公開
  • 2020/12/03: 追記

⚓ utf8の4バイト文字問題は突然に

こんにちは、hachi8833です。

160823_0838_ldO8ik

MySQLのデータベースでencoding=utf8が指定されていると、UTF-8の文字長が4バイトの文字をデータベースに保存できなくなる、いわゆるUTF-8の4バイト文字問題、またの名を「寿司ビール問題」が発生することがあります(「MySQLのutf8の4バイト文字問題とは」で後述)。

BPSWebチーム部長のmorimorihoge さんがこの問題に対応したときの手順をメモします。

⚓ utf8からutf8mb4に移行する手順

MySQLのストレージエンジンはInnoDBが前提です。utf8mb4を指定するにはMySQLのバージョンが5.5以上である必要があります。

⚓ 1. 以下のコマンドでdumpを取る

mysqldump --no-create-info --ignore-table=mydata_store.schema_migrations -uroot mydata_store

⚓ 2. my.cnfに以下を追加してrestart(MySQL 5.7.9 より前のバージョンの場合)

innodb_file_per_table
innodb_file_format = Barracuda
innodb_file_format_max = Barracuda
innodb_large_prefix

Indexサイズの問題を回避するため、ファイルフォーマットをAntelopeからBarracudaに切り替えます。

⚓ 3. config/initializersに以下の定義が入ったファイルを置く

# MySQLでutf8mb4を利用する場合、ROW_FORMART=DYNAMICが必要
# ※my.cnfへの設定追加も必要なので注意
#
# refer: http://3.1415.jp/mgeu6lf5/
ActiveSupport.on_load :active_record do
  module ActiveRecord::ConnectionAdapters
    class AbstractMysqlAdapter
      def create_table_with_innodb_row_format(table_name, options = {})
        table_options = options.reverse_merge(:options => 'ENGINE=InnoDB ROW_FORMAT=DYNAMIC')
        create_table_without_innodb_row_format(table_name, table_options) do |td|
          yield td if block_given?
        end
      end

      alias_method_chain :create_table, :innodb_row_format
    end
  end
end

⚓ 4. database.ymlを以下に設定

 encoding: utf8mb4
  charset: utf8mb4
  collation: utf8mb4_unicode_ci

⚓ 5. db:migrate:resetする

これで全データが消えて、全テーブルがutf8mb4になります。

⚓ 6. mysqlコマンドで最初にdumpしたデータをインポートする

これで既に入っているデータを保持しつつ、utf8mb4にmigrationできるようになります。

⚓ MySQLのutf8の4バイト文字問題とは

MySQL のencodingやcharsetのutf8は、実は真のUTF–8ではなく、4バイト長の文字に対応していません。

160823_0940_JE6dBa

追記(2020/12/03): 2017年の記事「Sushi = Beer ?! An introduction of UTF8 support in MySQL 8.0 | MySQL Server Blog」より↓

歴史的な理由で、MySQLのutf8文字セットはutf8mb4ではなくutf8mb3を参照しています。3バイトのutf8文字セットは、Unicodeで定義されている文字のうち限定的なセットしかサポートしないので、基本的には基本多言語平面(BMP: basic multilingual plane)になります。追加多言語面(SMP: supplementary multilingual plane)の絵文字やその他の文字はサポートされません。同様に、追加漢字面(SIP: supplementary ideographic plane)に含まれる追加の漢字(CJK統合漢字
拡張B
: CJK unified ideographs extension B)もutf8mb3ではサポートされません。
mysqlserverteam.comの同記事より

ちょっとググるだけで、Railsに限らず、MySQLでこの問題を踏んだ多くのエンジニアの悲しい叫び声が続々と見つかります。

4バイト長UTF–8文字が問題になるのは、主に中国語と日本語です。中国語としても使われている一部の漢字が4バイト長になっていますが、一部が日本語でも人名や地名に使われることがあります。そのため、𠮷(吉の異字体)のようなマイナーな文字が使われている人名がテーブルに登録されて発覚することがあります。

参考: 第86回 「𠮷」と「吉」 | 人名用漢字の新字旧字(安岡 孝一) | 三省堂 ことばのコラム

英語圏ではこの問題に直面することはあまりなかったようですが、近年UTF–8の絵文字が多用されるようになり、絵文字の一部が4バイト長になっているため、近年は英語圏でも問題になっています。

UTF-8絵文字の中でも、特に寿司アイコンとビールアイコン(🍣🍺)が同値判定されてしまう問題が、2015年頃に「寿司ビール問題」と呼ばれるようになりました。「ケツカンマ問題」と並んで、問題を端的に表現した素晴らしいネーミングだと思います。

MySQL のバージョン5.5以降であれば、encodingやcharsetなどの項目にutf8mb4を指定することで4バイト長の文字に対応できるようになります。

とはいうものの、utf8が真のUTF–8でないことに変わりはありません。

MySQL側でutf8をUTF-8としての正しい挙動に変更したときの影響の大きさを考えれば、utf8mb4追加による対応は致し方ないという気もしますが、MySQLを初めて扱うエンジニアが踏みがちなブービートラップとして当分永らえそうに思えました。

追記(2020/12/03): 同じく「Sushi = Beer ?! An introduction of UTF8 support in MySQL 8.0 | MySQL Server Blog」には2017年の時点でデフォルト文字セットutf8mb4に移行する構想が述べられており、その後MySQL 8.0.1からはデフォルト文字セットがutf8mb4になりました

参考: MySQL8.0の文字コード設定 | blog.kotamiyake.me

⚓ メモ: コレーションについて

MySQLに限らず、RDBMS、そして自然言語を対象にインデックスを生成するあらゆるソフトウェアでは、コレーション(collation)の指定も重要です。

コレーションは、インデックス作成時にどの文字とどの文字を同値として扱うかという戦略を指定するためのものであり、要件に応じて適切なものを指定する必要があります。たとえば、検索時にカタカナの濁点・半濁点(「ハ」「パ」「バ」)を区別するかどうかに影響します。

コレーションの問題は寿司ビール問題と同時に発生することもありえますが、寿司ビール問題がMySQL固有のエンコード/文字セットの扱いの問題である一方、コレーションは普遍的なテーマなので、それぞれ別の問題です。

追記(2020/12/03): MySQLのコレーションについては、8.0.1でutf8mb4_ja_0900_as_csを含む以下のコレーションが追加されました。

-- mysqlserverteam.comより
mysql> select collation_name from information_schema.collations where character_set_name='utf8mb4' and collation_name like '%as_cs' order by collation_name;
+----------------------------+
| collation_name             |
+----------------------------+
| utf8mb4_0900_as_cs         |
| utf8mb4_cs_0900_as_cs      |
| utf8mb4_da_0900_as_cs      |
| utf8mb4_de_pb_0900_as_cs   |
| utf8mb4_eo_0900_as_cs      |
| utf8mb4_es_0900_as_cs      |
| utf8mb4_es_trad_0900_as_cs |
| utf8mb4_et_0900_as_cs      |
| utf8mb4_hr_0900_as_cs      |
| utf8mb4_hu_0900_as_cs      |
| utf8mb4_is_0900_as_cs      |
| utf8mb4_ja_0900_as_cs      |
| utf8mb4_la_0900_as_cs      |
| utf8mb4_lt_0900_as_cs      |
| utf8mb4_lv_0900_as_cs      |
| utf8mb4_pl_0900_as_cs      |
| utf8mb4_ro_0900_as_cs      |
| utf8mb4_sk_0900_as_cs      |
| utf8mb4_sl_0900_as_cs      |
| utf8mb4_sv_0900_as_cs      |
| utf8mb4_tr_0900_as_cs      |
| utf8mb4_vi_0900_as_cs      |
+----------------------------+
22 rows in set (0.02 sec)

参考: MySQL 8.0.1: Accent and case sensitive collations for utf8mb4 | MySQL Server Blog
参考: 寿司とビールについて話し合いをしてきました | エンジニアブログ | GREE Engineering


週刊Railsウォッチ(20201208前編)レガシーRailsアプリを引き継ぐときの6つの作業、サーバーレスプロジェクトをRailsに移行ほか

$
0
0

こんにちは、hachi8833です。

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

今回は、BPS昼の定例勉強会でつっつき会を行いました。

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

公式の更新情報から見繕いました。


つっつきボイス:「6.1 RC2が出ましたし、最終リリースまであとちょっとという感じになってきましたね」「お、今見るとマイルストーンでオープンのissueが0件になってる」「RC2でまたいくつかissueが追加される可能性はありそう」「最終リリースは年内ぐらいかな?案外来週ぐらいにしれっと出たりするかもしれませんけど」「Rubyとどっちが早く出るかな?」

その後、ウォッチ公開日のマイルストーンでオープンのissueは3件になっています。

⚓ 新機能: ERBでHashをHTML属性に変換できるようにする

ERBで以下のようにHashをHTML属性に変換して展開可能にする。

<input <%= tag.attributes(type: :text, aria: { label: "Search" }) %>>
<%# => <input type="text" aria-label="Search"> %>

ActionView::Helpers::TagHelper#tag_optionsの実装を利用してERBと属性変換を組み合わることで、テンプレートでHTML文字列をtagcontent_tagで置き換えなくてもやれるようにする。
同PRより大意


つっつきボイス:「attributesヘルパーメソッドが追加されたんですね↓」「上のようにハッシュをHTML属性として展開するのは前からできてたような気もしたけど、あれはSlimの書き方だったか」「ERBでもこれが使えるようになったのはいいですね👍

# actionview/lib/action_view/helpers/tag_helper.rb#L56
+       def attributes(attributes)
+         tag_options(attributes.to_h).to_s.strip.html_safe
+       end

参考: slim/README.jp.md at master · slim-template/slim

以下の記事によるとHaml 4以降でもできるそうです。

参考: HAML 4+ expands nested element attributes - makandra dev

⚓ 新機能: where.associatedで関連付けにデータが存在するかどうかをチェックできるようになった

以下は自分たちのアプリからの抜粋。

class Account < ApplicationRecord
  has_many :users, -> { joins(:contact).where.not(contact_id: nil) }
end

このuserは、contactのdelegatedy typeであるcontactablecontactableを置き換えて1件のuserをバックグラウンドで削除するというのはよくあるパターン。

上の書き方では関連付けの先にデータが存在するかどうかだけを知りたい場合に構文が少々煩雑になるが、これを以下のように書けるようになる。

class Account < ApplicationRecord
  has_many :users, -> { where.associated(:contact) }
end

これは#34727で追加されたwhere.missingの鏡写しになる。
同PRより大意


つっつきボイス:「元のjoins(:contact).where.not(contact_id: nil)は、joinした先のcontact_idが存在するかどうかだけを確認したいときに使いそうなクエリですね: これはたしかに悩ましくて、contactに制約が付いていない場合なんかだとcontactにcontact_idの外部キーが存在しない可能性があるので、関連付け先にデータが確実に存在するかどうかを確認するにはjoinsを書かないといけなくなりますが、改修後はwhere.associated(:contact)のように既に関連付けにデータが存在するという意味でassociatedを使って簡潔に書けるようになったということでしょうね」「なるほど」「こう書きたい気持ちはわかります」

「慣れるまではassociatedがコードで使われていてもすぐにピンとこないかも」「制約を付けて回避できるなら制約でやる方がいいでしょうね」「プルリクメッセージで引用されているwhere.missing↓はassociatedと対照的に、関連付けが存在しないものをフィルタで取り出すメソッド」

⚓ rails statsにCSSやERBの情報も表示するようになった

GitHubのモノリスのサイズ情報を得る方法を探していて、rails statsではapp/viewsディレクトリやapp/assets/stylesheetsディレクトリの情報が含まれていないことに気づいた。
これらのフォルダについても情報を出せば便利だと思う。このプルリクはこれらをViewsとStylesheetsという項目として追加する。


同PRより大意


つっつきボイス:「rails statsの出力項目に情報が増えましたね」「ビューとCSSが今までなかったのか」

# railties/lib/rails/code_statistics.rb#L43
-   def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|ts|coffee|rake)$/)
+   def calculate_directory_statistics(directory, pattern = /^(?!\.).*?\.(rb|js|ts|css|scss|coffee|rake|erb)$/)

なお手元のRails 6.0.3では以下のように表示されました。ここにはminitestのディレクトリも出力されていますが、試しにtestディレクトリを削除したら出なくなりました。

⚓ 新機能: リッチテキスト関連付けを一括でeager loadingできるようになった

Action Textはwith_rich_text_#{name}ヘルパーを提供して、リッチテキストの関連付けを楽にプリロードできるようになっている。これは1個のモデル上の1個のリッチテキストフィールドではうまくいくが、リッチテキストフィールドが複数の場合は個別のフィールドを読み込むためにActionTextテーブルへのクエリが繰り返される。

以下のようにモデルの中にユーザーが生成した動的なコンテンツが多数ある場合を考える。

class Page < ApplicationRecord
  has_rich_text :header
  has_rich_text :sub_header
  has_rich_text :content
  has_rich_text :aside
  has_rich_text :footer
  ...
end

Pageのすべてのコンテンツをeager loadingで表示しようとすると以下のようになる。

Page
  .with_rich_text_header
  .with_rich_text_sub_header
  .with_rich_text_content
  .with_rich_text_aside
  .with_rich_text_footer
  .find(params[:id])

この場合Railsが6回もクエリを実行するとことになる(1回はPageの読み込み、5回は個別のActionText読み込み)。

このプルリクはwith_all_rich_textを追加する。これはeager_loadを使い、has_rich_text関連付けへのリフレクションを行って、すべてのリッチテキストの関連付けを一括読み込みする。

Page.with_all_rich_text.find(params[:id])

その他
#37976によると現在のActionTextの内部は流動的とのことだが、この機能を今後のリリースに追加することに関心があるか、あるいはアプリケーション固有のヘルパーの方がよいかどうかをチェックして欲しい。
同PRより大意


つっつきボイス:「with_rich_text_*はテンプレートで複数使うこともありそうなので、それをwith_all_rich_textでひとつのクエリで書けるようにしたということか」「項目の数だけクエリが発行されなくて済むのはいいですね👍

# actiontext/lib/action_text/attribute.rb#L50
+     def with_all_rich_text
+       eager_load(rich_text_association_names)
+     end
+
+     private
+       def rich_text_association_names
+         reflect_on_all_associations(:has_one).collect(&:name).select { |n| n.start_with?("rich_text_") }
+       end

⚓ travel_toブロックで日時をStringで取るときにアプリのタイムゾーンが使われるよう修正

バグのように見えるが、ドキュメントにこの機能の説明が見当たらなかった。
travel_toは”2004-11-24 01:04:44″のようなstringを引数に取れるが、Stringで追加定義されるto_timeメソッドによってアプリケーションのタイムゾーン情報が失われてローカルに設定されてしまう。

# 現状
travel_to "2004-11-24 01:04:44" do
  Time.zone.now.to_s(:db) # => "2004-11-24 06:04:44" 
end 

# 期待する動作
travel_to "2004-11-24 01:04:44" do
  Time.zone.now.to_s(:db) # => "2004-11-24 01:04:44"  
end 

同PRより大意

参考: ActiveSupport::Testing::TimeHelpers

# activesupport/lib/active_support/testing/time_helpers.rb#L157
        if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
          now = date_or_time.midnight.to_time
+       elsif date_or_time.is_a?(String)
+         now = Time.zone.parse(date_or_time)
        else
          now = date_or_time.to_time.change(usec: 0)
        end

つっつきボイス:「バグ修正のようです」「例を見るとたしかにtravel_toにStringを渡した場合にタイムゾーンが変わってる」「内部でto_timeが呼ばれるとString形式の日時からタイムゾーンが落ちてたのか」「ここを意識したことがなかったということは、今までの自分はTimeオブジェクトを渡していたということかも」

travel_toはテストで使うメソッドでしたっけ?」「travel_toのブロック内だけ日時を変更してテストしたいときなどに使いますね」

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

⚓Rails

⚓ レガシーRailsアプリを引き継いだときにやること6つ

つっつきボイス:「記事の冒頭に、Rails 1.0がリリースされてから15周年とある」「もうそんなに経つんですね」「動画配信サービスのHuluもRailsとは知らなかった」「おそらくすべてではなくユーザーが目にするフロント部分などで使っているんでしょうね」

「ドキュメントの確認や整備、カスタムフォルダが追加されているかの確認などはいずれも大事」「カバレッジを100%に持っていくのは大変ですけどね」「ルーティングやDB構造、デプロイやstagingサーバーの構築なども同じく重要」

「この記事ではそうした作業が終わってからLinterを入れるのか: 動くことを確認してからlintをかけないとどこで壊れたかわかりにくくなることを考えれば、一理ある」「たしかに」「そうしてひととおり動くようになってコードをきれいにしてからRailsやgemのアップグレードを始める」「レガシーアプリに対応するときに順序としては基本的にこういう形になるでしょうね」「定番の作業項目をチェックできるのはよさそう👍


同記事見出しより:

  • 1. コードレビューとローカルセットアップ
    • ドキュメントの概要をレビュー(技術的負債もチェック)
    • テストをレビュー
    • ルーティングやデータベース構造をレビュー
    • 残っているカスタムフォルダをレビュー
  • 2. テストのカバレッジを100%にする(理想的には)
  • 3. デプロイ方法のチェックとstagingサーバーのセットアップ
  • 4. RuboCopとPrettierでコードベースにlintをかける
  • 5. stagingとproductionにデプロイする
  • 6. Rails、Ruby、gemをアップグレードする

⚓ サーバーレスプロジェクトをRailsに移行する(Ruby Weeklyより)


つっつきボイス:「AWS Lambdaで動かしていたプロジェクトがつらくなってきたのでRailsに引っ越したという記事です」「たとえばサーバーサイドで複雑な処理を行うような場合はLambdaに合わないことは考えられますね」「たしかに」「AWS Lambdaはマイクロなプロセスを発行することを主に想定していますし、マイグレーション的なしくみが組み込まれていないので、CRUD的なデータ操作や重たいバッチ処理を多用するような複雑な処理をLambdaですべてまかなうのはしんどいでしょうね」

参考: AWS Lambda(イベント発生時にコードを実行)| AWS

⚓ Matestack: HTMLやJSを書かずにRailsをリアクティブにするエンジン(Ruby Weeklyより)

matestack/matestack-ui-core - GitHub


つっつきボイス:「書き方がちょっと面白かったので拾ってみました↓」「Matestackを見た感じでは、Railsに独自フロントエンドを入れてすべてをRubyで書きたいという人は今も結構いるようですね」「Basecampが出しているstimulus jsも使いたくなかったりするのかな?」「まだ新しそうなので日本語圏では情報がなさそうですが、英語情報はそこそこ出始めてるみたい」

# 同リポジトリより
class Components::Card < Matestack::Ui::Component

  requires :body
  optional :title
  optional :image

  def response
    div class: "card shadow-sm border-0 bg-light" do
      img path: image, class: "w-100" if image.present?
      div class: "card-body" do
        heading size: 5, text: title if title.present?
        paragraph class: "card-text", text: body
      end
    end
  end

end

参考: Stimulus: A modest JavaScript framework for the HTML you already have.


以下のツイートはつっつき後に見つけました。


前編は以上です。

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

週刊Railsウォッチ(20201124)strict loading violationの振る舞いを変更可能に、Railsモデルのアンチパターン、quine-relayとさまざまなクワインほか

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

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

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20201209後編)Ractorベンチマーク記事、Railsで複合主キーを使う、AWS re:Invent 2020ほか

$
0
0

こんにちは、hachi8833です。

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

⚓Ruby

⚓ Ractorはどのぐらい速いか(Ruby Weeklyより)


つっつきボイス:「RubyやRailsのベンチマーク記事↓でおなじみのNoah GibbsさんがRactorのベンチも取ってみたそうです」「お〜」

Railsアプリに最適なAWS EC2インスタンスタイプとは(翻訳)

「初期セットアップのオーバーヘッドの表↓を見た感じでは、Ractorのその部分はスレッドより速そう」


同記事より

「次のベンチマークの表↓を見ると、Ractorはスレッドとほぼ同じぐらいで、標準偏差が他の項目より大きいのか」「ホントだ」


同記事より

「記事の末尾には、Ractorは2コアだと最大で16%速いこともあると書かれてる」「記事にもあるようにRactorはこれからの技術ですが、この結果を見ると少なくとも遅くはならなさそうですし、Ractorに本格的に取り組んでみるのはよさそうですね👍

この記事は翻訳してみたいです。

⚓ Rubyの内部関数のバグを修正した話(Ruby Weeklyより)


つっつきボイス:「-p-iのようなオプションはRubyのワンライナーで使われることがありますね」「どうやらRubyのそういうオプションをshebangに書いたときに内部でパースの結果をチェックしていなかったらしい↓」

# (同記事のshebangを無効な内容に置き換えたもの: これでも動いてしまう)
#!/some/invalid/dir/ruby -pi.bak

BEGIN {
  puts "It is starting!"
}

$_.gsub!(/perl/, "ruby")


同記事より

# 同記事より
$ echo "I like perl, it is my favourite language." > temp.txt

$ ruby script.rb temp.txt
It is starting!

$ cat temp.txt
I like ruby, it is my favourite language.

$ cat temp.txt.bak
I like perl, it is my favourite language.

参考: シバン (Unix) - Wikipedia

なお、-i.bakは拡張子を変えてバックアップするオプション、-p$_の値を出力するオプションだそうです。

参考: Rubyの起動 (Ruby 2.7.0 リファレンスマニュアル)

「こんなバグよく見つけましたよね」「shebangで-pi.bakみたいな指定って普通やらないと思いますけどね…自分はたぶん今後も使わないかも」「記事の最後でRubyにプルリク投げてマージされたそうです↓」

このバグ修正はRuby 2.6や2.7にもバックポートされるそうです(#17117)。

⚓ Rubyのdefault gemとbundled gemの違い(RubyFlowより)


つっつきボイス:「Bundlerのdefault gemとbundled gemの違いについて解説した記事です」「gem化されてないライブラリも含めると3種類になるとかがよく話題になりますね」「@hsbtさんのこの発表は、そのあたりや今後の動きについて詳しく解説してくれているのでとても参考になります↓」

参考: bundled gem と default gem の違い - @znz blog

⚓DB

⚓ Railsで複合主キーを導入する(Ruby Weeklyより)

参考: 複合主キーとは - IT用語辞典 e-Words


つっつきボイス:「Shopifyの記事です」「記事の前半ではShopifyがマルチテナントアーキテクチャであることなどを考慮して、通常の主キーに代えてshop_idorder_idによる複合主キーを使う方向に寄せることを考えたという話などをしていますね」「ふむふむ」

# 同記事より
class Order < ApplicationRecord
  self.primary_key = :id
  # 省略
end

「そして上のモデルに対応するのが以下のSQLテーブル定義↓ですが、お〜、これを見ると複合主キーはあくまでデータのローカリティ(局所性)を高める、つまりインデックス用にだけ使うけど、Railsのサロゲートキーは変えずに残しておくという考え方のようですね」「あ、そういうことですか!」「PRIMARY KEYにはshop_ididを指定しているけど、idのAUTO_INCREMENTは変えずに残しているあたりがそういう感じ」

-- 同記事より
CREATE TABLE `orders` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `shop_id` bigint(20) NOT NULL,
  -- (他のカラムは省略)
  PRIMARY KEY (`shop_id`,`id`),
  KEY `id` (`id`)
  -- (他のセカンダリキーは省略)
)

「ちなみにRailsには昔からcomposite_primary_keysという複合主キーの定番gemがあるんですけど↓、こちらは他のgemとの組み合わせなどによっては副作用が生じることもあるので、そういうときには他のgem向けにRailsのサロゲートキーが別途欲しくなることも考えられそう」

composite-primary-keys/composite_primary_keys - GitHub

「Shopifyのようにサロゲートキーと複合主キーを両方持つやり方はちょっと特殊に思えますが、マルチテナンシーなどの要件がうまくマッチすれば、複合主キーをこのように使うこともあるのかもしれませんね」「なるほど」「Shopifyのような実装は、一般的な複合主キーの実装ではあまり見かけない感じに思えますが、いろいろ興味深いです👍


「ところで、記事の中でlhm(large hadron migrator)という粒子加速器みたいな名前のツールが紹介されていました↓」「見た感じでは、Shopifyが作った独自のデータマイグレーションツールのようで、複合主キーとは直接関係なさそうかな」「Percona Toolkitを使った似たようなマイグレーションツールがあったかも」

shopify/lhm - GitHub

jbravata/percona_migrator - GitHub

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

⚓ AWS re:Invent 2020での各種発表(Publickeyより)

aws/eks-distro - GitHub


つっつきボイス:「そのうちまとめ記事が出そうですが一応」「今回のAWS re:Inventも、AWS LambdaがDockerコンテナをサポートしたとかいろいろ発表されていますね: この見出しの他には、AWS Lambdaの課金が100ミリ秒単位だったのが1ミリ秒単位に変わったという発表もありました↓」「あ、そうでしたか」「1ミリ秒課金になったことで、重いDockerコンテナを使うよりもAWSの既存のランタイムを使う方がよい局面も出てくるかもしれませんね(どこまでやるかにもよりますが)」

参考: [アップデート] Lambdaの実行時間の課金単位が1ミリ秒に短縮されました #reinvent | Developers.IO

「見出し最後のEC2 Macインスタンスは力技で面白かった」「社内Slackでbabaさんが『Mac miniを物理的にずらっと並べてるのか』ってウケてたヤツですね↓」「AWSのアナウンスにも、仮想化したmacOSではなくてMac miniコンピュータを使っていると書かれていますね」

「ただEC2 Macインスタンスはアナウンスにも書かれているようにminimum host allocation durationが24時間なので、ビルドのときだけ立ち上げるような使い方だとコストがかさみそうなのは今後改良して欲しいかな」「出先でPCとMacを両方使いたいけど2台も持ち運んでいられないような人にはありがたいかも」「将来M1のインスタンスも使えるようになったらまた違ってきそうですね」「これでリモート開発やリモートデバッグができるようになって、Macを買わなくてもiPhoneアプリを開発できるようになったらよさそう(Appleのライセンスでそこまで許されるかどうかにもよるでしょうけど)」

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

⚓ 2021年度のWebデザイントレンド


つっつきボイス:「来年のWebデザイントレンドか」「以下に日本語のまとめ記事もあります↓」

参考: ついに出た!2021年注目のWebデザイン人気トレンド9個まとめ - PhotoshopVIP

「Neumorphismは、新しいSkeumorphismということらしい」「スキュー?」「だいぶ昔のiPhoneのアイコンなどで使われていた実物っぽいデザインがたしかSkeumorphismと呼ばれていましたね」「Neumorphismの影の付き方とかがそれっぽいかも」

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

「Abstract Art Compositionは、20世紀前半のモンドリアンの絵みたいな感じなのかな」「こういう感じのデザインはSVGで軽量に作りやすそうですし、CSSのレンダリングやアニメーションと相性がよさそうなので、それもあって流行りつつあるのかなと思いました」

参考: Broadway Boogie Woogie - Wikipedia

「主に海外の流行のようなので、ものによっては日本に入ってくるまで時間差があるかもしれませんが、こういう記事を一度眺めておくと、今後デザイナーに「XXのような感じでお願いします」と大枠の指定を出したりするときに役立つと思います👍

⚓その他

⚓ Big SurとRuby


つっつきボイス:「ついに@jnchitoさんも開発環境をBig Surに移行」「そういえばHomebrewもBig Surで動くようになったらしいので↓、MySQLを複数入れたりもできるでしょうね」

参考: 「macOS Big Sur」に対応、M1での動作も可能になった「Homebrew 2.6.0」 - 窓の杜

「昔はMySQLやぽすぐれをHomebrewで複数バージョンインストールして管理してましたけど、今ならもうDockerでやりたい」「そうですよね」「もちろんローカル環境でたくさんのプロジェクトを扱うのでなければ今までどおりHomebrewなどでインストールすればいいと思いますけど、自分の場合はいろんなプロジェクトを扱っているので、昔みたいにMySQLやPostgreSQLやRedisのバージョンが3つずつぐらい同居すると、メモリを食いすぎるので手動で起動したりとかいろいろつらい」「ポート番号も使い分けないといけないのもつらいです😢」「Dockerはやっぱりありがたい」

なお、Docker Desktop for MacもBig Surに対応したそうです↓。

参考: Docker for Mac Stable release notes | Docker Documentation


後編は以上です。

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

週刊Railsウォッチ(20201124)strict loading violationの振る舞いを変更可能に、Railsモデルのアンチパターン、quine-relayとさまざまなクワインほか

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

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

Ruby Weekly

RubyFlow

160928_1638_XvIP4h

Publickey

publickey_banner_captured

速報: Ruby on Rails 6.1がリリースされました

$
0
0

12/09にRails 6.1が正式にリリースされました🎉

お気づきの点がありましたら@hachi8833までお知らせください🙇

⚓ 6.1の概要

何度も引用して恐縮ですが、@willnetさんによる以下のスライドとGistが概要を押さえるのに便利だと思います。

⚓ 今回のリリース情報でフィーチャーされている機能

  • マルチプルDB機能の強化
    • データベース単位でのコネクション切り替え
    • 水平シャーディング
    • その他の各種強化
  • 関連付けのstrict loading
  • delegated typing
  • 関連付けをジョブで非同期削除可能に
  • ActiveModel::Error
  • Active Storageの強化
  • 特定のdeprecation warning発生時にエラーを発生するオプション
  • 各種パフォーマンス向上
  • 各種バグ修正
  • 「クラシック」オートローダーが非推奨化(今後はZeitwerkに)
  • その他多数の改修

⚓ Rails 6.1のChangelog

Action Pack
47件(うち非推奨機能の削除約2件、機能の非推奨化約1件)
Action View
50件(うち非推奨機能の削除約24件、機能の非推奨化約1件)
Active Record
142件(うち非推奨機能の削除約10件、機能の非推奨化約4件)
Active Model
6件
Active Support
56件(うち非推奨機能の削除約13件、機能の非推奨化約4件)
Active Storage
32件(うち非推奨機能の削除約4件、機能の非推奨化約4件)
Active Job
16件
Action Mailer
4件
Action Mailbox
9件
Action Cable
5件
Action Text
5件

なお、この中でActive Supportのrequire_dependency:zeitwerkモードではobsolete扱いとしており、まだ非推奨化はされていないものの今後は順次利用しないようにして欲しいという記述が目に止まりました。

⚓ Rails 6.0->6.1アップグレードガイド(edge)の要点

  • Rails.application.config_forの戻り値のハッシュをStringキーで参照する機能がサポート終了(edgeガイド
  • レスポンスのContent-Typeヘッダーが6.0と異なる場合がある(respond_to#anyを使う場合など)(edgeガイド
  • ActiveSupport::Callbacks#halted_callback_hookの第2引数にコールバック名を書けるようになった(edgeガイド
  • コントローラ内のヘルパー読み込みにrequire_dependencyが使われなくなったことによる注意事項(edgeガイド
  • HTTPからHTTPSへのリダイレクトで使われるデフォルトHTTPステータスコードが308に変わった(edgeガイド
  • Active Storageでimage_processing gemが必須になった(edgeガイド

現在のedgeアップグレードガイドには記載されていませんが、@willnetさんのGistにある「CSRFトークンのエンコード形式の変更」「ActiveRecordのmergeメソッドの挙動変更」も6.1へのアップグレードの場合に念のためチェックが必要そうです。

⚓ 参考: RailsDiff

素のRailsアプリのバージョンごとのコンフィグdiff情報については以下のサイトでおおよそを確認できます(なおこのサイトはHTTPS化されていません)。

railsdiff/railsdiff - GitHub

⚓ おまけ: 6.1でrails new

Rails 6: Docker/docker-compose/dipで`rails new`力を取り戻す

記念写真を撮りたかったので、上の記事で作っておいた以下のリポジトリを元にRails 6.1アプリをサンプルとして作ってみました。

hachi8833/rails6_docker_quicksetup_sqlite3 - GitHub

  • リポジトリをクローンする
  • docker-compose.ymlでRubyを最新バージョンにする
# #L
x-var: &APP_IMAGE_TAG
  "my_app:1.0.0"
x-var: &RUBY_VERSION
+ "2.7.1-slim-buster"
+ "2.7.2-slim-buster"
x-var: &NODE_MAJOR
  12
x-var: &YARN_VERSION
  1.22.4
  • dipがインストールされていれば、後はdip provisionを実行するだけでrails newも含めて完了します。

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

  • 仕上げに、lvh.meで参照するのであれば、config/environments/development.rbでRails.application.configureブロック内に以下の一行を追加します。
Rails.application.configure do
  # (略)
 config.hosts << "lvh.me"
  # (略)
end
  • dip rails sを実行し、ブラウザでlvh.me:3000を開きます。

Pumaの起動メッセージが少し賑やかになっていますね。

開始から10分かからずにrails newできました。次のRuby 3.0が楽しみです。

週刊Railsウォッチ(20201214前編)Rails 6.1の直近コミットを見る、RuboCop Rails 2.9リリース、ar_lazy_preload gemほか

$
0
0

こんにちは、hachi8833です。Rails 6.1がリリースされましたね。

速報: Ruby on Rails 6.1がリリースされました

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

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

今回はいつもと趣向を変えて、6-1-stableブランチの直近のプルリクを中心に見繕いました。したがって、以下のいずれのプルリクも6.1にマージされています。

⚓ 6.1: バリデーション時のstrict loadingを修正

strict loadingはバリデーション時にエラーを出して欲しくない(バリデーションのためにレコードを読み込む必要があるため)。
今回の変更では、owner.validation_contextをチェックするようにした。これがnilの場合は、createやupdateでオブジェクトを現在バリデーションしていないことがわかる。それ以外の値が設定されている場合はバリデーション中なので、strict loadingでraiseをスキップしたい。
同PRより大意


つっつきボイス:「validation_contextとは…?」「なるほど、strict loadingを有効にすると、バリデーションの実行中にエラーになったときにraiseされてしまっていたので、バリデーション中はエラーを投げないようにしたということのようですね」

# activerecord/lib/active_record/associations/association.rb#L212
      private
        def find_target
-         if owner.strict_loading?
+         if owner.strict_loading? && owner.validation_context.nil?
            Base.strict_loading_violation!(owner: owner.class, association: klass)
          end

-         if reflection.strict_loading?
+         if reflection.strict_loading? && owner.validation_context.nil?
            Base.strict_loading_violation!(owner: owner.class, association: reflection.name)
          end

「テストコードを見ると、AuditLogRequiredモデルのrequired: trueがバリデーションに失敗したときにralseしないことをチェックしている↓」

# activerecord/test/cases/strict_loading_test.rb#L89
  def test_strict_loading_is_ignored_in_validation_context
    with_strict_loading_by_default(Developer) do
      developer = Developer.first
      assert_predicate developer, :strict_loading?

      assert_nothing_raised do
        AuditLogRequired.create! developer_id: developer.id, message: "i am a message"
      end
    end
  end

「このプルリクの日時を見ると、わずか15時間前(つっつき時点)にマージされてたんですね」「Rails 6.1がリリースされる直前じゃないですか」「間に合ってよかった🎉

⚓ 6.1: I18n.translateでキーがStringの場合に対応

# actionview/lib/action_view/helpers/translation_helper.rb#L70
      def translate(key, **options)
        return key.map { |k| translate(k, **options) } if key.is_a?(Array)
+       key = key.to_s unless key.is_a?(Symbol)

        alternatives = if options.key?(:default)
          options[:default].is_a?(Array) ? options.delete(:default).compact : [options.delete(:default)]
        end

つっつきボイス:「translateのキーがシンボルでなくStringの場合も対応するようになったのね」「前はStringでもできてたような気がしたけど、#39989のコメントを見ると、そこでパフォーマンスを改善したときにto_sを外したことでキーがたとえばIntegerの場合にうまくいかなくなると指摘されているので、それを受けて上の#40773でキーがシンボルでない場合にも対応できるよう修正したという流れのようですね」「なるほど!」「translateのキーに数値を入れることは実際にはあまりなさそうですけどね」

「要するにシンボルでなかったら数値でも何でも文字列にしとけば大丈夫と」「Rubyのto_sはObjectクラスにあるので、Objectクラスを継承するオブジェクトは必ずto_sで文字列に変えられますよね」

ドキュメント: Object#to_s (Ruby 2.7.0 リファレンスマニュアル)

⚓ 6.1: Ruby 3.0のStringの挙動修正に対応

Ruby 3では、Stringクラスのメソッドがサブクラスのインスタンス上で呼び出されたときにStringクラスのメソッドが常にStringのインスタンスを返すという非互換の変更が導入された。
https://bugs.ruby-lang.org/issues/10845
ruby/ruby#3701

これはStringのサブクラスであるActiveSupport::SafeBufferにわずかに影響するので、SafeBuffer#[]SafeBuffer#*でRuby 2の振る舞い(別のSafeBufferインスタンスを返す)をRuby 3でも維持するパッチを用意した。

なお、Ruby 3.0で変更されたメソッドのほぼすべては、既にSafeBufferでStringを返すためのオーバーライドが完了しているので、このテストをパスするために必要なパッチはこの2つだけ。
同PRより大意


つっつきボイス:「@amatsudaさんによるプルリクです」「Ruby 3.0の足音が聞こえてきそう」「修正の経緯について#10845に書かれているみたい↓」

「まず、従来のRubyではStringを継承したクラスが返すオブジェクトが以下の*+%のように不揃いだった↓のが、Ruby 3.0でStringを返すように統一された」

# 10845より
class MyString < String
end

MyString.new("foo").*(2).class                        #=> MyString
MyString.new("foo").+("bar").class                #=> String
MyString.new("%{foo}").%(foo: "bar").class #=> String

「RailsのSafeBufferは以下のようにStringを継承しているので、Ruby 2.xまではSafeBufferを返していたメソッドがRuby 3.0ではStringを返すように変わってしまったということか」

# activesupport/lib/active_support/core_ext/string/output_safety.rb#133
module ActiveSupport #:nodoc:
  class SafeBuffer < String

...

    def [](*args)
      if html_safe?
-       new_safe_buffer = super
+       new_string = super

-       if new_safe_buffer
-         new_safe_buffer.instance_variable_set :@html_safe, true
-       end
+       return unless new_string

+       new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
+       new_safe_buffer.instance_variable_set :@html_safe, true
        new_safe_buffer
      else
        to_str[*args]
      end
    end

...

    def *(*)
-     new_safe_buffer = super
+     new_string = super
+     new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
      new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
      new_safe_buffer
    end  

「それを上のように、返すものがSafeBufferでない場合はSafeBuffer.newすることでRuby 2.xと3.0で挙動が変わらないよう三項演算子で修正したんですね」「あ、なるほど!」「Ruby 2.xではSafeBufferを返すのでこれまでと同じ挙動になる」「Rubyのバージョンをチェックするif文を書かずに挙動を揃えているのがうまいですね😋


後で以下の#3701を見ると、Ruby 3.0のStringクラスの#+#-以外のメソッドはすべて、StringのサブクラスのインスタンスではなくStringインスタンスを返すよう統一されるんですね。#3701のコメントの中でもSafeBufferについて言及されていました。

⚓ 6.1: Relation#mergeの利用法をガイドに追加

現在のActive Record クエリインターフェイスガイドの「結合されたテーブルで条件を指定する」にはRelation#mergeの利用法を示すサンプルがない。
既存のサンプルは比較的基本的なjoinedテーブルの条件を生成するにはよいが、Relation#mergeは高度なSQLクエリ生成や既存の名前付きスコープの利用に欠かせない。
同PRより大意


つっつきボイス:「Relation#mergeのドキュメントはたしかに欲しいですね」「こういう情報がガイドに追加されるのはありがたい🙏

# 同PRのガイドより
class Order < ApplicationRecord
  belongs_to :customer
  scope :created_in_time_range, ->(time_range) {
    where(created_at: time_range)
  }
end

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).merge(Order.created_in_time_range(time_range)).distinct

⚓ 6.1: require_dependencyhelperから削除

これだけ今年5月にマージ済みのプルリクです。

プルリクの動機は以下の2本立て:

  • require_dependencyは現在フレームワークから段階的に削除を進めている
  • config.add_autoload_paths_to_load_pathが無効の場合にhelperが動くようにする
    同commitより大意

コントローラのhelperクラスメソッドは、stringやsymbolで指定されているヘルパーモジュールをrequire_dependencyではなくString#constantizeで読み込むようになった
Action Pack Changelogより


つっつきボイス:「使わないことになったrequire_dependencyがRailsフレームワークに残っていたのを削除したんですね」「Rails 6.1のアップグレードガイド(edge版)を見て知りました」

参考: 2.6.3 require_dependencyについて — Rails アップグレードガイド - Railsガイド

require_dependencyの既知のユースケースはすべて排除されました。自分のプロジェクトをgrepしてrequire_dependencyを削除してください。
railsguides.jpより

⚓Rails

⚓ RuboCop Rails 2.9がリリース


つっつきボイス:「@koicさんのツイートでこれを含むさまざまな更新情報を知ることができて助かります🙏」「え、今日ちょうどRuboCopのバージョン上げたところなんですけど😳」「こちらはRuboCop Railsなので本体のRuboCopとは別物ですね」「よかった〜」「RuboCop本体もこの後Rubyのコーナーで取り上げます(明日のウォッチに掲載します)」

rubocop-hq/rubocop-rails - GitHub

RuboCop Rails 2.9.0リリースノートの変更点より:

  • RuboCop 0.90以上が必須になる
  • Rails/SquishedSQLHeredocsをunsafeに変更

⚓ acts_as_tenant: マルチテナンシーgem(Ruby Weeklyより)

ErwinM/acts_as_tenant - GitHub


つっつきボイス:「READMEのこのサンプルコード↓ではサブドメインでテナントを分けていますね: サブドメインでマルチテナント化する設計はよく使われていて、Slackなどでも行われています」

# 同リポジトリより
class ApplicationController < ActionController::Base
  set_current_tenant_by_subdomain(:account, :subdomain)
end

「ただ、マルチテナントの要件はプロジェクトごとに詳細がいろいろ異なるんですよ: acts_as_tenant gemはまだ使ったことはありませんが、詳細を把握しきっていないgemでマルチテナンシーすることを考えると、おそらく自分ならマルチテナンシーの機能を自分で実装する方を選ぶことが多いかもしれませんね」「あぁ、たしかに!」「gemが案件にフィットするかどうかは案件の成長なども含めて検討しておく必要があるでしょうね: acts_as_tenantはたぶんそういう種類のgemだと思います」

「もちろん、それまで十分使い慣れていて、要件に合致することが確かめられているなら使ってもよいと思います👍」「acts_as_tenant gemは歴史もあるしサポートも継続しているようなので、その点は大丈夫そうですね」

「Railsの認証機能でよく使われるDevise gem↓などもそうなんですが、gemで機能を取り入れるということは、そのgemの機能の範囲で構築せざるを得なくなることでもありますよね」「そうなんですよね…」「gemの詳細や案件との調和をよく調べないうちにDeviseのようなgemを安易に導入すると、後がつらくなるから気をつけようという話もよく目にします」「はい、身に沁みてます😅

heartcombo/devise - GitHub


「そういえば、昔のRails向けgemにはacts_as_なんちゃらという名前がよくあったというお話しを以前されてましたね」「昔はそういうネーミングのライブラリが多かったんですが、そういう名前でもライブラリが古いとは限らないでしょうね」「あ、そうか」「このacts_as_tenantは新しいのかな?」「リリースをさかのぼってみると2012年からありますね」「ホントだ」「★はあと少しで1000になるくらいかな」「おそらく今話したみたいに、Railsのマルチテナンシー機能については案件に応じて自前で実装することが多いのかもしれませんね」


追いかけボイス:「後でacts_as_tenantの実装を少し追いかけてみた限りでは、Deviseと比べてだいぶ薄めのgemのようです」「なお、マルチテナンシーだとapartmentというgem↓もあって、もしかするとこちらの方がメジャーかもしれません」

influitive/apartment - GitHub

⚓ ar_lazy_preload: GraphQLでも役立つlazy load gem(Ruby Weeklyより)

DmitryTsepelev/ar_lazy_preload - GitHub

TechRacho翻訳記事でお世話になっているEvil Martiansがスポンサーになっています。


つっつきボイス:「このgemの#lazy_preloadメソッドを使って読み込んでおくと、たとえば後でmapしてもクエリを1回しか実行しなくなるということか↓」

# 同リポジトリより
users = User.lazy_preload(:posts).limit(10)  # => SELECT * FROM users LIMIT 10
users.map(&:first_name)

「コンフィグでlazyなオートプリロードをオンにすることもできる↓」

# 同リポジトリより
ArLazyPreload.config.auto_preload = true

#preload_associations_lazilyも使える↓」

# 同リポジトリより
posts = User.preload_associations_lazily.flat_map(&:posts)
# => SELECT * FROM users LIMIT 10
# => SELECT * FROM posts WHERE user_id in (...)

「READMEを眺めた限りでは比較的シンプルな機能のgemみたい」「lib/を覗いてみても、コンフィグも少ないし、比較的シンプルそうですね」「同じコードを自力で書くよりはこういうgemでやる方がよさそう👍


ArLazyPreloadは、関連付けのlazy loading機能をRailsアプリケーションに導入するgemです。N+1クエリ問題を解決するRails組み込みメソッドはたくさんありますが、プリロードする関連付けのリストが明確でない場合があります。そんなときはこのgemで大半をカバーできます。

シンプル
利用に必要なのは、#includes#eager_load#preload#lazy_preloadに置き換えることだけです。
高速
ベンチマークをご覧ください(TASK=benchTASK=memory)。
GraphQLとの親和性
読み込む関連付けのリストをトップレベルのリゾルバで定義すれば後はこのgemにおまかせ
オートプリロードのサポート
関連付けのリストを指定したくない場合はArLazyPreload.config.auto_preloadtrueに設定します。

同リポジトリより

「READMEにはGraphQLでも便利と書かれていますね」「以下のような感じで、GraphQLのリゾルバでカラムを取得してからmapするような操作はGraphQLでよく使うので、たぶんそれを指しているんじゃないかな」

# 同リポジトリより
users = User.lazy_preload(:posts).limit(10)  # => SELECT * FROM users LIMIT 10
users.map(&:first_name)

「GraphQLでは、最終的に欲しいカラムをGraphQLのリクエスト側で指定できるんですが、おそらくこのgemの機能を使うと、GraphQLリゾルバの直前まではActive Recordのリレーションのまま加工して、最後の最後でGraphQLからのカラムを渡すと、そのカラムでSELECTするクエリを発行して結果を返す、という感じでクエリ発行が1回で済むようにするのがやりやすくなるんでしょうね」「なるほど!」「なかなかよさそうなgemですね👍

参考: GraphQL - Resolvers


前編は以上です。

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

週刊Railsウォッチ(20201209後編)Ractorベンチマーク記事、Railsで複合主キーを使う、AWS re:Invent 2020ほか

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

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

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20201216後編)Ruby 3.0.0-rc2とRuboCop 1.6がリリース、Ruby 3の静的型解析記事、CentOS 8のEOLが短縮ほか

$
0
0

こんにちは、hachi8833です。

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

⚓Ruby

⚓ Ruby 3.0.0-rc2リリース(Ruby公式ニュースより)


つっつきボイス:「Ruby 3.0.0のRC2がついに出ましたね」「Rubyはクリスマスにメジャーリリースするのが通例なので、もう後2週間🎄」「2.xから3は大きいですけど、無事に出てくれるかしら?」「大丈夫だと思いますよ」「今もTypeProf周りとかにゴリゴリ手を入れているようですし、たぶんギリギリまで作業するでしょうね」

「あ、今になってRuby 3.0アドベントカレンダーがあることに気づいた↓(後で読もうっと)」

参考: Ruby 3.0 Advent Calendar 2020 - Qiita

つっつきの後で、上の記事の中から銀座Rails#26で@osyoさんが発表したスライドを見つけたので貼っておきます。

他のRuby関連アドベントカレンダーも貼っておきます。

参考: Ruby Advent Calendar 2020 - Qiita
参考: Ruby その2 Advent Calendar 2020 - Qiita
参考: 一人 bugs.ruby Advent Calendar 2020 - Qiita

⚓ Rubyがruby-build-v20201210でM1チップでのビルドに対応

「M1チップ上で、インテルバイナリでないarm64バイナリのRubyが動くようになったとは」「お〜すげ〜!」「M1 Macまだ持ってないけど嬉しいです😂

⚓ Ruby 3.0の静的型解析関連記事


つっつきボイス:「そうそう、@mameさんの静的型解析関連記事が2本も出ていましたね」「@mameさんの解説が記事の形でみっちり書かれているのが本当にありがたい🙏」「動画を見るには集中力も必要だし、高度な話題を扱うとたまについていけなくなりますけど、文章で書かれていると自分のペースで読めますよね」「Ruby 3.0の静的型解析周りの機能については@jnchitoさんもやっているように自分で触って理解するのがいいでしょうね」「後で読まなきゃ」

「@mameさん記事の冒頭で、RBSとTypeProfとSteep/Sorbetの違いを3行でまとめてくれているのも助かる!」「新しいのでどれがどれだかたまにわからなくなりがちでした😅」「Ruby 3にバンドルされるのがRBSとTypeProfで、SteepやSorbetは自分で導入できるツールということか」

soutaro/steep - GitHub

sorbet/sorbet - GitHub

「今やってるRailsアプリ、Ruby 3.0にアップグレードしようかな、どうしようかな?」「動作確認してみるのはよいと思います!」「後はRailsで使うgemがどのぐらい3.0で動くかでしょうね」「あ、それもそうか」「しばらくは動作確認の期間になるかな」

⚓ RuboCop本体も1.6がリリース


つっつきボイス:「こちらはRuboCop本体のアップグレードです」「え、RuboCop本体はちょっと前にやっと1.0になった気がするんですけど、もう1.6ですか!この間やっとRuboCopを0.8xに上げたところなのに…」「リリース履歴を見てみよう↓」

「1.0のリリースが今年2020年の10月!」「1.3も28日前ですね」「わずか2か月で1.6とは」「1.6にいたってはsome hours agoですね(つっつき時点)」「これは早い」「怒涛の勢い」「RuboCopは1.0になるまでが長かったですけどね」

「自分のRailsアプリなどでRuboCop本体を最新のものにアップグレードすることについては基本的に問題はないと認識してます(rubocop.ymlの更新はそれなりに手間だと思いますが)」

⚓ Rubyコードにendが余分にあるエラーをsyntax_search gemで修正する(Ruby Weeklyより)

zombocom/syntax_search - GitHub


データベースのランダム読み出しは要注意(翻訳)

つっつきボイス:「TechRacho翻訳記事でもお世話になっているRailsコントリビューターの@schneemsさん↑の最近の記事で、Rubyコードのendが1個多いエラーを解決するためにsyntax_searchというツールを使ったそうです」「余分なendがどこにあるのかわかりにくいのあるある😆

Syntax Search: Extra end

「ところでこれってIDEとかのlinterにかければ一発でできるのでは?」「あ、それもそうか」「でもこうやって直したくなった気持ちもわかる」

⚓ RubyのRange#bsearchでは0と無限大の中間値が「1.5」になる(Ruby Weeklyより)


つっつきボイス:「Range#bsearchはRubyの二分探索(binary search)メソッド」「0と無限大の中間値って数学的にはどうなるんでしたっけ?」「数学好きなkazzさんが今日のつっつきにいてくれれば聞けたのに😢」「記事によるとRubyのRange#bsearch的には1.5が中間値ということになるのかな?」「0とinfiniteの中間値を取るのがそもそもありなのかどうかが気になる…」

参考: Range#bsearch (Ruby 2.7.0 リファレンスマニュアル)
参考: 二分探索 - Wikipedia

「記事ではビット表現を図解してそのあたりを追求してますね↓」


同記事より

⚓DB

⚓ Slackで使われているVitess(DB Weeklyより)

vitessio/vitess - GitHub


つっつきボイス:「Vitessは以前ちょっとだけウォッチで取り上げたことがあって(ウォッチ20181225)、YouTubeのバックエンドでも使われていたそうです」

「記事の冒頭を見ると、Slackも立ち上がりの頃はLAMPで構築されていたと書かれているところに歴史を感じますね」「ランプ?」「Linux、Apache、MySQL、PHPのことで、昔からよくある組み合わせです」「あ、そういう言葉があるんですか😳」「今のSlackの規模だとMySQL単体ではもう無理でしょうね」

「Vitessはこの図のような概念らしい↓」「記事によるとSlackのSQLトラフィックの99%がVitessに置き換わったそうです」


同記事より

「VitessのバックエンドはMySQLなのか」「MySQLをこういう感じにドーピングというか拡張するツールは昔からいろいろありますね」「Vitessは配下にあるMySQLサーバーのテーブル群をクラスタリングしてアクセスできるようadaptしてくれるみたい」「Vitessはこの図のような感じでクエリを分散したりシャーディングをすごく賢く行うなどの最適化を図るんでしょうね↓」


同記事より

「Vitess面白そうだけど、後から導入するのは大変なんでしょうか?」「この種の製品は、アプリケーションからVtGateにアクセスするときはMySQLと同じにやれるようにすると思うので、たぶん移行はそこまで大変じゃないと思いますけどね」「アプリケーションからするとつなぎ先を変えるだけという感じですか?」「アプリケーションコードを変える部分は少ないと思います」「お〜!」

「たぶん別製品だったと思いますけど、このような感じで既存のDB接続に挟んでいい感じに強化するツールの営業を昔受けたのを思い出しました」「そういえばHAProxyなんてのもあったかも↓」「HAProxyはVitessと設計上の位置が割と近そうですが、Vitessはテーブル同士のlocalityとかまで考慮してクラスタリングするようなので、よりDB特化したツールのイメージを感じますね」「Vitessはオープンソースなのか」「この種のソフトウェアは商用が多いです」

参考: HAProxy - Wikipedia

「この記事↓をざっと見た感じでは、Vitessにはkeyspaceという概念があるとのことなので、アプリケーションからはVitessのデータベースとして見えるようですね」「なるほど」「おそらくスキーマ定義さえされていれば、普通にクエリを投げるだけならMySQLとほぼ等価に扱えそうな雰囲気は感じられる」「そこから先は記事やドキュメントを詳しく読まないとですね」

参考: Vitess(ヴィテス)をさわってみよう!(Part 1) | スマートスタイル TECH BLOG|データベース&クラウドの最新技術情報を配信

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

⚓ 書籍『イラストでわかるDockerとKubernetes』


つっつきボイス:「はてブでバズっていました」「図を多用してわかりやすく解説するのは大事ですね👍」「Kubernetes界隈は動きが早いので、最新情報を取り入れるにもよさそう」「5年前と比べたらKubernetesも相当変わってるんだろうな…」

⚓言語/ツール/OS/CPU

⚓ CentOS 8がサポート期間を繰り上げて2021年に終了

参考: CentOS 8が2021年で終了することに関する基礎知識 - orangeitems’s diary


つっつきボイス:「CentOS 8の件、これを踏んだプロジェクトはつらいでしょうね」「CentOS使っているところは今後どうするか相談中みたいですよ」「そんなに影響あるんでしょうか?」「SI方面でよくある、パッケージのデフォルト構成を変えずに使いたいような要件の固い開発プロジェクトが主に影響を受けると思いますし、実際CentOSはそういうところでよく使われているんですよ」「なるほど、ちょっと想像が付きました」「CentOSは日本で使っているところが割と多いですね」

「この記事↓にもありますけど、今回CentOS 8のEOL(end-of-life)が大きく短縮されたために、今ではCentOS 7の方がEOLが長いんですよ」「まさかEOLが逆転するとは思いませんよね」「CentOS 7を使っているところはEOLが変わらないので難を逃れられますけど、CentOS 8を使っているところは…😇

「たとえばPHPの5系のような古いバージョンはPHPの公式ではサポート終了していますが、CentOSだとその後もサポートされていたりするんですよ」「そういえばCentOSのPHPって割と古かった覚えがあります」「CentOS 7ですら、今PHPのデフォルトバージョンを調べてみると5.4ですね」「そんなに前のバージョンだったとは…」

「そのようなPHPはPHP公式では既にサポートされなくなっていますが、CentOSでは致命的な脆弱性についてはパッチを当て続けてくれていたんですよ」「そうそう」「古いPHPが必要なプロジェクトでは、あえてCentOSを選んで主要なパッチがサポートされるようにすることもあります(すべてをサポートするわけではありませんが)」「PHPのバージョンを上げるのが大変だったのを思い出しました😢」「PHPが古いとフレームワークのバージョンも上げられなかったりしますよね」「CakePHPも2から3に上げるのは大変だって聞きました」「2から3はアーキテクチャが変わるので大変です」(以下延々)

参考: CakePHP - Build fast, grow solid | PHPフレームワーク


追記(2020/12/16): 以下の記事もどうぞ。

参考: CentOS Streamは継続的デリバリーです - 赤帽エンジニアブログ

⚓その他

⚓ おかえりなさい


つっつきボイス:「はやぶさ2のカプセルが無事帰ってきました」「あれ、帰ってきてたの知らなかった」「テレビあんまり見てなくて実感があまりなかったけど、ニュースでは報道されてるのかな?」「NHKが結構力入れて報道してました」


昔買った『探査機はやぶささん』(JAXA監修)↓を久しぶりに読み返しちゃいました。

その後カプセル内から砂粒が確認されたそうです↓。

参考: はやぶさ2「大粒試料どっさり、言葉失った」 小箱開封で黒い石確認 - 毎日新聞


後編は以上です。

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

週刊Railsウォッチ(20201214前編)Rails 6.1の直近コミットを見る、RuboCop Rails 2.9リリース、ar_lazy_preload gemほか

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

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

Ruby 公式ニュース

Ruby Weekly

DB Weekly

db_weekly_banner

週刊Railsウォッチ(20201221前編)aws-sdk-rails gemの機能をチェック、RubyWorld Conference 2020のDHHインタビューほか

$
0
0

こんにちは、hachi8833です。約400年ぶりという木星と土星の超大接近は12/21(月)なので今夜ですね。と思ったらもう西の空に沈んでしまったようです。

参考: 【特集】2020年12月 木星と土星の超大接近 - アストロアーツ

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

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

今回は以下のコミットリストのChangelogを中心に見繕いました。


つっつきボイス:「Rails 6.1がリリースされた直後なので、コミットリストも6.1修正が多そうですね」「今手掛けているRailsアプリも早速6.1にアップグレードしました」「お、どうでした?」「論理削除でおなじみのparanoia gemが動かなくなりました😇: それ以外は大丈夫っす」「gemが追いついてないのはよくありますね」

rubysherpas/paranoia - GitHub

「そうなんですよね、リリースされてから使い始める自分💦」「rcが取れるまでに多くの人がお試しするのが本当は望ましいですよね」

⚓ translatenilキーを渡したときの挙動を修正


つっつきボイス:「コミットメッセージにnilがいっぱい書かれててややこしい…」「translatenilを渡すとエラーになったのを修正したということのようだけど、defaultオプションを効くようにもしたらしいとありますね」

I18n.translatenilキーを渡すと、defaultも同時に指定されていない場合はnilを返す。defaultも指定しておくと、nilキーは「見つからないキー」として扱われる。

Rails 6.0のtranslateヘルパーは、nilキーを渡すと常にnilを返すが、#40773以後はtranslateヘルパーにnilキーを渡すと常にI18n::ArgumentErrorをraiseするようになった。このコミットは、defaultを指定せずにI18n.translatenilキーを渡すときの振る舞いと同じになるようtranslateヘルパーを修正する。
同コミットより大意

「テストコードとChangelogの方が見やすいかな↓: default:オプションを指定しておけばnilキーを渡してもdefault:で渡したキーにフォールバックしてくれるようになったのね」「あ、そういうことですか」「例外を出されて止まっちゃうと困るので、これは必要な修正ですね」

# actionview/test/template/translation_helper_test.rb#80
  def test_returns_nil_for_nil_key_without_default
    assert_nil translate(nil)
  end

  def test_returns_default_for_nil_key_with_default
    assert_equal "Foo", translate(nil, default: "Foo")
    assert_equal "Foo", translate(nil, default: :"translations.foo")
    assert_predicate translate(nil, default: :"translations.html"), :html_safe?
  end

translateヘルパーにnilキーを渡したときに、常にnilを返すのではなく、defaultで指定したキーに解決するようになった。
Changelogより

⚓ nonnamed expression indexを追加した後マイグレーションで元に戻せるように修正


つっつきボイス:「revertibleって何をrevertするんでしょう?」「Changelogには、ロールバックすると以下でエラーになったとあるけど…」

add_index(:items, "to_tsvector('english', description)")

「元のissueを見る方がいいかな↓」

「普通のデータベースインデックスではない、以下の"to_tsvector('english', description)", { using: :gin, name: 'index_items_on_to_tsvector_english_description' }のような関数インデックスをマイグレーションで追加すると、その関数インデックス名が自動生成されていた場合はマイグレーションをロールバックしたときにエラーになるということか」「あ、そういうことですか」「ロールバックするときに自動生成済みインデックス名を解決できなくて落ちてたのを、ロールバックできるように修正したということのようですね」

# #40732より
class AddFullTextSearchIndexToItemDescription < ActiveRecord::Migration[6.0]

  # Note: this migration is irreversible, to revert it,
  # please use the __Reversable__ block below and comment out the Irreversible block
  # then rake db:rollback will work
  def change
    # __Reversable__
    # add_index(
    #   :items,
    #   "to_tsvector('english', description)",
    #   { using: :gin, name: 'index_items_on_to_tsvector_english_description' }
    # )

    # Irreversible
    add_index(
      :items,
      "to_tsvector('english', description)",
      { using: :gin }
    )
  end
end

「つまりrevertibleはマイグレーションをロールバックできるという意味なんですね」「この修正、超大事じゃないですか!」「知らずにロールバックしたら即死ですもんね😇」「こういうマイグレーションってあまり書かないだけに踏んだときがつらそう…」

⚓ Unreleased: 失敗したリクエストをconfig.exceptions_appに渡すときのリクエストメソッドをGETに変更


つっつきボイス:「これはChangelogのUnreleasedに書かれていたので、6.1には入っていないようです」「こっちの関連issueを見てみるかな↓」

「RailsガイドによるとActionController::UnknownHttpMethodは自動的にrescueされてmethod_not_allowed(HTTP 405)になるはずだったのにHTTP 500エラー(内部エラー)になってしまったのね」「ガイドと挙動が違ってた問題ですか」「RailsのRackミドルウェアの構成によっては期待どおりに動かないことがあったらしい」

参考: 3.9 Action Dispatchを設定する — Rails アプリケーションを設定する - Railsガイド

「RailsのRackミドルウェアがUnknownHttpMethodのエラーを食べてしまうと正しく405エラーを返せないことがあったので、内部的にはrequest methodをGETということにして処理を継続するようにしたようですね↓」「それだけだと元のリクエストの種類がわからなくなるので、それをoriginal_request_methodに保存したということか」

# actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L43
      def render_exception(request, exception)
        backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
        wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
        status  = wrapper.status_code
        request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
        request.set_header "action_dispatch.original_path", request.path_info
+       request.set_header "action_dispatch.original_request_method", request.raw_request_method
        request.path_info = "/#{status}"
+       request.request_method = "GET"
        response = @exceptions_app.call(request.env)
        response[1]["X-Cascade"] == "pass" ? pass_response(status) : response
      rescue Exception => failsafe_error
        $stderr.puts "Error during failsafe response: #{failsafe_error}\n  #{failsafe_error.backtrace * "\n  "}"
        FAIL

「GETじゃなくて405を返すのはダメでしょうか?」「ミドルウェアスタックの途中で処理を止めたくないでしょうし、未定義のリクエストメソッドが来る可能性もあるので、処理継続のために便宜上GETにしたんじゃないかな」

参考: Rails と Rack - Railsガイド

UnknownHttpMethod自体普通はあまりないエラーだと思います」


#40246で修正された#38998と似たような感じで、ActionDispatch::Requestでメソッドが呼び出されると必ず行われるHTTPメソッドバリデーションで、予想外の妙な結果が生じることがある。たとえば、config.exceptions_app = self.routesの場合、ActionDispatch::ShowExceptionsというミドルウェアフェイルセーフで例外が発生する。

この冗長な例外を防ぐため、envconfig.exceptions_appに渡る前にrequest_methodを上書きした。オリジナルのリクエストメソッドは維持されるのでaction_dispatch.original_request_methodで維持されるので、インスペクトもできる。
同PRより

⚓ 6.1: attribute_for_databaseメソッドを追加

ここからは少し趣向を変えて、@kamipoさんの記事より見繕いました。


つっつきボイス:「上のマージ自体は今年10月に行われていて、ウォッチでは取り上げてませんでした」「ああ、例のenumのデータベース上の値を取れるようにした改修ですね」

enum state: {active: 0, inactive: 1}とかした時に、typecast前の0とか1を取りたい
blog.kamipo.netより

なお、attribute_for_databaseなどのattributeは属性名に置き換わるので、属性名_for_database属性名_before_type_castという感じの名前になります。

attribute_for_databaseメソッドはたしかに欲しい!」「attribute_before_type_castだとenumがtypecastしているかどうかを意識しながらになるので、それだと欲しいものとちょっと違うという気持ちもわかります」

参考: attribute_before_type_cast — ActiveRecord::AttributeMethods::BeforeTypeCast

「以下で言うと、book.statusはデータベースに2が入っていて、そのデータベースの値を取ろうとすると、Rubyはbook.status"published"を返すので、book.status_before_type_castを使ってデータベースから読み込み済みの2を取っていた」「ふむふむ」「それがこの改修で、book.status_for_databaseと書けばenumの"published"に対応する2をデータベースから読み込んで取れるようになった」「あ、なるほどわかりました」

# 同PRより
book = Book.new(status: "published")

# returns "published", but what we really want is 2.
book.status_before_type_cast

attribute_before_type_castよりattribute_for_databaseというメソッド名の方がデータベースから取ってくる操作なのがわかりやすいかも」「何かの都合でデータベースにあるenumの値を取りたいことはよくありますね」

⚓ 6.1: whereで関連付け名をjoinedテーブルのエイリアス名として参照できるようになった

belongs_to :author, class_name: 'User'したときにleft_joins(:author).where("author.id": nil)とか書きたい
blog.kamipo.netより


つっつきボイス:「こちらも今年8月にマージ済みでしたがウォッチで取り上げていなかったので」「そうそう、神速さんのツイートにもあるようにbelongs_toで別名を使うと、joinsは別名で、whereは元のテーブル名を使わないといけなかったんですよね↓」

「それがこの修正によって、以下のテストコードのFirm.includes(:clients).where("clients.new_name": "Summit")のようにドット付きの"clients.new_name"で参照できるようになったのか」「ネステッドハッシュによるアクセスとは違うけど、"clients.new_name"というドットアクセスをキーに書けるようになったんですね」

# activerecord/test/cases/associations/eager_test.rb#202
  def test_type_cast_in_where_references_association_name
    parent = comments(:greetings)
    child = parent.children.create!(label: "child", body: "hi", post_id: parent.post_id)

    comment = Comment.includes(:children).where("children.label": "child").last

    assert_equal parent, comment
    assert_equal [child], comment.children
  end

  def test_attribute_alias_in_where_references_association_name
    firm = Firm.includes(:clients).where("clients.new_name": "Summit").last
    assert_equal companies(:first_firm), firm
    assert_equal [companies(:first_client)], firm.clients
  end

「ツイートのコードで言うとauthor.何とかみたいに書けるようになったということですか?」「ですです」「お〜これは便利そう!」「joinsがスッキリ書けてありがたい🙏」「ネステッドハッシュで書けるかどうかは少なくともこのテストには見当たらないですね」


あるテーブルが複数回joinsされると、それらのテーブルは最初の名前でない別名になる。
これは自己参照的な関連付けで起こりやすく、現在はその場合にwhere条件の別名テーブルでカスタム属性(type casting)や属性エイリアス名の解決が効かない。
この問題を修正するために、whereで関連付け名をエイリアス名で参照できるようにする。関連付け名がwhereで参照されると、それらの名前にjoinedテーブルのエイリアス名が使われる。
同PRより大意

# 同PRより
class Comment < ActiveRecord::Base
  enum label: [:default, :child]
  has_many :children, class_name: "Comment", foreign_key: :parent_id
end

# ... FROM comments LEFT OUTER JOIN comments children ON ... WHERE children.label = 1
Comment.includes(:children).where("children.label": "child")

なお@kamipoさん記事3つ目の#39830は以下の記事をどうぞ。

Rails 6.1: 属性にデフォルト値を設定しても型が失われなくなった(翻訳)

この後さらに@kamipoさん記事が出ていました。こちらもつっつきで少し見てみましたが記事では割愛します🙇

⚓Rails

⚓ RubyWorld Conference 2020のキーノート: DHHインタビュー by Matz


つっつきボイス:「(2020/12/17 20:30頃)さっきRubyWorld Conference 2020がちょうど終わった頃だそうで、最後のキーノートであるMatzによるDHHインタビューが各所で評判になっていました↑」「お〜、DHHがRailsとフロントエンドの話もしたんだ!」「そこに関心のある人が多いからフロントエンドの話題は欠かせないでしょうね」「自分はRailsは当分滅びないと思っていますけど、フロントエンドをRailsでやる理由はだいぶ少なくなったとも思ってます」「今度動画見てみようっと」「誰か速攻で文字起こしとかしないかな」

なお、つっつきの時点では英語版動画にしか気づいていませんでしたが、以下の同時通訳付き動画でもキーノートインタビューを見られます(頭出し済み: 7:11:59)↓。

満を持してのDHH登壇だったそうです↓。

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

⚓ RailsとCableReadyとStimulus ReflexでリアクティブなTwitterクローンを構築する

以下のツイートで知りました。

hopsoft/chatter - GitHub

hopsoft/cable_ready - GitHub

hopsoft/stimulus_reflex - GitHub


つっつきボイス:「Stimulusといえば、11月の銀座RailsでもStimulus関連の発表がありました↓」「お、そうなんですね」

「Stimulusってつっつきで取り上げてましたっけ…よく知らなかった😅」「割と登場してたと思います(サイト内検索)」「StimulusはRailsでお馴染みのBasecampが作って使っているJavaScriptライブラリで、Rails方面の記事でちょくちょく見かけますね↓」「なるほど〜」「StimulusはRailsのオフィシャルに近い印象あります」「今のRails wayに則ってフルスタックでフロントエンドまでやるならStimulusとRailsの相性はいいでしょうね」

stimulusjs/stimulus - GitHub


なお、Stimulusの2.0もリリースされたそうです。

⚓ @kamipoさんの記事より


つっつきボイス:「@kamipoさんのこの記事は唸りました: こうやって解説してもらわないとわからない世界」「ですよね」

「SELECT … FOR UPDATEは割とよく使われるSQL構文で、デッドロックしないという都市伝説がまことしやかにあったんですが、実際はそんなことはなくてデッドロックは起きるというお話」「プライマリキーとセカンダリキーで並び順が変わるデータにしておくと、プライマリキーとセカンダリキーがクエリプランのSELECT … FOR UPDATEでロックの一方が上から順、もう一方が下から順に同時に進んでいって、それでデッドロックになるというのを記事で再現してますね」「あ〜、そういうことですか!」「同じテーブルの中でのロックを取る順序というものをこれまで気にしたことがなかったことに気付かされました」「そんなことがあるとは…」

「ロックを取る順序が一意になるようにクエリやクエリプランを揃えるのが一般的な回避法だそうですが、そこまでチェックしないといけないのか」「@kamipoさんも『眼の良さを活かして気合いで対処』なんですね」「これはデータベース強者でないとなかなか気づけない問題」「データベース強者、なりたいです…」「取りあえずオラマスを目指すところからですかね↓」

参考: ORACLE MASTER Portal - be an ORACLE MASTER - | オラクル認定資格制度 | Oracle University


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

⚓ aws-sdk-rails gemの新機能(Ruby Weeklyより)

aws/aws-sdk-rails - GitHub


つっつきボイス:「aws-sdk-rails gemって知らなかった」「aws-sdk-ruby↓ではないんですね😆」「どちらもAWS公式のgemですけど、そういえばaws-sdk-rubyの方は以前のSDKがバージョンアップされたときがちょっと大変でしたね」「私も思い出しました」

aws/aws-sdk-ruby - GitHub

参考: AWS SDK for Rubyの S3署名バージョン2廃止に対応しました [2019/6/24期限迫る!] - LCL Engineers’ Blog

「どれどれ」「お、DynamoDBセッションストアなんて機能がある↓」

# aws.amazon.comより
rails generate dynamo_db:session_store_migration

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

「次はActive Support Notification Instrumentationサポート」「お〜、言われてみればRailsのInstrumentationで投げた通知をAWS X-Rayあたりで拾いたいことって割とありそうなので、このSDKでできるなら便利そう」「その分AWSにロックインすることになりますけどね」

参考: AWS X-Ray(分散アプリケーションの分析とデバッグ)| AWS
参考: Active Support の Instrumentation 機能 - Railsガイド

「これを見ると↓、ActiveSupport::Notificationssubscribeでaws-sdk-railsのメソッドを拾うこともできる」「これは有能そう」「こういうのは欲しい機能ですね」

# 同リポジトリより
ActiveSupport::Notifications.subscribe('put_object.S3.aws') do |name, start, finish, id, payload|
 # process event
end

# Or use a regex to subscribe to all service notifications
ActiveSupport::Notifications.subscribe(/S3[.]aws/) do |name, start, finish, id, payload|
 # process event
end

「aws-sdk-railsのリポジトリも見てみると、Railsのencrypted credentialもサポートしてるのか↓」「お〜便利そう!」「SDKにロガーも付いているとは知らなかった」

# 同リポジトリより
# config/credentials.yml.enc
# viewable with: `rails credentials:edit`
aws:
  access_key_id: YOUR_KEY_ID
  secret_access_key: YOUR_ACCESS_KEY

参考: Rails5.2から追加された credentials.yml.enc のキホン - Qiita

「RailsのAction Mailerのdelivery_methodにAWS SES(Simple Email Service)を指定することもできる↓」「便利そうなものがいろいろ入ってますね」「aws-sdk-rails、あまり注目してなかったけどちょっと見直しました」

# 同リポジトリより
# for e.g.: config/environments/production.rb
config.action_mailer.delivery_method = :ses

参考: Action Mailer の基礎 - Railsガイド
参考: Amazon SES(高可用性で低価格なEメール送信サービス)| AWS

「Active JobのバックエンドでAWS SQS(Simple Queue Service)を使うこともできる↓」「これもよさそう😋

# 同リポジトリより
# config/application.rb
module YourApp
  class Application < Rails::Application
    config.active_job.queue_adapter = :amazon_sqs # note: can use either :amazon or :amazon_sqs
    # To use the non-blocking async adapter:
    # config.active_job.queue_adapter = :amazon_sqs_async
  end
end

# Or to set the adapter for a single job:
class YourJob < ApplicationJob
  self.queue_adapter = :amazon_sqs
  #....
end

「キューにSQSを使うだけかと思ったらワーカーをローカルでも動かせるのね↓」

RAILS_ENV=development bundle exec aws_sqs_active_job --queue default

参考: Active Job の基礎 - Railsガイド
参考: Amazon SQS(サーバーレスアプリのためのメッセージキューサービス)| AWS

「aws-sdk-rails、思ったよりよさそう👍」「こういうの見ると使いたくなっちゃいます」「最近やってるRailsアプリの環境がAWS ECSやAWS Fargateになっていることが増えてきて、ワーカーやキューをどうしようかと考えることが多いんですけど、aws-sdk-railsが使えるところがありそう」

参考: AWS Fargate(サーバーやクラスターの管理が不要なコンテナの使用)| AWS

「AWSが公式に提供しているのでサポート面でもありがたい🙏」「あとはSDKのアップグレードがaws-sdk-rubyのときほど大変にならなければさらに嬉しい」


前編は以上です。

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

週刊Railsウォッチ(20201216後編)Ruby 3.0.0-rc2とRuboCop 1.6がリリース、Ruby 3の静的型解析記事、CentOS 8のEOLが短縮ほか

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

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

Rails公式ニュース

Ruby Weekly

週刊Railsウォッチ(20201222後編)TypeProfプレイグラウンド、Ruby 3リリースイベント、Ruby 3は3倍速くなったかほか

$
0
0

こんにちは、hachi8833です。2020年度最後の週刊Railsウォッチをお送りします。来年もどうぞよろしくお願いします🙇

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

⚓Ruby

⚓ @mameさん作のRuby 3 TypeProfプレイグラウンド


つっつきボイス:「Ruby 3のTypeProfをすぐ試せるプレイグラウンドを@mameさんが作ってくれたそうです」「お〜これは凄い!」「バグレポート用のボタンも用意されていますね」

「デフォルトのサンプルコードで早速Analyzeボタンを押すと以下のようにエラー行が出力された↓」

# Errors
test.rb:6: [error] failed to resolve overload: String#+

# Classes
class Object
  private
  def hello_message: (User user) -> String
  def type_error_demo: (User user) -> untyped
end


## Version info:
##   * Ruby: 2.7.2
##   * RBS: 0.20.1
##   * TypeProf: 0.10.0

「RBSで以下のようにage: Integerと型が記述されていて、それが静的チェックで元コードの"The age is " + user.ageに対して上のfailed to resolve overload: String#+を表示したんですね」

class User
  def initialize: (name: String, age: Integer) -> void

  attr_reader name: String
  attr_reader age: Integer
end

def type_error_demo: (User user)が出力でuntypedと認識されてる」「では元コードを"The age is " + user.age.to_sのようにto_sでStringに変換してみると…エラーも消えた🎉」「おぉ〜」

「こうやってコードに対応するRBSで型を記述しておくことで、コード実行前にTypeProfで型の不一致を検出できるということですね」「こういうのをさっと動かして実感できるのありがたい🙏

「上は@mameさんの別のツイートです」「ライブラリのRBS整備、言われてみればたしかに」「Stringのところにうっかり数値を入れるなどはありがちなので、自分が作るコードでこうやってチェックできるだけでも大きいと思います👍」「人間が気づかないところはコンピュータにお任せしたい」「Rubyの型推論はこれからですね」

⚓ Ruby 3は3倍速くなったか(Ruby Weeklyより)


つっつきボイス:「RubyやRailsのベンチマークでおなじみのNoah Gibbsさんの記事です」「Ruby 2.0と比べてRuby 3.0がどのぐらい速くなったかという記事のようですね」「ざっと流し読みした限りでは、細かくはいろいろ述べられていますけど、Ruby 3.0は2.0と比べておおむね3倍速を達成したと思うそうです」「3倍速来た!」「RailsはRuby 2.0から3.0で70%(1.7倍)程度の高速化だそうです」

「いやほんとに、Ruby 2.0の頃から比べると今のRubyは随分速くなりましたよね」「記事によるとRuby 2.0〜2.6のときの高速化が大きかったそうです」「Ruby 1.9の頃のRubyは今思い出しても遅かった」

「Ruby 2.7から3.0ではどのぐらい速くなったのかな?」「今の話からするとRuby 2.7から3.0の高速化は2.0〜2.6のときほど大きくはなさそうですね」「2倍速くなるだけでもプログラミング言語としては凄い達成だと思いますよ」

「Rubyが遅いと言われがちだったのも、1.9のような昔の遅さの印象がアップデートされていないことが多い気がします」「メインで使ってないプログラミング言語やフレームワークの情報や印象がアップデートされないままになるのは自分たちも含めてよくあることなので、そこはしょうがないでしょうね」「ですよね、いろいろ思い当たります」

「PHPも書かなくなって随分経つので最近の様子はわからないです」「もうPHPは書かないんですか?」「業務上読むことはたまにありますけど、モダンなPHPは書けない」「PHPも今は8なんですね」「自分が一番PHPを書いてたのは4系の時代だったかな」「自分も5系が多かったかも」「その頃PHPに初めてクラスというものが導入されたときもありましたね」「それ覚えてます」「当時はコンストラクタってあったかな?」「あの頃自分でデストラクタを実装した覚えもちょっとあります」(以下延々)

参考: PHP: クラスの基礎 - Manual


なお、同記事のコメント欄にもNoah Gibbsさんが「Railsのエンドツーエンドの速度が1.7倍速くなったのは本当に凄いこと: これは覚えておいて欲しい」とコメントを書いていました。

⚓ Ruby 3.0リリースイベント


つっつきボイス:「12/26(土)にRuby 3.0リリースイベントがオンライン開催されるそうなので申し込みました」「お、自分も申し込んどこう」「パネリストが豪華!」「ああ、もう年末なんですね」(以下帰省の話など)

⚓ その他Ruby



つっつきボイス:「RubyのStringの移り変わりを追っている記事です」「そうそう、こういうふうにUTF-8文字の中にinvalidな文字があるときにencode("UTF-8", invalid: :replace)のような方法で除去したいときってありますよね」「2.1より前はinvalidな文字を検出できなかったけど、2.1から検出できるようになったのか」「という文字、ときどき見かけます」「なおこのという文字は:replaceオプションで変えられますよ」

# 同記事より
# in ruby <= 2.0.x
content =  "Is your pl\xFFace available?".force_encoding("UTF-8")
content.encode("UTF-8", invalid: :replace) # => "Is your pl\xFFace available?"

# in ruby 2.1.x
content =  "Is your pl\xFFace available?".force_encoding("UTF-8")
content.encode("UTF-8", invalid: :replace) # => "Is your pl�ace available?"

「RubyのStringは非常によく使うので、こうやって歴史を概観できるのはいいですね👍

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

⚓ AWSのカオスエンジニアリング機能: AWS Fault Injection Simulator


つっつきボイス:「AWS Fault Injection Simulator、まだ詳しく見てなかったけどどういう感じでできるのかな?」

参考: カオスエンジニアリングの原則 - Principles of chaos engineering

「このニュースでBPS CTOのbabaさんが喜んでいました」「stagingで確認できることが増えるのはいいですよね: DBのレプリケーションが詰まったらどうなるかを確認するとか」「ですよね」

「少しググってみた感じでは、AWS Fault Injection Simulatorの具体的な機能はリリースされてないみたい」「あ、まだ発表止まりですか」「今はComing soonで、2021年に登場予定らしい↓」「障害率とかを設定したりテストシナリオのテンプレートを使ったりできるのかなと予想はしていますけど、詳しくはリリースされてからですね」「そうですね」

参考: AWS Fault Injection Simulator – Fully managed chaos engineering service – Amazon Web Services

⚓ AWS Auroraにもある障害シミュレーション機能

「ちなみに、AWSのAuroraにも障害シミュレーション用のクエリが使える機能がありますよ」「あ、そうなんですか!」「やったことなかった」「AuroraのコンソールでALTER SYSTEM CRASHにオプションを指定して実行することで、DBクラッシュやレプリカやディスクの障害のような特定の障害状況をシミュレーションしてテストしたりできます」「こんなのやれるとは知らなかった」「これはAurora専用ですけどね」

参考: 障害挿入クエリを使用した Amazon Aurora のテスト - Amazon Aurora

「DBは壊れたときの影響が非常に大きいので、こういう障害シミュレーション機能を使って障害時にどんな振る舞いになるかを事前に確認できるのは大事」

⚓JavaScript

⚓ JavaScriptが25歳の誕生日を迎える

以下で知りました。

参考: 週刊気になったITニュース(2020/12/05号) - masa寿司の日記


つっつきボイス:「サイトを開くと昔懐かしのNetscapeっぽい画面!」「昔のブラウザってこうでしたよね」「今25歳のエンジニアがJavaScriptと同い年ってことか、はぁぁ」「1995生まれですね😆

参考: Netscapeシリーズ - Wikipedia

「上からスクロールしていくと、LiveScriptという初期の言語名やMicrosoftのJScript、いろいろ見られる」「ECMAScript 1が1997年から始まってたというのは思ったより早いかも」「これは歴史ですね」「出たXMLHttpRequest!」「まだAJAXという言葉がなかった頃ですね」「それまではXMLHttpRequestを生で使ってたの思い出しました」「そのAJAXの登場が2005年だったとは」

参考: XMLHttpRequest - Wikipedia
参考: Ajax - Wikipedia

「他にもAngularjs、Mozilla登場、jQuery、React、Vuejs、Next.jsなどなど」「こうやって歴史をコンパクトに辿れるのはいいですね👍

⚓言語/ツール/OS/CPU

⚓ if-then-elseは発明されなければならなかった


つっつきボイス:「!!Con West 2019というカンファレンスで発表されたものだそうです」「そういえばRubyでもifthenを使う書き方は一応できますね、あまり使われませんけど」「thenは使わないですね」

参考: !!Con West

「これも歴史ものかな?」「英語の接続詞として使われることのないelseがどこから来たかなどを調べたりしてるようです」「プログラミングではifthenelseと当たり前のように書いてますけど、言われてみればたしかに英語の話し言葉や書き言葉ではそういう言い回しはしませんよね」「会話でのelseというと思い付くのはsomeone elseぐらいかも」

「記事ではその辺の歴史をひもといているみたいですね」「何だかすっごく昔の知らない言語がいろいろ出てきてるんですけど」「WHENEVEROR WHENEVERとかOTHERWISEとか、いろんな書き方があったんですね」「プログラミング言語好きな人にはきっとたまらない内容❤」「ALGOL 60という言語のこのif文の構造↓がカオスすぎる😆」「この飛び方はエグい😆

参考: ALGOL - Wikipedia


後編は以上です。

良いお年をと言うにはまだ早いですね。どうぞ皆さまよいクリスマス、よいRuby 3.0、よいRails 6.1を!

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

週刊Railsウォッチ(20201221前編)aws-sdk-rails gemの機能をチェック、RubyWorld Conference 2020のDHHインタビューほか

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

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

Ruby Weekly

StatusCode Weekly

statuscode_weekly_banner


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

$
0
0

12/23の朝方、DHHが以下のツイートを発信しました。

取りあえず様子を知りたかったのでDHHのツイートを追ってみました。お気づきの点がありましたら@hachi8833までお知らせください🙇

⚓ Hotwireとは

サイト: HTML Over The Wire | Hotwire

上のツイートの要点は以下のとおりです。

  • Hotwireは、モダンなWebアプリケーションを構築する新しい手法
  • JavaScriptの利用を抑える
  • JSONではなくHTMLを送信する(HTML over the wire)
  • 先ごろリリースされたStimulus 2.0を利用する
  • 新しい「Turbo」フレームワークを取り入れている

morimorihoge注: 「JSONではなくHTMLを送信する」について

近年のフロントエンド開発ではサーバーサイドはHTMLを返さずJSONデータを返すAPI/GraphQLサーバーとしての動きを期待され、HTMLを作成するのはブラウザ側のJavaScriptで行う方式が主流となっています。

一方で、フルスタックフレームワークとしてのRailsのフロントエンドはHTMLの生成をサーバーサイドで行うという方式を採用しているため、根本的に設計思想が異なるという点に注意が必要です。

API mode等でRailsを単なるAPIサーバーとして使うことはできますので、こうしたRails提供のフロントエンドライブラリの利用はRailsを使う上で必須というわけではないことを付け加えておきます。

Stumulusが2.0になったことで、Stumulusサイトもhotwire.devのサブドメインに移行したようです↓。

サイト: Stimulus: A modest JavaScript framework for the HTML you already have.

Hotwireはhey.comでも使われているそうです。

  • Basecampが開発運営しているhey.comのフロントエンド構築のすべてがHotwireに注ぎ込まれている
  • 数年がかりの研究や実験
  • HTML中心のリリース
  • Webの他にBasecampのネイティブアプリにも使う

なお、hotwire.devページの下の方を見ると「Strada」というものも2021年にリリースされるそうです。Stradaについてまだ具体的な記載はありません。

⚓ Hotwireの構成

GitHubに新たに作成された上のHotwiredアカウントには、現時点で以下を含む13個のリポジトリがあります。

  • 新しいJavaScriptライブラリ: 2つ
  • 新しいRails gem: 3つ
  • デモWebサイト: 3つ

AndroidやiOS向けのTurboもあるんですね。

hotwired/turbo-android - GitHub

hotwired/turbo-ios - GitHub

⚓ デモ動画やリポジトリ

以下で公式のデモ動画やデモアプリケーションのリポジトリも公開されています。ちなみにしっかりRails 6.1が使われています。

hotwired/hotwire-rails-demo-chat - GitHub


gorails.comが早くもHotwireのスクリーンキャストを公開しています。

⚓ Turbo

Turbo: The speed of a single-page web application without having to write any JavaScript.

  • Turboはフレームワーク
  • Turboはページの動的な変更やフォーム送信を高速化する
  • Turboは複雑なページをコンポーネントに分割し、WebSocket上でパーシャルページの更新をストリーム化する
  • TurboではJavaScriptをまったく書く必要がない

参考: WebSocket - Wikipedia

⚓ FAQ

DHHのツイートがFAQ的なので、拾ってみました。


  • Q: Tubroは新しいTubolinksなのか?
  • A: 以前TurbolinksだったものがTurbo Driveになった。Turbo DriveはTurboに盛り込まれているさまざまな手法のひとつだ。

  • Q: HotwireはRailsと密結合するのか、それともバックエンドフレームワークを選ばないのか?
  • A: 「ジョブキュー」「WebSocketマネージャ」「テンプレートレンダリング」を備えるフレームワークならRailsのHotwireと同じことができる。

  • Q: WebSocketで何か困難があったかどうか知りたい。特に、WebSocketコネクションが許可されない企業環境やモバイル環境ではどうか?
  • A: 困難はない。WebSocketはHEYやBasecampで何年も普通に使われている。ただし、一般にはプログレッシブ表示の機能拡張として使う。そうすればWebSocketが使えない場合は単に動的でなくなるだけで、壊れたりはしない。

  • Q: HEYやBasecampがHotwireで更新されたことで、Action Cableを介したWebSocketのスケールはどのぐらいうまくやれるのか?Hotwireを採用するうえで、Webサイトに数百人がアクセスした場合に単体のWebサーバーのメモリが枯渇しないかどうかが唯一気になる点だ。
  • A: 特に問題ではない。BasecampのWebSocketコネクションは数万個に達しているが、数万台のサーバーなど使っていない。

  • Q: 既存のAction CableチャネルをTurbo Streamsで置き換えられるか?
  • A: HEYでは5つのチャネルを使っていたが、Turbo Streamsが最適だったので既存のチャネルはまとめて廃止した。したがって答えはイエス。

  • Q: ページのコンテンツは「通常の」HTMLドキュメントとしてビルドされるという理解でよいか?それとも影でまた面倒なDOMが糸を引いているのか?
  • A: 通常のHTMLで合っている。バーチャルDOMもドリフティングも存在しない。ブラウザは普通どおりに動く。


  • Q: Turbolinksは死んじゃうの?
  • A: Turbolinksが勝利したのです。

⚓ ruby-jp Slackやツイートより

Hotwireに似たものとして、Laravel LivewireやPhoenix LiveViewがあるようです。

phoenixframework/phoenix_live_view - GitHub


view_componentとHotwireの関係はどうなるのだろうという書き込みもruby-jp Slackで目にしました。

github/view_component - GitHub


同じくruby-jp Slackで、Hotwireと前後して発表されたReact Server Componentsも見かけました。

参考: Introducing Zero-Bundle-Size React Server Components – React Blog


以下のツイートも目にしました。


少し前にQuoraでMatzの回答に以下の文がありましたが、今思えばHotwireのことだったのかもしれないとruby-jp Slackで見かけました。

参考: (177) Rubyやrailsは今後革新的な進化をする事はあるのでしょうか?それとも、安定・停滞していくのでしょうか?に対するYukihiro Matsumotoさんの回答 - Quora

こないだDHHと話したら「フロントエンドを簡単に記述することができるアプローチにアイディアがある」って言ってました。私はAPIでフロントエンドとバックエンドを分離するのかと思っていたので、ちょっと意外でした。
jp.quora.comの同回答より

⚓ 最後に

上述のデモアプリのGemfileにはWebpackerが含まれていませんでした。

その後でDHHの以下のツイートを見つけました。HotwireではWebpackは必須ではなく、アセットパイプラインをWebpackで置き換えることもできるんですね。

関連記事

速報: Ruby on Rails 6.1がリリースされました

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

$
0
0

概要

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

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

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

Railsモデルのパターンとアンチパターンシリーズ」その1へようこそ。このシリーズでは、Railsアプリで仕事をしているときに出会うであろうあらゆる種類のパターンについて深く調べていきます。

今回は、パターン(デザインパターン)の概要を紹介するとともに、アンチパターンの概要についても解説します。よりわかりやすく説明するために、かなり前から世の中で使われているRuby on Railsフレームワークを用います。Railsが何らかの理由でお気に召さないとしても、本記事でご紹介するアイデアやパターンは、皆さんの手に馴染んでいる他の技術と共鳴することがあるかもしれません。

パターンとアンチパターンについて詳しく説明する前に、皆さんにお尋ねします。「パターンが必要になった経緯はどのようなものですか?」「あらゆるパターンが自分たちのソフトウェアで必要となる理由は何ですか?」「ソリューションを設計する必要がある理由は何ですか?」

⚓ あなたは(単なるプログラマーではなく)設計者です

コンピュータプログラミングの黎明期においても、プログラムを書くときに設計を避けて通ることはできませんでした。プログラム(ソフトウェア)を書くということは、すなわち問題解決の方法(ソリューション)を設計することです。プログラムを書いているときのあなたは設計者です(実際の肩書はどうぞご自由に)。私たちが書くソフトウェアは自分たちだけのものではなく、他の人も読んだり編集したりするものでもあるので、ソリューションを正しく設計することは重要です。

エンジニアは何世代にもわたって、これらのパターンをすべて頭に置いたうえで、コードやアーキテクチャからこれまで業務で目にした類似の設計を見出してきました。そして設計を抽出し、標準的な問題解決手法をドキュメント化してきました。こうした作業は、人間の性質に沿った自然な手法であると考える人もいます。人間は、あらゆるものごとを分類してパターンを見い出すことを好みます。ソフトウェアについても例外ではありません。

ソフトウェアが複雑になるにつれて、私たちは人間としてさまざまなパターンを見い出すようになってきました。そして世界中のエンジニアたちが力を合わせてソフトウェアのデザインパターンを発展させ、一定の形にまとめるようになりました。デザインパターンに関する書籍やエッセイや講演もたくさんあり、十分練り上げられ現場での実績を積んだソリューションに関するアイデアがさらに広く知られるようになってきました。そしてこうしたソリューションによって、工数や費用が大きく削減されました。そこで、デザインパターンという用語を改めて考察し、その真の意味を考えてみることにしましょう。

⚓ デザインパターンとは何か

ソフトウェアエンジニアリングにおける「パターン」とは、よくある問題を解決するときに再利用できるソリューションであると説明されます。ソフトウェアエンジニアは、こうしたパターンを何らかの形で有用なプラクティスとみなしています。ソフトウェアエンジニアはパターンで考えるので、あっという間にパターンと真逆のアンチパターンに陥ることもあります(アンチパターンについては後述します)。

あるデザインパターンは問題解決の道筋を示してくれますが、ソフトウェアのその他の部分とうまく調和する具体的なコード片を教えてくれるわけではありません。パターンは、十分に設計されたコードを書くためのガイドであると考えられますが、実装を思い付かなければ何にもなりません。日々のコーディングでパターンが用いられるようになった1980年代後半には、Kent BeckとWardn Cunninghamが「パターン言語」を使うことを思いつきました。

パターン言語というアイデアそのものは、1970年代後半にChristopher Alexanderの『A Pattern Language』に示されています。しかし意外にも、同書はソフトウェアエンジニアリングについてではなく、建築におけるアーキテクチャについて書かれていました。パターン言語とは、一貫した方法で編成されたさまざまなパターンのセットであり、1つのパターンは1つの問題を記述するとともに、さまざまに応用可能な問題解決方法の核心部分を記述します。どこかで見覚えがありませんか?(ヒント1: フレームワーク)(ヒント2: Rails)

やがて、1994年にGoF(Gang Of Four)が著した伝説の書籍『Design Patterns』(日本語版『オブジェクト指向における再利用のためのデザインパターン』)が刊行されると、デザインパターンはソフトウェアエンジニアリング界隈で広く知れ渡りました。同書では、現在も用いられている有名な「Factoryパターン」「Singletonパターン」「Decoratorパターン」を含む多くのパターンについて解説と定義を行っています。

デザインとパターンのおさらいはこのぐらいにして、次はアンチパターンとは何かを考えてみましょう。

⚓ 設計のアンチパターンとは何か

パターンが善玉だとすれば、アンチパターンは悪玉ということになります。もう少し正確に言うと、ソフトウェアのアンチパターンは「よく使われているが、非効率または生産性を下げると考えられるパターン」のことです。アンチパターンの典型的な例が「Godオブジェクト」で、他のオブジェクトに切り出すか分離できるはずの機能や依存関係が全部盛りされている強欲なオブジェクトです。

アンチパターンに共通する原因はひとつではなく、いろいろな原因があります。「善玉パターンが悪玉(アンチパターン)に豹変する」はその好例です。たとえば、あなたが以前在籍していた企業で特定の技術をよく用いていて、その技術については高度なレベルに達していたとします。説明用に、たとえばそれがDockerだとしましょう。あなたは、アプリケーションをDockerコンテナに効率よくパッキングしてクラウド上でオーケストレーションし、クラウドからログを引っ張ってくる技術を身に付けています。そしてある日突然、あなたがフロントエンドアプリケーションをリリースする必要のある新しい仕事を得たとしましょう。あなたはDockerおよびDockerでアプリをリリースする手法について知り尽くしているので、パッケージに一切合財を押し込んでクラウドにデプロイする方法を最初に決定しました。

しかし、現職のフロントエンドアプリはDockerが必要なほど複雑ではないことをほぼ見落としてしまいました。このアプリをコンテナに押し込めるソリューションは必ずしも効率が最大とは限りません。一見よさそうなアイデアでしたが、ゆくゆくは生産性が落ちてしまうことが明らかになります。これは「Golden Hammer(打ち出の小槌)」と呼ばれるアンチパターンです。

「打ち出の小槌」アンチパターンとは要するに「ハンマーを持つ者には、あらゆるものが釘に見える」ということです。Dockerやサービスのオーケストレーションに精通している人には、あらゆるものがクラウド上でオーケストレーションされるように作られたDockerサービスに見えてしまうのです。

このアンチパターンはこれまでも発生していましたし、今後も発生するでしょう。善玉が悪玉に豹変することもあれば、その逆もあります。RubyやRailsではどんなときに当てはまるのでしょうか?

⚓ まずRubyを学べ、Railsはその次

多くの人が、Ruby on Railsを使うようになって初めてRubyを使い始めました。Ruby on Railsは、Webサイトを短期間で構築できる有名なWebフレームワークです。私もご多分に漏れずRailsで初めてRubyに触れましたが、それ自体は何の問題もありません。Railsは「MVC(Model-View-Controller)」と呼ばれる十分確立したソフトウェアパターンをベースとしています。ただし、本記事でRailsのMVCパターンについて深く調べる前に、ひとつ大きな勘違いについて触れておきます。「Rubyを正しく学ばずにRailsに手を出してしまう」というものです。

かつてのRailsフレームワークは、アプリケーションの構想を得てそれを短期間で構築したいときにはとても頼りになるフレームワークのひとつでした。もちろんRailsは現在も使われていますが、全盛期ほどではありません。Railsは利用も実行も簡単なので、多くの初心者がrails newコマンドで自分のWebアプリを作り始めました。その後何が起こったかというと、もろもろの問題が噴出し始めたのです。初心者はRailsの開発スピードやシンプルさ、そして何もかもが魔法のようにスムーズに進められることに魅了されます。やがて「魔法」を当たり前と思うようになり、舞台裏でどんなことが起きているのかを理解できないままになります。

私も同じ問題に遭遇しましたし、多くの初心者や初級者が今も同じ問題に苦しめられていると思います。フレームワークを手に入れてアプリを構築するまではよいのですが、フレームワークの魔法にばかり頼っていたために、何か凝ったことをしようとした途端に挫折してしまいます。ここで大事なのは、勇気を出して初心に帰り、基礎を学び直すことです。後戻りするのは思ったほど大変ではありませんし、それがベストな方法です。しかしRubyなどの基本を学ばないまま進み続けると、問題は目に見えて大きくなります。『The Well-Grounded Rubyist』は、こんなときの助けになるおすすめの良書のひとつです。

訳注: 日本では、日本語で書かれたさまざまなRuby入門を容易に入手できます。ここでは以下を参考としてご紹介いたします。

あなたが初心者なら、同書を最初から最後までみっちり読みとおす必要はありません。その代わり、本をすぐ横に置いていつでも参照できるようにしておきましょう。「今すぐ自分がやっていることをすべて中止して本を全部読め」と申し上げたいのではありません。そうではなく、ときには作業の手を止めて、Rubyの基礎知識をリフレッシュする時間を確保しましょうと申し上げたいのです。きっと新しい地平線が見えてくることでしょう。

⚓ MVC: Railsになくてはならないもの

なるほど、ではMVCはどうでしょうか?MVCパターンは長年使われ続けており、Ruby(Rails)やPython(Django)、Java(PlayやSpring MVC)など、さまざまな言語の無数のフレームワークで採用されています。MVCの考え方は、以下のように独立したコンポーネントで作業を行うというものです。

モデル(Model)
データやビジネスロジックを扱う
ビュー(View)
データの表現方法やユーザーインターフェイス
コントローラ(Controller)
「モデルのデータを取得する」「ビューをユーザーに表示する」の2つを結びつける

理論上はよくできていそうですし、ロジックが最小限かつWebサイトに複雑なロジックがない場合は非常にうまくいきます。そこから先はややこしいことになりますが、これについては後述します。

MVCパターンは、燎原の火のようにWeb開発コミュニティ全体に急速に広まりました。近年異常なほど人気を集めているReactのようなライブラリですら、Webアプリのビューレイヤとして説明できます。これほど不動の人気を勝ち得たパターンはMVC以外にはありません。RailsはAction CableでPublish-Subscribe(Pub/Sub)パターンを追加しましたが、チャネルの概念はMVCパターンのコントローラとして説明されています。

さて、これほどまで広く用いられているMVCにもアンチパターンはあるのでしょうか?そこで、MVCにある個別のコンポーネントで最もよく目にするアンチパターンをいくつか見てみることにしましょう。

⚓ モデルの問題

アプリケーションが成長してビジネスロジックが拡張されると、モデルに多くのものが押し込まれる傾向があります。このまま成長し続けると「ファットモデル(fat model)」というアンチパターンになる可能性があります。

Railsの「モデルは厚くせよ、コントローラは薄くせよ」という有名なパターンは、悪玉とみなされることもあれば善玉とみなされることもあります。私たちは、ファットなものを抱え込むことは何であれアンチパターンだと考えています。この点をよりよく理解するために、ひとつ例をご紹介します。SpotifyやDeezerのようなストリーミングサービスを運営するとしましょう。このサービスの内部には、以下のように曲(歌)を扱うSongモデルがひとつあります。

class Song < ApplicationRecord
  belongs_to :album
  belongs_to :artist
  belongs_to :publisher

  has_one :text
  has_many :downloads

  validates :artist_id, presence: true
  validates :publisher_id, presence: true

  after_update :alert_artist_followers
  after_update :alert_publisher

  def alert_artist_followers
    return if unreleased?

    artist.followers.each { |follower| follower.notify(self) }
  end

  def alert_publisher
    PublisherMailer.song_email(publisher, self).deliver_now
  end

  def includes_profanities?
    text.scan_for_profanities.any?
  end

  def user_downloaded?(user)
    user.library.has_song?(self)
  end

  def find_published_from_artist_with_albums
    ...
  end

  def find_published_with_albums
    ...
  end

  def to_wav
    ...
  end

  def to_mp3
    ...
  end

  def to_flac
    ...
  end
end

このようなモデルの問題は、曲(歌)に関係しそうなさまざまなロジックが雑然と置かれたゴミ捨て場のようになってしまっていることです。モデルにメソッドがひとつずつ追加され続けると、長年経つうちにこのようにモデル全体が巨大かつ複雑になります。ロジックを切り出してさまざまな場所に移動しておけば、将来プロジェクトにとって役に立つ可能性があります。

このモデルを反面教師とすることで、今すぐ皆さんにおすすめできる設計上のよい習慣をいくつも見いだせます。まず、このモデルは単一責任の原則(SRP: Single Responsibility Principle)に違反しています。このモデルはフォロワーや音楽出版社への通知を扱い、テキストが公序良俗に反するかどうかをチェックし、さまざまなオーディオフォーマットの曲をエクスポートするメソッドがいくつもあり…という具合に責務を抱え込みすぎています。そのせいでモデルが複雑になってしまい、モデルのテストをどう書けばいいのか私には見当もつかないほどです。

このモデルをリファクタリングする方法は、「メソッドがどのように呼び出されるか」「他の場所でどのように使われるか」によって大きく変わります。ここではそうしたケースを扱うための一般的なアイデアをいくつかご紹介しますので、その中から皆さんのケースに最もよく合うものをお使いいただけます。

⚓ コールバックをジョブに切り出す

フォロワーや音楽出版社への通知を行うコールバックは、ジョブに切り出せます。以下のようにこのジョブをキューに入れることで、ロジックをモデルの外に追い出せます。

class NotifyFollowers < ApplicationJob
  def perform(followers)
    followers.each { |follower| follower.notify }
  end
end

class NotifyPublisher < ApplicationJob
  def perform(publisher, song)
    PublisherMailer.song_email(publisher, self).deliver_now
  end
end

ジョブはそれ用の別プロセスで実行され、モデルから切り離されます。これでジョブのロジックを別のテストで検証できるようになり、ジョブがモデルから正しくキューに送られるかどうかをチェックすれば済むようになります。

⚓ Decoratorパターンを使う

公序良俗に反する書き込みのチェックと、ユーザーが曲をダウンロードしたかどうかのチェックは、いずれもアプリのビューで発生します。このような場合はDecoratorパターンが利用できます。Draper gemはDecoratorを手軽に利用できるソリューションとして人気があります。このgemを使って、以下のようなDecoratorを書けます。

class SongDecorator < Draper::Decorator
  delegate_all

  def includes_profanities?
    object.text.scan_for_profanities.any?
  end

  def user_downloaded?(user)
    object.user.library.has_song?(self)
  end
end

続いて、コントローラで以下のようにdecorateを呼びます。

def show
  @song = Song.find(params[:id]).decorate
end

呼び出した結果をビューで以下のように利用します。

<%= @song.includes_profanities? %>
<%= @song.user_downloaded?(user) %>

gemを追加するのが好みでなければ、自分でDecoratorをこしらえる方法もありです(これについては別記事にする予定です)。モデルの「関心(concern)」の大部分を切り離すことに成功したので、今度は曲を検索するメソッドと曲を変換するメソッドに手を付けましょう。これらは、以下のように専用のモジュールを作ることで分離できます。

module SongFinders
  def find_published_from_artist_with_albums
    ...
  end

  def find_published_with_albums
    ...
  end
end

module SongConverter
  def to_wav
    ...
  end

  def to_mp3
    ...
  end

  def to_flac
    ...
  end
end

SongモデルでSongFindersモジュールをextendすると、モジュールのメソッドをクラスメソッドとして利用できるようになります。SongモデルでSongConverterモジュールをincludeすると、モジュールのメソッドをモデルのインスタンスメソッドとして利用できるようになります。

以上のリファクタリングをすべて行ったことで、Songモデルがかなりすっきりしました。

class Song < ApplicationRecord
  extend SongFinders
  include SongConverter

  belongs_to :album
  belongs_to :artist
  belongs_to :publisher

  has_one :text
  has_many :downloads

  validates :artist_id, presence: true
  validates :publisher_id, presence: true

  after_update :alert_artist_followers, if: :published?
  after_update :alert_publisher

  def alert_artist_followers
    NotifyFollowers.perform_later(self)
  end

  def alert_publisher
    NotifyPublisher.perform_later(publisher, self)
  end
end

モデルのアンチパターンはこの他にもいろいろあります。ここでご紹介したのは、モデルでどんな間違いが起きる可能性があるかを示すほんの一例にすぎません。モデルに関する他のアンチパターンについては、本シリーズの続編で扱いますのでご期待ください。とりあえず今はここまでとし、続いてビューでどんな間違いが起きるかを見ていきましょう。

⚓ ビューの問題

Rails開発者は、複雑なモデルに加えて、複雑なビューにも悩まされることがあります。その昔、Webアプリケーションのビュー王国を支配していたのはHTMLとCSSでした。やがて時とともにJavaScriptがじわじわとビュー王国に支配の手を伸ばし、とうとうフロントエンドのほとんどすべてがJavaScriptで書かれるようになりました。Railsフレームワークは、これに関して少し異なるパラダイムに従っています。ビューを全面的にJavaScriptに委ねるのではなく、JavaScriptコード片をビューに「振りかける」だけです。

いずれにしろ、HTMLとCSSとJavaScriptとRubyを一箇所でまとめて扱うのは面倒になりがちです。Railsでビューを構築するときにややこしいのは、ドメインロジックが(モデルではなく)ビューの中に入ってしまう場合がある点です。これはそもそもMVCパターンを壊すことになるので、(本来)あってはならないことです。

もうひとつの問題は、ビューやパーシャルにRubyコードを埋め込みすぎてしまうことでしょう。ロジックの一部は、ビューヘルパーやDecorator(「ビューモデル(View Model)」やプレゼンター(Presenter)と呼ばれることもあります)の中に置かれる可能性もあります。これらの例のいくつかについては今後の本シリーズ記事で見ていきますので、どうぞご期待ください。

⚓ コントローラの問題

Railsのコントローラもまた、実にさまざまな問題に苦しめられることがあります。そのひとつが「ファットコントローラ」と呼ばれるアンチパターンです。

先ほどのモデルも太っていましたが、こちらはある程度の減量に成功しました。しかしその作業中に、今度はコントローラが少々太ってしまいました。これは、ビジネスロジックがコントローラの内部に置かれている場合によく起きますが、ビジネスロジックの本来の置き場所は、「モデル」または「それ以外のどこか」になります。先ほどモデルの問題で触れた内容の多くは、コントローラにも当てはまります。「コードをPresenterに切り出す」「Active Recordコールバックを使う」「最後の手段としてService Objectを使う」などです。

開発者によっては、Trailblazerdry-transactionなどのgemを最後の手段にすることもあります。要は、特定のトランザクションを扱うためのクラスを作るということです。コントローラからいろんなものを追い出してすっきりと保ち、追い出したものやテストは別クラスに置きます。その別クラスを何と呼ぶかは開発者によって異なり、「Service」「トランザクション」「アクション」などさまざまです。

⚓ まとめ

世の中にはさらに多くのアンチパターンがあり、さらに多くのソリューションもあります。本記事だけですべてをカバーするのは紙面と時間を使いすぎますし、今回述べたモデルやコントローラのように記事まで太ってしまうでしょう。RailsのMVCパターンのあらゆる側面を深く追求する本シリーズのフォローをぜひお願いします。それによって、皆さんも有名なアンチパターンのほとんどを扱えるようになるでしょう。第1回はパターンとアンチパターンの概要、そしてRuby on Railsフレームワークで最もよくあるアンチパターンについてご紹介しましたが、皆さんにお楽しみいただければと思います。

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

⚓ お知らせ

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

関連記事

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

Hotwireデモアプリを動かしてみた

$
0
0

Hotwireデモアプリを動かしてみたのは昨年以下の速報記事を出したときですが、せっかくなのでRuby 3.0でもやってみました。

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

Hotwireデモアプリを動かしてみた

hotwired/hotwire-rails-demo-chat - GitHub

デモアプリはRails 6.1です。データベースはSQLite3を使っています。

動かした環境:

  • OS: macOS X Catalina 10.15.7
  • Ruby: 2.7.1および3.0.0 (rbenvにて設定)
  • Redis: 6.0.9

Redisがローカル環境にない場合はインストールが必要です。自分はHomebrewでインストールしました。

$ brew install redis
$ brew services start redis

デモアプリをgit cloneします。

$ git clone https://github.com/hotwired/hotwire-rails-demo-chat.git

Ruby 3.0.0を使う場合はGemfileの該当行を3.0.0に置き換えます。

-ruby '2.7.1'
+ruby '3.0.0'

続いてセットアップスクリプトを実行します。bundle installやマイグレーションもここで行われます。

$ ./bin/setup

(注: 現在は不要です)ただし昨年試した時点ではディレクトリが1つ足りなかった(#2)のでエラーになります。そのときはとりあえずapp/assets/javascripts/librariesディレクトリを作って回避しました。

$ mkdir app/assets/javascripts/libraries

#2はその後マージされたので、上のディレクトリ作成は不要になりました。

後は./bin/rails sを実行します。http://localhost:3000をブラウザで開いて操作すると、以下のようにWebSocket通信が行われます。

Finished "/cable/" [WebSocket] for 127.0.0.1 at 2020-12-23 18:30:31 +0900
Turbo::StreamsChannel stopped streaming from Z2lkOi8vY2hhdC9Sb29tLzE
Started GET "/rooms/1" for 127.0.0.1 at 2020-12-23 18:30:31 +0900
Processing by RoomsController#show as HTML
  Parameters: {"id"=>"1"}
  Room Load (0.1ms)  SELECT "rooms".* FROM "rooms" WHERE "rooms"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/rooms_controller.rb:35:in `set_room'
  Rendering layout layouts/application.html.erb
  Rendering rooms/show.html.erb within layouts/application
  Rendered rooms/_room.html.erb (Duration: 0.1ms | Allocations: 14)
  Message Load (0.1ms)  SELECT "messages".* FROM "messages" WHERE "messages"."room_id" = ?  [["room_id", 1]]
  ↳ app/views/rooms/show.html.erb:15
  Rendered collection of messages/_message.html.erb [5 times] (Duration: 0.5ms | Allocations: 268)
  Rendered rooms/show.html.erb within layouts/application (Duration: 2.7ms | Allocations: 1600)
  Rendered layout layouts/application.html.erb (Duration: 9.6ms | Allocations: 7171)
Completed 200 OK in 11ms (Views: 10.1ms | ActiveRecord: 0.2ms | Allocations: 7875)


Started GET "/rooms/1/messages/new" for 127.0.0.1 at 2020-12-23 18:30:31 +0900
Processing by MessagesController#new as HTML
  Parameters: {"room_id"=>"1"}
  Room Load (0.1ms)  SELECT "rooms".* FROM "rooms" WHERE "rooms"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/controllers/messages_controller.rb:19:in `set_room'
  Rendering messages/new.html.erb
  Rendered messages/new.html.erb (Duration: 1.0ms | Allocations: 544)
Completed 200 OK in 3ms (Views: 1.4ms | ActiveRecord: 0.1ms | Allocations: 1374)


Started GET "/cable" for 127.0.0.1 at 2020-12-23 18:30:31 +0900
Started GET "/cable/" [WebSocket] for 127.0.0.1 at 2020-12-23 18:30:31 +0900
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: keep-alive, Upgrade, HTTP_UPGRADE: websocket)
Turbo::StreamsChannel is transmitting the subscription confirmation
Turbo::StreamsChannel is streaming from Z2lkOi8vY2hhdC9Sb29tLzE

なお、自分の場合最初のうちRedisの通信がタイムアウトしまくってたのですが、ESET Cyber Security Proのパーソナルファイアウォールに阻まれていたのが原因でした。Redisを触ったのが初めてだったので、気づくのにちょっとかかりました。

ファイアウォールを一時的に止めるとうまくいったので、後でESETにポート6379番を通すフィルタリングルールを追加しました(面倒)。

関連記事

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

速報: Ruby 3.0.0がリリースされました

Rails5: ActiveRecord標準のattributes APIドキュメント(翻訳)

$
0
0

更新情報
2017/12/11: 初版公開
2020/12/23: 細部を更新

ActiveRecordに任意の属性を定義したり既存の属性を上書きしたりできるRails 5以降の標準機能です。

概要

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

参考: Rails 5のActive Record attributes APIについて y-yagiさんの良記事です。

⚓ Rails5: ActiveRecord標準のattributes API(翻訳)

⚓ メソッド

⚓ 定数

  • NO_DEFAULT_PROVIDED = Object.new

⚓ attribute(name, cast_type = Type::Value.new, **options)

(publicインスタンスメソッド)

型を持つ属性をこのモデルに定義します。必要な場合、既存の属性の型をオーバーライドします。これにより、モデルへの代入時に値がSQLと相互に変換される方法を制御できるようになります。また、ActiveRecord::Base.whereに渡される値の振る舞いも変更されます。これを使って、実装の詳細やモンキーパッチに依存せずに、ActiveRecordの多くに渡ってドメインオブジェクトを使えるようになります。

  • name” 属性メソッドの定義対象となるメソッド名、およびこれを適用するカラム。
  • cast_type: この属性で使われる:string:integerなどの型オブジェクト。利用例について詳しくは以下のカスタム型オブジェクトの情報をご覧ください。

⚓ オプション

以下のオプションを渡せます。

  • default: 値が渡されなかった場合のデフォルト値。このオプションを渡さなかった場合、前回のデフォルト値があればそれが使われる。前回のデフォルト値がない場合はnilになる。
  • array:(PostgreSQLのみ)array型にならなければならないことを指定する(以下の例を参照)。

  • range:(PostgreSQLのみ)range型にならなければならないことを指定する(以下の例を参照)。

⚓

ActiveRecordで検出される型はオーバーライド可能です。

# db/schema.rb
create_table :store_listings, force: true do |t|
  t.decimal :price_in_cents
end
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
end
store_listing = StoreListing.new(price_in_cents: '10.1')

# 変更前
store_listing.price_in_cents # => BigDecimal.new(10.1)

class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :integer
end

# 変更後
store_listing.price_in_cents # => 10

デフォルト値を指定することもできます。

# db/schema.rb
create_table :store_listings, force: true do |t|
  t.string :my_string, default: "original default"
end

StoreListing.new.my_string # => "original default"
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :my_string, :string, default: "new default"
end

StoreListing.new.my_string # => "new default"

class Product < ActiveRecord::Base
  attribute :my_default_proc, :datetime, default: -> { Time.now }
end

Product.new.my_default_proc # => 2015-05-30 11:04:48 -0600
sleep 1
Product.new.my_default_proc # => 2015-05-30 11:04:49 -0600

属性の背後にデータベースのカラムがなくても構いません。

# app/models/my_model.rb
class MyModel < ActiveRecord::Base
  attribute :my_string, :string
  attribute :my_int_array, :integer, array: true
  attribute :my_float_range, :float, range: true
end
model = MyModel.new(
  my_string: "string",
  my_int_array: ["1", "2", "3"],
  my_float_range: "[1,3.5]",
)
model.attributes
# =>
  {
    my_string: "string",
    my_int_array: [1, 2, 3],
    my_float_range: 1.0..3.5
  }

⚓ カスタム型の作成

値型で定義されるメソッドと対応していれば、独自の型を定義することもできます。この型オブジェクトでは、deserializeメソッドまたはcastメソッドが呼び出され、データベースやコントローラから受け取った生の入力を取ります。前提とされるAPIについてはActiveModel::Type::Valueをご覧ください。型オブジェクトは既存の型かActiveRecord::Type::Valueのいずれかを継承することが推奨されます。

class MoneyType < ActiveRecord::Type::Integer
  def cast(value)
    if !value.kind_of?(Numeric) && value.include?('$')
      price_in_dollars = value.gsub(/\$/, '').to_f
      super(price_in_dollars * 100)
    else
      super
    end
  end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)
# app/models/store_listing.rb
class StoreListing < ActiveRecord::Base
  attribute :price_in_cents, :money
end
store_listing = StoreListing.new(price_in_cents: '$10.00')
store_listing.price_in_cents # => 1000

カスタム型の作成について詳しくは、ActiveModel::Type::Valueのドキュメントをご覧ください。型をシンボルで参照できるように登録する方法については、ActiveRecord::Type.registerをご覧ください。シンボルの代わりに型オブジェクトを直接渡すこともできます。

⚓ クエリ

ActiveRecord::Base.whereが呼び出されると、そのモデルクラスで定義された型が使われ、型オブジェクトでserializeを呼んで値がSQLに変換されます。次の例をご覧ください。

class Money < Struct.new(:amount, :currency)
end
class MoneyType < Type::Value
  def initialize(currency_converter:)
    @currency_converter = currency_converter
  end

  # deserializeまたはcastの結果が値になる
  # ここではMoneyのインスタンスになることが前提
  def serialize(value)
    value_in_bitcoins = @currency_converter.convert_to_bitcoins(value)
    value_in_bitcoins.amount
  end
end
# config/initializers/types.rb
ActiveRecord::Type.register(:money, MoneyType)
# app/models/product.rb
class Product < ActiveRecord::Base
  currency_converter = ConversionRatesFromTheInternet.new
  attribute :price_in_bitcoins, :money, currency_converter: currency_converter
end
Product.where(price_in_bitcoins: Money.new(5, "USD"))
# => SELECT * FROM products WHERE price_in_bitcoins = 0.02230

Product.where(price_in_bitcoins: Money.new(5, "GBP"))
# => SELECT * FROM products WHERE price_in_bitcoins = 0.03412

⚓ dirtyトラッキング

属性の型には、dirtyトラッキングの実行方法を変更する機会が与えられます。ActiveModel::Dirtychanged?changed_in_place?が呼び出されます。これらのメソッドについて詳しくはActiveModel::Type::Valueをご覧ください。

⚓ define_attribute( name, cast_type, default: NO_DEFAULT_PROVIDED, user_provided_default: true )

(publicインスタンスメソッド)

これはattributeの背後にある低レベルAPIです。型オブジェクトのみを受け取り、スキーマの読み込みを待たずに即座に動作します。自動スキーマ検出と#attributeはどちらもこのメソッドを背後で呼び出します。このメソッドが提供されていることでプラグイン作者によって使われる可能性もありますが、おそらくアプリのコードで#attributeを使うべきです。

  • name: 定義される属性の名前。Stringで定義します。
  • cast_type: この属性で使う型オブジェクト。

  • default: 値が渡されなかった場合のデフォルト値。このオプションを渡さなかった場合、前回のデフォルト値があればそれが使われる。前回のデフォルト値がない場合はnilになる。procを渡すことも可能であり、新しい値が必要になるたびにprocが1度ずつ呼び出される。

  • user_provided_default: デフォルト値がcastdeserializeでキャストされるべきかどうかを指定。

  • GitHubソース

関連記事

Rails: Form ObjectとVirtusを使って属性をサニタイズする(翻訳)

Rails: dry-rbでForm Objectを作る(翻訳)

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

$
0
0

概要

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

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

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

Railsのパターンとアンチパターン」シリーズ第2弾へようこそ。第1回では、ソフトウェア開発における一般的なパターンやアンチパターンの概要を扱うとともに、Rails界隈でよく知られているパターンやアンチパターンもいくつかご紹介しました。今回は、Railsのモデルにおけるアンチパターンやパターンをいくつか見ていくことにしましょう。

本記事は、普段からモデルで苦労している方のお役に立つでしょう。今回は、モデルをすっきりスリムにダイエットする方法と、モデルのマイグレーションを書くときに避けるべき注意点についてです。

⚓ ファットな重量過積載モデル

Railsアプリケーションを開発するとき、フルスタックのRails WebサイトにするかAPIサーバーにするかにかかわらず、多くの開発者がロジックをモデル内に配置する傾向があります。前回記事では、何でもかんでも引き受けてしまっていたSongクラスを例に用いました。モデルの責務が過剰になると、単一責任の原則(SIP)に違反してしまいます。

まずはSongクラスをもう一度見返してみましょう。

class Song < ApplicationRecord
  belongs_to :album
  belongs_to :artist
  belongs_to :publisher

  has_one :text
  has_many :downloads

  validates :artist_id, presence: true
  validates :publisher_id, presence: true

  after_update :alert_artist_followers
  after_update :alert_publisher

  def alert_artist_followers
    return if unreleased?

    artist.followers.each { |follower| follower.notify(self) }
  end

  def alert_publisher
    PublisherMailer.song_email(publisher, self).deliver_now
  end

  def includes_profanities?
    text.scan_for_profanities.any?
  end

  def user_downloaded?(user)
    user.library.has_song?(self)
  end

  def find_published_from_artist_with_albums
    ...
  end

  def find_published_with_albums
    ...
  end

  def to_wav
    ...
  end

  def to_mp3
    ...
  end

  def to_flac
    ...
  end
end

このようなモデルの問題は、歌ものの曲(song: 以下「曲」)に関連するさまざまなロジックがゴミ捨て場のように集積されていることです。メソッドは急に増えるというよりも、長い時間をかけてゆっくりジワジワと増えていきます。

モデル内にこのようなコードが積み重なってきたら、もっと小さいモジュールに切り出しましょう。ただし、それだけでコードがきれいになるのではなく、単にコードを別の場所に移動したということでしかありません(散らかっているものをタンスや押し入れに押し込めて見た目を取り繕うようなものです)。しかしそれでも、コードをモジュールに切り出せば少なくともコードは整頓されますし、読みづらい太り過ぎのモデルになることも避けられます。

中には、最後の手段としてRailsのconcernを用いるとロジックを複数のモデル間で再利用できることに目をつける開発者もいます。以前私が記事にも書いたように、Railsのconcernを好む開発者もいますが、好まない開発者もまたいるのです。いずれにしろ、Railsのconcernはモジュールと本質的に同じようなものです。コードをモジュールに切り出してincludeできるようにすることは「単にコードを移動する以上のものではない」ことを、ぜひ肝に銘じておくべきです。

別の方法は、必要に応じて普通に小さなクラスを作成することです。たとえば、先のSongクラスの変換コードは以下のように別のクラスに切り出せます。

class SongConverter
  attr_reader :song

  def initialize(song)
    @song = song
  end

  def to_wav
    ...
  end

  def to_mp3
    ...
  end

  def to_flac
    ...
  end
end

class Song
  ...

  def converter
    SongConverter.new(self)
  end

  ...
end

これで、曲をさまざまなフォーマットに変換するSongConverterクラスにメソッドを切り出せました。これなら、SongConverterクラス独自のテストを書くことも、今後ここに新しい変換メソッドを追加することも問題なくできます。ある曲をMP3に変換したければ、以下のように書くだけで済みます。

@song.converter.to_mp3

私は、このように専用の小さなクラスを作る方が、モジュールやconcernを使うよりもコードが若干明確になると感じます。おそらく私が「継承」よりも「コンポジション」を好んでいることもその理由でしょう。私はこの方が直感的にも理解しやすく、読みやすさも向上すると考えています。皆さんがコードを書くときには、「クラス方式」と「モジュールまたはconcern方式」の両方について検討しておくことをおすすめします。もちろん両方使うことも可能ですし、それを止めることはできません。

⚓ SQLパスタ・パルメザンチーズ風味

おいしいパスタがキライな人はいないと思いますが、コードのパスタ(いわゆるスパゲッティコード)が好きな人もまずいないでしょうし、それにはもっともな理由があります。Railsのモデルでは、Active Recordの使い方がたちまちスパゲッティコードになってしまい、コードベース全体でとぐろを巻く可能性があるのです。どうやったらスパゲッティコードを避けられるでしょうか?

長いクエリがスパゲッティ化しない方法はいくつか考えられます。最初に、データベース関連のコードがどのように広がっていくかを見ていくことにしましょう。今回も例のSongモデルを使います。特に、何かをフェッチする操作がどの行で行われようとしているかに注目してください。

class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = Song.where(status: :published)
                .where(artist_id: artist_id)
                .order(:title)

    ...
  end
end

class SongController < ApplicationController
  def index
    @songs = Song.where(status: :published)
                 .order(:release_date)

    ...
  end
end

class SongRefreshJob < ApplicationJob
  def perform
    songs = Song.where(status: :published)

    ...
  end
end

上のコード例では、Songモデルの3箇所でクエリを投げています。データのレポートに用いるSongReporterServiceクラスでは、特定のアーチストがリリースした曲をフェッチしようとしています。次のSongControllerクラスでは、出版済みの曲をリリース日順にフェッチしようとしています。最後のSongRefreshJobクラスでは、出版済みの曲をフェッチしてから何か処理を行おうとしています。

これらの操作自体は別によいのですが、あるとき突然に「ステータス名をreleasedに変更しよう」「曲をフェッチする方法をもう少し変えよう」という決定が下されたらどうしますか?3箇所あるフェッチ操作を同じように修正しなければならなくなります。そもそも現在の書き方はDRYではありませんよね。同じ書き方がアプリケーション全体で繰り返されています。これを見てがっかりすることはありません。こんなときのためのソリューションがあるのです。

Railsの「スコープ(scope)」を使えばコードをDRYにできます。スコープを使えば、よく使われるクエリを定義しておいて、さまざまな関連付けやオブジェクトで手軽に呼び出せるようになります。そしてスコープにする方がコードも読みやすくなり、変更も楽にできます。しかしスコープで最も重要なメリットは、Active Recordのjoinswhereなどのメソッドをチェインできるようになることです。先のコードにスコープを導入するとどうなるかを見てみましょう。

class Song < ApplicationRecord
  ...

  scope :published, ->            { where(published: true) }
  scope :by_artist, ->(artist_id) { where(artist_id: artist_id) }
  scope :sorted_by_title,         { order(:title) }
  scope :sorted_by_release_date,  { order(:release_date) }

  ...
end

class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = Song.published.by_artist(artist_id).sorted_by_title

    ...
  end
end

class SongController < ApplicationController
  def index
    @songs = Song.published.sorted_by_release_date

    ...
  end
end

class SongRefreshJob < ApplicationJob
  def perform
    songs = Song.published

    ...
  end
end

一丁上がりです。これで、同じようなコードの繰り返しをモデルから消し去ることができました。しかし、相手が太りに太ったファットモデルやGodオブジェクトともなると、この手が常に通用するとも限りません。1個のモデルにメソッドを次々に追加したり、モデルにさまざまな責務を乗っけるのはよくありません。

私からのアドバイスは、「スコープは最小限にとどめる」ことと「共通のクエリだけを切り出す」ことです。先のwhere(published: true)のようなクエリは、ほぼあらゆる場所で使われるのでスコープで使うのに完璧です。それ以外のSQL関連コードについては、これより紹介するRepositoryパターンも使えます。それでは詳しく見てみましょう。

⚓ Repositoryパターン

ここでこれから述べる内容は、ドメイン駆動設計の書籍で定義されているRepositoryパターンと1対1対応するものではありません。Railsで私たちが用いるRepositoryパターンを支えるアイデアは、ビジネスロジックからデータベースロジックを切り離すことです。Active Recordに代わって生SQLを呼び出す本格的なRepositoryクラスを作ることも一応できますが、どうしても必要でない限りおすすめいたしません。

ここでは、SongRepositoryというクラスを1つ作成して、データベースロジックをそこに集約します。

class SongRepository
  class << self
    def find(id)
      Song.find(id)
    rescue ActiveRecord::RecordNotFound => e
      raise RecordNotFoundError, e
    end

    def destroy(id)
      find(id).destroy
    end

    def recently_published_by_artist(artist_id)
      Song.where(published: true)
          .where(artist_id: artist_id)
          .order(:release_date)
    end
  end
end

class SongReportService
  def gather_songs_from_artist(artist_id)
    songs = SongRepository.recently_published_by_artist(artist_id)

    ...
  end
end

class SongController < ApplicationController
  def destroy
    ...

    SongRepository.destroy(params[:id])

    ...
  end
end

上のコードで行っているのは、クエリのロジックを切り出してテスト可能なクラスに配置することです。これにより、モデルはスコープやロジックを気にかける必要がなくなります。コントローラもモデルも薄くなり、万事丸く収まるとは思いませんか?ところで、切り出したコードではActive Recordがまだまだ仕事をしていますね。私たちのシナリオではfindを使いますが、これは以下のSQLを生成します。

SELECT "songs".* FROM "songs" WHERE "songs"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]

原則論からすると、こういったものはすべてSongRepositoryの内部で定義するのが正しいということになりますが、それについては既に申し上げたように基本的におすすめしません。私たちはそれらをSongRepository内で定義する必要なしに、完全に制御したいと考えています。Active Recordを使わない場合のユースケースとしては、Active Recordでは簡単にサポートできない複雑なSQLが必要な場合などが考えられます。

生SQLとActive Recordとくれば、もうひとつお話ししておかなければならないトピックがあります。マイグレーションの話、そしてマイグレーションを正しく行う方法についてです。それでは見てみましょう。

⚓ マイグレーションのコードも大事

「マイグレーションのコードは、アプリケーション本体のコードのように隅々まできれいに書くものではない」というような議論をちょくちょく目にします。しかし私はそうした主張に納得が行きません。「マイグレーションはどうせ1回実行してそれっきりなんだから、少々コードが臭くても構わないじゃないか」という言い訳に走りがちです。プロジェクトの開発者がせいぜい数人どまりで、すべてのコードやデータが常に同期されているのであれば、それでもよいのかもしれません。

しかし現実はそううまくいかないものです。アプリケーションが大勢の開発者によって開発され、各開発者がアプリケーションの他のパーツについてよくわかっていないということもざらではありません。そんなプロジェクトに怪しい使い捨てコードを押し込めたら、データベースのステートが損なわれたりマイグレーションがおかしなことになったりして、数時間後に別の誰かが悲鳴を上げるかもしれないのです。これをアンチパターンと呼んでいいのかどうかは私には何とも言えませんが、そういうことが起こる可能性があることを十分承知しておく必要はあります。

他の開発者にとってもっと有用なマイグレーションを書くにはどうすればよいでしょうか?プロジェクトメンバーの誰もがマイグレーションを実行しやすくなる方法をいくつかご紹介します。

⚓ 1. downメソッドも必ず書くこと

マイグレーションのロールバックがいつ何時行われるかは、事前に予測しようがありません。自分の書いたマイグレーションが実はロールバックできないものであれば、必ず以下のようにActiveRecord::IrreversibleMigrationraiseしましょう。

def down
  raise ActiveRecord::IrreversibleMigration
end

⚓ 2. マイグレーションの中からActive Recordを呼び出すことはできる限り避けること

これは、マイグレーションが実行される時点で何らかの外部要素(データベースのステートは除きます)に依存することは最小限にとどめましょうということです。当てにしているActive Recordのバリデーションが将来消されてしまえば、マイグレーションを実行した瞬間にその日を棒に振ることになります。Active Recordに依存しないことで、そのような事態を避けられるでしょう。

Active Recordに依存しない最後の手段は、生SQLです。それでは特定のアーチストの曲をすべて公開するマイグレーションを書いてみましょう。

class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0]
  def up
    execute <<-SQL UPDATE songs
      SET published = true
      WHERE artist_id = 46 SQL
  end

  def down
    execute <<-SQL UPDATE songs
      SET published = false
      WHERE artist_id = 46 SQL
  end
end

ここでSongモデルをどうしても使わざるを得ないのであれば、「マイグレーションの中でSongモデルを定義する」という手法がおすすめです。それなら、app/models/ディレクトリ以下に置かれた実際のActive Recordモデルが将来どうなっても、マイグレーションが影響を受ける可能性を避けられます。

しかしそれだけで十分なのでしょうか?次のポイントをご紹介します。

⚓ 3. 「スキーマのマイグレーション」と「データのマイグレーション」は分けること

RailsガイドのActive Record マイグレーションの冒頭にはマイグレーションについて以下の概要が書かれています。

マイグレーション (migration) はActive Recordの機能の1つであり、データベーススキーマを長期にわたって安定して発展・増築し続けることができるようにするための仕組みです。マイグレーション機能のおかげで、Rubyで作成されたマイグレーション用のDSL (ドメイン固有言語) を用いて、テーブルの変更を簡単に記述できます。スキーマを変更するためにSQLを直に書いて実行する必要がありません。
Railsガイドより

マイグレーションガイドの概要では、データベースの構造をマイグレーションで編集することについては触れていますが、データベースのテーブルにある実際のデータをマイグレーションで編集する方法については言及していません。つまり、先ほどの2.のように曲データをマイグレーションで定期的に更新する運用は、実は正しい方法とは言い切れないのです。

プロジェクトのデータに定期的に手を加える必要があるなら、data_migrate gemを検討してみましょう。

ilyakatz/data-migrate - GitHub

このgemは、データのマイグレーションとスキーマのマイグレーションをいい感じに分ける方法のひとつで、先のマイグレーション例もこのgemで簡単に書き換えられます。データマイグレーションを生成するには、以下のコマンドを実行します。

bin/rails generate data_migration update_artists_songs_to_published

続いて、生成ファイルにマイグレーションのロジックを追加します。

class UpdateArtistsSongsToPublished < ActiveRecord::Migration[6.0]
  def up
    execute <<-SQL UPDATE songs
      SET published = true
      WHERE artist_id = 46 SQL
  end

  def down
    execute <<-SQL UPDATE songs
      SET published = false
      WHERE artist_id = 46 SQL
  end
end

これで、db/migrate/以下のスキーママイグレションに手を加えることなく、db/data/ディレクトリ以下にデータマイグレーションを保存できます。

⚓ 最後に

Railsのモデルが読みづらくならないようにする作業は、常に苦労の連続です。本記事で、踏む可能性のある落とし穴やよくある問題の解決法を皆さんが学ぶことができれば、これに勝る喜びはありません。モデルのパターンおよびアンチパターンは他にもまだまだありますので、本記事ではその一部しか紹介できませんでしたが、いずれも最近私が目にした中で最も目立つものを取り上げました。

Railsのパターンやアンチパターンについて関心がおありでしたら、本シリーズの続編にご期待ください。今後の記事では、RailsのMVCのうちビューやコントローラで起こりがちな問題やその解決方法をご紹介する予定です。

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

⚓ お知らせ

Rubyのマジックに関する記事をお読みになりたい方は、お見逃しにならぬよう、ぜひ私どものRuby Magicニュースレターの購読をお願いします。

関連記事

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

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

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

$
0
0

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

Changelogを見るには、GItHubのリリースタグ↓が便利です。6.1.1タグの日付は日本時間の2021/01/08 8:11でした。

6.1.1にはセキュリティ修正は含まれていません(Railsのセキュリティ修正には、たとえば6.1.1.1のように4つめのバージョン番号が付けられます)。

本記事では、GitHubリリースタグに掲載されているChangelogに対応するプルリクやコミットへのリンクを取り急ぎ貼りました。これ以外にも多くの変更が含まれているので、詳しくは以下のコミットリストをご覧ください。

⚓ Active Support

# 変更前:
IPAddr.new("127.0.0.1").to_json
# => "{\"addr\":2130706433,\"family\":2,\"mask_addr\":4294967295}"

# 変更後:
IPAddr.new("127.0.0.1").to_json
# => "\"127.0.0.1\""

⚓ Active Record

Alex Ghiculescu


Ryuta Kamizono


Ryuta Kamizono


Ryuta Kamizono


Ryuta Kamizono


Ryuta Kamizono


Rafael Mendonça França


Ryuta Kamizono


Muhammad Usman


従来はfalseを渡すとオプションのバリデーションロジックがトリガーされ、:polymorphicが正しいオプションでないというエラーが表示される。
changelogより大意

glaszig


従来は、ロールバック中に以下のコードでエラーが発生し、インデックス名を明示的に指定しなければならなかった。修正後はインデックス名が自動で推論されるようになった。
changelogより大意

add_index(:items, "to_tsvector('english', description)")

fatkodima

⚓ Action View

Marek Kasztelnik


aar0nr


Étienne Barrié


stylesheet_link_tagjavascript_include_tagを使うとデフォルトで追加されるLinkヘッダーを無効にできるconfig.action_view.preload_links_headerを追加した。
changelogより大意

Andrew White


translateヘルパーにnilキーが渡されたときに、nilを常に返すのではなくデフォルト値に解決されるようになった。
changelogより大意

Jonathan Hefner

⚓ Action Pack

Jan Klimo


Alex Robbin


Alex Robbin

⚓ Active Job

Rafael Mendonça França


Mikkel Malmberg

⚓ Action Mailer

Paul Keen

⚓ Active Storage

Matt Muller

⚓ Railties

Markus Doits


Jonathan Hefner

変更なし

  • Active Model
  • Action Cable
  • Action Text

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

関連記事

速報: Ruby on Rails 6.1がリリースされました

速報: Ruby 3.0.0がリリースされました

週刊Railsウォッチ(20210112前編)Active Recordの範囲指定バリデーション改善、soleとfind_sole_byメソッド、AlgoliaとRailsほか

$
0
0

こんにちは、hachi8833です。今年も週刊Railsウォッチをよろしくお願いします🎍🙇

  • 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇
  • お知らせ: TechRachoではRubyやRailsの最新情報などの記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

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

12/25以降のコミットリストのうち、Changelogに記載されているものから見繕いました。

⚓ button_toが常にHTMLの<button>をレンダリングするようになった

ActionView::Helpers::UrlHelper#button_toの第1引数やブロックでコンテンツを渡さない場合も「常に」<button>要素をレンダリングするように変更された。

        <%= button_to "Delete", post_path(@post), method: :delete %>
        <%# => <form method="/posts/1"><input type="_method" value="delete"><button type="submit">Delete</button></form>

        <%= button_to post_path(@post), method: :delete do %>
          Delete
        <% end %>
        <%# => <form method="/posts/1"><input type="_method" value="delete"><button type="submit">Delete</button></form>

Sean Doyle, Dusan Orlovic
changelogより大意


つっつきボイス:「今まで<input type="submit" />で作っていたボタンが<button>タグになるのね」「<input type="submit" />で作るボタン、懐かしい👴」「今まで<button>じゃない部分が残ってたとは知らなかった」「button_toメソッド、使ったことなかったかも」「button_toなんてメソッドがあったんですね、今度使ってみよう」

「もしかするとbutton_toはあまり使われてなかったのかもしれませんが、もしふんだんに使っている人がJavaScriptとボタンを連携させていたりしたらHTMLタグが変わるのでbreaking changeになるかもしれませんね」「あ、たしかに」「button_toをオーバーライドすればいいと思います」「どんなにマイナーな機能でも使っている人がいる可能性はあると思った方がよいでしょうね」

<button>タグがある今、ボタンを作るのに<input type="submit" />を使うこともあまりやらなくなりましたよね」「Webの歴史を感じてしまいました」「若い人だと<input type="submit" />でボタンを作れること自体知らないかも」「そういう時代になったんですね…」

参考: <BUTTON> -HTMLタグリファレンス

⚓ バリデーションのnumericalityにrange..で値を渡せるようになった

数値バリデーション(パーセント値など)の範囲指定方法を簡潔にするプルリク。

validates :percentage, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }

# ↓

validates :percentage, numericality: { in: 0..100 }

length: { in: x..y }バリデーションがインライン化される。
同PRより大意


つっつきボイス:「お〜なるほど、in:でrangeリテラル..を使って書けるようになったのか!」「これいいじゃないですか!」「これはよい👍

「今までなかったのがちょっと不思議なぐらいですね」「自分も今までgreater_than_or_equal_to: 0, less_than_or_equal_to: 100みたいに書いてたけど、言われてみれば..で書ける方が楽ですよね」「greater_than_or_equal_toって長い…」「長い長い」

「かといってgteqとかlteqみたいに詰めるのもちょっと考えてしまう」「Perlはeqとか使う文化ですね」「Bashも-gtとか-eqとか使います」「-geとか-leもあった」「やっぱり詰めると読みづらいですよね…」「やむを得ずPerlのソースを読むことになったときに最初に戸惑ったのがその辺の表記でした😢」「Perlのように歴史の長い言語だと、前方互換のために後から導入する記号のやりくりで苦心しがちですよね」

参考: Perl - Wikipedia

⚓ ActiveModel::Nameの初期化でlocaleを渡せるようになった

概要
とある理由のため、モデル名の複数形化を言語に合わせた形で行いたいと思った。調べてみるとpluralizeメソッドはlocaleを引数に取れるが、それをActiveModel::Nameの初期化に渡す方法がなかった。
同PRより大意

# activemodel/test/cases/naming_test.rb#161
class NamingWithSuppliedLocaleTest < ActiveModel::TestCase
  def setup
    ActiveSupport::Inflector.inflections(:cs) do |inflect|
      inflect.plural(/(e)l$/i, '\1lé')
    end

    @model_name = ActiveModel::Name.new(Blog::Post, nil, "Uzivatel", :cs)
  end

  def test_singular
    assert_equal "uzivatel", @model_name.singular
  end

  def test_plural
    assert_equal "uzivatelé", @model_name.plural
  end
end

つっつきボイス:「ActiveModel::Name.newにロケールを渡せるようになった」「単数形や複数形は言語によっていろいろ違っていますね」

「↓以下のように元々pluralizeにはlocaleを渡せるようになっていて、ActiveModel::Name.newがそれに対応してなかったのをできるようにしたようですね: これができるようになったときのことをちょっと覚えてます」「その下で単数形用のsingularlizeにもロケールを渡すようになってる」

# activemodel/lib/active_model/naming.rb#L170
      @unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
      @klass        = klass
      @singular     = _singularize(@name)
-     @plural       = ActiveSupport::Inflector.pluralize(@singular)
+     @plural       = ActiveSupport::Inflector.pluralize(@singular, locale)
      @element      = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name))
      @human        = ActiveSupport::Inflector.humanize(@element)
      @collection   = ActiveSupport::Inflector.tableize(@name)
      @param_key    = (namespace ? _singularize(@unnamespaced) : @singular)
      @i18n_key     = @name.underscore.to_sym

-     @route_key          = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup)
-     @singular_route_key = ActiveSupport::Inflector.singularize(@route_key)
+      @route_key          = (namespace ? ActiveSupport::Inflector.pluralize(@param_key, locale) : @plural.dup)
+     @singular_route_key = ActiveSupport::Inflector.singularize(@route_key, locale)
      @route_key << "_index" if @plural == @singular

pluralizeしたものやsingularlizeしたものをさらにconstantizeすることもあると思いますけど、そうやってできたクラス名にUzivateléみたいな名前が出てきたらちょっとびっくりしそう…」

⚓ 新機能: solefind_sole_by


つっつきボイス:「soleと言っても靴の裏のことじゃなくて、solely(単独で)のsoleだそうです」「どことなくラテン語風味を感じる言葉」「たしかにsolelyって単語ありますね」「onlyやaloneあたりを固い言葉で言い換えるときに使う印象あります」「たしかに技術ブログではたまに見ますね」「話し言葉ではあまり聞かない印象もあります」

「で、このsoleはレコードが1個しかない場合だけそれを返すメソッドのようです」「該当レコードが複数あったらどうなるんだろう?」「複数の場合や1件もない場合はエラーにするそうです」「あ、ActiveRecord::SoleRecordExceededっていうのがそれですね」

レコードが1個きっかり存在することを調べたりアサーションしたりするFinderMethods#soleおよび#find_sole_byを追加。
用途としては、レコードを1行だけ取りたいが、その条件にマッチするレコートが他に複数存在しないこともアサーションしたい場合(特にデータベースの制約が不十分だったりちゃんと効いてない場合)。

同Changelogより大意

「プルリクメッセージを見ると、Django(PythonベースのWebフレームワーク)にはそういうメソッドがあるということみたい↓」

参考: Proposal + patch: FinderMethods#only! for asserting there’s only one result row - rubyonrails-core - Ruby on Rails Discussions
参考: Django - Wikipedia

「Railsのfind_byで結果が複数ある場合はどうなるんだったかな…普段そういう使い方してないからな〜」「私もしません😆」「find_byは1件目だけを返すとAPIドキュメントにありますね↓」

Finds the first record matching the specified conditions.
api.rubyonrails.orgより

「条件に合致するレコードが2件以上あったり1件もなかったりしてはいけない場合にこれらのメソッドが使えるということのようですね」「find_byで実はレコードが複数ある場合を排除したいときとか」「アサーションを書くのに便利そう」「こういうコードを書くのはテストコードが多いでしょうね」「レコードが1件しかあってはならないことをこうやってメソッドで明示するのは好きです」

find_sole_byか…このメソッド名でいいのかな?」「プルリクメッセージを見ると、find_sole_byというメソッド名について議論されてますね」「あれ、Changelogにはfind_by_soleって書かれてる↓」「これはドキュメントの変更漏れかな?」「ホントだ」「来週のウォッチ公開日にもし修正されてなかったら、Railsにプルリクするチャンスですね」

# 同Changelogより
Product.where(["price = %?", price]).sole
# => ActiveRecord::RecordNotFound      (if no Product with given price)
# => #<Product ...>                    (if one Product with given price)
# => ActiveRecord::SoleRecordExceeded  (if more than one Product with given price)

user.api_keys.find_by_sole(key: key)  # 編集部注: find_by_soleは修正前の誤りです
# as above

「メソッド名の議論を見るとこんなコメントがあった↓」「find_sole_byの方がsoleで探し方を示していてよさそうだということですね」「DHHがfind_sole_bysole_firstがいいと言ってる」「こういうやりとりの間にfind_by_soleがドキュメントに残ってしまったのかもしれませんね」

May I propose a slight tweak: find_sole_by. “Find solely by” seems to describe the way to find, rather than what to find. For example, “I tracked him down solely by date of birth” means “I used only his date of birth to track him down”, rather than “He was the only person with that date of birth.”
#40768のコメント(by jonathanhefner)より

なお、その後調べてみると、現時点でのRailsのmasterブランチ↓では既に修正済みでした(2717b08)。

Product.where(["price = %?", price]).sole
# => ActiveRecord::RecordNotFound      (if no Product with given price)
# => #<Product ...>                    (if one Product with given price)
# => ActiveRecord::SoleRecordExceeded  (if more than one Product with given price)

user.api_keys.find_sole_by(key: key)
# as above

find_sole_byなら昔からあるfind_by_XXXな書き方と取り違えられずに済むでしょうから、それもあってこの名前にしたのかもしれませんね」

その後調べると、古い動的ファインダーメソッドの一部はRails 4.0で以下のgemに切り出されたようです↓。

rails/activerecord-deprecated_finders - GitHub

また、RuboCopのRailsスタイルガイド↓ではfind_by_XXXのような書き方は警告されると社内で教わりました🙇

参考: rubocop-hq/rails-style-guide: A community-driven Ruby on Rails style guide

⚓ ActiveRecord::AttributeMethods::Queryのgetterメソッドをオーバーライドできるようにした

# 同PRより
# 修正前
  class User

    def admin
      false # getterをオーバーライドして常にfalseを返すようにする
    end

  end

  user = User.first
  user.update(admin: true)

  user.admin # false (getterのオーバーライドによる期待どおりの結果)
  user.admin? # true (DBカラムの値が返った: 期待どおりでない)

修正後はuser.admin?が期待どおりfalseを返すようになる。
同PRより大意


つっつきボイス:「ああ、やりたいことはわかりました: こういうことはあまりやって欲しくない気持ちがありますが」「どういう改修でしょうか?」「Userテーブルにadminカラムがあるときに、Active Recordが生成するadminメソッドを上のようにオーバーライドして、たとえば常にfalseを返すようにするというのは、たまに見かける書き方ではあります」「わかります😆」「で、adminメソッドはオーバーライドできるけど、admin?メソッドがオーバーライドされてなくてデータベースの値を読み込んで返していた、それをオーバーライドされるように改修したということですね」「なるほど!」

オーバーライド(override)
Ruby では上位クラスや include したモジュールで定義されているメソッドを再定義することを「オーバーライドする」という。オーバーライドしたメソッドからは super によって元のメソッドを呼び出すことができる。
Ruby用語集 (Ruby 3.0.0 リファレンスマニュアル)より

「これが欲しい気持ちはわかるんですけど、adminをオーバーライドしたときにadmin?もオーバーライドする機能って果たして必要なんだろうかって思う気持ちもありますね」「自分もこれは要らない気がします…」

「むしろwarningを出して欲しいですよね」「たしかに!」「『adminがオーバーライドされたけど、admin?はオーバーライドされてないよ』という具合に」「現場ではその方が嬉しいかも」「知らないうちにadmin?もオーバーライドされることで何か起きるんじゃないかとちょっと心配」「たぶん自分は使わないかな」

「RuboCopが注意してくれるといいかも」「言われてみれば、attributeを直接オーバーライドするメソッドの警告はRuboCopにありそうですね」

後でrubocop-railsを探してみましたが、Active Recordの組み込みメソッドを直接オーバーライドしたときの警告は見つかったものの、それ以外の警告は見つけられませんでした。

参考: Rails/ActiveRecordOverride Rails Cops - A RuboCop extension focused on enforcing Rails best practices and coding conventions.

⚓Rails

⚓ rbs_railsでRailsアプリにSteepを導入


つっつきボイス:「昨年末のRuby 3.0リリースイベントの↓中でこの記事の話題が出ていたことで知りました」「Pockeさんは最近RBSとRails関連の記事をいろいろ書いてますね」

参考: Ruby 3.0 release event - connpass — 終了

pocke/rbs_rails - GitHub

「Pockeさんのこの記事ぐらい新しければ大丈夫ですけど、RubyのRBSや型推論周りはここ数か月でだいぶ動いたので、ちょっと前の記事だともう現状に追いついてないでしょうね」「あ、たしかに」「少なくともRuby 3.0リリース後の記事を見つけるようにしたいですね: もちろんRBSは今後も変わるかもしれませんが、正式リリース後はそうそう破壊的な変更にならないだろうと予想しています」「steepやsorbetは今後も変わるのかな?」

soutaro/steep - GitHub

sorbet/sorbet - GitHub

⚓ activerecord-importの:on_duplicate_key_ignoreオプション


つっつきボイス:「@kamipoさんの記事1本目です」「activerecord-import gem自体はお馴染みのものですけど、@kamipoさんの記事にはおぉ〜っと思いましたね」

zdennis/activerecord-import - GitHub

「記事を読んでて、MySQLでそんなことができるとは、そう言えばどこかで聞いたような、と思ったのがINSERT IGNOREでした」「おぉ?」「MySQLにはDB制約を無効にする機能があって、セッションレベルで無効にすることもできれば、この記事のようにINSERT IGNOREを使ってそのINSERT文だけで制約をオフにすることもできます」「へ〜!」

参考: MySQL :: MySQL 8.0 Reference Manual :: 13.2.6 INSERT Statement

「これはデータベースへのインポートでよく使われる機能で、外部キーが相互参照しているようなデータをmysqldumpすると、インポートするときに制約を無効にする必要が生じることがあるんですよ」「なるほど!」「制約を無効にしないと整合性が『タマゴが先かニワトリが先か』のような状態になってしまうので、バッチでデータをインポートするときなどにこの機能が必要になります」「今思えばINSERT IGNOREどこかで使ったことあったかも」

「そんなときはRails 6.0からinsert_all↓というそれ用のメソッドがあるからそっちを使ってねというお話で締めくくられています」「記事の『MySQLチョットデキル』、こんなこと自分も言ってみたいです〜」「この言い回しカッコいいですよね」

⚓ @kamipoさんのツイートより


つっつきボイス:「こちらも@kamipoさんのツイートです」「そうそう、Rails 6.1からこのあたりのクエリがちょっとキレイになったんですよね: それまではorを重ねていくとwhere文のネストがものすごく深くなって#39032のようにStack level too deepになったりしてた」「こういうのを修正する@kamipoさんはやっぱりすごい人」

⚓ その他Rails


つっつきボイス:「アルゴリア!」「これ何でしたっけ?」「いい感じのインクリメンタル検索を実現するAPIサービスですね」「あ、思い出しました」「TechRachoでもAlgoliaの作者インタビューを翻訳したことあります↓」「インクリメンタル検索をやりたいときには便利ですよね」

インタビュー: 超高速リアルタイム検索APIサービス「Algolia」の作者が語る高速化の秘訣(翻訳)

「AlgoliaはよくAPIドキュメントサイトなどで使われてますね」「そうそう、単純なインクリメンタル検索じゃなくて、部分一致の複数検索とか重要度の高いものから上に出すとか」「記事をざっと眺めた限りでは、Railsで特殊なことをあまりせずにAlgoliaを使えるような感じですね」

以下はAlgolia自身のインクリメンタルなAPIドキュメントサイトですが、他にもAlgoliaを使っているドキュメントサイトを見かけます。

参考: REST API | API Reference | Algolia Documentation

「ところで記事の中に出ているAutocomplete.jsってjQueryじゃなかったっけ?」「あ、そうかも」「記事のサンプルコードの書き方↓がどことなくjQueryの匂いを感じたけどやっぱりjQueryみたい」


同記事より

「特に上の1行目の('#search-input', { hint: true }とか、下から3行目の.onあたりにjQueryフレーバーを感じました↑」「あ〜わかる気がします」

algolia/autocomplete.js - GitHub

「見つけたこのリポジトリ↑がalgolia/autocomplete.jsになっているから、この記事のはAlgoliaが出しているAutocomplete.jsの方か」「AlgoliaのAutocomplete.jsもやっぱりjQueryでした」

参考: jQuery - Wikipedia


前編は以上です。

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

週刊Railsウォッチ(20201222後編)TypeProfプレイグラウンド、Ruby 3リリースイベント、Ruby 3は3倍速くなったかほか

週刊Railsウォッチ(20201221前編)aws-sdk-rails gemの機能をチェック、RubyWorld Conference 2020のDHHインタビューほか

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

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

Rails公式ニュース


週刊Railsウォッチ(20210113後編)Ruby 3.0 Ractor解説記事、Vercelホスティングサービス、教育用OS xv6ほか

$
0
0

こんにちは、hachi8833です。

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

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

⚓Ruby

⚓ @_ko1さんのRuby 3.0 Ractor解説


つっつきボイス:「こちらの記事も、昨年のRuby 3.0リリースイベント↓の中で@_ko1さんが『つい先ほど公開しました』と話していたことで知りました」「お、読んだ気がすると思ったら年越し前に同じくこのイベントで知ったので休みの間に読んじゃいました」「そうか、もう公開から2週間経ったんですね」「Ractorはいいと思います!」「とりあえずRactorの概要を知っておくことが大事ですね」

参考: Ruby 3.0 release event - connpass — 終了

⚓ Ruby 3のFiber Schedulerを作った


つっつきボイス:「中国語が見える」「中国の方が書いたらしいですが、この記事は英語ですね」

後で「代码混音师」をGoogle翻訳にかけると「コードミキサー」と出たので、ブログドメイン名のcodemixer.comそのままでした。

「Ruby 3でFiber Schedulerをカスタマイズできるようになったので(ウォッチ20200609)、やってみたようです」「お〜、眺めた限りですが真面目にスケジューラを作ってるように見える」「スゴそう」

「記事の中でio_uringepollkqueueのようなLinuxの新しいAPIをチェックしている」「そういえばちょっと前にio_uringなどの話題になりましたね(ウォッチ20200804)」

参考: ソケットAPIが遅すぎる?新たなio_uringを試す!. 新しいAPIが作られるたびに、私たちは、古いAPIを置き換えるだけで高速化という… | by FUJITA Tomonori | nttlabs | Dec, 2020 | Medium
参考: Man page of EPOLL
参考: kqueue|kamezawa.hiroyuki|note


後で気が付きましたが、同記事末尾で著者がFiberをRactor-safeにするプルリクを投げたと書かれていて、見てみると既にマージされていました↓。

Fiber#schedulerは今のところ英語版のRubyドキュメントにのみ載っているようです↓。るりまサーチではまだ見つかりません。

Ruby 3.0.0 リリースノートでFiber Schedulerについて以下の動画を紹介していたのでここにも貼ります。

⚓ Rubyスレッドリークの隠れたコスト


つっつきボイス:「コードはよくあるApache Kafkaのconsumerのようですね」

参考: 開発者のための Apache Kafka サービス | Heroku
参考: Apache KafkaのProducer/Broker/Consumerのしくみと設定一覧 - Qiita

元記事中で使っているKarafkaはRuby向けのKafkaフレームワークです↓。

karafka/karafka - GitHub

# 同記事より: 問題が起きたコードの簡略版
class EventsConsumer < Karafka::BaseConsumer
  def initialize(...)
    super
    @processor = Processor.new
  end

  def consume
    @processor.call(params_batch.payloads)
  end
end

「上が時間とともにメモリリークしてパフォーマンスが落ちていったので、とりあえず下のように解決してしのいだそうです」「どうやら、上のProcessor.newがconsumerの数だけProcessorを増やしてしまっていたのを、Processor.instanceのようにシングルトンに変えて同じキューが使われるようにしたということのようですね」「マルチスレッド系のバグはコードを見るときに疑ってかからないと発見が難しいですね」

# 同記事より: 解決版
class Processor
  include Singleton

  # 略
end

class EventsConsumer < Karafka::BaseConsumer
  def initialize(...)
    super
    @processor = Processor.instance
  end

  # 略
end

「記事の最後は、何も対策しなかった場合にどうパフォーマンスが落ちるかをちょっと比較してみたそうです」「グラフは上に行くほど低い…のね」「お〜、JRuby以外はstaleしたスレッドが増えるとパフォーマンスが落ちてる」「CRubyはRuby 2.7.2より3.0.0-preview2の方が落ち方が少し増えてるのか」


同記事より

念のため元記事末尾の文を引用します。

Ruby 3.0(訳注: preview版です)の方がRuby 2.7.2より遅かったことに興味を惹かれたので、その理由について近々調べてみようかと思います。
注: 自分はこれがJITのベストなユースケースとは信じていませんので、どうか上のグラフを見てパフォーマンスについて騒ぎ立てないでいただきたく思います。
同記事より大意

⚓ その他Ruby


つっつきボイス:「へ〜、Ruby 2.7以降ではIRB.confでUSE_READLINEを指定すると、irbでライブラリのreadlineが使われてしまって、irb独自のオートハイライトやオートインデントが効かなくなるのか」

参考: それ行けLinux~readline~

2.7以降のirbで使われているのが以下のrelineというライブラリです(ウォッチ20191001)。

ruby/reline - GitHub

「ややこしいですけど、このオプションをtrueにするとrelineの機能がirbで使えなくなるということでしょうか?」「はい、そういうことですね」「記事にもありますけど、以前はreadline付きでRubyをビルドするのに苦労することもあったりしたようですね」「今ならそういう苦労なしにRuby 3.0をインストールできるのに昔は大変だったんだな…」

後でRuby 3.0.0でやってみました↓。

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

⚓ AWS CloudShell

参考: 待望の新サービス AWS CloudShell がリリースされました! #reinvent | Developers.IO


つっつきボイス:「BPSの社内Slackに貼っていただいた記事です」「AWS CloudShellは、AWSが提供するコンテナのLinuxシェル環境にログインできるもので、ホームディレクトリに容量制限はあるものの、CloudShellを使うとAWS CLIなどが使えるシェルにアクセスできます: 言い換えると、自分の手元ではEC2インスタンスを1個も立ち上げずに、AWS CLIをコマンドラインで使うためだけのコンソールを使えるようにするサービスですね」「なるほど、ちょっと想像が付きました」「それをブラウザでできるんですね」「はい、ブラウザ以外でもできるかどうかはまだよくわかりませんが」

参考: AWS コマンドラインインターフェイス(CLI: AWSサービスを管理する統合ツール)| AWS

「あとお値段の方も、リージョン1つにつき最大10コンカレントシェルまで無料とあるので、普通の使い方をしていれば基本的に無料」「ありがたい!」「その代わりホームディレクトリは1GBまでという容量制限があるので、ここにソースコードを置いてあれこれ変えるようなユースケースはあまり想定されていないと思います」

「AWS CloudShellでTerraformを実行するぐらいはできるかな?: デフォルトでは入ってなさそうですが、npm(Nodejsのパッケージマネージャ)やpip(Pythonのパッケージ管理ソフトウェア)がインストール済みとあるからできるかも」

参考: Terraformとは?基本知識とTerraformのメリット4つを紹介 | テックマガジン from FEnetインフラ

hashicorp/terraform - GitHub

参考: pip - Wikipedia
参考: npm (パッケージ管理ツール) - Wikipedia

後で探すと、既にやってみた人の記事を見つけました↓。

参考: AWS CloudShell で Terraform を実行してみた! - ForgeVision Engineer Blog

「ただ、AWS CloudShellはCloudWatch Logsなどにログが残らない残らないという話も見たんですよ」「あ〜、そうなんですか」「少なくとも今はなさそうなので、ログを残さないといけないというポリシーがある組織やプロジェクトだと合わない可能性がありますね」

「でもAWS CLIが使えるシェルが手軽に欲しいということは割とありますし、値段も安いので、トラブルシューティングのときとかにCloudShellを使うのは悪くないと自分は思いました」「そうですね」「さしあたってはAWS CloudShellというものがある、という存在を知っておけば後々いろんな使いみちがあるでしょうね👍

⚓JavaScript

⚓ Vercel: Next.jsの開発元が立ち上げたホスティングサービス

Next.jsの他にVueでお馴染みのNuxt.jsやAngularなども公式にサポートしているそうです↓。

参考: Deploying a Static Nuxt.js App with Vercel - Vercel Guides


つっつきボイス:「お、今日のWebチームミーティングのチーム内発表に登場したVercelですね」「はい、ウォッチで扱ったことがありそうでありませんでした」

「発表によればVercelはNext.jsの開発元が作った、フロントエンド向けのHeroku的なホスティングサービスということでしたが、後発だけあって使い勝手などがかなりよくできている印象でしたね」「そうですね、stagingとproductionが最初から使えるとか、アカウントもGitHubでもGitLabでもBitBucketでも使えるとか」

元々ZEITという社名だったのがVercelに社名変更したそうです↓。

参考: Vercel (日本語訳)

「VercelにはServerless functionsという機能もあって、pages/apiディレクトリにバックエンドの処理も置けるそうですが、サーバーサイド側がどこまで使えるかが知りたいところですね」「商用で使う前に、自社内用のツールをVercelにホスティングしていろいろ試してみてもいいなと思いました」

「お値段も、カスタムドメイン、Continuous Deplyment(CD)、CDN(Contents Delivery Network)、API制限なし、Serverless Functionsまで使えて無料なんですよね」「お〜!」「メンバーが10人を超えたら有料なのか」「自分の趣味プログラムをホスティングするなら事実上無料ですね」

「お、もうひとつありがたいことが書かれていますね: 登録にクレジットカードが不要だそうです」「へ〜!」「自分は大学でWeb開発を教えているんですけど、学生に使ってもらうときにとても大事なポイントなんですよ: クレジットカードを持ってる大人は関係ありませんけど、学生がクレジットカードを持っているとは限らないので使ってもらいにくい」「なるほど!」

「GoogleのGCPやAWSだと、1円も課金しないで使うとしてもクレジットカード登録が必要なんですよ」「クレカ登録が不要なら小学生でも使えますね」

注: Vercelのサービス規約上未成年でも利用できるかについては確認していません。ご利用の際はクレカの有無だけでなく各サービスの利用規約についても各自ご確認をお願いします。

「あとはVercelが今後もサービスを継続できるかどうかでしょうね」「発表のときもVercelが資金調達したことが話題になってましたね(本項冒頭のツイート)」「つい最近なんですね」

「Next.js自体も@mizchiさんなどが推していてReactのフルスタックフレームワークとして現時点で出来がいい方らしいので、VercelはそのNext.jsの開発元が公式に提供しているホスティングサービスということで、筋はよさそうに思えますね」

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

⚓ @kazuhoさんが解説するHTTP/3


つっつきボイス:「お、HTTP/3の話」「H2Oの作者であるFastlyの@kazuhoさんによるHTTP/3の講演をPublicKeyががっつり解説記事にしていました」

参考: H2O (Webサーバ) - Wikipedia

「HTTP/2の登場が2015年とあるから、もう5年も経ったのか」「2016年にHTTP/2対応したのを思い出しました」「優先順位をExtensible prioritiesで設定できるようになるのか: 今までネットワークレイヤでやってた制御がここに上がってきた感じ」

「HTTP/2の時点で、1個のTCPコネクションの中でマルチストリームができるようになりましたけど、HTTP/3ではその優先順位をこれで制御するんだろうな」「通信を多重化するとたいていQoS(Quality of Service)をやりたくなりますよね」

参考: Quality of Service - Wikipedia

「HTTP/3のEarly Hintsは、そういえばRailsもHTTP/2のときに対応してましたね↓」「Changelogで見た覚えあります」(以下レイテンシやパケットロスの話題など延々)

【速報】Rails 5.2.0正式リリース!Active Storage、Redis Cache Store、HTTP/2 Early Hintほか

「HTTP/3も実用の段階が近づきつつあるんですね」「この辺の技術を有効に使い尽くそうとすると、たぶんRailsのようなモデルよりも最近のフロントエンドのようなモデルにしないとなかなかやりづらいだろうなと感じるところはあります」「そうなんですね」「ゲーム業界のように速度を果てしなく追求する分野だと、RailsのようにサーバーでHTMLをレンダリングして返すモデルは上限に突き当たるのが早そうな予感がします」

「ソシャゲでRails使って応答速度を追求するのはしなさそうですね」「もちろん、ゲーム業界でも極端な応答速度を求められない機能ならRailsや他のフレームワークで作るというのはあるでしょうが、そうするとサーバーサイドが複数言語のハイブリッド構成になって複雑化してしまうんですよ: それなら最初から全部JavaScriptやGolangを使って書くことで言語はひとつにしたい、というのが昨今のRailsやめたい勢の意見のひとつかなと感じています」

「あと、AWSのALBがHTTP/3にどう対応するかも気になるところですね」

参考: Application Load Balancer とは - Elastic Load Balancing

⚓ BCP56

「以下はHTTP/3と直接関係ありませんが、@kazuhoさんのツイートを拾いました↓」「このBCP56というドキュメントはなかなか便利ですね👍

「いわゆるRFCの仕様はHTTPサーバーを実装する側向けのドキュメントですが、BCP56はHTTPを利用してコンテンツやアプリケーションを作る側にとってのベストプラクティス的なことが盛り込まれていて、Webアプリの本来あるべき姿が示されているのはありがたいです」「お〜」「@kazuhoさんも続きでまさに同じことをおっしゃってますね↓」「こういうドキュメントがあると設計の議論がしやすくなりそう」

「BCP56はかなり新しいドキュメントみたい」「ドキュメントの日付を見ると2021年01/07…って今日?」「上のツイートを見つけたのが昨日なのに?」「ドラフトなので更新の日付でしょうね」「あ、更新中ですか」

「このBCP56和訳しませんか?」「翻訳してる間に本家がガンガン更新されそうな予感です😆」「でしょうね」「BCP56の一部をピックアップして解説する記事はあってもいいでしょうね」

⚓言語/ツール/OS/CPU

⚓ xv6: 教育用のミニUnix実行系

mit-pdos/xv6-public - GitHub

参考: xv6 - Wikipedia


つっつきボイス:「このxv6は、Railsチュートリアルでお馴染みのYassLabの安川さん↓に教えていただいたもので、コードベースがとても小さいので教育用のUnix系OSとして米国の大学でとてもよく使われているんだそうです」「へ〜!」

「昔は教育用といえばMINIXが使われてましたけど、あれも巨大化しちゃいましたよね」「昔はMINIXのソースコードをみんなで読んで勉強してましたけど、その現代版という感じ」「xv6のリポジトリを見てみたんですが、OSと思えないぐらいコードが少なくて、ちゃんと最小限のシェルまであるみたいです」「シェルがないと使えませんから😆

参考: MINIX - Wikipedia

「そういえばこの間のRuby 3.0.0リリースイベントが終わった後の雑談で安川さんがこのあたりの話をしてたような覚えがうっすらあります」「そうでしたか!私は力尽きて雑談まで見られなかった…」

「xv6もリポジトリが14 years agoとかあるので結構歴史ある感じ」「POSIXに準拠してるみたい」「MINIXは1980年代だからさらに古いですけどね」

「xv6、コマンドやシェルを除くとたしかにとても小さそう」「これなら印刷して読める分量ですね」「たしかに」

「お、xv6のRISC-V版の方がディレクトリが整理されているっぽい↓」「更新もされてますね」「FreeBSDやNetBSDなどのコードも拝借してると書かれてる」

mit-pdos/xv6-riscv - GitHub

⚓ 『詳解UNIXプログラミング』

「以下は直接関係ありませんが貼ってみました」「お〜なつかしい、と思ったけど第3版は自分が読んだ頃と表紙が違う気がする…」「もっと色少なかったですよね」

「自分が読んだのはこれだ↓」「そうそう、これですよ」「どちらも筆でざっくり描いたようなところが共通点かも」

「他にもUnixプログラミングの定番の名著はいくつかあるんですけど、タイトルが似通っているものが多くて今うまく見つけられない…」「探しにくいですよね」「この種の本は現代の人が学ぶときに環境設定などで効率はよくない面はあるけど、やはり名著」

「システムコールは増えることはあっても減ることはないので、こういう本に書かれているものは今でも動くのがいいですよね」「そうそう」「ユーザーアプリケーションから見て下のレイヤにあるシステムコールでどんなことができるのかを知っておくと、たとえばアプリでこれをやろうとするとどうやってもボトルネックに引っかかる、みたいなことがわかってくるので、よいと思います」

「シェルのコマンドをつなぐときも、システムコールの種類や特性を知ったうえでやるといいですよね」「その一方で、さっきのepollみたいな新しいシステムコールに気づかないまま古いものを使ってしまうこともあったりしそうですけど😆」「Linuxは今でもシステムコール増えてますよね」

「Rubyのアップデートでも、新しいシステムコールが出たので使いました、ということがありますよ」「へ〜」「RubyのリリースノートやRailsウォッチを追っていると、そういう新しいシステムコールを知る機会にもなりますね」


後編は以上です。

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

週刊Railsウォッチ(20210112前編)Active Recordの範囲指定バリデーション改善、soleとfind_sole_byメソッド、AlgoliaとRailsほか

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

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

Publickey

publickey_banner_captured

StimulusReflex の Setup~Quick Start をやってみる

$
0
0

こんにちは ebi です。
先日、Hotwire のリリースがありましたが、 Hotwire 以前から存在する Rails ベースで SPA 風のサイトを実現するのに適した StimulusReflex について、公式サイトの導入などの触りをやってみたので記録として残しておきます。
まだ全容が分かってないなりにメモがてら色々書いているため、誤った表現、コメントが含まれるであろう旨は予めご容赦ください。

hopsoft/stimulus_reflex - GitHub

はじめに

Stimulus は Ruby on Rails の生みの親である DHH 氏でお馴染みの Basecamp 社製の JavaScript フレームワークで、後述する StimulusReflex のベースになっていることもあって Rails ウォッチなどでも度々取り上げられています。

Stimulus は React などのように、ページやパーツ全体の DOM 構造を JS 内で定義して空のページからスタートするのではなく、あくまでサーバ上から送られる HTML をベースにする方針で、何か動的な制御を加えたい部分に関する処理の定義だけを付け足すのに適しています。

参考:

個人的にはこうした用途に関しては Stimulus を使うよりも Vue.js でもいいのかな、とまだ思っていたりはします。

ただ、こと Rails においての Vue.js 、 Stimulus との相性とかは全く把握しきれていないので、 Hotwire が打ち出すように Turbolinks などとセットで使うにあたっては Stimulus を用いた方が都合が良かったりするのかもしれません(この辺りは是非もっと僕のような一平凡なデベロッパーにも伝わる形で、有識者の方に知見を発信していただきたいところ)。

StimulusReflex は、この Stimulus と CableReady をベースに Rails に備わる ActionCable を利用した WebSocket 接続を活用して、各ユーザからの更新が即時反映されるようなリアクティブなユーザ体験を持たせる機能導入に特化したライブラリです。

10分で Twitter クローンを作成するデモ映像がありますので、まだ見ていない方はこれで雰囲気確認してみてください。

Hotwire との違いについて

この記事を書いてみたりするまで僕自身もあまり理解しきれていなかったのですが、 hotwire-rails 中に StimulusReflex は含まれておらず、 Turbo 中の Turbo Streams の概念が WebSocket との繋ぎこみを担うようなものになっているようです
( Turbo 自体は Rails/ActionCable に依存しないコンセプトのはずであるため、ここでは敢えて hotwire-rails と表記しています)。

要するに Hotwire と StimulusReflex は似たようなことを実現するツールのようですが全く別物なんですね。
(冷静に考えてみれば、 StimulusReflex は StimulusReflex で独自に開発されたものですもんね……)
いわゆる Rails Way を意識するなら Hotwire の方がまずは抑えるべき技術かもしれません

CableReady のドキュメント中には、 Hotwire のことをライバルや代替ツールとして認めつつも CableReady は Turbo Streams からのアップグレードパスとして機能するだろう 、のような趣旨の内容が含まれています。

忙しい人向け

--webpack=stimulus 付きで rails new する。
あとは公式に書いてある通り、

bundle add stimulus_reflex
rails stimulus_reflex:install

で StimulusReflex をセットアップする。

rails g stimulus_reflex user

でベースになる app/javascript/controllers/application_controller.jsapp/reflexes/application_reflex.rb の準備ができ、
app/javascript/controllers/user_controller.jsapp/reflexes/user_reflex.rb みたいな取り上げたいターゲット向けの処理を定義するファイルが新規追加される

事前準備

さて、ここから今回の記事の本題に入っていきます。

任意の Rails 環境を用意して rails new します。 ActionCable を利用するうえで Redis の導入も必須なのでそこだけ注意してください。

僕の場合は適当に EvilMartins の docker-compose テンプレートを Ruby 3.0 なんかに対応させつつ、 rails new . -f -d postgresql -M --skip-sprockets --webpack=stimulus から始めました。
ソースコードを GitHub にプッシュしているので詳しく確認したい方はそこからどうぞ。

ちなみに今回は Windows ホストのファイルシステム内じゃなくて、 WSL2 上の Ubuntu 20.04 環境内で Docker for Windows を動かしているのだけど、コンテナ内で rails new とかすると軒並み root の持ち物になってしまうので、仕方なく rails new 直後に chown -R 1000:1000 . も実行して Ubuntu のホストユーザの権限に寄せています。

この辺は Windows 側にファイル置いてた方が何も考えなくて済むのだけど、代償にもっとエグイ問題踏んだりすることも多いので Rails 開発をするにあたっては Ubuntu 側にファイル置いておくことにします。なんとかならないものか 🤔

何はともあれ rails new 直後の起動画面が出るところまで準備します。
ここまでは StimulusReflex とはまるで関係ないので、よく分からない人は rails new に重点置いた他の記事からチャレンジしてみてください。

StimulusReflex を追加する – Setup

StimulusReflex 公式サイトのドキュメントに沿って作業、動作確認してみます。

bundle add stimulus_reflex
rails stimulus_reflex:install

を実行します。

途中 Trying to register Bundler::GemfileError for status code 4 but Bundler::GemfileError is already registered 出ましたが、最終的に

StimulusReflex and CableReady have been successfully installed!
Go to https://docs.stimulusreflex.com/quickstart if you need help getting started.

の表記が出ていたので無視します。

Gemfile の差分見た時に redis gem がコメントアウトされたままなのに気付いたので、 redis gem も install させます。

Quick Start – Hello, Reflex World!

StimulusReflex の利用の仕方には2パターンあるそうです。
data-reflex 属性を使ってコードなしに reflex を宣言するか、 Stimulus コントローラの中で stimulate メソッドを呼び出すか。

現時点では何を言っているのか全く分からないのでとにかくサンプルコードを動かしてみます。

下準備

まず下準備で適当な controller と view を準備します

ebi@LAPTOP-5KDTING8:~/stimulus-tutorial$ docker-compose run --rm rails rails g controller Pages index
Creating stimulus-tutorial_rails_run ... done
Running via Spring preloader in process 20
      create  app/controllers/pages_controller.rb
       route  get 'pages/index'
      invoke  erb
      create    app/views/pages
      create    app/views/pages/index.html.erb
      invoke  test_unit
      create    test/controllers/pages_controller_test.rb
      invoke  helper
      create    app/helpers/pages_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    css
      create      app/assets/stylesheets/pages.css

routes.rb を編集して routes を今作った pages/index.html.erb と対応させます

Rails.application.routes.draw do
  root 'pages#index'
  # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end

あれー、なんか滅茶エラー出てる 😅

よく分からないけど、確かに app/javascript/controllers/application_controller.js なんて用意されてないのでとりあえず app/javascript/controllers/index.js 上の該当の import 処理はコメントアウトしておきます

// Load all the controllers within this directory and all subdirectories. 
// Controller files must be named *_controller.js.

import { Application } from "stimulus"
import { definitionsFromContext } from "stimulus/webpack-helpers"
import StimulusReflex from 'stimulus_reflex'
import consumer from '../channels/consumer'
// import controller from '../controllers/application_controller'

const application = Application.start()
const context = require.context("controllers", true, /_controller\.js$/)
application.load(definitionsFromContext(context))
StimulusReflex.initialize(application, { consumer, controller, isolate: true })
StimulusReflex.debug = process.env.RAILS_ENV === 'development'

はい、これで新規画面の追加ができました。

Trigger Reflex actions with data-reflex attributes

ここからはまず一つ目のサンプルコード例を実際に動かしてみます。

app/views/pages/index.html.erb に追記

<h1>Pages#index</h1>
<p>Find me in app/views/pages/index.html.erb</p>

<a href="#"
  data-reflex="click->CounterReflex#increment"
  data-step="1" 
  data-count="<%= @count.to_i %>"
>Increment <%= @count.to_i %></a>

app/reflexes/counter_reflex.rb のファイルを新規作成

class CounterReflex < ApplicationReflex
  def increment
    @count = element.dataset[:count].to_i + element.dataset[:step].to_i
  end
end

うーん、書くだけ書いてみたけどそれらしく動いていない。
まず先ほど見て見ぬふりをした application_controller.js はやはりあった方が良い気がしてきました。と言うか StimulusReflex.initialize(application, { consumer, controller, isolate: true }) で使おうとしているように見える。

そこで、先にUseful Patternsの方に飛んじゃうんですが、普段から馴染みのある Rails の MVC のコントローラ同様にベースとしての application_controler.js を用意するみたいな Application controller パターンの説明が書いてあるところを見つけたのでこれを適用しておきます。

app/javascript/controllers/application_controller.js を新規作成。先ほどの index.js 中のコメントアウトも解除しておきます。
ちなみにしれっとここで適用した connect メソッドの定義がないと StimulusReflex は動かないっぽい。

import { Controller } from 'stimulus'
import StimulusReflex from 'stimulus_reflex'

export default class extends Controller {
  connect () {
    StimulusReflex.register(this)
  }

  sayHi () {
    console.log('Hello from the Application controller.')
  }
}

しかし、まだ上手く動きません。
落ち着いて docker-compose のログを見てみるとこんなログが

rails_1      | Reflex CounterReflex#increment failed: uninitialized constant ApplicationReflex
rails_1      | Did you mean?  ApplicationHelper
rails_1      |                ApplicationRecord
rails_1      |                ApplicationCable

あーね、 ApplicationReflex もベースの定義ファイル必要なのね。確かに落ち着いてコード見返すとそう言う記述になってますね 🙄
と言うことで app/reflexes/application_reflex.rb のファイルを新規作成(これも Useful Pattern ページの内容を参考)

class ApplicationReflex < StimulusReflex::Reflex
end

晴れてカウンターが動作するようになりました。

無事動いたので自分なりのまとめ。

  • これはどちらかと言うと Stimulus に関する話のはずですが、 javascript/controllers 配下に *_controller.js のファイルを切って制御処理を定義していくことになります。
  • 更にその Stimulus コントローラのベースとして application_controller.js を切ります。
    どうやら StimulusReflex を呼び出す data-reflex や後述する stimulate メソッドを利用するにあたって、 StimulusReflex.register(this) の宣言が必須で、
    それを Stimulus が DOM と接続したタイミング( Stimulus のライフライクルをご覧ください )に行う処理 connect メソッド中に登録していて、この application_controller.js に定義しておくことが推奨されてそうです。
  • Stimulus では HTML 標準構文の data 属性 を利用して、引数や設定なんかの宣言を行っていきます。
  • data-reflex が StimulusReflex の呼び出しに関連する属性です。 [DOM-event]->[ReflexClass]#[action] の構文で制御指定をします。
    上記の例では click->CounterReflex#increment と書かれていたので、 click イベントをトリガーに CounterReflex クラスの increment アクションを呼び出すって指示を「Increment」の文字と対応した a タグに指定しています。
  • increment アクションでは @count = element.dataset[:count].to_i + element.dataset[:step].to_i の定義があります。
    element.dataset[:count] , element.dataset[:step] はそれぞれこの CounterReflex クラスを呼び出した a タグに定義されている data-count="<%= @count.to_i %>" , data-step="1"data 属性と対応しています。
    data-step の定義が一クリックごとにいくつの値を足すかの定数指定、 data-count ( @count )がカウント数を保持する変数になります。
  • 初期表示時には @count は未定義ですが、 to_i されているので 0 が表示されているって感じですね

Trigger Reflex actions inside Stimulus controllers

さて今度は Stimulus コントローラから StimulusReflex の利用宣言をしてみます。
先に同じ機能を作ってしまっているので、ここでは decrement するカウンターを足すことにしてみます。

app/views/pages/index.html.erb に追記

<br><a href="#"
  data-controller="counter"
  data-action="click->counter#decrement"
>Decrement <%= @count_2 %></a>

app/javascript/controllers/counter_controller.js を新規作成

import ApplicationController from './application_controller.js'

export default class extends ApplicationController {
  decrement(event) {
    event.preventDefault()
    this.stimulate('Counter#decrement', 1)
  }
}

app/reflexes/counter_reflex.rb に decrement の定義を追記

  def decrement(step = 1)
    session[:count_2] = session[:count_2].to_i - step
  end

app/controllers/pages_controller.rb にインスタンス変数 @count_2 の定義を追加

class PagesController < ApplicationController
  def index
    @count_2 = session[:count_2].to_i
  end
end

Decrement 機能はできたのですが、同時に Increment 向けの @count が初期化される動きになりました 🤔
よく分からなかったので潔く無視します(誰か教えてください……)。

再度自分なりのまとめ

  • 今回は DOM に対して直接 StimulusReflex の宣言をするのではなく、 Stimulus コントローラと対応付けたうえでコントローラ側から StimulusReflex を呼び出すような流れで定義を行いました。
  • data-controller="counter" で DOM と CounterController との対応付けを行っています。
    data-action="click->counter#decrement" は click イベントをトリガーにして CounterController の decrement アクションを呼び出す指定になってます。
  • decrement アクション内では、まず event.preventDefault() しています。この指定がないと、本来の a タグの動作である画面遷移やページ内リンクへの遷移処理が行われてしまい、変な動きになってきます。
  • 次に this.stimulate('Counter#decrement', 1) の指定で CounterReflex の decrement アクションを引数 step=1 を渡しながら実行する指示が書いてあります。この stimulate メソッドを利用するんですね。

StimulusReflex Generator

無駄に苦労させられましたが、どうやら bundle exec rails generate stimulus_reflex user を実行すれば

app/javascript/controllers/application_controller.js
app/javascript/controllers/user_controller.js
app/reflexes/application_reflex.rb
app/reflexes/user_reflex.rb

を自動生成してくれそうです
と言うオチでした。

上記を実行した時の各ファイルの中身はこんな感じになります。
app/javascript/controllers/application_controller.js

import { Controller } from 'stimulus'
import StimulusReflex from 'stimulus_reflex'

/* This is your ApplicationController.
 * All StimulusReflex controllers should inherit from this class.
 *
 * Example:
 *
 *   import ApplicationController from './application_controller'
 *
 *   export default class extends ApplicationController { ... }
 *
 * Learn more at: https://docs.stimulusreflex.com
 */
export default class extends Controller {
  connect () {
    StimulusReflex.register(this)
  }

  /* Application-wide lifecycle methods
   *
   * Use these methods to handle lifecycle concerns for the entire application.
   * Using the lifecycle is optional, so feel free to delete these stubs if you don't need them.
   *
   * Arguments:
   *
   *   element - the element that triggered the reflex
   *             may be different than the Stimulus controller's this.element
   *
   *   reflex - the name of the reflex e.g. "Example#demo"
   *
   *   error/noop - the error message (for reflexError), otherwise null
   *
   *   reflexId - a UUID4 or developer-provided unique identifier for each Reflex
   */

  beforeReflex (element, reflex, noop, reflexId) {
    // document.body.classList.add('wait')
  }

  reflexSuccess (element, reflex, noop, reflexId) {
    // show success message
  }

  reflexError (element, reflex, error, reflexId) {
    // show error message
  }

  reflexHalted (element, reflex, error, reflexId) {
    // handle aborted Reflex action
  }

  afterReflex (element, reflex, noop, reflexId) {
    // document.body.classList.remove('wait')
  }

  finalizeReflex (element, reflex, noop, reflexId) {
    // all operations have completed, animation etc is now safe
  }
}

app/javascript/controllers/user_controller.js

import ApplicationController from './application_controller'

/* This is the custom StimulusReflex controller for the User Reflex.
 * Learn more at: https://docs.stimulusreflex.com
 */
export default class extends ApplicationController {
  /*
   * Regular Stimulus lifecycle methods
   * Learn more at: https://stimulusjs.org/reference/lifecycle-callbacks
   *
   * If you intend to use this controller as a regular stimulus controller as well,
   * make sure any Stimulus lifecycle methods overridden in ApplicationController call super.
   *
   * Important:
   * By default, StimulusReflex overrides the -connect- method so make sure you
   * call super if you intend to do anything else when this controller connects.
  */

  connect () {
    super.connect()
    // add your code here, if applicable
  }

  /* Reflex specific lifecycle methods.
   *
   * For every method defined in your Reflex class, a matching set of lifecycle methods become available
   * in this javascript controller. These are optional, so feel free to delete these stubs if you don't
   * need them.
   *
   * Important:
   * Make sure to add data-controller="user" to your markup alongside
   * data-reflex="User#dance" for the lifecycle methods to fire properly.
   *
   * Example:
   *
   *   <a href="#" data-reflex="click->User#dance" data-controller="user">Dance!</a>
   *
   * Arguments:
   *
   *   element - the element that triggered the reflex
   *             may be different than the Stimulus controller's this.element
   *
   *   reflex - the name of the reflex e.g. "User#dance"
   *
   *   error/noop - the error message (for reflexError), otherwise null
   *
   *   reflexId - a UUID4 or developer-provided unique identifier for each Reflex
   */

  // Assuming you create a "User#dance" action in your Reflex class
  // you'll be able to use the following lifecycle methods:

  // beforeDance(element, reflex, noop, reflexId) {
  //  element.innerText = 'Putting dance shoes on...'
  // }

  // danceSuccess(element, reflex, noop, reflexId) {
  //   element.innerText = '\nDanced like no one was watching! Was someone watching?'
  // }

  // danceError(element, reflex, error, reflexId) {
  //   console.error('danceError', error);
  //   element.innerText = "\nCouldn\'t dance!"
  // }

  // afterDance(element, reflex, noop, reflexId) {
  //   element.innerText = '\nWhatever that was, it\'s over now.'
  // }

  // finalizeDance(element, reflex, noop, reflexId) {
  //   element.innerText = '\nNow, the cleanup can begin!'
  // }
}

app/reflexes/application_reflex.rb

# frozen_string_literal: true

class ApplicationReflex < StimulusReflex::Reflex
  # Put application-wide Reflex behavior and callbacks in this file. 
  #
  # Example:
  #
  #   # If your ActionCable connection is: `identified_by :current_user`
  #   delegate :current_user, to: :connection
  #
  # Learn more at: https://docs.stimulusreflex.com/reflexes#reflex-classes
end

app/reflexes/user_reflex.rb

# frozen_string_literal: true

class UserReflex < ApplicationReflex
  # Add Reflex methods in this file.
  #
  # All Reflex instances include CableReady::Broadcaster and expose the following properties:
  #
  #   - connection  - the ActionCable connection
  #   - channel     - the ActionCable channel
  #   - request     - an ActionDispatch::Request proxy for the socket connection
  #   - session     - the ActionDispatch::Session store for the current visitor
  #   - flash       - the ActionDispatch::Flash::FlashHash for the current request
  #   - url         - the URL of the page that triggered the reflex
  #   - params      - parameters from the element's closest form (if any)
  #   - element     - a Hash like object that represents the HTML element that triggered the reflex
  #     - signed    - use a signed Global ID to map dataset attribute to a model eg. element.signed[:foo]
  #     - unsigned  - use an unsigned Global ID to map dataset attribute to a model  eg. element.unsigned[:foo]
  #   - cable_ready - a special cable_ready that can broadcast to the current visitor (no brackets needed)
  #   - reflex_id   - a UUIDv4 that uniquely identies each Reflex
  #
  # Example:
  #
  #   before_reflex do
  #     # throw :abort # this will prevent the Reflex from continuing
  #     # learn more about callbacks at https://docs.stimulusreflex.com/lifecycle
  #   end
  #
  #   def example(argument=true)
  #     # Your logic here...
  #     # Any declared instance variables will be made available to the Rails controller and view.
  #   end
  #
  # Learn more at: https://docs.stimulusreflex.com/reflexes#reflex-classes

end

おわり

まだ初歩の初歩しかやってないので、これからもう少しまともなサンプルを作ってみてまた記事にしたいです。
もしくは今回が HELLO WORLD 編だったので、 RTFM 編にて。

関連記事

Windows 10 Home 対応の Docker Desktop for Windows を一足早く試してみました

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

肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)

$
0
0

更新情報:
2013/11/19: 初版公開
2021/01/08: 訳文見直し、追記

こんにちは、hachi8833です。今回は、自分が知りたかった、Active Recordモデルのリファクタリングに関する記事を翻訳いたしました。1年前の記事なのでRails 3が前提ですが、Rails 4以降でも基本的には変わらないと思います。リンクは可能なものについては日本語のものに置き換えています。

なお、ここでご紹介したオブジェクトは、app以下にそれぞれ以下のようにフォルダを追加してそこに配置します。

注記: 以下は使われそうなフォルダを列挙しただけであり、実際にはこの一部しか使いません。

refactor

  1. Value Object
  2. Service Object
  3. Form Object
  4. Query Object
  5. View Object
  6. Policy Object
  7. Decorator

⚓ 肥大化したActive Recordモデルをリファクタリングする7つの方法(翻訳)

元記事: 7 Patterns to Refactor Fat ActiveRecord Models
Posted by @brynary on Oct 17th, 2012 (Code Climate Blog)

Railsアプリケーションの品質を高めるためにチーム内でCode Climateを使用していれば、モデルの肥大化を自然と避けるようになるでしょう。モデルが肥大化(ファットモデル)すると、大規模アプリケーションのメンテナンスが困難になります。ファットモデルは、コントローラがドメインロジックで散らかってしまうよりは1段階だけましであるとはいえ、たいていの場合単一責任の原則 (Single Responsibility Principle: SRP)の適用に失敗した状態であると言えます。

SRPの適用は、元々難しいものではありません。ActiveRecordクラスは永続性と関連付けを扱うものであり、それ以外のものではありません。しかしクラスはじわじわ成長していきます。永続性について本質的に責任を持つオブジェクトは、やがて事実上ビジネスロジックも持つようになるのです。1年2年が経過すると、User クラスには500行ものコードがはびこり、パブリックなインターフェイスには数百ものメソッドが追加されることでしょう。それに続くのはコールバック地獄です。

アプリケーションに何か本質的に複雑な要素を追加したら、ちょうどケーキのタネをケーキ型に流し込むのと同じように、それらを小規模かつカプセル化されたオブジェクト群(あるいはより上位のモジュール)に整然と配置することが目標になります。ファットモデルは、さしずめタネをケーキ型に流し込むときに見つかるダマ(混ざらなかった粉の固まり)のようなものでしょう。これらのダマを砕いて、ロジックが等分に広がって配置されるようにしなければなりません。これを繰り返し、それらが最終的にシンプルできちんと定義されたインターフェイスを持つ一連のオブジェクトとなって、それらが見事に協調動作するようにしましょう。

そうは言っても、きっとこう思う人もいることでしょう。

“でもRailsでちゃんとOOPするのってめちゃくちゃ大変ぢゃなくね?!”

私も以前は同じことを思ってました。でも若干の調査と実践の結果、RailsというフレームワークはOOPを妨げてなどいないという結論に達しました。スケールできないでいるのはRailsのフレームワークではなく、従来のRailsの慣習・流儀の方です。より具体的に言えば、Active Recordパターンできちんと扱える範囲を超えるような複雑な要素を扱うための定番の手法がまだないのです。幸いにも、オブジェクト指向における一般的な原則とベストプラクティスというものがあるので、Railsに欠けている部分にこれらを適用することができます。

⚓ (その前に)ファットモデルからミックスインで展開しないこと

肥大化したActiveRecordクラスから単に一連のメソッドを切り出してconcernsやモジュールに移動するのはよくありません。移動したところで、後でまた1つのモデルの中でミックスインされてしまうのですから。いつだったか、こんなことを言っていた人がいました。

“app/concerns ディレクトリを使っているようなアプリケーションって、だいたい後から頭痛の種になる(=concerning)んだよね”

私もそう思います。ミックスインよりも、継承で構成する方がよいと思います継承よりコンポジションの方がよいと思います。このようなミックスインは、部屋に散らかっているガラクタを引き出しに押し込めてピシャリと閉めたのと変わりません。一見片付いているように見えても、引き出しの中はぐちゃぐちゃ、どこに何があるのかを調べるだけでも大変です。ドメインモデルを明らかにするために必要な分解と再構成を実装するのも並大抵ではありません。
これはもうリファクタリングするしかないでしょう。

⚓ 1. Value Objectに切り出す

Value Objectは、異なるオブジェクト同士であっても値が等しければ等しいと見なされる、シンプルなオブジェクトです。Value Objectは変更不可能であるのが普通です。Rubyの標準ライブラリにはDateURIPathname などのValue がありますが、Railsアプリケーションでもドメイン固有のValue Objectを定義できますし、そうすべきです。ActiveRecordからValue Objectへの展開は、すぐにもメリットの得られるリファクタリングです。

Railsでは、ロジックが関連付けられている属性が1つ以上ある場合にはValue Objectが有用です。単なるテキストフィールドやカウンタ以上の要素は、何でもValue Objectの候補になりえます。

ちょうど著者が仕事をしている某テキストメッセージングアプリケーションには、PhoneNumber というValue Objectがあります。そして某EコマースアプリケーションではMoneyクラスを必要としています。私たちのCode Climateには RatingというValue Objectがあり、受け取ったクラスやモジュールのランキングをAからFまでの段階で表します。ここではRuby のStringクラスのインスタンスを使うこともできます(実際使っていました)が、このRatingを使用すると以下のように振る舞いとデータを一体化することができます。

class Rating
  include Comparable

  def self.from_cost(cost)
    if cost <= 2
      new("A")
    elsif cost <= 4
      new("B")
    elsif cost <= 8
      new("C")
    elsif cost <= 16
      new("D")
    else
      new("F")
    end
  end

  def initialize(letter)
    @letter = letter
  end

  def better_than?(other)
    self > other
  end

  def <=>(other)
    other.to_s <=> to_s
  end

  def hash
    @letter.hash
  end

  def eql?(other)
    to_s == other.to_s
  end

  def to_s
    @letter.to_s
  end
end

次にすべてのConstantSnapshotRatingのインスタンスをパブリックなインターフェイスに公開します。

class ConstantSnapshot < ActiveRecord::Base
  # …

  def rating
    @rating ||= Rating.from_cost(cost)
  end
end

このパターンは、ConstantSnapshotがスリムになるだけでなく、他にも多くの利点があります。

  • #worse_than?メソッドと#better_than?メソッドは、レートを比較する場合には<>などのRubyの組み込み演算子よりも適切です。
  • #hash#eql?を定義しておけばRatingをハッシュキーとして使用できます。Code Climateではこれを用いて、定数をレートごとにEnumberable#group_byでグループ化しています。
  • #to_sメソッドを定義してあるので、Ratingを簡単に文字列やテンプレートに変換できます。
  • このクラス定義は、ファクトリーメソッドを導入する場合にも便利です。矯正コスト (=クラスの「臭い」を除去するのにかかる時間) に見合う正しい Rating を得られます。

⚓ 2. Service Objectに切り出す

アクションによってはService Objectを用いて操作をカプセル化できることもあります。著者の場合、以下の基準に1つ以上マッチすればService Objectの導入を検討します。

  • アクションが複雑になる場合 (決算期の終わりに帳簿をクローズする、など)
  • アクションが複数のモデルにわたって動作する場合 (eコマースの購入でOrder, CreditCard, Customer を使用する、など)
  • アクションから外部サービスとやりとりする場合 (SNSに投稿する、など)
  • アクションが背後のモデルの中核をなすものではない場合 (一定期間ごとに古くなったデータを消去する、など)
  • アクションの実行方法が多岐にわたる場合 (認証をアクセストークンやパスワードで行なう、など)。これはGoF (Gang of Four)の書籍で言うStrategyパターンです。

例として、User#authenticateメソッドを取り出して UserAuthenticatorに配置しましょう。

class UserAuthenticator
  def initialize(user)
    @user = user
  end

  def authenticate(unencrypted_password)
    return false unless @user

    if BCrypt::Password.new(@user.password_digest) == unencrypted_password
      @user
    else
      false
    end
  end
end

このとき、SessionsController は以下のような感じになります。

class SessionsController < ApplicationController
  def create
    user = User.where(email: params[:email]).first

    if UserAuthenticator.new(user).authenticate(params[:password])
      self.current_user = user
      redirect_to dashboard_path
    else
      flash[:alert] = "Login failed."
      render "new"
    end
  end
end

⚓ 3. Form Objectに切り出す

1つのフォーム送信で複数のActive Recordモデルを更新する場合、Form Objectを使用して集約することができます。Form Objectを使えば、(個人的には使用を避けたい) accepts_nested_attributes_forよりもずっときれいなコードになります。CompanyUserを同時に作成するユーザー登録フォームを例にとってみましょう。

class Signup
  include Virtus

  extend ActiveModel::Naming
  include ActiveModel::Conversion
  include ActiveModel::Validations

  attr_reader :user
  attr_reader :company

  attribute :name, String
  attribute :company_name, String
  attribute :email, String

  validates :email, presence: true
  # …その他のバリデーション …

  # フォームそのものは決して永続化しない
  def persisted?
    false
  end

  def save
    if valid?
      persist!
      true
    else
      false
    end
  end

private

  def persist!
    @company = Company.create!(name: company_name)
    @user = @company.users.create!(name: name, email: email)
  end
end

これらのオブジェクトではVirtus gemのActive Record的な属性機能を利用しています。Form ObjectはActive Recordと同様に振る舞うので、コントローラは通常と変わらないものになります。

class SignupsController < ApplicationController
  def create
    @signup = Signup.new(params[:signup])

    if @signup.save
      redirect_to dashboard_path
    else
      render "new"
    end
  end
end

Form Objectは、上のようなシンプルな例ではうまくいきますが、永続性のロジックが含まれていてフォームが複雑になるのであれば、Service Objectも併用するのがよいでしょう。

Form Objectを導入することでさらにボーナスが付きます。バリデーションのロジックはコンテキストに依存しがちですが、Active Record自身の中でバリデーションを走らせるという融通の効かない方法に代えて、バリデーションロジックを実際に必要な場所で定義できます。

⚓ 訳追記(2021/01/08)

Rails 5からはActive Recordでattributes APIも使えるようになりました。記事末尾の関連記事もどうぞ。

参考: Rails 5のActive Record attributes APIについて | 日々雑記

Rails 5.2からはActive Modelでもattributes APIを使えるようになりました。

参考: 【Rails】「ActiveModel::Attributes」が便利という話 - 日々の学びのアウトプットするブログ

⚓ 4. Query Objectに切り出す

スコープやクラスメソッドなどのActiveRecordサブクラスが乱雑に定義された、複雑なSQLクエリがある場合は、Query Objectに切り出すことを検討します。1つのQuery Objectは、ビジネスロジックに基づいた結果セットを1つだけ返す責務を担当します。

たとえば、誰も訪問していないお試しを検索するQuery Objectは以下のような感じになります。

class AbandonedTrialQuery
  def initialize(relation = Account.scoped)
    @relation = relation
  end

  def find_each(&block)
    @relation.
      where(plan: nil, invites_count: 0).
      find_each(&block)
  end
end

このオブジェクトを使ってバックグラウンドで以下のようにメールを送信します。

AbandonedTrialQuery.new.find_each do |account|
  account.send_offer_for_support
end

ActiveRecord::RelationインスタンスはRails 3によってファーストクラスオブジェクトとして扱われるため、Query Objectでも多くの機能を使えます。このおかげで、コンポジションを使ってクエリを結合できます。

old_accounts = Account.where("created_at < ?", 1.month.ago)
old_abandoned_trials = AbandonedTrialQuery.new(old_accounts)

この種のクラスは個別にテストする必要はありません。オブジェクトのテストとデータベースのテストは同時に行うようにし、それによって正しい行が正しい順序で返されることと、join(結合)やeager loadingがすべて動作する(N + 1クエリ問題などを回避できているなど)ことを確認します。

⚓ 5. View Objectを導入する

表示にしか使わないようなロジックが必要な場合、それはモデルに置くべきではありません。「仮にアプリケーションのUIががらりと変わったら(たとえば音声駆動UIになったとしたら)、その時にもこれをモデルに置く必要があるだろうか」と自問自答してみましょう。モデルに置く必要のない表示ロジックであることがわかったら、ヘルパーに置くか、できればなるべくView Objectに置くようにしましょう。

たとえば、Code ClimateではRails on Code Climateなどでコードベースのスナップショットに基づいたクラスのレート付けを行う円グラフを使用していますが、これらは次のようにView Objectにカプセル化できます。

class DonutChart
  def initialize(snapshot)
    @snapshot = snapshot
  end

  def cache_key
    @snapshot.id.to_s
  end

  def data
    # @snapshotからデータを取り出してJSON構造に変換するコードを置く
  end
end

ところで、私はビューとERBテンプレート(またはHamlやSlim)が一対一対応していることが多いのに気が付きました。そこで、Railsで使えるTwo Step Viewパターンを実装できないか調べ始めたのですが、今のところこれについては明快なソリューションを見つけられずにいます。

⚓ メモ

Railsコミュニティでよく使われている「Presenter」という用語についてですが、この用語が他の用法と重複したり誤解を招いたりする可能性があるため、著者はこの用語を避けるようにしています。Presenterという語は、本記事で言うところのForm Objectを説明するためにJay Fieldsによって導入されました。また、運の悪いことにRailsでは「View」という用語もいわゆる「(ビューの)テンプレート」を指すものとして使われています。曖昧さを避けるため、著者はView Objectを「Viewモデル」と書くことがあります。

⚓ 6. Policy Objectに切り出す

複雑な読み出し操作はそのオブジェクト自身で行なうのがふさわしいことがあります。私はこのような場合にPolicy Objectを検討します。Policy Objectを使うことで、本質的でないロジック (分析用にどのユーザーをアクティブとみなすか、など) を、中核となるドメインオブジェクトから切り離すことができます。以下は例です。

class ActiveUserPolicy
  def initialize(user)
    @user = user
  end

  def active?
    @user.email_confirmed? &&
    @user.last_login_at > 14.days.ago
  end
end

このPolicy Objectには1つのビジネスルールがカプセル化されています。このビジネスルールでは、emailが確認済みで、かつ2週間以内にログインしたことがあるユーザーをアクティブなユーザーとみなすようになっています。Policy Objectは、複数のビジネスルール (特定のデータへのアクセスを許可するAuthorizer など) をカプセル化することもできます。

Policy ObjectはService Objectと似ていますが、私は「Service Objectは書き込み操作用」「Policy Objectは読み出し操作用」と使い分けています。これらはQuery Objectとも似ていますが、Policy Objectはメモリに読み込み済みのドメインモデルについて操作を行なうのに対し、Query Objectは特定の結果セットを返す「SQLの実行」に特化している点が異なります。

⚓ 7. Decoratorに切り出す

Decoratorは既存の操作に関する機能を階層化することによって、コールバックとよく似た機能を果たします。Decoratorは、特定の環境でしか実行したくないコールバックロジックがある場合や、ある機能をモデルに含めるとモデルの責務が増え過ぎる(=モデルが肥大化する)場合に便利です。

あるブログ投稿にコメントが付くと誰かのFacebookウォールに自動的に投稿されるようにしたとします。この場合、このロジック自体をCommentクラスにハードコードしなければならないわけではありません。コールバックに負わせる責務が多すぎると、テストの実行が遅くなり、不安定になるという形で兆候が現れます。こうした副作用は、何の関連もないテストケースから取り除くべきでしょう。

Facebookへの投稿ロジックをDecoracorに展開する方法を以下に示します。

class FacebookCommentNotifier
  def initialize(comment)
    @comment = comment
  end

  def save
    @comment.save && post_to_wall
  end

private

  def post_to_wall
    Facebook.post(title: @comment.title, user: @comment.author)
  end
end

このDecoratorをコントローラで以下のように使います。

class CommentsController < ApplicationController
  def create
    @comment = FacebookCommentNotifier.new(Comment.new(params[:comment]))

    if @comment.save
      redirect_to blog_path, notice: "Your comment was posted."
    else
      render "new"
    end
  end
end

DecoratorはService Objectとは異なります。Service Objectは既存のインターフェイスに対する責務を階層化しますが、これをDecorator化すると、FacebookCommentNotifier インスタンスをあたかも単なるCommentであるかのように取り扱います。

Rubyは、メタプログラミングを使用してDecoratorを簡単に作成するための仕組みを標準ライブラリに多数備えています。

⚓ 最後に

複雑なモデル層をうまく取り扱うためのツールは、Railsアプリケーションにも多数存在します。これらのツールを使用するためにRailsを捨てる必要などありません。Active Recordsは素晴らしいライブラリですが、これだけに頼っていてはどんなパターンも失敗します。ActiveRecordsは、極力永続的な振る舞いにとどめておくようにしてください。本記事で紹介したテクニックを一部だけでも適用して、自分のアプリケーションのドメインモデルに固まっているロジックを分散させることができれば、アプリケーションのメンテナンスはずっと容易になるでしょう。

本記事で紹介したパターンの多くはシンプルです。これらのオブジェクトは、いずれも「昔ながらのPORO(Plain Old Ruby Object: シンプルなRubyオブジェクト)」であって、ただその使い方が異なるだけです。これはOOPの一部であり、OOPの美しさをなすものです。問題を解決するのにフレームワークやライブラリだけに頼る必要はありません。手法に適切な名前を付けることも重要な技法です。

本記事で紹介した7つの手法はいかがでしたでしょうか。お気に入りの手法は見つかりましたでしょうか。それはどんな理由でしょうか。皆さまのコメントをお待ちしています。

追伸: この記事を気に入っていただけましたら、元記事の下にあるフォームからCode Climateのニュースレターをぜひ購読してみてください。OOPやRailsアプリケーションのリファクタリングなど、今回の記事のようなトピックを扱っています。記事のボリュームは控えめにしています。

より詳しい情報

本記事をレビューしてくれた皆様に感謝します: Steven Bristol, Piotr Solnica, Don Morrison, Jason Roelofs, Giles Bowkett, Justin Ko, Ernie Miller, Steve Klabnik, Pat Maddox, Sergey Nartimov, Nick Gauthier

関連記事

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

Rails5: ActiveRecord標準のattributes APIドキュメント(翻訳)

Rails 6.1: CHECK制約のサポートをマイグレーションに追加(翻訳)

$
0
0

概要

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

なお、該当のAPIドキュメントは以下です。

Rails 6.1: CHECK制約のサポートをマイグレーションに追加(翻訳)

従来のRailsでは、ADD CONSTRAINT カラム名 CHECK 制約を行うにはマイグレーションで生SQLを実行しなければなりませんでした。

Bookモデル用のテーブルを作成し、そのpriceフィールドに「価格は100より大きいこと」という制約を付けたいとします。従来は以下のように、テーブル作成後にマイグレーションで生SQLを書くしか方法がありませんでした。

class CreateBooks < ActiveRecord::Migration
  def change
    create_table :books do |t|
      t.string :name
      t.integer :price
    end
  end
end
class AddConstraintToBooks < ActiveRecord::Migration
  def up
    execute "ALTER TABLE books ADD CONSTRAINT price_check CHECK (price > 100)"
  end

  def down
    execute "ALTER TABLE books DROP CONSTRAINT price_check"
  end
end

しかもこのマイグレーションはそのままではロールバックできないので、upメソッドとdownメソッドを別々に書かなければいけません。

解決方法check_constraint:add_check_constraintremove_check_constraint

Rails 6.1のマイグレーションにcheck_constraintメソッドが追加され、テーブル作成時にDSLとしてcheck_constraintを使うことも、テーブル作成後のマイグレーションで使うこともできるようになりました(#31323)。

テーブル作成時にcheck_constraintを使う場合の構文は以下のとおりです。

create_table :table_name do |t|
  ...
  t.check_constraint [constraint_name], [constraint]
end

既存のテーブルで制約を追加または削除するには、check_constraintを以下の構文で用います。

add_check_constraint :table_name, :constraint_condition, name: "constraint_name"
remove_check_constraint :table_name, name: "constraint_name"

add_check_constraintメソッドとremove_check_constraintメソッドは、どちらもロールバック可能である点にご注目ください。

例:

先ほどのBookモデルの例を用います。

以下のマイグレーションでは、booksテーブル自身を作成するときにcheck_constraintメソッドを追加していることにご注目ください。

class CreateBooks < ActiveRecord::Migration
  def change
    create_table :books do |t|
      t.string :name
      t.integer :price
      t.check_constraint "price_check", "price > 100"
    end
  end
end

別のマイグレーションで、booksテーブルにprice_check制約を追加する場合は以下のように書きます。

class CreateBooks < ActiveRecord::Migration
  def change
    add_check_constraint :books, "price > 100", name: "price_check"
  end
end

別のマイグレーションで、price_check制約をbooksテーブルから削除するには以下のように書きます。

class CreateBooks < ActiveRecord::Migration
  def change
    remove_check_constraint :books, name: "price_check"
  end
end

関連記事

Rails 6.1: 属性にデフォルト値を設定しても型が失われなくなった(翻訳)

Rails 6.1: 関連付けをバックグラウンド削除する「dependent: :destroy_async」(翻訳)

$
0
0

概要

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

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

Rails 6.1: 関連付けをバックグラウンド削除するdependent: :destroy_async(翻訳)

Railsに組み込まれているdependent:オプションを使うと、dependent:に指定したオーナーがdestroyされたときに関連付けられたレコードをどうするかを指定できます。

従来のRailsでは、dependent::destroy:delete_allなどの値を渡せます。

:destroy:delete_allは互いによく似通っていますが、唯一の違いは、:delete_allは関連付けられたオブジェクトをすべて削除するときにコールバックを一切実行しないことです。

訳注: dependent:オプションに:destroy:delete_allがあるのはhas_many関連付けの場合です。

has_many
dependent:削除系オプション
:destroyまたは:delete_all
(他のオプションは省略)
belongs_to
dependent:削除系オプション
:destroyまたは:delete
(他のオプションは省略)
has_one
dependent:削除系オプション
:destroyまたは:delete
(他のオプションは省略)

オブジェクトの関連付けが何層にも積み重なっている大規模なシステムでは、削除のカスケードが失敗することがあります。あるモデルが持つ関連付けが削除されると、それによって他の削除もトリガーされて、複雑なツリーの下まで続く可能性があります。このようなカスケード削除は非常に時間がかかるので、サーバーでタイムアウトエラーが発生するとロールバックする可能性があります。

解決方法: dependent:オプションに:destroy_asyncを指定する

Rails 6.1から、関連付けでdependent:の値に:destroy_asyncを渡せるサポートが追加されました(#40157)。

AuthorモデルとBookモデルがあるとします。

以下のように、has_many :books関連付けのdependent:オプションに:destroy_asyncという値を渡していることにご注目ください。

class Author < ApplicationRecord
  has_many :books, dependent: :destroy_async
end
class Book < ApplicationRecord
  belongs_to :author
end

たとえばidが1のauthorがBooksモデルの4つのid(1, 2, 3, 4)に関連付けられているとすると、idが1のauthorを削除したときにRailsが以下のようなジョブを1件作成します。

Performing ActiveRecord::DestroyAssociationAsyncJob (Job ID: 40d5c0cf-ed73-493b-8964-541ae6f1960f) from Async(active_record_destroy) enqueued at 2020-11-16T15:27:00Z with arguments: {:owner_model_name=>”Author”, :owner_id=>1, :association_class=>”Book”, :association_ids=>[1, 2, 3, 4], :association_primary_key_column=>:id,

このジョブが実行されると、Authorモデルにあるそのレコードがデータベースから削除され、以下のようなログが出力されます。

Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: 40d5c0cf-ed73-493b-8964-541ae6f1960f) from Async(active_record_destroy) in 21.8ms

関連記事

Rails 6.1: CHECK制約のサポートをマイグレーションに追加(翻訳)

Viewing all 1386 articles
Browse latest View live