在Rails 5中意外地将Jsonb的Postgres数组存储在转义字符串中

时间:2019-05-15 19:23:24

标签: ruby-on-rails arrays json postgresql serialization

也许我对应该如何工作的理解是错误的,但是当我期望字符串是jsonb array时,我看到了存储在数据库中的字符串。这是我的设置方法:

迁移

t.jsonb :variables, array: true

模型

attribute :variables, :variable, array: true

自定义ActiveRecord ::类型

ActiveRecord::Type.register(:variable, Variable::Type)

自定义变量类型

class Variable::Type < ActiveRecord::Type::Json
  include ActiveModel::Type::Helpers::Mutable

  # Type casts a value from user input (e.g. from a setter). This value may be a string from the form builder, or a ruby object passed to a setter. There is currently no way to differentiate between which source it came from.
  # - value: The raw input, as provided to the attribute setter.
  def cast(value)
    unless value.nil?
      value = Variable.new(value) if !value.kind_of?(Variable)
      value
    end
  end

  # Converts a value from database input to the appropriate ruby type. The return value of this method will be returned from ActiveRecord::AttributeMethods::Read#read_attribute. The default implementation just calls #cast.
  #  - value: The raw input, as provided from the database.
  def deserialize(value)
    unless value.nil?
      value = super if value.kind_of?(String)
      value = Variable.new(value) if value.kind_of?(Hash)
      value
    end
  end

因此,此方法从应用程序的角度来看确实有效。我可以将值设置为variables = [Variable.new, Variable.new],并将其正确存储在数据库中,并以[Variable, Variable]的数组形式取回。

让我担心的是这个问题的根源,即在数据库中,该变量是使用双转义字符串而不是json对象存储的:

{
  "{\"token\": \"a\", \"value\": 1, \"default_value\": 1}",
  "{\"token\": \"b\", \"value\": 2, \"default_value\": 2}"
}

我希望将它们存储得更像一个json对象,像这样:

{
  {"token": "a", "value": 1, "default_value": 1},
  {"token": "b", "value": 2, "default_value": 2}
}

这样做的原因是,据我了解,如果将来采用json格式而不是字符串格式,则直接从数据库中对该列进行的查询将更快/更轻松。通过Rails查询不会受到影响。

如何使我的Postgres数据库通过导轨正确存储jsonb数组?

3 个答案:

答案 0 :(得分:0)

一种解决方案是仅通过JSON.parse解析变量,将其压入一个空数组中,然后将其分配给该属性。

 variables = []
 variable = "{\"token\": \"a\", \"value\": 1, \"default_value\": 1}"
 variable.class #String

 parsed_variable = JSON.parse(variable) #{"token"=>"a", "value"=>1, "default_value"=>1}
 parsed_variable.class #Hash

 variables.push parsed_variable

答案 1 :(得分:0)

因此,事实证明Rails 5 attribute api尚不完善(并且没有充分记录),并且Postgres数组支持至少在我要使用它的方式上引起了一些问题。对于解决方案,我使用了相同的方法来解决问题,但是我不是在告诉Rails使用自定义类型的数组,而是使用自定义类型的数组。代码胜于雄辩:

迁移

t.jsonb :variables, default: []

模型

attribute :variables, :variable_array, default: []

自定义ActiveRecord ::类型

ActiveRecord::Type.register(:variable_array, VariableArrayType)

自定义变量类型

class VariableArrayType < ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Jsonb

  def deserialize(value)
    value = super # turns raw json string into array of hashes
    if value.kind_of? Array
      value.map {|h| Variable.new(h)} # turns array of hashes into array of Variables
    else
      value
    end
  end

end

现在,正如预期的那样,数据库条目不再存储为字符串,而是可搜索/可索引的jsonb。进行这种歌舞的全部原因是,我可以使用普通的旧红宝石对象设置variables属性...

template.variables = [Variable.new(token: "a", default_value: 1), Variable.new(token: "b", default_value: 2)]

...然后将其序列化为数据库中的jsonb表示形式...

[
  {"token": "a", "default_value": 1},
  {"token": "b", "default_value": 2}
]

...但是更重要的是,它会自动反序列化并重新水化为普通的旧红宝石对象,以便我与之交互。

Template.find(123).variables = [#<Variable:0x87654321 token: "a", default_value: 1>, #<Variable:0x12345678 token: "b", default_value: 2>]

使用旧的serialize api会导致每次保存都进行一次写操作(这是由Rails体系结构设计故意创建的),而不管序列化属性是否已更改。由于可以通过多种方式分配属性,因此通过覆盖setter / getter手动完成所有操作是不必要的麻烦,并且部分原因是使用更新的attributes API。

答案 2 :(得分:0)

如果它对其他人有帮助,Rails 希望您提供可能的密钥以在控制器中允许使用强参数:

def controller_params
  params.require(:parent_key)
    .permit(
      jsonb_field: [:allowed_key1, :allowed_key2, :allowed_key3]
    )
end