Rails jsonb - 在将 jsonb 保存到 Postgresql 数据库时防止 JSON 键重新排序

Rails jsonb - Prevent JSON keys from reordering when jsonb is saved to Postgresql database

我有一个列 amount_splits,我需要将我的 JSON 保存到我指定的键顺序。

如何防止 Rails / Postgres jsonb 在我将 JSON 键保存到数据库时自动排序? (用于创建或更新)

看起来正在尝试按字母顺序排序,但效果不佳。

这是我保存的内容:

{
    "str_fee": 3.17,       # key 1
    "eva_fee": 14.37,      # key 2
    "fran_royalty": 14.37, # key 3
    "fran_amount": 67.09   # key 4
}

这是实际节省的方式:

{
    "eva_fee": 14.37,     # key 2
    "str_fee": 3.17,      # key 1
    "fran_amount": 67.09, # key 4
    "fran_royalty": 14.37 # key 3
}

目的:

在回答"sorting doesn't matter when the JSON is consumed on the receiving end"之前,请先停下来想一想...然后请继续阅读

我需要按我需要的方式对键进行排序,因为 使用此 JSON 的客户端界面正在显示 JSON需要密钥按照文档告诉他们的顺序排列的开发人员。它需要按该顺序排列的原因是要显示按哪个顺序首先发生的计算过程:

正确的顺序告诉开发者:

首先应用了 str_fee,然后是 eva_fee,然后是 fran_royalty...使 fran_amount 成为结束金额。

但是根据 jsonb 的排序方式,它错误地告诉我们的开发人员:

首先应用了 eva_fee,然后是 str_fee,然后是 fran_amount...使 fran_royalty 成为结束金额。

[更新于 2021/02/12] 请参阅 我的“已接受”答案(我不想接受我自己的答案,因为它是Rails 破解)。

基本上为了保存 jsonb 列中的顺序,我需要使用数组(即 [{str_fee: 6}, {eva_fee: 11}, ...])。


[老掉牙的回答]

我找不到关于如何修改 jsonb save/update 行为的任何信息,但您可以通过 Rails 控制 return 您的 as_json 的方式] 型号.

因此,不要通过直接调用 self.amount_splits 列来 returning 您的 JSON(在错误的键顺序中 return)... 手动分解每个键

NOTE: this will only work if you know your key names ahead of time... if key names are dynamically created before you know them, you'll need to try something else... likely saving your JSON as a string instead of as a Hash.

class Transaction < ApplicationRecord
  store_accessor :amount_splits, :str_fee, :eva_fee, :fran_royalty, :fran_amount

  [...]

  def as_json(options={})
    # simple JSON response:
    json = {
      [...]
      "amount_splits"   => {
        "str_fee"       => self.str_fee,
        "eva_fee"       => self.eva_fee,
        "fran_royalty"  => self.fran_royalty,
        "fran_amount"   => self.fran_amount
      },
      [...]
    }
    return json
  end

  [...]

end

NOTE: I've significantly abbreviated my custom as_json method, only leaving the relevant part of the JSON that it will return

实际上,它们不是按字母顺序排序,而是按密钥长度然后按字母顺序排序,这解释了您获得的顺序。 jsonb 类型已创建为 json 类型的更好版本来写入和访问数据,并且可能出于索引和搜索目的,它们更改了键顺序。如果你希望你的键顺序不改变,你可以使用在数据库中存储数据时不改变键顺序的json类型。

希望对您有所帮助。

Postgres docs 建议使用 json 类型来保留对象键的顺序:

In general, most applications should prefer to store JSON data as jsonb, unless there are quite specialized needs, such as legacy assumptions about ordering of object keys.

您可以使用 postgresql 的 json 类型并保留顺序。如果您想利用 jsonb 的许多性能优势,您将失去本机顺序保留。

这是一种通过在每个键中注入数字索引来保持顺序的方法:

class OrderedHashSerializer < ActiveRecord::Coders::JSON
  class << self
    def dump(obj)
      ActiveSupport::JSON.encode(
        dump_transform(obj)
      )
    end

    def load(json)
      json = ActiveSupport::JSON.decode(json) if json.is_a?(String)

      load_transform(json)
    end

    private

    # to indicate identifiers order as the postgresql jsonb type does not preserve order:
    def dump_transform(obj)
      obj.transform_keys.with_index do |key, index|
        "#{index + 1}_#{key}"
      end
    end

    def load_transform(hash)
      hash
        &.sort { |item, next_item| item.first.to_i <=> next_item.first.to_i }
        &.map { |key, value| format_item(key, value) }
        &.to_h
    end

    def format_item(key, value)
      [
        key.gsub(/^\d+_/, '').to_sym,
        value.in?([nil, true]) ? value : value.try(:to_sym) || value
      ]
    end
  end
end

注意 这将破坏在 sql 查询中使用嵌入式 json 数据,因为所有键名都将被污染。但是,如果您需要的保留顺序多于 json 查询,这是一种解决方案。 (虽然 json 类型在那一点开始看起来不错,无可否认)

测试看起来像:

describe OrderedHashSerializer do
  describe '#load' do
    subject(:invoke) { described_class.load(data) }

    let(:data) do
      {
        '1_error' => 'checksum_failure',
        '2_parent' => nil,
        '22_last_item' => 'omega',
        '3_code' => 'service_server_failure',
        '4_demographics': { age: %w[29], 'flavor' => %w[cherry vanilla rhubarb] }
      }.to_json
    end

    it 'formats data properly when loading it from database' do
      is_expected.to eq(
        error: :checksum_failure,
        parent: nil,
        last_item: :omega,
        code: :service_server_failure,
        demographics: { 'age' => ["29"], 'flavor' => %w[cherry vanilla rhubarb] },
      )
    end

    it 'preserves intended key order' do
      expect(invoke.keys.last).to eq :last_item
    end
  end

  describe '#dump' do
    subject(:invoke) { described_class.dump(data) }

    let(:data) do
      {
        'error' => 'checksum_failure',
        'parent' => nil,
        'code' => 'service_server_failure',
        demographics: { age: %w[65], 'flavor' => %w[cherry vanilla rhubarb] },
        'last_item' => 'omega'
      }
    end

    it 'prefixes keys with the numbers, in order' do
      is_expected.to eq(
        {
          "1_error" => :checksum_failure,
          "2_parent" => nil,
          "3_code" => :service_server_failure,
          "4_demographics" => { age: %w[65], flavor: %w[cherry vanilla rhubarb] },
          "5_last_item" => :omega
        }.to_json
      )
    end
  end
end