リア充爆発日記

You don't even know what ria-ju really is.

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してくれてる。

便利じゃのう!