我有一个Rails模型细分,其属性由范围数组组成:例如[0..20, 32..41, 90..200]
。
我想向Array添加一些方法,这些方法特别适用于处理范围数组,例如一种求和Array中每个Range的长度的方法:
class RangeArray < Array
def sum
inject(0) { |total, range| total + (1 + range.max - range.min) }
end
end
理想情况下,我可以覆盖Segment中的getter和setter,这样,当从数据库中读取范围数组时,它将自动转换为RangeArray,并且每当将其写入数据库时,都将其写为数组。这似乎很容易做到,因为RangeArray是从Array继承的,但是我在某些方面遇到了一些麻烦。我与这个设置非常接近:
class Segment < ApplicationRecord
attr_accessor :ranges
def ranges
puts "read"
RangeArray.new(super)
end
def ranges=(var)
puts "write"
super(RangeArray.new(var))
end
end
通过此设置,我可以读取范围,明确设置范围,保存到数据库等:
2.6.5 :071 > s = Segment.new
=> #<Segment id: nil, ranges: [], type: nil, created_at: nil, updated_at: nil>
2.6.5 :072 > s.ranges = [2000..3000]
write
=> [2000..3000]
2.6.5 :073 > s.ranges.class
read
=> RangeArray
2.6.5 :074 > s.ranges.sum
read
=> 1001
我遇到麻烦的是RangeArray应该从Array继承的push
和<<
方法:
2.6.5 :075 > s.ranges << (40..50)
read
=> [2000..3000, 40..50]
2.6.5 :076 > s.ranges
read
=> [2000..3000]
这似乎失败了,因为尝试通过<<
向范围中添加元素首先会调用读取器,该读取器会生成一个新的RangeArray对象,将40..50
推入该对象,然后丢弃该对象。
将值存储在中间实例变量中感觉应该可以,但是似乎没有太大区别:
def ranges
puts "read"
@ranges = RangeArray.new(super)
end
def ranges=(var)
puts "write"
@ranges = super(RangeArray.new(var))
end
我感觉我已经很近了,但是我丢失了一些东西,并且不确定如何在<<
或push
期间调用作家。我该怎么办?除了覆盖getter和setter之外,还有其他方法可以更改ActiveRecord将ranges
属性强制转换为的类型吗?还是我应该使模块混入直接向Array添加方法,并为需要它的每个模型添加include
混入?
Rails 5引入了以前只是内部API的Attributes API。
它允许您声明属性,处理默认值和类型转换,就像ActiveRecord对数据库列自动执行一样。它还更进一步-它使您可以声明自己的类型以进行序列化/反序列化。
此示例使用Postgres和JSONB类型列作为基础存储机制。
首先声明您的自定义类型:
# app/types/range_array_type.rb
# This is the type that handles casting the attribute from
# user input and serializing/deserializing the attribute from the database
# @see https://api.rubyonrails.org/classes/ActiveModel/Type/Value.html
class RangeArrayType < ActiveRecord::Type::Value
# Type casts a value from user input (e.g. from a setter).
def cast(value)
value.is_a?(RangeArray) ? value : deserialize(value)
end
# Casts the value from the ruby type to a type that the database knows
# how to understand.
# in this case an array of pairs representing the bounds of the array
# which can be serialized as JSON
def serialize(value)
value.map {|range| [range.begin, range.end] }.to_json
end
# Casts the value from an array of pairs representing the bounds or ranges
def deserialize(value)
case value
when Array
RangeArray.new( value.map {|x| x.is_a?(Range) ? x : Range.new(*x) } )
when String
deserialize(JSON.parse(value)) # recursion
else
nil
end
end
end
然后在启动器中注册类型:
# config/initializers/types.rb
ActiveRecord::Type.register(:range_array, RangeArrayType)
然后在模型中使用新的幻想类型:
class Segment < ApplicationRecord
# segments.ranges is a JSONB column
attribute :ranges, :range_array
end
这将覆盖ActiveRecord从数据库架构派生的类型。
让我们尝试一下:
Loading development environment (Rails 6.0.2.1)
[1] pry(main)> segment = Segment.new(ranges: [[1,2], 3..4])
=> #<Segment:0x0000000005481868 id: nil, ranges: [1..2, 3..4], test_array: nil, created_at: nil, updated_at: nil>
[2] pry(main)> segment.save!
(0.3ms) BEGIN
Segment Create (1.0ms) INSERT INTO "segments" ("ranges", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id" [["ranges", "[[1,2],[3,4]]"], ["created_at", "2020-02-02 07:20:30.982678"], ["updated_at", "2020-02-02 07:20:30.982678"]]
(1.0ms) COMMIT
=> true
[3] pry(main)> s = Segment.first
Segment Load (0.9ms) SELECT "segments".* FROM "segments" ORDER BY "segments"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> #<Segment:0x0000000006518820
id: 1,
ranges: [1..2, 3..4],
test_array: nil,
created_at: Sun, 02 Feb 2020 07:02:21 UTC +00:00,
updated_at: Sun, 02 Feb 2020 07:02:21 UTC +00:00>
[4] pry(main)> s.ranges
=> [1..2, 3..4]
[5] pry(main)> s.ranges.class.name
=> "RangeArray"