RoR 将虚拟属性转换为两个数据库属性

RoR converting a virtual attribute into two database attributes

我目前无法找到一种很好的方法来编写以下情况的代码:

有一个名为 TcpService 的模型,它有两个属性,port_fromport_to,都是整数。它还有一个名为 portrange 的虚拟属性,它是一个字符串。 portrange 是属性 port_fromport_to 的字符串表示形式,因此 portrange = "80 90" 应该产生 port_from = 80port_to = 90。我现在要做的是对 creatingupdating 对象使用相同的 Formtastic 形式。表格看起来很标准(HAML 代码):

= semantic_form_for @tcp_service do |f|
  = f.inputs do
    = f.input :portrange, as: :string, label: "Portrange"
    -# calls @tcp_service.portrange to determine the shown value

  = f.actions do
    = f.action :submit, label: "Save"

问题是,我不知道有什么简单的方法可以使我想要的值出现在表单中。在 new 上,我希望该字段为空,如果 create 失败,我希望它显示错误的用户输入以及错误,否则使用 port_fromport_to 填充 portrange。在 edit 上,我希望 port_fromport_to 的字符串表示形式出现,如果 update 失败,我希望它显示错误的用户输入以及错误,否则填充 port_fromport_to 使用 portrange.

模型看起来像这样,我觉得很乱。 有没有更好的方法让它达到我的需要?

class TcpService < ActiveRecord::Base
  # port_from, port_to: integer
  attr_accessor :portrange

  validate :portrange_to_ports   # populates `port_from` and `port_to` 
                                 # using `portrange` AND adds errors

  # raises exception if conversion fails
  def self.string_to_ports(string)
    ... # do stuff
    return port_from, port_to
  end

  # returns string representation of ports without touching self
  def ports_to_string
    ... # do stuff
    return string_representation
  end

  # is called every time portrange is set, namely during 'create' and 'update'
  def portrange=(val)
    return if val.nil?
    @portrange = val
    begin
      self.port_from, self.port_to = TcpService.string_to_ports(val)
    # catches conversion errors and makes errors of them
    rescue StandardError => e
      self.errors.add(:portrange, e.to_s())
    end
  end

  # is called every time the form is rendered
  def portrange
    # if record is freshly loaded from DB, this is true
    if self.port_from && self.port_to && @portrange.nil?
      self.ports_to_string()
    else
      @portrange
    end
  end

  private
    # calls 'portrange=(val)' in order to add errors during validation
    def portrange_to_ports
      self.portrange = self.portrange
    end

end

感谢阅读

在你的模型中

def portrange
  return "" if self.port_from.nil? || self.port_to.nil?
  "#{self.port_from} #{self.port_to}"
end

def portrange=(str)
  return false unless str.match /^[0-9]{1,5}\ [0-9]{1,5}/
  self.port_from = str.split(" ").first
  self.port_to = str.split(" ").last
  self.portrange
end

使用它,您应该能够在表单中使用 portrange setter 和 getter。