在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
开源软件名称:jrochkind/attr_json开源软件地址:https://github.com/jrochkind/attr_json开源编程语言:Ruby 98.3%开源软件介绍:AttrJsonActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0 through 6.1. Ruby 2.4+. Typed and cast like Active Record. Supporting nested models, dirty tracking, some querying (with postgres jsonb contains), and working smoothy with form builders. Use your database as a typed object store via ActiveRecord, in the same models right next to ordinary ActiveRecord column-backed attributes and associations. Your json-serialized Why might you want or not want this? Developed for postgres, but most features should work with MySQL json columns too, although has not yet been tested with MySQL. Basic Use# migration
class CreatMyModels < ActiveRecord::Migration[5.0]
def change
create_table :my_models do |t|
t.jsonb :json_attributes
end
# If you plan to do any querying with jsonb_contains below..
add_index :my_models, :json_attributes, using: :gin
end
end
class MyModel < ActiveRecord::Base
include AttrJson::Record
# use any ActiveModel::Type types: string, integer, decimal (BigDecimal),
# float, datetime, boolean.
attr_json :my_string, :string
attr_json :my_integer, :integer
attr_json :my_datetime, :datetime
# You can have an _array_ of those things too.
attr_json :int_array, :integer, array: true
#and/or defaults
attr_json :int_with_default, :integer, default: 100
end These attributes have type-casting behavior very much like ordinary ActiveRecord values. model = MyModel.new
model.my_integer = "12"
model.my_integer # => 12
model.int_array = "12"
model.int_array # => [12]
model.my_datetime = "2016-01-01 17:45"
model.my_datetime # => a Time object representing that, just like AR would cast You can use ordinary ActiveRecord validation methods with All the But one way to see something like what it's really like in the db is to
save the record and then use the standard Rails model.save!
model.json_attributes_before_type_cast
# => string containing: {"my_integer":12,"int_array":[12],"my_datetime":"2016-01-01T17:45:00.000Z"} Specifying db column to useWhile the default is to assume you want to serialize in a column called
class OtherModel < ActiveRecord::Base
include AttrJson::Record
# as a default for the model
attr_json_config(default_container_attribute: :some_other_column_name)
# now this is going to serialize to column 'some_other_column_name'
attr_json :my_int, :integer
# Or on a per-attribute basis
attr_json :my_int, :integer, container_attribute: "yet_another_column_name"
end Store key different than attribute name/methodsYou can also specify that the serialized JSON key
should be different than the attribute name/methods, by using the class MyModel < ActiveRecord::Base
include AttrJson::Record
attr_json :special_string, :string, store_key: "__my_string"
end
model = MyModel.new
model.special_string = "foo"
model.json_attributes # => {"__my_string"=>"foo"}
model.save!
model.json_attributes_before_type_cast # => string containing: {"__my_string":"foo"} You can of course combine You can register your custom ActiveRecord::Type.register(:my_type, MyActiveModelTypeSubclass) QueryingThere is some built-in support for querying using postgres jsonb containment
( model = MyModel.create(my_string: "foo", my_integer: 100)
MyModel.jsonb_contains(my_string: "foo", my_integer: 100).to_sql
# SELECT "products".* FROM "products" WHERE (products.json_attributes @> ('{"my_string":"foo","my_integer":100}')::jsonb)
MyModel.jsonb_contains(my_string: "foo", my_integer: 100).first
# Implemented with scopes, this is an ordinary relation, you can
# combine it with whatever, just like ordinary `where`.
MyModel.not_jsonb_contains(my:string: "foo", my_integer: 100).to_sql
# SELECT "products".* FROM "products" WHERE NOT (products.json_attributes @> ('{"my_string":"foo","my_integer":100}')::jsonb)
# typecasts much like ActiveRecord on query too:
MyModel.jsonb_contains(my_string: "foo", my_integer: "100")
# no problem
# works for arrays too
model = MyModel.create(int_array: [10, 20, 30])
MyModel.jsonb_contains(int_array: 10) # finds it
MyModel.jsonb_contains(int_array: [10]) # still finds it
MyModel.jsonb_contains(int_array: [10, 20]) # it contains both, so still finds it
MyModel.jsonb_contains(int_array: [10, 1000]) # nope, returns nil, has to contain ALL listed in query for array args
Anything you can do with Nested models -- Structured/compound dataThe That is, you can serialize complex object-oriented graphs of models into a single jsonb column, and get them back as they went in.
class LangAndValue
include AttrJson::Model
attr_json :lang, :string, default: "en"
attr_json :value, :string
# Validations work fine, and will post up to parent record
validates :lang, inclusion_in: I18n.config.available_locales.collect(&:to_s)
end
class MyModel < ActiveRecord::Base
include AttrJson::Record
include AttrJson::Record::QueryScopes
attr_json :lang_and_value, LangAndValue.to_type
# YES, you can even have an array of them
attr_json :lang_and_value_array, LangAndValue.to_type, array: true
end
# Set with a model object, in initializer or writer
m = MyModel.new(lang_and_value: LangAndValue.new(lang: "fr", value: "S'il vous plaît"))
m.lang_and_value = LangAndValue.new(lang: "es", value: "hola")
m.lang_and_value
# => #<LangAndValue:0x007fb64f12bb70 @attributes={"lang"=>"es", "value"=>"hola"}>
m.save!
m.attr_jsons_before_type_cast
# => string containing: {"lang_and_value":{"lang":"es","value":"hola"}}
# Or with a hash, no problem.
m = MyModel.new(lang_and_value: { lang: 'fr', value: "S'il vous plaît"})
m.lang_and_value = { lang: 'en', value: "Hey there" }
m.save!
m.attr_jsons_before_type_cast
# => string containing: {"lang_and_value":{"lang":"en","value":"Hey there"}}
found = MyModel.find(m.id)
m.lang_and_value
# => #<LangAndValue:0x007fb64eb78e58 @attributes={"lang"=>"en", "value"=>"Hey there"}>
# Arrays too, yup
m = MyModel.new(lang_and_value_array: [{ lang: 'fr', value: "S'il vous plaît"}, { lang: 'en', value: "Hey there" }])
m.lang_and_value_array
# => [#<LangAndValue:0x007f89b4f08f30 @attributes={"lang"=>"fr", "value"=>"S'il vous plaît"}>, #<LangAndValue:0x007f89b4f086e8 @attributes={"lang"=>"en", "value"=>"Hey there"}>]
m.save!
m.attr_jsons_before_type_cast
# => string containing: {"lang_and_value_array":[{"lang":"fr","value":"S'il vous plaît"},{"lang":"en","value":"Hey there"}]} You can nest AttrJson::Model objects inside each other, as deeply as you like. Model-type defaultsIf you want to set a default for an AttrJson::Model type, you should use a proc argument for the default, to avoid accidentally re-using a shared global default value, similar to issues people have with ruby Hash default. attr_json :lang_and_value, LangAndValue.to_type, default: -> { LangAndValue.new(lang: "en", value: "default") } You can also use a Hash value that will be cast to your model, no need for proc argument in this case. attr_json :lang_and_value, LangAndValue.to_type, default: { lang: "en", value: "default" } Polymorphic model typesThere is some support for "polymorphic" attributes that can hetereogenously contain instances of different AttrJson::Model classes, see comment docs at AttrJson::Type::PolymorphicModel. class SomeLabels
include AttrJson::Model
attr_json :hello, LangAndValue.to_type, array: true
attr_json :goodbye, LangAndValue.to_type, array: true
end
class MyModel < ActiveRecord::Base
include AttrJson::Record
include AttrJson::Record::QueryScopes
attr_json :my_labels, SomeLabels.to_type
end
m = MyModel.new
m.my_labels = {}
m.my_labels
# => #<SomeLabels:0x007fed2a3b1a18>
m.my_labels.hello = [{lang: 'en', value: 'hello'}, {lang: 'es', value: 'hola'}]
m.my_labels
# => #<SomeLabels:0x007fed2a3b1a18 @attributes={"hello"=>[#<LangAndValue:0x007fed2a0eafc8 @attributes={"lang"=>"en", "value"=>"hello"}>, #<LangAndValue:0x007fed2a0bb4d0 @attributes={"lang"=>"es", "value"=>"hola"}>]}>
m.my_labels.hello.find { |l| l.lang == "en" }.value = "Howdy"
m.save!
m.attr_jsons
# => {"my_labels"=>#<SomeLabels:0x007fed2a714e80 @attributes={"hello"=>[#<LangAndValue:0x007fed2a714cf0 @attributes={"lang"=>"en", "value"=>"Howdy"}>, #<LangAndValue:0x007fed2a714ac0 @attributes={"lang"=>"es", "value"=>"hola"}>]}>}
m.attr_jsons_before_type_cast
# => string containing: {"my_labels":{"hello":[{"lang":"en","value":"Howdy"},{"lang":"es","value":"hola"}]}} GUESS WHAT? You can QUERY nested structures with MyModel.jsonb_contains("my_labels.hello.lang" => "en").to_sql
# => SELECT "products".* FROM "products" WHERE (products.json_attributes @> ('{"my_labels":{"hello":[{"lang":"en"}]}}')::jsonb)
MyModel.jsonb_contains("my_labels.hello.lang" => "en").first
# also can give hashes, at any level, or models themselves. They will
# be cast. Trying to make everything super consistent with no surprises.
MyModel.jsonb_contains("my_labels.hello" => LangAndValue.new(lang: 'en')).to_sql
# => SELECT "products".* FROM "products" WHERE (products.json_attributes @> ('{"my_labels":{"hello":[{"lang":"en"}]}}')::jsonb)
MyModel.jsonb_contains("my_labels.hello" => {"lang" => "en"}).to_sql
# => SELECT "products".* FROM "products" WHERE (products.json_attributes @> ('{"my_labels":{"hello":[{"lang":"en"}]}}')::jsonb) Remember, we're using a postgres containment ( Single AttrJson::Model serialized to an entire json columnThe main use case of the gem is set up to let you combine multiple primitives and nested models under different keys combined in a single json or jsonb column. But you may also want to have one AttrJson::Model class that serializes to map one model class, as a hash, to an entire json column on it's own.
class MyModel
include AttrJson::Model
attr_json :some_string, :string
attr_json :some_int, :int
end
class MyTable < ApplicationRecord
serialize :some_json_column, MyModel.to_serialization_coder
end
MyTable.create(some_json_column: MyModel.new(some_string: "string"))
# will cast from hash for you
MyTable.create(some_json_column: { some_int: 12 })
# etc To avoid errors raised at inconvenient times, we recommend you set these settings to make 'bad'
data turn into class MyModel
include AttrJson::Model
attr_json_config(bad_cast: :as_nil, unknown_key: :strip)
# ...
end And/or define a setter method to cast, and raise early on data problems: class MyTable < ApplicationRecord
serialize :some_json_column, MyModel.to_serialization_coder
def some_json_column=(val)
super( )
end
end Serializing a model to an entire json column is a relatively recent feature, please let us know how it's working for you. Storing Arbitrary JSON dataArbitrary JSON data (hashes, arrays, primitives of any depth) can be stored within attributes by using the rails built in class MyModel < ActiveRecord::Base
include AttrJson::Record
attr_json :arbitrary_hash, ActiveModel::Type::Value.new
end Forms and Form BuildersUse with Rails form builders is supported pretty painlessly. Including with simple_form and cocoon (integration-tested in CI). If you have nested AttrJson::Models you'd like to use in your forms much like Rails associated records: Where you would use Rails To get simple_form to properly detect your attribute types, define your attributes with For more info, see doc page on Use with Forms and Form Builders. Dirty trackingFull change-tracking, ActiveRecord::Attributes::Dirty-style, is available in
Rails 5.1+ on class MyModel < ActiveRecord::Base
include AttrJson::Record
include AttrJson::Record::Dirty
attr_json :str, :string
end
model = MyModel.new
model.str = "old"
model.save
model.str = "new"
# All and only "new" style dirty tracking methods (Raisl 5.1+)
# are available:
model.attr_json_changes.saved_changes
model.attr_json_changes.changes_to_save
model.attr_json_changes.saved_change_to_str?
model.attr_json_changes.saved_change_to_str
model.attr_json_changes.will_save_change_to_str?
# etc More options are available, including merging changes from 'ordinary' ActiveRecord attributes in. See docs on Dirty Tracking Do you want this?Why might you want this?
Why might you not want this?
|
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论