简体   繁体   English

使用 Array 子类覆盖 Rails 模型 getter/setter

[英]Overriding Rails model getters/setters with an Array subclass

I have a Rails model Segment that has an attribute consisting of an array of ranges: [0..20, 32..41, 90..200] , for instance.我有一个 Rails 模型 Segment,它有一个由一系列范围组成的属性:例如[0..20, 32..41, 90..200]

There are a few methods that I would like to add to Array that would be useful specifically for dealing with arrays of ranges, like a method to sum the length of each Range in the Array:有一些方法我想添加到 Array 中,这些方法专门用于处理范围数组,例如对数组中每个范围的长度求和的方法:

class RangeArray < Array
  def sum
    inject(0) { |total, range| total + (1 + range.max - range.min) }
  end
end

Ideally, I'd be able to override the getters and setters in Segment so that when the array of ranges is read from the database it is automatically cast to a RangeArray, and whenever it is written to the database it is written as an Array.理想情况下,我能够覆盖 Segment 中的 getter 和 setter,这样当从数据库中读取范围数组时,它会自动转换为 RangeArray,并且每当它写入数据库时​​,它都会被写入为一个数组。 This seems like it should be easy to do since RangeArray is inherits from Array, but I'm having some real trouble with a few things.这似乎应该很容易做到,因为 RangeArray 是从 Array 继承的,但我在一些事情上遇到了一些真正的麻烦。 I get quite close with this setup:我非常接近这个设置:

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

With this setup, I can read the ranges, set them explicitly, save to the database, etc:通过此设置,我可以读取范围、显式设置它们、保存到数据库等:

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 

Where I'm having trouble is with the push and << methods that RangeArray should be inheriting from Array:我遇到问题的地方是 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] 

It seems like this fails because trying to add an element to ranges via << calls the reader first, which generates a new RangeArray object, pushes 40..50 to it, and then discards the object.这似乎失败了,因为尝试通过<<将元素添加到范围首先调用读取器,它会生成一个新的 RangeArray 对象,将40..5040..50它,然后丢弃该对象。

Storing the values in an intermediate instance variable feels like it should work, but doesn't seem to make much of a difference:将值存储在中间实例变量中感觉应该可以工作,但似乎没有太大区别:

  def ranges
    puts "read"
    @ranges = RangeArray.new(super)
  end

  def ranges=(var)
    puts "write"
    @ranges = super(RangeArray.new(var))
  end

I feel like I'm close here, but I'm missing something, and am not sure how to get the writer to be called during << or push .我觉得我在这里很近,但我错过了一些东西,并且不确定如何在<<push期间调用作者。 What should I do?我该怎么办? Rather than override the getters and setters, is there some other way to change the type to which ActiveRecord casts the ranges attribute?除了覆盖 getter 和 setter,还有其他方法可以更改 ActiveRecord 将ranges属性转换为的类型吗? Or perhaps I should just make a module mix-in adds methods directly to Array, and include that mix-in for each model that needs it?或者,也许我应该制作一个模块混入,将方法直接添加到 Array,并为每个需要它的模型include该混入?

Rails 5 introduced the Attributes API which previously was just an internal API. Rails 5 引入了 Attributes API,之前它只是一个内部 API。

It allows you to declare attributes and handle defaults and typecasting just like ActiveRecord does automatically for your database columns.它允许您声明属性并处理默认值和类型转换,就像 ActiveRecord 自动为您的数据库列所做的那样。 It also goes a step further - it lets you declare your own types for serialization / deserialization.它还更进一步 - 它允许您声明自己的序列化/反序列化类型。

This example uses Postgres and a JSONB type column as the underlying storage mechanism.此示例使用 Postgres 和 JSONB 类型列作为底层存储机制。

Start by declaring your custom type:首先声明您的自定义类型:

# 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

And you want to setup your RangeArray class so that it does not inherit from Array:并且您想设置 RangeArray 类,使其不从 Array 继承:

require 'delegate'
class RangeArray < SimpleDelegator
  def sum
    __getobj__.sum(&:size)
  end
end

Inheriting from the core library classes can be problematic since they are implemented in C and do not behave like other classes.从核心库类继承可能会出现问题,因为它们是用 C 实现的,并且与其他类的行为不同。 See Beware subclassing Ruby core classes .请参阅注意子类化 Ruby 核心类

Then register the type in an intitializer:然后在初始化程序中注册类型:

# config/initializers/types.rb
ActiveRecord::Type.register(:range_array, RangeArrayType)

And then use your new fancy type in the model:然后在模型中使用你的新花式类型:

class Segment < ApplicationRecord
  # segments.ranges is a JSONB column
  attribute :ranges, :range_array
end

This overrides the type that ActiveRecord derives from the database schema.这将覆盖 ActiveRecord 从数据库架构派生的类型。

Lets try it out:让我们试试看:

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"

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM