简体   繁体   English

如何在 Ruby on Rails 中发生错误时回滚事务块中的所有事务

[英]How to rollback all transactions in transaction block on error in Ruby on Rails

I have a Mission model which has the following associations:我有一个具有以下关联的任务模型:

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

When a user creates a Mission, they input all of the information for the Mission, Phone, Address, CameraSpec, and DroneSpec into one large form.当用户创建任务时,他们将任务、电话、地址、CameraSpec 和 DroneSpec 的所有信息输入到一个大表格中。 I'm able to save records correctly when all of the information is correct.当所有信息都正确时,我能够正确保存记录。 However, if there is an error in any of the models, I want to rollback ALL transactions and render the form with errors.但是,如果任何模型中存在错误,我想回滚所有事务并呈现有错误的表单。

This topic has been covered in other places, however, I'm unable to rollback all transactions using the methods I've seen.此主题已在其他地方讨论过,但是,我无法使用我见过的方法回滚所有事务。 Currently, if there is a DB/ActiveRecord error one of the models, let's say CameraSpec, then the previously created Mission, Address, and Phone are not rolled back.当前,如果模型之一出现 DB/ActiveRecord 错误,比如说 CameraSpec,那么之前创建的 Mission、Address 和 Phone 不会回滚。 I've tried nested transactions like:我试过嵌套事务,如:

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

I've tried throwing different kinds of errors such as ActiveRecord::Rollback .我尝试抛出不同类型的错误,例如ActiveRecord::Rollback I'm always able to catch the error, but the DB doesn't rollback.我总是能够捕捉到错误,但数据库不会回滚。 I've tried both using and not using a begin-rescue statement.我已经尝试过使用和不使用开始救援语句。 My current attempt is to not nest the transactions and instead commit them in a single transaction block, but this also is not working.我目前的尝试是不嵌套事务,而是将它们提交到单个事务块中,但这也不起作用。 Here is my current code.这是我当前的代码。

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

I have a Mission model which has the following associations:我有一个任务模型,该模型具有以下关联:

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

When a user creates a Mission, they input all of the information for the Mission, Phone, Address, CameraSpec, and DroneSpec into one large form.用户创建任务时,他们会将任务,电话,地址,CameraSpec和DroneSpec的所有信息输入一种大格式。 I'm able to save records correctly when all of the information is correct.当所有信息正确时,我能够正确保存记录。 However, if there is an error in any of the models, I want to rollback ALL transactions and render the form with errors.但是,如果任何模型中都有错误,我想回滚所有事务并使用错误呈现表单。

This topic has been covered in other places, however, I'm unable to rollback all transactions using the methods I've seen.在其他地方已经讨论了该主题,但是,我无法使用已经看到的方法回滚所有事务。 Currently, if there is a DB/ActiveRecord error one of the models, let's say CameraSpec, then the previously created Mission, Address, and Phone are not rolled back.当前,如果其中一个模型存在DB / ActiveRecord错误,例如CameraSpec,则不会回滚先前创建的Mission,Address和Phone。 I've tried nested transactions like:我已经尝试过嵌套的事务,例如:

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

I've tried throwing different kinds of errors such as ActiveRecord::Rollback .我尝试抛出各种错误,例如ActiveRecord::Rollback I'm always able to catch the error, but the DB doesn't rollback.我总是能够捕捉到错误,但是数据库不会回滚。 I've tried both using and not using a begin-rescue statement.我已经尝试过使用和不使用begin-rescue语句。 My current attempt is to not nest the transactions and instead commit them in a single transaction block, but this also is not working.我当前的尝试是不嵌套事务,而是在单个事务块中提交它们,但这也行不通。 Here is my current code.这是我当前的代码。

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

This is insanely overcomplicated and you have completely missunderstood how to use nested attributes:这太复杂了,您完全误解了如何使用嵌套属性:

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

All the work is actually done automatically by the setters declared by accepts_nested_attributes .所有的工作实际上都是由accepts_nested_attributes声明的 setter 自动完成的。 You just pass the hash or array of hashes of whitelisted parameters to it and let it do its thing.您只需将白名单参数的哈希或哈希数组传递给它,然后让它做它的事情。

You can prevent the parent object from being saved if the child object is invalid by using validates_associated :如果子对象无效,您可以使用validates_associated防止父对象被保存:

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

This just adds the error key “Phone is invalid” to the errors which isn't very user friendly.这只是将错误键“电话无效”添加到对用户不太友好的错误中。 If you want to display the error messages per nested record you can get the object wrapped by the form builder when using fields_for :如果要显示每个嵌套记录的错误消息,可以在使用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 %>

Got a look at your code I can see you were using the ActiveRecord::Base.transaction block with the help of begin rescue block.看看你的代码,我可以看到你在开始救援块的帮助下使用了 ActiveRecord::Base.transaction 块。 But ActiveRecord::Base.transaction supports rescue block and can be use the code below但是 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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM