Rails: ループ内の手続処理を止めてみよう

いよいよ年末になりました。1年が短く感じる今日この頃です。 今年はいろいろあったなー(結婚したり子供が産まれたり😊) お久しぶりの記事ですが、今回は新人君のコードをレビューしたら、こんな感じでループ処理書いてもいいのでは?とおもったのでまとめてみました。 長文ですがお付き合いください🙇 新人君のコードとレビュー まず新人君は、とあるシステムでユーザー登録を実装していました。要件は以下の通りです 1. ユーザー登録の要件 ユーザーは、システムにメールアドレスを入力する システムは、ユーザー登録時にパスワードを自動発行する システムは、メールアドレスとパスワードをDBに永続化する システムは、入力されたメールアドレスに、発行したパスワードを即時送信する 1度DBに永続化したパスワードは、再び参照することができない 2. 新人君のコード 以下が新人君のコード(をだいぶ添削したもの)です。ご覧ください。 class User < ApplicationRecord def registration! password = PasswordGenerator.gen(email) self.password = password save! RegistrationMailer.send_mail(email: email, password: password) end end ########## メイン処理 ########## User.new(email: ‘hoge@example.com’).registration! Railsのコードを前提としていますが、以下のコードをrequireすると素のRubyでも動作が確認できます。以降の実行例もrequireしてある前提です。 3. 追加要求発動: それバッチでもやりたい 1ユーザーの登録ができたのですが、ディレクターからは当然のごとく 初期登録用に複数のユーザーを一気に登録したい要件が追加されました 4. ユーザー一括登録の要件 管理者は、システムにメールアドレスリストを入力する ユーザー登録要件に従い、各メールアドレスに対してユーザー登録を行う 一括登録中にエラーが発生した場合は全ての処理を行われなかったことに(ロールバック)する 5. 新人君のコード(実装後) ということで、新人君は以下のコードを提出してきました。 class User < ApplicationRecord def self.import_users(emails) transaction do emails.each do |email| User.new(email: email).registration! end end end def registration! password = PasswordGenerator.gen(email) self.password = PasswordGenerator.gen(email) save! RegistrationMailer.send_mail(email: email, password: password) end end さて、勘のいい方はお気づきの事と思いますが、このコードには致命的な問題があります。 ロールバックユーザーにメール送っちゃうよ! 全てのメールアドレスが正常処理となった場合には、このコードで何も問題がありません。問題なのは、途中にエラーとなるメールアドレスがあった場合です。 以下の実行例を見てみましょう emails = %w[ hoge1@email.com hoge2@email.com hoge3@email.com invalid_mail ### これ失敗する hoge4@email.com hoge5@email.com ] User.import_users(emails) rescue puts $!.message これを実行すると、以下のような出力となります。 トランザクション開始 [hoge1@email.com] をDBに保存しました [ようこそhoge1@email.comさん。パスワードは2f90b7e4d41753b2です] と送信しました [hoge2@email.com] をDBに保存しました [ようこそhoge2@email.comさん。パスワードは54e36f92df5ba1aです] と送信しました [hoge3@email.com] をDBに保存しました [ようこそhoge3@email.comさん。パスワードは3f2f7d323f1f0f1eです] と送信しました ロールバックしました DBへの登録はロールバックされていますが、hoge1、hoge2、hoge3さんにメールを送信してしまっています。 当然メール送信はロールバックされません。メールを受け取ってしまったhoge1さんたちはログイン画面でパスワードを入力してもログインできることはないでしょう 問題の構造 ユーザー一人分の処理は、「パスワード作成」→「DB登録」→「メール送信」の順で機能を実行しました。ユーザーが複数になる場合には、以下の2通りの方法が考えられます。 パターンA: ユーザー毎に実行する 1人目:「パスワード作成」→「DB登録」→「メール送信」 2人目:「パスワード作成」→「DB登録」→「メール送信」 … n人目:「パスワード作成」→「DB登録」→「メール送信」 パターンB: 機能毎に実行する パスワードの作成:「1人目」→「2人目」→…「n人目」 DB登録:「1人目」→「2人目」→…「n人目」 メール送信:「1人目」→「2人目」→…「n人目」 パターンBの実装例 新人君はパターンAでやってしまいましたが、ベテランエンジニアなら今回の要件の場合パターンBでやらないといけないことに気がつくと思います。なぜ新人君はパターンAでやってしまったのでしょうか? お先にパターンBの実装例を見てみましょう class User < ApplicationRecord def self.registration!(emails) # メール-パスワード対応表を作成 email_passwords = emails.map {|email| [email, PasswordGenerator.gen(email)]} # ユーザー毎に永続化 transaction do email_passwords.each do |email, password| user = new(email: email) user.password = password user.save! end end # 全ユーザーの永続化に成功してから、全員にメールを送信 email_passwords.each do |email, password| RegistrationMailer.send_mail(email: email, password: password) … Continue reading Rails: ループ内の手続処理を止めてみよう