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

时间:2015-01-27 21:28:28

标签: ruby-on-rails ruby-on-rails-4

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

我在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模型?对控制器来说似乎太多了,但我无法弄清楚如何遍历提交的范围,并从地址模型本身构建并保存每个地址。​​

感谢您的任何建议!

吉姆

1 个答案:

答案 0 :(得分:4)

我认为你的直觉是正确的,我们可以把很多东西转移到模型中。

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

因此,我们需要处理两个逻辑部分。首先是确保提供:ipv4_range_start_stop。为此,我们可以使用验证。由于您似乎不希望所有地址需要这些属性,因此我们可以使用: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中编写一个类方法。 Rails模型中类方法的优点在于它们可以自动在关联集合中使用,因此,例如,如果我们定义Address.foo类方法,我们可以免费获得@pool.addresses.foo。这是一个将创建地址数组的类方法:

# 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内执行此操作,因此,假设没有发生错误,该方法将返回已创建对象的数组。

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

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)。如果我们希望我们可以将返回的数组分配给实例变量并在视图中显示创建的记录。

这应该是您所需要的一切。

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