I have two models the account model depends on the id of the user model. I need them to be saved in the same transaction and if the validation on one of them fails then the transaction should fail. The problem is that the user model is getting saved even if the account model is invalid. What is that I'm doing wrong ?
Also why is rails open another transaction after saving ?
And here is the code :
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# username :string(255)
# password_digest :string(255)
# active :boolean
# created_at :datetime
# updated_at :datetime
#
class User < ActiveRecord::Base
has_secure_password
has_one :account, dependent: :destroy, autosave: true
validates :username, presence: true, uniqueness: true
end
# == Schema Information
#
# Table name: accounts
#
# id :integer not null, primary key
# name :string(255)
# first_name :string(255)
# last_name :string(255)
# user_id :integer
# account_type_id :integer
# created_at :datetime
# updated_at :datetime
#
class Account < ActiveRecord::Base
belongs_to :user
validates_presence_of :first_name, :last_name, if: Proc.new { |record| record.is_customer? }
validates_presence_of :name, if: Proc.new { |record| record.is_business? }
validates_presence_of :user_id, :account_type_id
def is_customer?
self.account_type_id == 1
end
def is_business?
self.account_type_id == 2
end
def define_business
self.account_type_id = 2
end
end
def create
@user = User.new user_params
@user.create_account business_params
@user.account.define_business
respond_to do |format|
if @user.save && @account.save
set_current_account @user
format.html { redirect_to business_dashboard_path }
format.json { head :created, location: business_dashboard_path }
else
format.html { render :new }
format.json { render json: { errors: @account.errors }, status: :unprocessable_entity }
end
end
end
Console output:
SQL (0.4ms) INSERT INTO "users" ("created_at", "password_digest", "updated_at", "username") VALUES (?, ?, ?, ?) [["created_at", Fri, 21 Mar 2014 21:10:15 UTC +00:00], ["password_digest", "$2a$10$93pKlgadnoetCpBCdSSRtenLDbWnribprhLVX.gZ6i35WcsI6UuRi"], ["updated_at", Fri, 21 Mar 2014 21:10:15 UTC +00:00], ["username", "business12@email.com"]]
(7.2ms) commit transaction
(0.0ms) begin transaction
(0.0ms) rollback transaction
Rails wraps model operations in a transaction by default. If you execute:
@user.save && @account.save
you get two transactions, one for each call of the method save
. If the first DB operation succeeds, the second is executed.
With this code, three things can possibly happen:
@user
is invalid, so @user.save
fails and returns false
in which case no record is created in the DB. @user
is valid, so @user.save
succeeds and returns true
, but @account
is invalid, so @account.save
fails in which case 1 (user) record is created in the DB. @user
is valid, so @user.save
succeeds and returns true
, and @account
is valid as well in which case both records are created in the DB. You want to eliminate #2. You cannot use @user.save && @account.save
in your case. It does not execute both DB operations in one transaction.
Let me start by saying that in order to save two models in a single transaction, you have two options:
You could explicitly do both in a transaction like so:
User.transaction do
@user.save!
@account.save!
end
Note that bang methods are used here ( save!
, not save
) because they raise exceptions in case of an invalid model which triggers a roll-back of the transaction.
This way, you end up with either 0 or 2 records in your DB. You could implement this as a convenience method in one of your models or in your controller.
You could also use callbacks . This can only be done in a model, like so:
class User < ActiveRecord::Base
has_secure_password
has_one :account, dependent: :destroy, autosave: true
after_create :create_account
private
def create_account
# do something
end
end
This triggers a method create_account
in your user model (consider making it private) which is executed after the user is successfully created in the DB.
Given that your user model might not have all the relevant information it needs to create the account, option #1 (ie Account.transaction do
) seems like the better fit for you.
You're already using autosave: true
which is correct but unfortunately you're using .create_account
instead of build_account
. The has_one Association Reference explains:
The build_association method returns a new object of the associated type. This object will be instantiated from the passed attributes, and the link through its foreign key will be set, but the associated object will not yet be saved.
Since you're using autosave: true
on the User association, you could build (not create) the account and then only save the user, which then saves the associated account as well. The benefit of using this is you don't need to worry about transactions at all.
I've prepared a gist to demonstrate the difference between your code and mine. You can download both files and run them like so:
ruby what_you_have.rb
ruby what_you_need.rb
You'll notice the tests will fail on what_you_have.rb
and pass on what_you_need.rb
.
When using create_account
and user.save && account.save
, the tests always fail with an invalid user and a valid account. Why is that? Because with user.create_account
, you've already saved a valid account in your database before attempting to save the user.
When using build_account
and then only user.save
, you tell ActiveRecord to save the user (given it's valid) and save the account along the way. If one of them is invalid, user.save
does not save anything.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.