如何在 Rails Ruby 出错时回滚事务块中的所有事务
How to rollback all transactions in transaction block on error in Ruby on Rails
我有一个具有以下关联的 Mission 模型:
...
# === missions.rb (model) ===
has_many :addresses, as: :addressable, dependent: :destroy
accepts_nested_attributes_for :addresses
has_many :phones, as: :phoneable, dependent: :destroy
accepts_nested_attributes_for :phones
has_one :camera_spec, as: :camerable, dependent: :destroy
accepts_nested_attributes_for :camera_spec
has_one :drone_spec, as: :droneable, dependent: :destroy
accepts_nested_attributes_for :drone_spec
...
当用户创建任务时,他们将任务的所有信息Phone、地址、CameraSpec 和 DroneSpec 输入到一个大表格中。当所有信息都正确时,我能够正确保存记录。但是,如果任何模型出现错误,我想回滚 ALL 事务并呈现有错误的表单。
该主题已在其他地方介绍过,但是,我无法使用我见过的方法回滚所有事务。目前,如果其中一个模型存在 DB/ActiveRecord 错误,比方说 CameraSpec,那么之前创建的 Mission、Address 和 Phone 不会回滚。我尝试过嵌套交易,例如:
Mission.transaction do
begin
# Create the mission
Mission.create(mission_params)
# Create the Address
raise ActiveRecord::Rollback unless Address.transaction(requires_new: true) do
Address.create(address_params)
raise ActiveRecord::Rollback
end
...
rescue ActiveRecord::Rollback => e
...
end
end
我试过抛出不同类型的错误,例如 ActiveRecord::Rollback
。我总是能够捕获错误,但数据库不会回滚。我试过使用和不使用 begin-rescue 语句。我目前的尝试是不嵌套事务,而是将它们提交到单个事务块中,但这也行不通。这是我当前的代码。
# === missions_controller.rb ===
def create
# Authorize the user
# Prepare records to be saved using form data
mission_params = create_params.except(:address, :phone, :camera_spec, :drone_spec)
address_params = create_params[:address]
phone_params = create_params[:phone]
camera_spec_params = create_params[:camera_spec]
drone_spec_params = create_params[:drone_spec]
@mission = Mission.new(mission_params)
@address = Address.new(address_params)
@phone = Phone.new(phone_params)
@camera_spec = CameraSpec.new(camera_spec_params)
@drone_spec = DroneSpec.new(drone_spec_params)
# Try to save the company, phone number, and address
# Rollback all if error on any save
ActiveRecord::Base.transaction do
begin
# Add the current user's id to the mission
@mission.assign_attributes({
user_id: current_user.id
})
# Try to save the Mission
unless @mission.save!
raise ActiveRecord::Rollback, @mission.errors.full_messages
end
# Add the mission id to the address
@address.assign_attributes({
addressable_id: @mission.id,
addressable_type: "Mission",
address_type_id: AddressType.get_id_by_slug("takeoff")
})
# Try to save any Addresses
unless @address.save!
raise ActiveRecord::Rollback, @address.errors.full_messages
end
# Add the mission id to the phone number
@phone.assign_attributes({
phoneable_id: @mission.id,
phoneable_type: "Mission",
phone_type_id: PhoneType.get_id_by_slug("mobile")
})
# Try to save the phone
unless @phone.save!
raise ActiveRecord::Rollback, @phone.errors.full_messages
end
# Add the mission id to the CameraSpecs
@camera_spec.assign_attributes({
camerable_id: @mission.id,
camerable_type: "Mission"
})
# Try to save any CameraSpecs
unless @camera_spec.save!
raise ActiveRecord::Rollback, @camera_spec.errors.full_messages
end
# Add the mission id to the DroneSpecs
@drone_spec.assign_attributes({
droneable_id: @mission.id,
droneable_type: "Mission"
})
# Try to save any DroneSpecs
unless @drone_spec.save!
raise ActiveRecord::Rollback, @drone_spec.errors.full_messages
end
# If something goes wrong, render :new again
# rescue ActiveRecord::Rollback => e
rescue => e
# Ensure validation messages exist on each instance variable
@user = current_user
@addresses = @user.addresses
@phones = @user.phones
@mission.valid?
@address.valid?
@phone.valid?
@camera_spec.valid?
@drone_spec.valid?
render :new and return
else
# Everything is good, so redirect to the show page
redirect_to mission_path(@mission), notice: t(".mission_created")
end
end
end
这太复杂了,您完全误解了如何使用嵌套属性:
class MissionsController
def create
@mission = Mission.new(mission_attributes)
if @mission.save
redirect_to @mission
else
render :new
end
end
...
private
def mission_params
params.require(:mission)
.permit(
:param_1, :param_2, :param3,
addresses_attributes: [:foo, :bar, :baz],
phones_attributes: [:foo, :bar, :baz],
camera_spec_attributes: [:foo, :bar, :baz],
)
end
end
所有的工作实际上都是由accepts_nested_attributes
声明的setter自动完成的。您只需将白名单参数的哈希值或哈希值数组传递给它,然后让它执行它的操作即可。
如果子对象无效,您可以通过使用validates_associated
来阻止保存父对象:
class Mission < ApplicationRecord
# ...
validates_associated :addresses
end
这只是将错误键“Phone 无效”添加到不是很用户友好的错误中。如果您想显示每个嵌套记录的错误消息,您可以在使用 fields_for
:
时获取由表单构建器包装的对象
# app/shared/_errors.html.erb
<div id="error_explanation">
<h2><%= pluralize(object.errors.count, "error") %> prohibited this <%= object.model_name.singular %> from being saved:</h2>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
...
<%= f.fields_for :address_attributes do |address_fields| %>
<%= render('shared/errors', object: address_fields.object) if address_fields.object.errors.any? %>
<% end %>
查看了您的代码,我发现您在 begin rescue 块的帮助下使用了 ActiveRecord::Base.transaction 块。但是 ActiveRecord::Base.transaction 支持救援块,可以使用下面的代码
ActiveRecord::Base.transaction do
# Add the current user's id to the mission
@mission.assign_attributes({
user_id: current_user.id
})
# Try to save the Mission
unless @mission.save!
raise ActiveRecord::Rollback, @mission.errors.full_messages
end
# Add the mission id to the address
@address.assign_attributes({
addressable_id: @mission.id,
addressable_type: "Mission",
address_type_id: AddressType.get_id_by_slug("takeoff")
})
# Try to save any Addresses
unless @address.save!
raise ActiveRecord::Rollback, @address.errors.full_messages
end
# Add the mission id to the phone number
@phone.assign_attributes({
phoneable_id: @mission.id,
phoneable_type: "Mission",
phone_type_id: PhoneType.get_id_by_slug("mobile")
})
# Try to save the phone
unless @phone.save!
raise ActiveRecord::Rollback, @phone.errors.full_messages
end
# Add the mission id to the CameraSpecs
@camera_spec.assign_attributes({
camerable_id: @mission.id,
camerable_type: "Mission"
})
# Try to save any CameraSpecs
unless @camera_spec.save!
raise ActiveRecord::Rollback, @camera_spec.errors.full_messages
end
# Add the mission id to the DroneSpecs
@drone_spec.assign_attributes({
droneable_id: @mission.id,
droneable_type: "Mission"
})
# Try to save any DroneSpecs
unless @drone_spec.save!
raise ActiveRecord::Rollback, @drone_spec.errors.full_messages
end
# If something goes wrong, render :new again
# rescue ActiveRecord::Rollback => e
rescue Exception => e
# Ensure validation messages exist on each instance variable
@user = current_user
@addresses = @user.addresses
@phones = @user.phones
@mission.valid?
@address.valid?
@phone.valid?
@camera_spec.valid?
@drone_spec.valid?
render :new and return
else
# Everything is good, so redirect to the show page
redirect_to mission_path(@mission), notice: t(".mission_created")
end
我有一个具有以下关联的 Mission 模型:
...
# === missions.rb (model) ===
has_many :addresses, as: :addressable, dependent: :destroy
accepts_nested_attributes_for :addresses
has_many :phones, as: :phoneable, dependent: :destroy
accepts_nested_attributes_for :phones
has_one :camera_spec, as: :camerable, dependent: :destroy
accepts_nested_attributes_for :camera_spec
has_one :drone_spec, as: :droneable, dependent: :destroy
accepts_nested_attributes_for :drone_spec
...
当用户创建任务时,他们将任务的所有信息Phone、地址、CameraSpec 和 DroneSpec 输入到一个大表格中。当所有信息都正确时,我能够正确保存记录。但是,如果任何模型出现错误,我想回滚 ALL 事务并呈现有错误的表单。
该主题已在其他地方介绍过,但是,我无法使用我见过的方法回滚所有事务。目前,如果其中一个模型存在 DB/ActiveRecord 错误,比方说 CameraSpec,那么之前创建的 Mission、Address 和 Phone 不会回滚。我尝试过嵌套交易,例如:
Mission.transaction do
begin
# Create the mission
Mission.create(mission_params)
# Create the Address
raise ActiveRecord::Rollback unless Address.transaction(requires_new: true) do
Address.create(address_params)
raise ActiveRecord::Rollback
end
...
rescue ActiveRecord::Rollback => e
...
end
end
我试过抛出不同类型的错误,例如 ActiveRecord::Rollback
。我总是能够捕获错误,但数据库不会回滚。我试过使用和不使用 begin-rescue 语句。我目前的尝试是不嵌套事务,而是将它们提交到单个事务块中,但这也行不通。这是我当前的代码。
# === missions_controller.rb ===
def create
# Authorize the user
# Prepare records to be saved using form data
mission_params = create_params.except(:address, :phone, :camera_spec, :drone_spec)
address_params = create_params[:address]
phone_params = create_params[:phone]
camera_spec_params = create_params[:camera_spec]
drone_spec_params = create_params[:drone_spec]
@mission = Mission.new(mission_params)
@address = Address.new(address_params)
@phone = Phone.new(phone_params)
@camera_spec = CameraSpec.new(camera_spec_params)
@drone_spec = DroneSpec.new(drone_spec_params)
# Try to save the company, phone number, and address
# Rollback all if error on any save
ActiveRecord::Base.transaction do
begin
# Add the current user's id to the mission
@mission.assign_attributes({
user_id: current_user.id
})
# Try to save the Mission
unless @mission.save!
raise ActiveRecord::Rollback, @mission.errors.full_messages
end
# Add the mission id to the address
@address.assign_attributes({
addressable_id: @mission.id,
addressable_type: "Mission",
address_type_id: AddressType.get_id_by_slug("takeoff")
})
# Try to save any Addresses
unless @address.save!
raise ActiveRecord::Rollback, @address.errors.full_messages
end
# Add the mission id to the phone number
@phone.assign_attributes({
phoneable_id: @mission.id,
phoneable_type: "Mission",
phone_type_id: PhoneType.get_id_by_slug("mobile")
})
# Try to save the phone
unless @phone.save!
raise ActiveRecord::Rollback, @phone.errors.full_messages
end
# Add the mission id to the CameraSpecs
@camera_spec.assign_attributes({
camerable_id: @mission.id,
camerable_type: "Mission"
})
# Try to save any CameraSpecs
unless @camera_spec.save!
raise ActiveRecord::Rollback, @camera_spec.errors.full_messages
end
# Add the mission id to the DroneSpecs
@drone_spec.assign_attributes({
droneable_id: @mission.id,
droneable_type: "Mission"
})
# Try to save any DroneSpecs
unless @drone_spec.save!
raise ActiveRecord::Rollback, @drone_spec.errors.full_messages
end
# If something goes wrong, render :new again
# rescue ActiveRecord::Rollback => e
rescue => e
# Ensure validation messages exist on each instance variable
@user = current_user
@addresses = @user.addresses
@phones = @user.phones
@mission.valid?
@address.valid?
@phone.valid?
@camera_spec.valid?
@drone_spec.valid?
render :new and return
else
# Everything is good, so redirect to the show page
redirect_to mission_path(@mission), notice: t(".mission_created")
end
end
end
这太复杂了,您完全误解了如何使用嵌套属性:
class MissionsController
def create
@mission = Mission.new(mission_attributes)
if @mission.save
redirect_to @mission
else
render :new
end
end
...
private
def mission_params
params.require(:mission)
.permit(
:param_1, :param_2, :param3,
addresses_attributes: [:foo, :bar, :baz],
phones_attributes: [:foo, :bar, :baz],
camera_spec_attributes: [:foo, :bar, :baz],
)
end
end
所有的工作实际上都是由accepts_nested_attributes
声明的setter自动完成的。您只需将白名单参数的哈希值或哈希值数组传递给它,然后让它执行它的操作即可。
如果子对象无效,您可以通过使用validates_associated
来阻止保存父对象:
class Mission < ApplicationRecord
# ...
validates_associated :addresses
end
这只是将错误键“Phone 无效”添加到不是很用户友好的错误中。如果您想显示每个嵌套记录的错误消息,您可以在使用 fields_for
:
# app/shared/_errors.html.erb
<div id="error_explanation">
<h2><%= pluralize(object.errors.count, "error") %> prohibited this <%= object.model_name.singular %> from being saved:</h2>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
...
<%= f.fields_for :address_attributes do |address_fields| %>
<%= render('shared/errors', object: address_fields.object) if address_fields.object.errors.any? %>
<% end %>
查看了您的代码,我发现您在 begin rescue 块的帮助下使用了 ActiveRecord::Base.transaction 块。但是 ActiveRecord::Base.transaction 支持救援块,可以使用下面的代码
ActiveRecord::Base.transaction do
# Add the current user's id to the mission
@mission.assign_attributes({
user_id: current_user.id
})
# Try to save the Mission
unless @mission.save!
raise ActiveRecord::Rollback, @mission.errors.full_messages
end
# Add the mission id to the address
@address.assign_attributes({
addressable_id: @mission.id,
addressable_type: "Mission",
address_type_id: AddressType.get_id_by_slug("takeoff")
})
# Try to save any Addresses
unless @address.save!
raise ActiveRecord::Rollback, @address.errors.full_messages
end
# Add the mission id to the phone number
@phone.assign_attributes({
phoneable_id: @mission.id,
phoneable_type: "Mission",
phone_type_id: PhoneType.get_id_by_slug("mobile")
})
# Try to save the phone
unless @phone.save!
raise ActiveRecord::Rollback, @phone.errors.full_messages
end
# Add the mission id to the CameraSpecs
@camera_spec.assign_attributes({
camerable_id: @mission.id,
camerable_type: "Mission"
})
# Try to save any CameraSpecs
unless @camera_spec.save!
raise ActiveRecord::Rollback, @camera_spec.errors.full_messages
end
# Add the mission id to the DroneSpecs
@drone_spec.assign_attributes({
droneable_id: @mission.id,
droneable_type: "Mission"
})
# Try to save any DroneSpecs
unless @drone_spec.save!
raise ActiveRecord::Rollback, @drone_spec.errors.full_messages
end
# If something goes wrong, render :new again
# rescue ActiveRecord::Rollback => e
rescue Exception => e
# Ensure validation messages exist on each instance variable
@user = current_user
@addresses = @user.addresses
@phones = @user.phones
@mission.valid?
@address.valid?
@phone.valid?
@camera_spec.valid?
@drone_spec.valid?
render :new and return
else
# Everything is good, so redirect to the show page
redirect_to mission_path(@mission), notice: t(".mission_created")
end