【Rails】デプロイのたびにseed処理でパスワードが更新され強制ログアウトされていた

問題の概要

先日、開発・検証環境でデプロイするたびにユーザーが強制的にログアウトする問題に当たりました。

関連して使っていたgemは以下です。

原因を調査すると、以下のことがわかりました。

  • この現象が起きるユーザーはseedで作成したユーザーであること
  • デプロイ時にdb:seed_doを実行している際に起きている現象であること

これを踏まえてseedファイルを確認すると、書き方に原因があったことがわかりました。

問題のあった書き方

User.seed(
  :email,
  { :email => "jon@example.com",   :password => "pass1234" },
  { :email => "emily@example.com", :password => "pass1234" }
)

seed-doのgemを素直に使うとこんなコードになりますが、素のパスワード情報は当然DB側で持っていないので、元のパスワードが何であれ(pass1234であっても)、毎回パスワードを変更する処理がかかります。
パスワード変更することでセッションが切れて、seed_do実行するごとにログアウトされていました。

パスワードも常にリセットしたい場合はこれでもいいのですが、私の場合はログアウトされないようにしたかったのでログアウトされないような対応をすることにしました。

対応策

1. find_or_create_byを使う

User.find_or_create_by!(email: "jon@example.com") do |user|
  user.password = "pass1234"
end

上記のようなコードであれば、同じemailのユーザーが存在すれば処理をスキップするので、passwordの更新はかかりません。

2. bulk_insertを使う

私が遭遇したプロダクトにはすでに activerecord-import gemがインストールされていたので、このgemを使うことにしました。

※ 前提として、Userモデルのテーブルのemailカラムにユニーク制約が設定されているとします。

users = [
  User.new(email: 'jon@example.com', password: 'pass1234'),
  User.new(email: 'emily@example.com',  password: 'pass1234'),
]

# on_duplicate_key_ignore: 一意制制約に違反する場合は処理をスキップする(パスワードを上書きしない)
User.import!(users, on_duplicate_key_ignore: true)

on_duplicate_key_ignore オプションを使えば、ユニーク制約をかけているemailを見て、重複レコードになりそうであればスキップしてパスワードを上書きしません。

注意点

もしseed実行時に間違いなく設定ファイル通りにデータをリセットしたい場合は、今回のような方法だとかえって混乱する場合がありますので、その辺りは理解した上で修正する必要があります。
(氏名レコードが別途ある場合、emailだけ見てスキップするコードだとパスワードの他にも氏名もリセットされないなど)

『ドメイン駆動設計入門』読書メモ・感想

www.shoeisha.co.jp

先日、上記の本を読んだので読書メモを残します。 (知識のまとめに若干AIの力を借りています)

書籍の感想

「ドメイン駆動設計」というものについて言葉だけ知っているが意味はよく知らない、という状態だったので読んでみました。
「ボドムアップでわかる!」あるように具体例から解説をされていて、なぜこれが必要なのか、という点が理解しやすく感じました。
普段Railsでアプケーション開発をしていると、Railsの「レール」で開発をしていると遭遇しない層の話などもあり、Railsの「設定より規約」の思想をより感じることになった一冊だったような気がします。

ドメイン駆動設計とは

ビジネスの問題を解決するためにビジネスの理解を進め、ビジネスの表現をする。ビジネスとコードを結びつけて継続的かつ反復的な改良を施せるように枠組みを作ることにより、ソフトウェアをより役立つものにしようというものです。

成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (p. 4). (Function). Kindle Edition.

先日読んだ『ソフトウェアアーキテクチャの基礎』でもそうだったが、「継続的に反復する」というのは現代で良い設計とされるための重要な要素なんだろうという感想です。

Chapter1 ドメイン駆動設計とは

  • ドメインは「プログラムを適用する対象となる領域」
  • ドメインの概念をモデリングして得られたモデルをドメインモデルと呼ぶ
    • モデリング: ドメインの概念を、ソフトウェアで必要な情報に限定して抽象化する作業
  • ドメインモデルをソフトウェアで動作するモジュールとして表現したものがドメインオブジェクト
  • したがって、ドメインの変化があれば、ドメインモデルを通じてドメインオブジェクトに伝達される

