用Rails Attributes API自动铸造参数的方法指南

114 阅读2分钟

Rails应用程序的一个常见做法是将逻辑提取到普通的Ruby对象(PORO)中。但是你经常从控制器params ,直接将数据传递给这些对象,而且数据是以字符串的形式出现的:

class SalesReport
  attr_accessor :start_date, :end_date, :min_items

  def initialize(params = {})
    @start_date = params[:start_date]
    @end_date = params[:end_date]
    @min_items = params[:min_items]
  end

  def run!
    # Do some cool stuff
  end
end

report = SalesReport.new(start_date: "2020-01-01", end_date: "2020-03-01", min_items: "10")

# But the data is just stored as strings :(
report.start_date
# => "2020-01-01"
report.min_items
# => "10"

你可能希望start_date 是一个日期,min_items 是一个整数。你可以在构造函数中加入你自己的基本类型转换:

class SalesReport
  attr_accessor :start_date, :end_date, :min_items

  def initialize(params)
    @start_date = Date.parse(params[:start_date])
    @end_date = Date.parse(params[:end_date])
    @min_items = params[:min_items].to_i
  end

  def run!
    # Do some cool stuff
  end
end

但更好的是,你可以利用Attributes API来自动处理这种转换。

使用方法

Rails Attributes API 在内部用于对ActiveRecord 模型的属性进行类型转换。当你查询一个在数据库中有datetime 列的模型时,被拉出来的 Ruby 对象有一个DateTime 字段 - 这就是 Attributes API 的作用。

我们可以通过混合使用ActiveModel::ModelActiveModel::Attributes 模块来修饰我们的报告模型:

class SalesReport
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :start_date, :date
  attribute :end_date, :date
  attribute :min_items, :integer

  def run!
    # Do some cool stuff
  end
end

report = SalesReport.new(start_date: "2020-01-01", end_date: "2020-03-01", min_items: "10")

# Now the attributes are native types!

report.start_date
# => Wed, 01 Jan 2020
report.min_items
# => 10

这种模式对于减少表单对象、报告对象或你的Rails应用程序中任何其他类似于模型的Ruby类中的模板代码是非常好的。让框架为你做类型转换,而不是自己去重新实现它!

选项

从Rails 6.1开始,该模块在技术上是一个私有API。使用时请自行承担风险!

Attribute API 将自动处理大多数基元的类型转换。所有的基础知识都包括在内:

attribute :start_date, :date
attribute :max_size, :integer
attribute :enabled, :boolean
attribute :score, :float

你可以在这里找到开箱即用类型的完整列表:activemodel/lib/active_model/type

最酷的是,这些类型在接受什么样的输入方面非常强大。例如,boolean Attribute类型适用于false 的任何值:

FALSE_VALUES = [
  false, 0,
  "0", :"0",
  "f", :f,
  "F", :F,
  "false", :false,
  "FALSE", :FALSE,
  "off", :off,
  "OFF", :OFF,
]

你也可以注册你自己的自定义类型,实现castserialize

ActiveRecord::Type.register(:zip_code, ZipCodeType)

class ZipCodeType < ActiveRecord::Type::Value
  def cast(value)
    ZipCode.new(value) # cast to your own ZipCode class for special handling
  end

  def serialize(value)
    value.to_s
  end
end

此外,你还可以用Attributes API为其设置一个默认值:

attribute :start_date, :date, default: 30.days.ago
attribute :max_size, :integer, default: 15
attribute :enabled, :boolean, default: true
attribute :score, :float, default: 9.75