【问题标题】:How to rollback all transactions in transaction block on error in Ruby on Rails如何在Ruby on Rails中出现错误时回滚事务块中的所有事务
【发布时间】:2020-10-12 22:56:19
【问题描述】:

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

...
  # === 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 的所有信息输入到一个大表单中。当所有信息都正确时,我能够正确保存记录。但是,如果任何模型中出现错误,我想回滚 ALL 事务并呈现有错误的表单。

此主题已在其他地方讨论过,但是,我无法使用我见过的方法回滚所有事务。目前,如果其中一个模型(例如 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-rails ruby database activerecord transactions


    【解决方案1】:

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

    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 %>
    

    【讨论】:

    • 但实际上我会避免将这么多塞进单个表单/端点中,而是使用 AJAX 来使其在用户实际单独对资源进行 CRUD 处理时显得无缝,因为它具有较低的复杂性和更好的用户体验。
    • 我很尴尬地承认我不知道 validates_associated。这确实简化了很多事情,因为其中大部分是由于在关联无效时尝试回滚。我明白你对用户体验的看法。我将所有这些分解成一个具有多个折叠步骤的向导,所以它不是那么压倒性的。我认为将任务保存为“草稿”并在将任务标记为完成之前提出多个请求可能是最好的方法。无论如何,感谢您的出色回答。
    【解决方案2】:

    查看您的代码,我可以看到您在开始救援块的帮助下使用了 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
    

    【讨论】:

      猜你喜欢
      • 2016-12-19
      • 2010-11-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多