Chapter2 値オブジェクト

  • 値オブジェクトとは、「氏名」などのようにシステム固有の値を表したオブジェクトのこと
  • 値オブジェクトの値は不変
  • 値オブジェクトにすべきかどうかの判断基準は、「そこにルールがあるか」「それ単体で扱いたいか」
    • 振る舞いをまとめてルールを語らせる(自己文書化)
  • 「表現力を増す」「不正な値を存在させない」「誤った代入を防ぐ」「ロジックの散在を防ぐ」を実現するための手段となる

Chapter3 エンティティ

  • 値オブジェクトとの主な違いは「同一性によって区別される」「可変」「同じ属性でも区別される」「ライフサイクルがある」 -「ユーザー」のイメージ
    • idが同じであれば同じ、同じ生年月日でも別々のユーザーと認識される、退会でユーザーが削除される、など
値オブジェクト エンティティ
同一性の基準値 (中身) が同じなら同じもの ID が同じなら同じもの
不変性 不変 変化してよい
金額、住所、色、座標 ユーザー、注文、商品

Chapter4 ドメインサービス

  • ドメインオブジェクト(値オブジェクトやエンティティ)に記述すると不自然になる振る舞いを記述する場所
  • 例えばユーザーの重複チェックをユーザーオブジェクトにさせると、ユーザーオブジェクト自身に問い合わせることになり不自然
  • ただし、ドメインオブジェクトに記載できることまでドメインサービスに書くと、ドメイン貧血症(ドメインが何も語らなくなる)になるので注意

Chapter5 リポジトリ

  • データの永続化にまつわる処理を抽象化するための層(DB操作など)
  • ドメインオブジェクトがDBの変更等の影響を受けない

感想

RailsだとActiveRecordでいわゆる「ドメインオブジェクト」と「リポジトリ」の役割が両方入っており明確に違うなーと勉強になった。

Chapter6 アプリケーションサービス

  • アプリケーション向けのユースケースを実現するオブジェクト
    • ユーザー登録など

感想

RailsのServiceクラスの役割に一番近いのはこれかなーと思ったりした。

Chapter7 柔軟性をもたらす依存関係のコントロール

  • 依存関係逆転の法則
      1. 上位レベルのモジュールは下位レベルのモジュールに依存してはならない、どちらのモジュールも抽象に依存すべきである。
      1. 抽象は、実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである。
  • とあるアプリケーションサービスがリポジトリクラスに依存してしまっている場合(上位が下位に依存し、抽象が詳細に依存してしまっている状態)、抽象層を作成することでアプリケーションサービスとリポジトリクラスが共に抽象に依存できるようになり、リポジトリの変更がアプリケーションサービスに影響しないようにできる
    • データベースの操作のコードを変えたらより高レベルなアプリケーションサービスのコードを変えないといけない、というのは避けたい

Chapter9 複雑な生成処理を行う「ファクトリ」

  • 複雑なオブジェクトの生成処理をオブジェクトとして定義すること
  • 「コンストラクタ内で他のオブジェクトを生成するかどうか」はファクトリを作る際の動機づけに良い指標

Chapter10 データの整合性を保つ

  • ユニーク制約やトランザクションを使って整合性を保つ
  • ただ、整合性は高次元な概念
    • ビジネスロジックには整合性を保つための詳細ではなく、整合性が必要な処理であることの明確な主張が書かれるべき

Chapter12 ドメインのルールを守る「集約」

  • 集約ルートを通ってのみオブジェクトを操作させるようにする
  • 関連するものとして、オブジェクト指向では「デルメルの法則」がある
    • userクラスの中で、address.location.cityとせず、userがcityメソッドを呼べるようにして、内部構造(addressやlocation)を知る必要をなくすようなこと

Chapter13 複雑な条件を表現する「仕様」

  • あるオブジェクトがある評価基準に達しているかを判定するオブジェクト
  • ドメインオブジェクトが必要以上に他のオブジェクトの情報を知らないといけなかったりする際に導入し、複雑な評価処理をカプセル化する

『ソフトウェアアーキテクチャの基礎』読書メモ・感想

年末年始に、そのボリュームの多さになかなか手がつけられていなかった『ソフトウェアアーキテクチャの基礎 -エンジニアリングに基づく体系的アプローチ』を読みましたので、自分なりのまとめと感想を記事にしました。

www.oreilly.co.jp

どんな本?

  • 現代のソフトウェアアーキテクチャのバリエーションやトレンド、それぞれの特徴について解説しつつ、ソフトウェアアーキテクトという職業に求められるスキルなどについても解説している本
  • いわゆるコードレベルの設計ではなく、例えばマイクロサービスとか、モノリシックなアーキテクチャとか、そういう次元の話
  • 自分は本書でいう「アーキテクト」の仕事はしたことはなく、「開発者」の立場として読んだが、興味深かった
  • あまり知らない領域の話だったものの、丁寧に解説されていて、大事なことは何度も繰り返し主張されるので、意外と理解に困ることはなかった

各章の自分的まとめ・感想

2章「アーキテクチャ思考」

  • アーキテクトは、技術的な深さよりも幅が求められ、そのための努力が必要
  • アーキテクチャ決定は必ずトレードオフであることを意識する必要がある

3章「モジュール性」

  • 計算式は理解が難しくて完全にはわからなかった
  • 適切な抽象化やコナーセンスの度合いを抑えることで変更につよいシステムを構築することが必要

qiita.com

4章「アーキテクチャ特性」

  • いわゆる「非機能要件」の言い換え的な話
  • 色々な特性(信頼性、拡張性、etc)があるが、全てを取るのは不可能であり、どの特性が必要かを選択する必要がある
  • 詰まるところ、これもトレードオフの関係になるし、イテレーティブな設計になっていればやり直しがしやすくなる

5章「アーキテクチャ特性を明らかにする」

  • ドメインの関心事をアーキテクチャ特性として翻訳する
    • この際に、短絡的にならないように注意。
    • 例えば、市場投入までの時間というドメインの関心事に対して、「アジリティ」というアーキテクチャ特性だけに翻訳してしまうと、テスト容易性やデプロイ容易性の観点を損ない、目的が達成できない可能性がある
  • アーキテクチャ特性の優先順位を決めすぎず、上位3を決める、というような方法にする
    • どのアーキテクチャ特性が重要ではないかを決めるのは必要だが、一番必要なのはどれか、というところは不毛な議論になりやすい
  • アーキテクチャアーキテクチャ特性を満たさなくても、アプリケーションの設計で満たすことができる可能性もある

6章「アーキテクチャ特性の計測と統制」

  • アーキテクチャ特性を計測する仕組みを作ることが必要
  • また、必要と決めたアーキテクチャ特性については、適応度関数の仕組みなどを入れて統制させる仕組みづくりが必要

7章「アーキテクチャ特性のスコープ」

  • アーキテクチャ特性について、以前はシステム全体に対する特性、だったが、現代の開発では、マイクロサービスの登場などにより、もっと細かい単位でスコープの設定をするべき状況になっている
  • その単位を著者らは「アーキテクチャ量子」と定義している
  • 本章では、オークションサイトのアーキテクチャ例を紹介しながら、例えば入札者と競売人で必要なアーキテクチャ特性が異なることなどを説明している

8章「コンポーネントベース思考」

  • モジュールをパッケージ化した「コンポーネント」を発見することが必要
  • そのためには、アーキテクチャをどのように分割するか(最上位の分割)を決める。
    • これには、ドメインによる分割や、技術による分割などの手法がある

10章「レイヤードアーキテクチャー」

  • シンプルで親しみやすく、コストが低い
  • アーキテクチャが固まっていない時点の出発点としても適切になることが多い
  • 一方で、システム単位が大きくなるので、デプロイ容易性や、弾力性、耐障害性(どこかでメモリ不足になると全体がクラッシュ)が低いなどのデメリットもある

11章「パイプラインアーキテクチャ

  • bashzshの基本原則になっているようなアーキテクチャ
  • 特にシンプルな一方通行の処理を数sめるタスクで用いられる
  • シンプルなのは強みである一方で、基本的にはモノリシックなので、弾力性などは低い

12章「マイクロカーネルアーキテクチャ

13章「サービスベースアーキテクチャ

バランス型、という感じで、書籍の中でも割と推されているような記述ぶりに感じた。

14章「イベント駆動アーキテクチャ

  • 非同期的にイベントを送受信して処理するアーキテクチャ
  • リクエストベースモデルとイベントベースモデルという分類がある
  • ブローカートポロジーは、イベントの処理が終わったのち、受信する人がいるかどうかを気にせずイベントを発生するもの。拡張性に優れるが、ワークフローを管理する役割を持つ機能が不在
  • メディエータートポロジーは、イベントの受信を請け負って、ワークフロー全体の動向を管理するが、その分拡張性はブローカーほどではない
  • 他のアーキテクチャとハイブリッド形式で採用されることも多い

15章「スペースベースアーキテクチャ

  • キャッシュを使った(DBへの接続回数を減らす)アーキテクチャ
    • 複数の「処理ユニット」間で、データのキャッシュをレプリケーションして同期的に持つ(ただし、レプリケーションではなく、一元的にキャッシュを管理して各処理ユニットがリモートアクセスする分散キャッシュを利用することもある)
    • キャッシュを更新してからDBを非同期で更新する
  • それゆえ、更新のタイミングによってはキャッシュのデータに不整合が生じることもある
  • クラウドとオンプレミス環境を跨いでデプロイする(例: DBと読み書きをするデータリーダー/ライターだけオンプレにする)こともできる
  • これらの特徴から、コンサートチケットのサイトなどに適している(DBへのアクセスが少なく、弾力性が高い)
  • 一方で、非常に複雑なので、テストが難しく、コストも高くなる

16章「オーケストレーション駆動サービス指向アーキテクチャ

  • 過去に分散アーキテクチャとして考案されたものの、あまりうまくいかなかったもの
  • 再利用することに対するトレードオフは結合が高まること

この章の話は、全体的にあまりピンと来なかった

17章「マイクロサービスアーキテクチャ

  • 高度な分離をし、「境界づけられたコンテキスト」の論理的概念を物理的にモデル化することを目的としたアーキテクチャ
  • 「マイクロ」という名前は最小のという意味より、単なるラベル名と思うべき。トランザクションがサービス間を跨ぐことがないようなものであることは必要。
  • データベース含めて高度に分離されるので、各サービスで言語環境を変えるなどの組織計画も可能になる
  • サービス間で連携が必要な場合は、イベント駆動アーキテクチャでいう「ブローカー」や「メディエーター」のパターンを採用する
    • それぞれのメリット・デメリットはブローカー/メディエーターと同じ
  • どうしてもサービスを跨いでトランザクションを構築したい場合は、サーガという「メディエーター」の役割をするサービスを別途構築してそのサービスに管理させるという方法があるものの、結合してしまうトレードオフがあるので、基本的にはサービスを跨いでトランザクションを構築するのは推奨されず、サービスの粒度の見直しを優先的に検討するべき
  • コストや複雑性は課題だが、現在のエンジニアリングプラクティスで重視されるデプロイやテストの容易性に強みを持つ

19章「アーキテクチャ決定」

20章「アーキテクチャ上のリスクを分析する」

  • リスクマトリックスを利用し、アーキテクチャ内のどの部分にどのようなリスクがどのくらいあるかを分析する
  • リスクアセスメントとしてドメイン領域などで区切ってレポートにまとめる
    • この際、リスクの方向(改善か、悪化か)も重要な情報となる。リスクの方向の伝え方は工夫が必要(矢印などだと誤解を生みやすい)
  • 開発者(テックリード)と一緒にリスクストリーミングを行い、
    • リスクの特定(これは個人作業)
    • リスクの合意(どのアーキテクチャにどのレベルのリスクがあるかの合意を取る)
    • リスクの軽減(最も重要。合意したリスクに対して、それを軽減する方法を検討する。これを丁寧に行うことで、コスト増とのトレードオフ調整などがしやすくなる側面もある)

21章「アーキテクチャの図解やプレゼンテーション」

  • アーキテクトは良いアーキテクチャを考えても、きちんと伝えなければ意味がない
  • 図解を作成する上では、時間をかけすぎると作成した資料に執着が生まれてしまい、検討の邪魔になるので、最初は付箋などアナログな方法を活用してイテレーティブにできると良い
  • 図解を示したり、プレゼンテーションを工夫しないといけない

具体的な方法がいくつか述べられていた。今は生成AIを活用するのもよさそうと思った。

22章「効果的なチームにする」

  • アーキテクトが作成する制約はキツすぎても(コントロールフリーク)、緩すぎても(アームチェアアーキテクト)いけない
    • 開発者からアーキテクトになりたてだと、コントロールフリークに陥りやすい
    • 逆に、開発から離れていると、アームチェアアーキテクトに陥りやすい
  • どのように管理するのが良いかは、チームやプロジェクトの性質によるので、都度分析が必要
  • プロジェクトメンバーが多すぎると、多元的無知(自分の意見が言えなくなる)、責任の分散(自分で何かしようと思わなくなる)など、不具合が発生しやすくなる
  • チェックリストを活用する。静的解析など自動化できるものは自動化する
  • 開発者が技術スタックを使用したいときには、「ビジネス上の根拠」も確認するようにする
    • 開発者は、これが達成できないのに使用をしたいということがある

