使用Array子类重写Rails模型获取器/设置器

问题描述 投票:0回答:1

我有一个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混入?

ruby ruby-on-rails-5
1个回答
0
投票

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"
© www.soinside.com 2019 - 2024. All rights reserved.