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::Model 和ActiveModel::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,
]
你也可以注册你自己的自定义类型,实现cast 和serialize :
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