マネージャーとアーキテクトの棲み分けもなかなか難しそうと思った。
また、これを見るとアーキテクトは設計して終わりではなくて、開発・運用する間も一緒にプロジェクトを走らないと意味ないんだなーという感想を持った。

23章「交渉とリーダーシップのスキル」

  • 交渉
    • アーキテクトが下すほぼ全ての決定には異議が唱えられるため、交渉の力が非常に重要
    • ステークホルダーとの普段の会話から重要なアーキテクチャ特性を見極め、交渉の要素にすると良い
    • 交渉の際には、費用は時間は非常に重要であるものの、交渉の材料としては最後の手段にするべき
    • ステークホルダーが譲れないと言っている条件が、実はシステム全体ではなくシステムの一部が対象であり、その結果コストを抑えられることもある
    • 他のアーキテクトと衝突した場合は、議論しすぎるのではなく、冷静に実証することに努める
    • 相手が聞くのをやめてしまわないよう、根拠や理由を最初に説明する
  • リーダシップ
    • アーキテクトという肩書きで仕事をするのではなく、手本を示して周りを動かす
    • 開発者が質問や問題を相談できる相手になることを心がける
    • 開発チームに溶け込む時間を確保するため、アーキテクトは招待されたMTGについては自分の必要性を確認しつつ、開発者がMTGに巻き込まれているのを見たら、自分が代わりに参加して開発者の時間を確保することを検討する

交渉の方は、本で読むだけでしんどい仕事だなー、、、と感じた。
バズワードを利用する」というのは一般的には反感を買いそうな言葉だなーと思ったが、交渉のテクニックとしては必要か、、という気持ち。
リーダシップの方は、アーキテクトに関わらずリーダの役割として仕事をする上で大切なことが書かれている気がした。
「肩書きで仕事しない」の箇所は、「役職で仕事をしないこと」と公務員をしていた頃に散々聞いてきた話を久々に聞いた感じがして懐かしい気持ちになった。

第24章「キャリアパスを開く」

  • 技術は目まぐるしく変化するので、常に学び続ける必要がある
  • 朝一、20分の時間を使って技術的な幅を広げるための時間を作る(「未知の未知」を「既知の未知」にする)
  • テクノロジーレーダーを活用して、自分が次に何を追求するかのビジョンを描く
  • ソーシャルメディアを使用して、「弱いつながり」(新しい仕事のオファーや普段の経験の外側からのアドバイスが期待できる)を広げる

みんな分かっているけどなかなかできないよね、、という章。
ただ、20分を使って、「未知の未知」を「既知の未知」にする、というのはハードルが下がってよさそうな気がした。
あとSNSは、どうしても自分には向いていないと思ってしまうので、どうしたもんかな、、、という感想でした。

『リファクタリング 第2版』読書メモ

積読していた『リファクタリング 第2版』を読みました。

www.ohmsha.co.jp

どんな本?

リファクタリングとは、リファクタリングの意義を前半で解説した上で、後半(6章〜)はリファクタリングのサンプルカタログのようになっています。

私は、1-5章は丁寧に読み、6章以降は、具体的な手順はそこそこに、どんなケースでどんなリファクタリングをするべきなのか、という点を確認していく読み方をしました。

感想

自分が考えていたリファクタリングは、本書で解説されているリファクタリングより少し狭かったなーと感じました。

というのも、自分の考えていたリファクタリングは、

  • 従来よりもコード数が少なくなる(よりスッキリ書く)
  • 適切なクラスに責務を負わせる
  • 実装効率が改善する

などで、「可読性」についてはちょっと意識が少なかったかも、、という感想を持ちました。

以下、「印象に残った箇所」でも紹介していますが、 例えば「せっかく一度読んだコードの意味を覚えておくために関数化する」「可読性を採った結果、多少非効率に見える実装になっても、パフォーマンス上は実は無視できるレベルであることも多い」というのは、あまり考えていなかったポイントでした。

本書を読み進めている最中から、早速実務の方でも意識的に取り組んでみましたが、「可読性」を重視したリファクタリングは、結果スッキリ見えるし、メンテナンス性も上がるよなーという印象です。

印象に残った箇所

第1章

  • P7

