How to use the ActiveRecord json field type

36,705

Solution 1

Yes, ActiveRecord allows to use Postgres' json-fields simply as Hashes in their models. However, there are a couple of things to consider:

  1. Hash may be NULL on initialization
    In your create_table migration you allow the field :game_board to be NULL. Thus, on first usage the field :game_board of your model-instance will be NULL and you have to initialize the Hash first before using it. (See example below)

  2. In JSON all keys are Strings
    Thus, on save (and reload) all keys will be transformed into Strings if you have used Symbols or Numbers before. Thus, to prevent unwanted behavior it is recommended to use String-keys unless your ORM is configured to symbolize all keys.


Your examples:

self.game_board         ||= {}
self.game_board[:player1] = 1
self.game_board[:cards]   = cards.to_hash

# after reload from database (access via String-key):
self.game_board['player1']  # And retrieve value 1 (that we put here before)


@ Performance:

  1. Yes, every time ActiveRecord reads an entry from the database and creates a model-instance, JSON-fields get unserialized into Hashes. But if you think that is a performance-hit to your application than you should either use a text-field and serialize/deserialize the JSON/Hashes when you need to or, even better, don't use ActiveRecord at all. By creating heaps of classes and using magic-methods, ActiveRecord creates so much overhead that you shouldn't worry about the deserialization of JSON. Convenience has its costs.

  2. Yes, every time you change a value in the Hash, the (whole) JSON-field gets replaced and updated with the new serialized version.
    Two notes on this:

    • Even in Postgres itself (not only in ActiveRecord) the possibility of performing updates on certain JSON-elements is missing until now. Compare this Stackoverflow-question
    • In general, JSON-fields should be used with a fixed structure or, at least, in manageable sizes and the field-type is not supposed to be a document-store like eg. in MongoDB. Compare the Postgres documentation

Solution 2

Just to further clarify - when you're saving the JSON object to an attribute of your model instance make sure to save it as a hash.

Active Record will not complain if you forget to parse a JSON string:

  game = Game.create(game_board: '"key":"value"')

When you retrieve a string from a json attribute, it won't complain and just return the String.

  game.game_board
  => '"key":"value"'

Thus game.game_board['key'] would lead to an error because you're trying to treat a String like a Hash.

So make sure you use JSON.parse(string) before saving.

  game = Game.create(game_board: JSON.parse('"key":"value"'))

So now you have the expected behavior

game.game_board['key']
=> 'value'

Probably not useful for this case, but came across this issue when saving a JSON payload from an API I was integrating with. Anyway, hope this helps.

Share:
36,705

Related videos on Youtube

Freedom_Ben
Author by

Freedom_Ben

Updated on January 04, 2020

Comments

  • Freedom_Ben
    Freedom_Ben over 4 years

    I have a Rails model which has a database column of type "json":

    create_table "games", force: true do |t|
      t.json     "game_board"
      t.datetime "created_at", null: false
      t.datetime "updated_at", null: false
    end
    

    Great! Now how do I use it? Is it really just as simple as treating the field like a Hash?

    self.game_board[:player1] = 1
    self.game_board[:cards] = cards.to_hash
    

    If I were to write that, would everything just work as expected, so in a future API call from a client I could do this?:

    self.game_board[:player] # And get back the 1 that I put here before
    

    What about performance as well? Will the entire game_board be de-serialized every time even if that field is never read? Will the field be re-written (IOW a database write) each time I change part of the "Hash?"