imdrasil / jennifer.cr

Crystal ORM using ActiveRecord pattern with flexible query DSL
MIT License
418 stars 55 forks source link

Creating a record with a JSON column is hard #324

Closed airhorns closed 3 years ago

airhorns commented 4 years ago

If I have a model with a jsonb column, it's a little tricky right now to populate that column on record create. With a model like:

class Product < Jennifer::Model::Base
  with_timestamps

  mapping(
    id: Primary64,
    name: String,
    config: JSON::Any,
  )
end

The thing that I tried first doesn't work:

Product.create!(name: "example", config: { "foo" => "bar" })
# or
Product.create!({:name => "example", :config => { "foo" => "bar" }})

because the Hash(String, String) isn't a JSON::Any.

In lib/jennifer/src/jennifer/model/base.cr:132:13

 132 | o = new(values)
           ^--
Error: no overload matches 'Product.new' with type Hash(Symbol, Hash(String, String))

Sadly, you can't really wrap hash literals in JSON::Anys in order to pass them in:

JSON::Any.new({"foo" => "bar"})
JSON::Any.new(Hash(String, JSON::Any){"foo" => "bar"})

both don't work as JSON::Any is really designed to be constructed by the JSON parser as opposed to manually.

The only way I have figured out to actually get data into a jsonb column is by passing in JSON as a string:

Product.create!(name: "example", config: "{\"foo\": \"bar\"}")

which works, and notably saves the value as an actual JSONB value, and not a string inside a JSONB column.

It'd be nice (in this developers opinion) to be able to manipulate JSONB columns with something a bit higher level like a Hash, or even if we couldn't do that, an easy way to take a JSON::Any and turn it into something mutable and support serializing that without copying it back into the tricky-to-produce-manually JSON::Any type. For Rails at least, it just returns JSONB columns as a Hash and then .to_jsons them on the way back to the database.

imdrasil commented 4 years ago

hi, unfortunately ATM the only way to convert hash into json is JSON.parse(hash.to_json) as there is to #as_json. We can extend Jennnifer::Mode::JSONConverter.from_hash to recursively convert hash into json (this sounds achievable).

P.S. Product.create!(name: "example", config: "{\"foo\": \"bar\"}") works because Jennnifer::Mode::JSONConverter.from_hash automatically converts string into json

imdrasil commented 4 years ago

Some time before I had an idea to add support of embedded objects (similar to serialize from rails) to allow map json from the database to the object with defined json mapping. The greatest obstacle is that model attributes must to be a database supported values which means nothing from Jennifer::DBAny is supported ATM

imdrasil commented 3 years ago

This is resolved by #352