理解したコードを埋め込んでおく方法とは、意味のあるコードの塊を関数に

処理をまとめて関数にすることの理由を、再利用とか見やすくするとかぼんやり理解していたが、「せっかく理解したコードの内容を忘れないようにする」ということをしていたんだなと気づきがあった。

  • P14

ローカル変数を削除することによる大きな利点は、扱うべきローカルスコープが減ることにより、メソッドの抽出がずっと楽になること

この箇所では、ローカル変数を削除する代わりにメソッドを何度も呼ぶことになるコードが紹介されていた。 自分だったら、何度も計算するのが嫌でローカル変数にすぐ入れてしまうなーと思ったので、パフォーマンス上問題ない限りは、リファクタリングする上ではローカル変数は減らすべきというのは頭に入れていきたい。 筆者は、別の箇所で、パフォーマンス改善もリファクタリングしてからの方がしやすい、とも言っている。一度パフォーマンスのことは気にせず、整理してからパフォーマンスに向き合うというのもアリなんだなーという気づきを得た。

  • P27あたり

中間データ構造を設けて、計算を担当する部分と表示を担当する部分を切り離すというもの。 中間データ構造を設けることで、関連する関数同士を切り離せるようにするという発想は色々な場面で使えそう。

  • P33

「簡潔さは知恵の要」と言われますが、明確さは進化するソフトウェアの要

コード量が長くなるのは良くないと思いがちだが、それによって明確さが増すのであればむしろ良いコードになるということを意識したい。

第2章

  • P46

ここのリファクタリングは非常に小さいステップ、またはそれらの組み合わせてでできています。その結果、リファクタリングではコードが壊れた状態になっている期間は非常に短く、たとえ未完成であっても、いつでも中断が可能です。

大規模なリファクタリング計画があったとしても、小さいステップで手順を踏み、その都度テストが通るように行なうリファクタリングを積み重ねるというプロセスを取るべきというもの。 実際に私も、リファクタリングするぞ、となった時、ついついいきなり大きく変更を加えてしまうことがあるが、これではリファクタリングになっていないということを意識しないといけないと感じた。

リファクタリングの途中で気がついたバグについては、リファクタリングも残っているべき

リファクタリング中にバグに気づくのはあるあるで、つい直してしまうが、勇気を持って見逃す(メモはしておく)というのも時には必要。 今までは、気付いたからには直さないといけないのでは?という気持ちで、リファクタリングを中断してバグ修正を先に行うなどしていたが、この書籍を以て、バグの程度によってはバグを保ったままリファクタリングを続けるという選択肢も持っておきたい。

  • P57

リファクタリングは、コードベースがどれだけ美しいかではなく、純粋に経済的な基準で測られるもの

あくまでリファクタリングは機能追加等をしやすくするためのもので、3行で書いていたコードをトリッキーなことをして1行にした!(ただし、読みづらくなっている)というような類ではないよ、という話。 これは結構勘違いしやすいところがある気がする。

  • P74, P75

良い名前が思いつかないということは、設計がまだ固まっていないことの兆候でもあります


コメントが必要だと感じたとき、代わりにわかりやすい名前を付けるようにする

今まで何度か聞いた言葉だが、改めて意識したい。

  • P236「ループの分離」

1つのループで複数のことをやらず、ループを分離しよう、というもの。 書籍の例に漏れず無駄に2回ループ回すのを避けてしまうが、パフォーマンス上問題ないケースも確かに多くありそう。

自分はポリモーフィズムを使うというリファクタリングをする勇気が持てないことが多いので、ここにあるガイドブックをもとに少しずつ挑戦してみようかな、、という気持ちになった。

  • p 332, p335 「問い合わせによるパラメータの置き換え」「パラメータによる問い合わせの置き換え」

どちらを取るかという1つの基準は「参照透過性」というのはあまり考えたことがなかったので、参考になった。

CSVを使った一括インポート機能を作成する際に確認するポイント

先日、CSVファイルを使ったデータの一括インポート機能を作成する機会がありました。 一連の実装を振り返ると、気にすることや決めるべきことが色々あるなーと思ったので、メモしておきます。

異常系への対応

✅ ファイルの形式が間違っているケースの対応をする

  • HTMLのaccept属性で制限するのとは別に、csvファイル以外がアップロードされた場合のサーバーサイドでの対策が必要です
  • 例えばrubycsvライブラリでは、CSV::MalformedCSVErrorが発生するので、それをrescueする処理を書いておくのが1つの方法です
