我有两个模型,帐户模型取决于用户模型的ID。我需要将它们保存在同一个事务中,如果其中一个验证失败,则事务应该失败。问题是即使帐户模型无效,用户模型也会被保存。我做错了什么?
为什么rails在保存后会打开另一个交易?
以下是代码:
# == 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
控制台输出:
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
答案 0 :(得分:1)
默认情况下,Rails会在事务中包装模型操作。如果你执行:
@user.save && @account.save
你得到两个交易,每次调用一个方法save
。如果第一个DB操作成功,则执行第二个DB操作。
使用此代码,可能会发生三件事:
@user
无效,因此@user.save
失败并返回false
,在这种情况下,数据库中不会创建任何记录。@user
有效,因此@user.save
成功并返回true
,但@account
无效,因此@account.save
失败,在这种情况下1(用户)记录在DB中创建。@user
有效,因此@user.save
成功并返回true
,@account
也有效,在这种情况下,两个记录都会在数据库中创建。你想要消除#2。 您无法在案例中使用@user.save && @account.save
。它不会在一个事务中执行两个数据库操作。
首先我要说的是,为了在一个事务中保存两个模型,您有两个选择:
您可以在transaction中明确地同时执行这两项操作:
User.transaction do
@user.save!
@account.save!
end
请注意,此处使用了爆炸方法(save!
,而不是save
),因为它们会在触发回滚事务的无效模型的情况下引发异常。
这样,您最终会在数据库中输入0或2条记录。您可以在其中一个模型或控制器中将其实现为便捷方法。
您也可以使用callbacks。这只能在模型中完成,如下所示:
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
这会触发用户模型中的方法create_account
(考虑将其设为私有),该方法在用户在数据库中成功创建后执行。
鉴于您的用户模型可能没有创建帐户所需的所有相关信息,因此选项#1(即Account.transaction do
)似乎更适合您。
答案 1 :(得分:1)
您已使用autosave: true
这是正确的,但遗憾的是您正在使用.create_account
而不是build_account
。 has_one Association Reference解释说:
build_association方法返回关联类型的新对象。 该对象将从传递的属性中实例化, 并设置通过其外键的链接, 但相关对象尚未保存。
由于您在用户关联中使用autosave: true
,您可以构建(而不是创建)帐户,然后然后只保存用户,然后将关联的帐户保存为好。使用它的好处是您根本不需要担心交易。
我prepared a gist来证明您的代码与我的代码之间的区别。您可以下载这两个文件并按如下方式运行它们:
ruby what_you_have.rb
ruby what_you_need.rb
您会注意到what_you_have.rb
上的测试失败并传递what_you_need.rb
。
使用create_account
和user.save && account.save
时,测试始终会因无效用户和有效帐户而失败。这是为什么?由于使用user.create_account
,您在尝试保存用户之前已经在数据库中保存了有效帐户。
使用build_account
然后只使用user.save
时,您告诉ActiveRecord保存用户(假设它有效)并保存帐户。如果其中一个无效,则user.save
不会保存任何内容。