Хранение Postgres Array of Jsonb в Rails 5 неожиданно экранирует строки - PullRequest
0 голосов
/ 15 мая 2019

Возможно, мое понимание того, как это должно работать, неверно, но я вижу строки, хранящиеся в моей БД, когда я ожидаю, что они будут 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, а не в формате строки. Запросы через рельсы останутся без изменений.

Как мне заставить мою Postgres DB правильно хранить массив jsonb через рельсы?

Ответы [ 2 ]

0 голосов
/ 17 мая 2019

Получается, что API Rails 5 attribute еще не идеален (и не очень хорошо документирован), и поддержка массива 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, используя простые старые объекты ruby ​​...

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), независимо от того, был ли изменен атрибут сериализации.Выполнение всего этого вручную путем переопределения сеттеров / геттеров является ненужным осложнением из-за многочисленных способов назначения атрибутов и отчасти является причиной появления более новых attributes api.

0 голосов
/ 15 мая 2019

Одним из решений может быть просто проанализировать переменную через 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...