begin 
  CSV.parse(...)
rescue CSV::MalformedCSVError
  flash.now[:alert] = 'CSVファイルをアップロードしてください'
end

✅ インポートできるデータの上限数を決めておく

  • 無制限にしてしまうと、サーバーの圧迫などを招く可能性があります
  • したがって、例えば以下の要素を検討し、適切な上限を設定する必要があります
    • ユーザーが不便に感じない上限数
    • パフォーマンス上問題ない上限数
    • プレビュー画面で表示した際にユーザーが見やすい上限数

✅ インポートされたデータが0件のケースの対応をする

  • 意外と忘れがちなのが、ユーザーがテンプレートをそのままアップロードしてしまった場合など、登録するべきデータが0件の場合です。
  • 何も対策していないと、バリデーションなどを通過してしまい、保存処理時に500エラーが起きる、といったこともあり得るので、0件の場合はきちんとエラーを返して保存処理に進ませない対策が必要です

ユーザービリティ関係の対応

✅ 何を記入したらいいかわからないようになっていないか

  • 電話やメールアドレスなどは明らかですが、DBの制約上、複数の選択肢から選んで欲しい場合(例えば「職種」など)などもあります
  • 何も説明がないと、自由記述とユーザーは思ってしまうので、何かしらの対策が必要です
  • 選択肢が少ない場合は、行のところに「職種(エンジニア or その他 で入力)」など書いておけばいいですが、選択肢が多い場合は非常に悩ましいです

✅ (記入例行がある場合)記入例行を削除するユーザーに対応できているか

  • 「何を記入したらいいかわからないようになっていないか」とも関連しますが、例えばテンプレートに記入例行を設けておく場合などは取り扱いに注意が必要です
  • 機械的に「上から1行は記入例だから無視」としてしまうと、記入例に上書き・削除して記載するユーザがいた時に、最悪保存できたつもりが保存できていない、という状況に陥り、不具合報告につながる可能性があります
  • 記入例行が記入例と違えば登録処理の対象に入れる、という仕様もありますが、仕様が複雑になったり、ユーザーのわかりづらさにつながるので、個人的には、可能な限り記入例行を作らずにテンプレートは作成できると良いのかなーと思っています

✅ バックグラウンド処理を使わずに実装できないか

  • 一括インポートは当然処理時間が長くなりがちのため、バックグラウンド処理が1つの選択肢となります
  • ただし、バックグラウンド処理をすると、考えるとが増えるので、できればbulk insertなどを有効活用しながら、同期処理で行うのが良いと思います
    • この点については、morihirokさんのKaigi on Rails 2025での発表が非常に参考になります

speakerdeck.com

【Rails】transactionのrequires_newの挙動について整理する

先日、Railsのtransactionのrequires_newオプションを知りました。

techracho.bpsinc.jp

tech.smarthr.jp

記事などを見れば理解できるのですが、(非推奨の):joinableと混同して「どっちだっけ?」となるので自分なりに表にまとめて整理しました。

まとめ

No. requires_new 例外の種類 ロールバックする処理
1 false false AR:Transaction 子の処理もロールバックしない
2 true false AR:Transaction 子の処理もロールバックしない
3 false true AR:Transaction 子の処理のみロールバックする
4 true true AR:Transaction 子の処理のみロールバックする
5 false false AR:Transaction以外 親の処理までロールバックする
requires_newは無関係

<補足>

  • 検証バージョン: Rails.8.0
  • requires_newfalseがデフォルトです
  • AR::RollbackActiveRecord::Rollbackの略です
  • 「親の処理」「子の処理」は以下のイメージです
ActiveRecord::Base.transaction do
  Car.create! # ←「親の処理」と呼んでいます
  ActiveRecord::Base.transaction do
    Bike.create! # ←「子の処理」と呼んでいます
    # ここで例外が起きるイメージ
  end
end

検証用コード

上記の表No.1~No.5について、検証用のRSpecの結果を書いておきます。

No.1

it do
  expect do
    ActiveRecord::Base.transaction do
      Car.create!
      ActiveRecord::Base.transaction do
        Bike.create!
        raise ActiveRecord::Rollback
      end
    end
  end.to change(Car, :count).by(1)
    .and change(Bike, :count).by(1)
end

# ↓結果
# 1 example, 0 failures

No.2

