如何将逻辑从控制器转移到模型中? [英] How To Move Logic From Controller Into Model?

查看:64
本文介绍了如何将逻辑从控制器转移到模型中?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

我的addresses_controller 中有这个逻辑:

 def 创建@pool = Pool.find(params[:pool_id])除非 address_params[:ipv4_range_start].blank?||address_params[:ipv4_range_stop].blank?(address_params[:ipv4_range_start]..address_params[:ipv4_range_stop]).每个都做|octet|params[:address][:ipv4_octet3] = 八位字节@address = @pool.addresses.build(address_params)如果 !@address.save呈现新"结尾结尾redirect_to pool_path(@pool),注意:地址范围已创建."否则#缺少一些东西@address = @pool.addresses.build(address_params)@address.errors.add_on_blank(:ipv4_range_start)@address.errors.add_on_blank(:ipv4_range_stop)呈现新"结尾结尾

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

感谢您的建议!

吉姆

解决方案

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

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

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

# 地址模型validates_presence_of :ipv4_range_start, :ipv4_range_stop,在::require_range

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

现在我们可以在控制器中做到这一点:

# AddressesController定义创建@pool = Pool.find(params[:pool_id])@address = @pool.addresses.build(address_params)如果@address.invalid?(:require_range)呈现新"并返回结尾# ...结尾

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

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

# 地址模型def self.create_from_range!(attrs)start = attrs.fetch(:ipv4_range_start)停止 = attrs.fetch(:ipv4_range_stop)自我交易做(start..stop).map 做 |octet|self.create!(attrs.merge ipv4_octet3: octet)结尾结尾结尾

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

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

def 创建# 我们之前的代码@pool = Pool.find(params[:pool_id])@address = @pool.addresses.build(address_params)如果@address.invalid?(:require_range)呈现新"并返回结尾# 新代码开始@pool.addresses.create_from_range!(address_params)救援 ActiveRecord::ActiveRecordErrorflash[:error] = "地址范围未创建!"呈现新"并返回结尾redirect_to @pool,注意:地址范围已创建."结尾

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

这应该是您所需要的.

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

A pool has many addresses. Want to create multiple address records based on a submitted range.

I have this logic in my 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

Wondering how I might move this into the Address model? Seems like too much for the controller but I can't figure out how to iterate through the submitted range and build and save each address from the Address model itself.

Thanks for any suggestions!

Jim

解决方案

I think your intuition is correct that we can move a lot of this into the model.

Disclaimer: None of this code is tested; copying it and pasting it directly into your code will probably end in tears.

So there are two parts of your logic we need to deal with. The first is making sure :ipv4_range_start and _stop are provided. For that we can use a validation. Since it seems like you don't want those attributes to be required for all Addresses, we can use the :on option to supply a validation context. (More info on contexts here.):

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

That on: :require_range part means this validation won't usually run—it'll only run when we tell ActiveRecord to use the :require_range context.

Now we can do this in the controller:

# 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

This accomplishes the same thing as the code in your else block, but the real logic is in the model, and Rails populates the errors object for us.

Now that we have that out of the way, we can deal with creating the objects. For that we can write a class method in Address. The great thing about class methods in Rails models is that they're automatically available on association collections, so for example if we define an Address.foo class method, we get @pool.addresses.foo for free. Here's a class method that will create an array of Addresses:

# 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

This is pretty much the same as your else block, just a little cleaner. We do self.create! in a transaction, so that if any of the create!s fails they'll all be rolled back. We do that inside a map block instead of each so that, assuming no error occurs, the method will return an array of the created objects.

Now we just have to use it in our controller:

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

As you can see, we used @pool.addresses.create_from_range!, so the association (Address#pool_id) will be filled in for us. If we wanted we could assign the returned array to an instance variable and show the created records in the view.

That should be about all you need.

P.S. One thing worth noting is that Ruby has a great built-in IPAddr class, and because an IP address is just a number (e.g. 3338456716 is the decimal form of 198.252.206.140) it may serve you to store each IP address as a single 4-byte integer instead of four separate columns. Most databases have useful built-in functions for dealing with IP addresses (PostgreSQL actually has a built-in inet column type for this) as well. It really depends on your use case, however, and at this point such an optimization may very well be premature. Cheers!

这篇关于如何将逻辑从控制器转移到模型中?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