has_oneのcreateとbuild and saveの違い
userモデルとhas_oneの関係にあるpassword_resetというモデルがある。
パスワードリセットのときに使うtokenとタイムスタンプをとっておくためのモデルでuserモデルの一部でもいいんだけど、たまにしか使わない要素は分けたい派なので分けている。
具体的にはこんなコード。
class User < ActiveRecord::Base has_one :password_reset, dependent: :destroy ~snip~ class PasswordReset < ActiveRecord::Base before_save { unique_token(self.class, :token) } after_save { UserMailer.password_reset(self.user).deliver! } belongs_to :user ~snip~ end
で、PasswordResetはuser_idでユニークになる。
このPasswordResetを作るにはuser.create_password_resetすればいいだけなんだけど、これだとちょっと問題がある。これを2連発するとユニーク制約に引っかかって例外が起きる。createはその名の通り、とにかくcreateを試みるということなのかな。
いっぽうでsaveは空気を読んで動いてくれる。空気っていうかhas_oneをちゃんと理解して動作する。つまり
user.build_password_reset.save user.build_password_reset.save
のようにsaveを2連発しても、2回めのsaveのときは1つ前で作られたpassword_resetデータを破棄して新しいpassword_resetを生成してくれる。
これを実際に確認してみる。
irb(main):018:0> PasswordReset.all PasswordReset Load (0.3ms) SELECT `password_resets`.* FROM `password_resets` => [] irb(main):019:0> u.build_password_reset.save (0.2ms) BEGIN SQL (0.2ms) DELETE FROM `password_resets` WHERE `password_resets`.`id` = 3 (0.1ms) COMMIT (0.1ms) BEGIN PasswordReset Exists (0.2ms) SELECT 1 AS one FROM `password_resets` WHERE `password_resets`.`token` = 'jZFhYZXPEgGmKs7f5EUNHg' LIMIT 1 SQL (0.2ms) INSERT INTO `password_resets` (`created_at`, `token`, `updated_at`, `user_id`) VALUES ('2013-06-30 03:51:06', 'jZFhYZXPEgGmKs7f5EUNHg', '2013-06-30 03:51:06', 1) User Load (0.2ms) SELECT `users`.`id`, `users`.`username`, `users`.`email`, `users`.`name`, `users`.`bio`, `users`.`locale`, `users`.`status`, `users`.`avatar_file_name`, `users`.`avatar_content_type`, `users`.`avatar_file_size`, `users`.`avatar_updated_at`, `users`.`created_at`, `users`.`updated_at` FROM `users` WHERE `users`.`id` = 1 LIMIT 1 PasswordReset Load (0.2ms) SELECT `password_resets`.* FROM `password_resets` WHERE `password_resets`.`user_id` = 1 LIMIT 1 (0.5ms) COMMIT => true irb(main):020:0> PasswordReset.all PasswordReset Load (0.3ms) SELECT `password_resets`.* FROM `password_resets` => [#<PasswordReset id: 5, user_id: 1, token: "jZFhYZXPEgGmKs7f5EUNHg", created_at: "2013-06-30 03:51:06", updated_at: "2013-06-30 03:51:06">] irb(main):021:0> u.build_password_reset.save (0.1ms) BEGIN SQL (0.3ms) DELETE FROM `password_resets` WHERE `password_resets`.`id` = 5 (1.6ms) COMMIT (0.1ms) BEGIN PasswordReset Exists (0.2ms) SELECT 1 AS one FROM `password_resets` WHERE `password_resets`.`token` = 'o5s3XR7WYDB3sQBzI9pMlw' LIMIT 1 SQL (0.2ms) INSERT INTO `password_resets` (`created_at`, `token`, `updated_at`, `user_id`) VALUES ('2013-06-30 03:51:21', 'o5s3XR7WYDB3sQBzI9pMlw', '2013-06-30 03:51:21', 1) User Load (0.2ms) SELECT `users`.`id`, `users`.`username`, `users`.`email`, `users`.`name`, `users`.`bio`, `users`.`locale`, `users`.`status`, `users`.`avatar_file_name`, `users`.`avatar_content_type`, `users`.`avatar_file_size`, `users`.`avatar_updated_at`, `users`.`created_at`, `users`.`updated_at` FROM `users` WHERE `users`.`id` = 1 LIMIT 1 PasswordReset Load (0.3ms) SELECT `password_resets`.* FROM `password_resets` WHERE `password_resets`.`user_id` = 1 LIMIT 1 (0.5ms) COMMIT => true irb(main):022:0> PasswordReset.all PasswordReset Load (0.3ms) SELECT `password_resets`.* FROM `password_resets` => [#<PasswordReset id: 6, user_id: 1, token: "o5s3XR7WYDB3sQBzI9pMlw", created_at: "2013-06-30 03:51:21", updated_at: "2013-06-30 03:51:21">] irb(main):023:0>
と、見ての通りdeleteしてからinsertしてくれてる。
便利じゃのう!