it do
  expect do
    ActiveRecord::Base.transaction(requires_new: true) do
      Car.create!
      ActiveRecord::Base.transaction do
        Bike.create!
        raise ActiveRecord::Rollback
      end
    end
  end.to change(Car, :count).by(1)
    .and change(Bike, :count).by(1)
end

# ↓結果
# 1 example, 0 failures

No.3

it do
  expect do
    ActiveRecord::Base.transaction do
      Car.create!
      ActiveRecord::Base.transaction(requires_new: true) do
        Bike.create!
        raise ActiveRecord::Rollback
      end
    end
  end.to change(Car, :count).by(1)
    .and change(Bike, :count).by(0)
end

# ↓結果
# 1 example, 0 failures

No.4

it do
  expect do
    ActiveRecord::Base.transaction(requires_new: true) do
      Car.create!
      ActiveRecord::Base.transaction(requires_new: true) do
        Bike.create!
        raise ActiveRecord::Rollback
      end
    end
  end.to change(Car, :count).by(1)
    .and change(Bike, :count).by(0)
end

# ↓結果
# 1 example, 0 failures

No.5

it do
  expect do
    ActiveRecord::Base.transaction do
      Car.create!
      ActiveRecord::Base.transaction do
        Bike.create!
        nil.foo # NoMethodError
      end
    end
  end.to raise_error(NoMethodError)
    .and change(Car, :count).by(0)
    .and change(Bike, :count).by(0)
end

# ↓結果
# 1 example, 0 failures

【Rails】debug_exception_log_levelが効かない?と思ったらbetter_errorsが制御していた

前回に引き続き、better_errors gemによって想定とは違う挙動をしていた体験をしたので、記事にしました。

↓参考: 前回の記事

blog.m-ito27.com

↓better_errors

github.com

背景: Rails7.1から例外のデフォルトログレベルがWARNになった

先日、とあるWebアプリのRailsアップグレード(Rails7.1→Rails8.0)をしていました。
config.load_defaultsが古かったので、Railsに合わせて引き上げる作業をしていたのですが、Rails7.1からconfig.action_dispatch.debug_exception_log_levelのデフォルトの値が従来の:fatalから:warnに変更されました。

railsguides.jp

この設定が変更されたPRを確認すると、「Ruby LoggerのドキュメントではFATALは『プログラムのクラッシュを引き起こす、処理不能なエラー』と定義されており、DebugExceptionがエラーを処理している現状と合わない。」という旨の説明がされています。

github.com

設定値変えてもログに変化がない?

そこで、この設定の値を :warnにして、ローカル環境で例外を発生させてみたのですが、ログには FATALとして表示されました。 ちなみに、ログレベルはseverityを使って表示できます。

config/environments/development.rb(簡略化しています。)

config.action_dispatch.debug_exception_log_level = :warn
config.log_formatter = proc do |severity, time, progname, msg|
  "[#{time.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} -- #{progname}: #{msg}\n"
end

出力されるログのサンプル

[2025-09-14 23:17:44] FATAL -- : 
RuntimeError - 例外が発生:
  app/controllers/...

あれ?ここがWARNになるってことではないの、、?

better_errorsがログレベルを制御していた

アプリ内のコードを確認しても、特別ログレベルをカスタムしている場所はなかったので、Gemfileを確認すると、better_errorsがあり、怪しそうに思ったので試しにbetter_errorsコメントアウトしてみました。

すると、、、

[2025-09-14 23:45:39] WARN -- :   
RuntimeError (例外が発生):

WARNになりました!

具体的には、以下の箇所で制御されていました。

better_errors/lib/better_errors/middleware.rb

def log_exception
  return unless BetterErrors.logger

  message = "\n#{@error_page.exception_type} - #{@error_page.exception_message}:\n"
  message += backtrace_frames.map { |frame| "  #{frame}\n" }.join

  BetterErrors.logger.fatal message # ←ここ
end

実際にここをwarnに変えたらログはWARNで表示されました。

さいごに

コントリビュートチャンス?と思いましたが、better_errorsは2年前を最後にコミットが追加されておらず、メンテが止まっているようです。
内容が軽微なこともあり、モンキーパッチを当てるなどはせず、「config.load_defaultsに合わせてデフォルト値は変わるが、ローカル環境のログ上はFATALのまま」、というのを許容することにしました。
better_errosの良い代替gemってあるのかなー、、とぼんやり思ったりしています。