如何将逻辑从控制器转移到模型中?

How To Move Logic From Controller Into Model?

一个池有多个地址。想要根据提交的范围创建多个地址记录。

我的 addresses_controller 中有这个逻辑:

  def create
    @pool = Pool.find(params[:pool_id])

    unless address_params[:ipv4_range_start].blank? || address_params[:ipv4_range_stop].blank?
      (address_params[:ipv4_range_start]..address_params[:ipv4_range_stop]).each do |octet|
        params[:address][:ipv4_octet3] = octet
        @address =  @pool.addresses.build(address_params)
        if !@address.save
           render 'new'
        end
      end
      redirect_to pool_path(@pool), notice: "Address range created."

    else #something was missing
      @address = @pool.addresses.build(address_params)
      @address.errors.add_on_blank(:ipv4_range_start)
      @address.errors.add_on_blank(:ipv4_range_stop)
      render 'new'
    end
  end

想知道如何将其移动到 Address 模型中?对于控制器来说似乎太多了,但我不知道如何遍历提交的范围并从地址模型本身构建和保存每个地址。​​

感谢您的任何建议!

吉姆

我认为您的直觉是正确的,我们可以将其中的很多内容移动到模型中。

免责声明:None 此代码已经过测试;将其复制并直接粘贴到您的代码中可能会以泪流满面。

所以我们需要处理您的逻辑的两个部分。首先是确保提供 :ipv4_range_start_stop。为此,我们可以使用验证。由于您似乎不希望 all 地址需要这些属性,我们可以使用 :on 选项来提供验证上下文。 (More info on contexts here.):

# Address model
validates_presence_of :ipv4_range_start, :ipv4_range_stop,
                      on: :require_range

on: :require_range 部分意味着这个验证通常不会 运行 — 当我们告诉 ActiveRecord 使用 :require_range 上下文时它只会 运行。

现在我们可以在控制器中执行此操作:

# AddressesController
def create
  @pool = Pool.find(params[:pool_id])

  @address = @pool.addresses.build(address_params)
  if @address.invalid?(:require_range)
    render 'new' and return
  end

  # ...
end

这与您的 else 块中的代码完成相同的事情,但真正的逻辑在模型中,Rails 为我们填充 errors 对象。

现在我们已经解决了这个问题,我们可以处理创建对象了。为此,我们可以在 Address 中编写一个 class 方法。 Rails 模型中 class 方法的伟大之处在于它们在关联集合中自动可用,因此例如,如果我们定义一个 Address.foo class 方法,我们得到 @pool.addresses.foo 免费。这是一个 class 方法,它将创建一个地址数组:

# Address model
def self.create_from_range!(attrs)
  start = attrs.fetch(:ipv4_range_start)
  stop = attrs.fetch(:ipv4_range_stop)

  self.transaction do
    (start..stop).map do |octet|
      self.create!(attrs.merge ipv4_octet3: octet)
    end
  end
end

这与您的 else 块几乎相同,只是更干净一点。我们在事务中执行 self.create!,因此如果任何 create! 失败,它们都会被回滚。我们在 map 块而不是 each 中执行此操作,以便假设没有发生错误,该方法将 return 一个已创建对象的数组。

现在我们只需要在我们的控制器中使用它:

def create
  # Our code from before
  @pool = Pool.find(params[:pool_id])

  @address = @pool.addresses.build(address_params)
  if @address.invalid?(:require_range)
    render 'new' and return
  end

  # The new code
  begin
    @pool.addresses.create_from_range!(address_params)
  rescue ActiveRecord::ActiveRecordError
    flash[:error] = "Address range not created!"
    render 'new' and return
  end

  redirect_to @pool, notice: "Address range created."
end

如您所见,我们使用了@pool.addresses.create_from_range!,因此关联(Address#pool_id)将被填充给我们。如果需要,我们可以将 returned 数组分配给实例变量并在视图中显示创建的记录。

这应该就是您所需要的。

P.S。值得注意的一件事是 Ruby 有一个很好的内置 IPAddr class,因为 IP 地址只是一个数字(例如 33384567163338456716 的十进制形式 198.252.206.140) 它可以帮助您将每个 IP 地址存储为单个 4 字节整数而不是四个单独的列。大多数数据库都有用于处理 IP 地址的有用的内置函数(PostgreSQL 实际上也有一个内置的 inet 列类型)。然而,这实际上取决于您的用例,此时这种优化可能还为时过早。干杯!