madeintandem / jsonb_accessor

Adds typed jsonb backed fields to your ActiveRecord models.
MIT License
1.11k stars 93 forks source link

accessor attributes don't return correct values when Jsonb field elements are updated directly #166

Closed drkelly58 closed 8 months ago

drkelly58 commented 1 year ago

I have a simple class as follows:

class Foo < ApplicationRecord jsonb_accessor :data, field1: :string, field2: [:string, default: 'default for field2'], field3: :string end

if I create a new instance of foo, and set foo.field1, & foo.field2. Values get populated into jsonb and are also present via accessor attributes.

But if i set foo['field1'] or data.update({field1:'new', field2:'value'}). Jsonb gets correct values but accessor attributes still have old values.

Once record is persisted & reloaded values are correct, however prior to a reload values remain out of sync. This presents a real problem with callbacks after_save & after_commit as they get out of sync values.

haffla commented 11 months ago

Hi @drkelly58 . Please provide a proper code example that I can run myself. Like this it's hard to understand what you mean.

drkelly58 commented 11 months ago
class TestJob < ApplicationRecord
  jsonb_accessor :metadata,
  attribute1: :integer,
  attribute2: :string

  def test
    self.attribute1 = 100
    self.attribute2 = 'just testing'
    puts metadata
    puts "value of attribute1 via accessor: #{ attribute1 }"
    puts "value of attribute1 via square bracket notation: #{ metadata['attribute1'] }"
    metadata['attribute1'] = 105
    puts 'updated attribute1 value using square bracket notation'
    puts "value of attribute1 via accessor: #{ attribute1 }"
    puts "value of attribute1 via square bracket notation: #{ metadata['attribute1'] }"
  end

end

Sample results from console

job = TestJob.new

3.0.3 :048 > job.test {"attribute1"=>100, "attribute2"=>"just testing"} value of attribute1 via accessor: 100 value of attribute1 via square bracket notation: 100 updated attribute1 value using square bracket notation value of attribute1 via accessor: 100 value of attribute1 via square bracket notation: 105

haffla commented 11 months ago

You are right, there is something fishy. I am on it.

haffla commented 11 months ago

OK so I have looked into this. Unfortunately your use case is not supported.

There are some ways to update a value. Either you replace the whole metadata (in your case) hash using instance.metadata = { ... } or you use the generated setters, e.g. instance.attribute1 = 100 or even instance.update(attribute1: 100).

The explanation is that this gem relies on https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html. For each key in your config (attribute1 and attribute2) it creates a Rails attribute. After an object initializes it goes through the raw JSON hash and uses write_attribute to populate the attributes. The gem also overwrites the method metadata=. Here again it can go through that hash and use write_attribute to make sure the Rails attributes are in sync.

But when you do instance.metadata['attribute1'] = 105 you are updating the hash itself and the gem has no way of knowing about this update. So in this case instance.attribute1 (the Rails attribute) gets out of sync.

drkelly58 commented 8 months ago

@haffla Thanks for the explanation, that makes sense