首页 > 解决方案 > 如何在Ruby on Rails中出现错误时回滚事务块中的所有事务

问题描述

我有一个具有以下关联的任务模型:

...
  # === 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
...

当用户创建任务时,他们将任务、电话、地址、CameraSpec 和 DroneSpec 的所有信息输入到一个大表单中。当所有信息都正确时,我能够正确保存记录。但是,如果任何模型中有错误,我想回滚所有事务并呈现有错误的表单。

该主题已在其他地方介绍过,但是,我无法使用我见过的方法回滚所有事务。目前,如果其中一个模型(比如 CameraSpec)出现 DB/ActiveRecord 错误,则之前创建的 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

标签: ruby-on-railsrubydatabaseactiverecordtransactions

解决方案


这非常复杂,您完全误解了如何使用嵌套属性:

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

所有的工作实际上都是由声明的 setter 自动完成的accepts_nested_attributes。您只需将白名单参数的散列或散列数组传递给它,让它做它的事情。

如果子对象无效,您可以使用以下方法阻止保存父对象validates_associated

class Mission < ApplicationRecord
  # ...
  validates_associated :addresses
end

这只是将错误键“电话无效”添加到对用户不太友好的错误中。如果要显示每个嵌套记录的错误消息,可以在使用时获取由表单构建器包装的对象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 %>

推荐阅读