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

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

我在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模型本身的每个地址。

谢谢你的任何建议!

吉姆

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

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

因此,我们需要处理两个逻辑部分。 首先要确保:ipv4_range_start:ipv4_range_start 。 为此,我们可以使用validation。 由于您似乎不希望所有地址需要这些属性,因此我们可以使用:on选项来提供validation上下文。 ( 有关上下文的更多信息。 ):

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

那个on: :require_range部分意味着这个validation通常不会运行 – 它只会在我们告诉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块中的代码相同的function,但真正的逻辑在模型中,Rails为我们填充了errors对象。

现在我们已经解决了这个问题,我们可以处理创建对象的问题。 为此,我们可以在Address中编写一个类方法。 Rails模型中类方法的@pool.addresses.foo在于它们可以自动在关联集合中使用,例如,如果我们定义一个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 )将为我们填写。 如果我们希望我们可以将返回的数组分配给实例变量并在视图中显示创建的记录。

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

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