簡體   English   中英

如何在 Ruby on Rails 中發生錯誤時回滾事務塊中的所有事務

[英]How to rollback all transactions in transaction block on error in 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 的所有信息輸入到一個大表格中。 當所有信息都正確時,我能夠正確保存記錄。 但是,如果任何模型中存在錯誤,我想回滾所有事務並呈現有錯誤的表單。

此主題已在其他地方討論過,但是,我無法使用我見過的方法回滾所有事務。 當前,如果模型之一出現 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 我總是能夠捕捉到錯誤,但數據庫不會回滾。 我已經嘗試過使用和不使用開始救援語句。 我目前的嘗試是不嵌套事務,而是將它們提交到單個事務塊中,但這也不起作用。 這是我當前的代碼。

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

我有一個任務模型,該模型具有以下關聯:

...
  # === 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的所有信息輸入一種大格式。 當所有信息正確時,我能夠正確保存記錄。 但是,如果任何模型中都有錯誤,我想回滾所有事務並使用錯誤呈現表單。

在其他地方已經討論了該主題,但是,我無法使用已經看到的方法回滾所有事務。 當前,如果其中一個模型存在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

這只是將錯誤鍵“電話無效”添加到對用戶不太友好的錯誤中。 如果要顯示每個嵌套記錄的錯誤消息,可以在使用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 %>

看看你的代碼,我可以看到你在開始救援塊的幫助下使用了 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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM