attr_json 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +17 -0
- data/.yardopts +1 -0
- data/Gemfile +42 -0
- data/LICENSE.txt +21 -0
- data/README.md +426 -0
- data/Rakefile +8 -0
- data/bin/console +23 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/setup +11 -0
- data/config.ru +9 -0
- data/doc_src/dirty_tracking.md +155 -0
- data/doc_src/forms.md +124 -0
- data/json_attribute.gemspec +50 -0
- data/lib/attr_json.rb +18 -0
- data/lib/attr_json/attribute_definition.rb +93 -0
- data/lib/attr_json/attribute_definition/registry.rb +93 -0
- data/lib/attr_json/model.rb +270 -0
- data/lib/attr_json/model/cocoon_compat.rb +27 -0
- data/lib/attr_json/nested_attributes.rb +92 -0
- data/lib/attr_json/nested_attributes/builder.rb +24 -0
- data/lib/attr_json/nested_attributes/multiparameter_attribute_writer.rb +86 -0
- data/lib/attr_json/nested_attributes/writer.rb +215 -0
- data/lib/attr_json/record.rb +140 -0
- data/lib/attr_json/record/dirty.rb +281 -0
- data/lib/attr_json/record/query_builder.rb +84 -0
- data/lib/attr_json/record/query_scopes.rb +35 -0
- data/lib/attr_json/type/array.rb +55 -0
- data/lib/attr_json/type/container_attribute.rb +56 -0
- data/lib/attr_json/type/model.rb +77 -0
- data/lib/attr_json/version.rb +3 -0
- data/playground_models.rb +101 -0
- metadata +177 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 72f8c96e1a2f0a885c25f6d606cf22a00b813245
|
4
|
+
data.tar.gz: 6b038484149947ae8458addd45d54eabbcff2447
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 893ca2a4eabb9457df9abeaaee9b4ac09a068bdd86eede605c76ded7dc3c1f41a39d80fb2ab1de29091808b35b31af749cbf68163ee59df7a006bd133e0c4c9d
|
7
|
+
data.tar.gz: ea4a9e617dabb6b4d5e5779e302f7fb55722588314740509b81ca9d3af64a98523ed41199219608e627b24c35b3673dc59fafaec57568cc6a431d60bfd25c392
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#
|
2
|
+
dist: trusty
|
3
|
+
sudo: false
|
4
|
+
addons:
|
5
|
+
postgresql: '9.4'
|
6
|
+
chrome: stable
|
7
|
+
language: ruby
|
8
|
+
cache: bundler
|
9
|
+
rvm:
|
10
|
+
- 2.4
|
11
|
+
- 2.5.0
|
12
|
+
env:
|
13
|
+
- RAILS_GEM="~> 5.0.0" PG_GEM="~> 0.18"
|
14
|
+
- RAILS_GEM="~> 5.1.0"
|
15
|
+
- RAILS_GEM=">= 5.2.0.rc2,< 5.3.0"
|
16
|
+
before_install:
|
17
|
+
- gem install bundler -v 1.14.6
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
- doc_src/**/*.md --plugin activesupport-concern --markup=markdown
|
data/Gemfile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in attr_json.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
# for our integration test in a real rails env, we add em in development too,
|
7
|
+
# so we can bring up the app or a console in development to play with it.
|
8
|
+
group :test, :development do
|
9
|
+
gem 'combustion', '~> 0.9.0'
|
10
|
+
# all of rails is NOT a dependency, just activerecord.
|
11
|
+
# But we use it for integration testing with combustion. Hmm, a bit annoying
|
12
|
+
# that now our other tests can't be sure they're depending, this might not
|
13
|
+
# be the way to do it.
|
14
|
+
gem "rails", ENV["RAILS_GEM"] && ENV["RAILS_GEM"].split(",")
|
15
|
+
|
16
|
+
# Rails 5.0 won't work with pg 1.0, but that isn't actually in it's gemspec.
|
17
|
+
# So we specify a compatible PG_GEM spec when testing with rails 5.
|
18
|
+
ENV['PG_GEM'] ||= ">= 0.18.1"
|
19
|
+
gem "pg", ENV['PG_GEM']
|
20
|
+
|
21
|
+
gem "rspec-rails", "~> 3.7"
|
22
|
+
gem "simple_form", ">= 4.0"
|
23
|
+
gem 'cocoon', ">= 1.2"
|
24
|
+
gem 'jquery-rails'
|
25
|
+
gem 'capybara', "~> 3.0"
|
26
|
+
gem "chromedriver-helper"
|
27
|
+
gem "selenium-webdriver"
|
28
|
+
# rails 5.1+ includes it by default, but rails 5.0 needs it:
|
29
|
+
gem 'rails-ujs', require: false
|
30
|
+
end
|
31
|
+
|
32
|
+
if ENV['RAILS_GEM']
|
33
|
+
gem "activerecord", ENV['RAILS_GEM'].split(",")
|
34
|
+
|
35
|
+
# This shouldn't really be needed, but seems to maybe be a bundler bug,
|
36
|
+
# this makes standalone_migrations dependencies resolve properly even when our
|
37
|
+
# RAILS_REQ is for 5.2.0.rc2. If in the future you delete this and everything
|
38
|
+
# still passes, feel free to remove.
|
39
|
+
gem "railties", ENV['RAILS_GEM'].split(",")
|
40
|
+
end
|
41
|
+
|
42
|
+
gem "byebug"
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Jonathan Rochkind
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,426 @@
|
|
1
|
+
# AttrJson
|
2
|
+
|
3
|
+
ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0, 5.1, or 5.2.
|
4
|
+
|
5
|
+
Typed and cast like Active Record. Supporting [nested models](#nested), [dirty tracking](#dirty), some [querying](#querying) (with postgres [jsonb](https://www.postgresql.org/docs/9.5/static/datatype-json.html) contains), and [working smoothy with form builders](#forms).
|
6
|
+
|
7
|
+
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 `attr_json` attributes use as much of the existing ActiveRecord architecture as we can.
|
8
|
+
|
9
|
+
[![Build Status](https://travis-ci.org/jrochkind/attr_json.svg?branch=master)](https://travis-ci.org/jrochkind/attr_json)
|
10
|
+
|
11
|
+
AttrJson is pre-1.0. The functionality that is documented here _is_ already implemented (these docs are real, not vaporware) and seems pretty solid. It may still have backwards-incompat changes before 1.0 release. Review and feedback is very welcome.
|
12
|
+
|
13
|
+
Developed for postgres, but most features should work with MySQL json columns too. Has not yet been tested.
|
14
|
+
|
15
|
+
|
16
|
+
## Basic Use
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
# migration
|
20
|
+
class CreatMyModels < ActiveRecord::Migration[5.0]
|
21
|
+
def change
|
22
|
+
create_table :my_models do |t|
|
23
|
+
t.jsonb :json_attributes
|
24
|
+
end
|
25
|
+
|
26
|
+
# If you plan to do any querying with jsonb_contains below..
|
27
|
+
add_index :my_models, :json_attributes, using: :gin
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class MyModel < ActiveRecord::Base
|
32
|
+
include AttrJson::Record
|
33
|
+
|
34
|
+
# use any ActiveModel::Type types: string, integer, decimal (BigDecimal),
|
35
|
+
# float, datetime, boolean.
|
36
|
+
attr_json :my_string, :string
|
37
|
+
attr_json :my_integer, :integer
|
38
|
+
attr_json :my_datetime, :datetime
|
39
|
+
|
40
|
+
# You can have an _array_ of those things too.
|
41
|
+
attr_json :int_array, :integer, array: true
|
42
|
+
|
43
|
+
#and/or defaults
|
44
|
+
attr_json :int_with_default, :integer, default: 100
|
45
|
+
end
|
46
|
+
```
|
47
|
+
|
48
|
+
These attributes have type-casting behavior very much like ordinary ActiveRecord values.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
model = MyModel.new
|
52
|
+
model.my_integer = "12"
|
53
|
+
model.my_integer # => 12
|
54
|
+
model.int_array = "12"
|
55
|
+
model.int_array # => [12]
|
56
|
+
model.my_datetime = "2016-01-01 17:45"
|
57
|
+
model.my_datetime # => a Time object representing that, just like AR would cast
|
58
|
+
```
|
59
|
+
|
60
|
+
You can use ordinary ActiveRecord validation methods with `attr_json` attributes.
|
61
|
+
|
62
|
+
All the `attr_json` attributes are serialized to json as keys in a hash, in a database jsonb/json column. By default, in a column `json_attributes`.
|
63
|
+
If you look at `model.json_attributes`, you'll see values already cast to their ruby representations.
|
64
|
+
|
65
|
+
But one way to see something like what it's really like in the db is to
|
66
|
+
save the record and then use the standard Rails `*_before_type_cast` method.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
model.save!
|
70
|
+
model.attr_jsons_before_type_cast
|
71
|
+
# => string containing: {"my_integer":12,"int_array":[12],"my_datetime":"2016-01-01T17:45:00.000Z"}
|
72
|
+
```
|
73
|
+
|
74
|
+
## Specifying db column to use
|
75
|
+
|
76
|
+
While the default is to assume you want to serialize in a column called
|
77
|
+
`json_attributes`, no worries, of course you can pick whatever named
|
78
|
+
jsonb column you like.
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class OtherModel < ActiveRecord::Base
|
82
|
+
include AttrJson::Record
|
83
|
+
|
84
|
+
# as a default for the model
|
85
|
+
self.default_json_container_attribute = :some_other_column_name
|
86
|
+
|
87
|
+
# now this is going to serialize to column 'some_other_column_name'
|
88
|
+
attr_json :my_int, :integer
|
89
|
+
|
90
|
+
# Or on a per-attribute basis
|
91
|
+
attr_json :my_int, :integer, container_attribute: "yet_another_column_name"
|
92
|
+
end
|
93
|
+
```
|
94
|
+
|
95
|
+
## store key different than attribute name
|
96
|
+
|
97
|
+
You can also specify that the serialized JSON key
|
98
|
+
should be different than the attribute name with the `store_key` argument.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class MyModel < ActiveRecord::Base
|
102
|
+
include AttrJson::Record
|
103
|
+
|
104
|
+
attr_json :special_string, :string, store_key: "__my_string"
|
105
|
+
end
|
106
|
+
|
107
|
+
model = MyModel.new
|
108
|
+
model.special_string = "foo"
|
109
|
+
model.attr_jsons # => {"__my_string"=>"foo"}
|
110
|
+
model.save!
|
111
|
+
model.attr_jsons_before_type_cast # => string containing: {"__my_string":"foo"}
|
112
|
+
```
|
113
|
+
|
114
|
+
You can of course combine `array`, `default`, `store_key`, and `container_attribute`
|
115
|
+
params however you like, with whatever types you like: symbols resolvable
|
116
|
+
with `ActiveModel::Type.lookup`, or any [ActiveModel::Type::Value](https://apidock.com/rails/ActiveRecord/Attributes/ClassMethods/attribute) subclass, built-in or custom.
|
117
|
+
|
118
|
+
<a name="querying"></a>
|
119
|
+
## Querying
|
120
|
+
|
121
|
+
There is some built-in support for querying using [postgres jsonb containment](https://www.postgresql.org/docs/9.5/static/functions-json.html)
|
122
|
+
(`@>`) operator. (or see [here](https://blog.hasura.io/the-unofficial-guide-to-jsonb-operators-in-postgres-part-1-7ad830485ddf) or [here](https://hackernoon.com/how-to-query-jsonb-beginner-sheet-cheat-4da3aa5082a3)). For now you need to additionally `include AttrJson::Record::QueryScopes`
|
123
|
+
to get this behavior.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
model = MyModel.create(my_string: "foo", my_integer: 100)
|
127
|
+
|
128
|
+
MyModel.jsonb_contains(my_string: "foo", my_integer: 100).to_sql
|
129
|
+
# SELECT "products".* FROM "products" WHERE (products.json_attributes @> ('{"my_string":"foo","my_integer":100}')::jsonb)
|
130
|
+
MyModel.jsonb_contains(my_string: "foo", my_integer: 100).first
|
131
|
+
# Implemented with scopes, this is an ordinary relation, you can
|
132
|
+
# combine it with whatever, just like ordinary `where`.
|
133
|
+
|
134
|
+
# typecasts much like ActiveRecord on query too:
|
135
|
+
MyModel.jsonb_contains(my_string: "foo", my_integer: "100")
|
136
|
+
# no problem
|
137
|
+
|
138
|
+
# works for arrays too
|
139
|
+
model = MyModel.create(int_array: [10, 20, 30])
|
140
|
+
MyModel.jsonb_contains(int_array: 10) # finds it
|
141
|
+
MyModel.jsonb_contains(int_array: [10]) # still finds it
|
142
|
+
MyModel.jsonb_contains(int_array: [10, 20]) # it contains both, so still finds it
|
143
|
+
MyModel.jsonb_contains(int_array: [10, 1000]) # nope, returns nil, has to contain ALL listed in query for array args
|
144
|
+
```
|
145
|
+
|
146
|
+
`jsonb_contains` will handlesany `store_key` you have set -- you should specify
|
147
|
+
attribute name, it'll actually query on store_key. And properly handles any
|
148
|
+
`container_attribute` -- it'll look in the proper jsonb column.
|
149
|
+
|
150
|
+
Anything you can do with `jsonb_contains` should be handled
|
151
|
+
by a [postgres `USING GIN` index](https://www.postgresql.org/docs/9.5/static/datatype-json.html#JSON-INDEXING)
|
152
|
+
(I think! can anyone help confirm/deny?). To be sure, I recommend you
|
153
|
+
investigate: Check out `to_sql` on any query to see what jsonb SQL it generates,
|
154
|
+
and explore if you have the indexes you need.
|
155
|
+
|
156
|
+
<a name="nested"></a>
|
157
|
+
## Nested models -- Structured/compound data
|
158
|
+
|
159
|
+
The `AttrJson::Model` mix-in lets you make ActiveModel::Model objects that can be round-trip serialized to a json hash, and they can be used as types for your top-level AttrJson::Record.
|
160
|
+
`AttrJson::Model`s can contain other AJ::Models, singly or as arrays, nested as many levels as you like.
|
161
|
+
|
162
|
+
That is, you can serialize complex object-oriented graphs of models into a single
|
163
|
+
jsonb column, and get them back as they went in.
|
164
|
+
|
165
|
+
`AttrJson::Model` has an identical `attr_json` api to
|
166
|
+
`AttrJson::Record`, with the exception that `container_attribute` is not supported.
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
class LangAndValue
|
170
|
+
include AttrJson::Model
|
171
|
+
|
172
|
+
attr_json :lang, :string, default: "en"
|
173
|
+
attr_json :value, :string
|
174
|
+
|
175
|
+
# Validations work fine, and will post up to parent record
|
176
|
+
validates :lang, inclusion_in: I18n.config.available_locales.collect(&:to_s)
|
177
|
+
end
|
178
|
+
|
179
|
+
class MyModel < ActiveRecord::Base
|
180
|
+
include AttrJson::Record
|
181
|
+
include AttrJson::Record::QueryScopes
|
182
|
+
|
183
|
+
attr_json :lang_and_value, LangAndValue.to_type
|
184
|
+
|
185
|
+
# YES, you can even have an array of them
|
186
|
+
attr_json :lang_and_value_array, LangAndValue.to_type, array: true
|
187
|
+
end
|
188
|
+
|
189
|
+
# Set with a model object, in initializer or writer
|
190
|
+
m = MyModel.new(lang_and_value: LangAndValue.new(lang: "fr", value: "S'il vous plaît"))
|
191
|
+
m.lang_and_value = LangAndValue.new(lang: "es", value: "hola")
|
192
|
+
m.lang_and_value
|
193
|
+
# => #<LangAndValue:0x007fb64f12bb70 @attributes={"lang"=>"es", "value"=>"hola"}>
|
194
|
+
m.save!
|
195
|
+
m.attr_jsons_before_type_cast
|
196
|
+
# => string containing: {"lang_and_value":{"lang":"es","value":"hola"}}
|
197
|
+
|
198
|
+
# Or with a hash, no problem.
|
199
|
+
|
200
|
+
m = MyModel.new(lang_and_value: { lang: 'fr', value: "S'il vous plaît"})
|
201
|
+
m.lang_and_value = { lang: 'en', value: "Hey there" }
|
202
|
+
m.save!
|
203
|
+
m.attr_jsons_before_type_cast
|
204
|
+
# => string containing: {"lang_and_value":{"lang":"en","value":"Hey there"}}
|
205
|
+
found = MyModel.find(m.id)
|
206
|
+
m.lang_and_value
|
207
|
+
# => #<LangAndValue:0x007fb64eb78e58 @attributes={"lang"=>"en", "value"=>"Hey there"}>
|
208
|
+
|
209
|
+
# Arrays too, yup
|
210
|
+
|
211
|
+
m = MyModel.new(lang_and_value_array: [{ lang: 'fr', value: "S'il vous plaît"}, { lang: 'en', value: "Hey there" }])
|
212
|
+
m.lang_and_value_array
|
213
|
+
# => [#<LangAndValue:0x007f89b4f08f30 @attributes={"lang"=>"fr", "value"=>"S'il vous plaît"}>, #<LangAndValue:0x007f89b4f086e8 @attributes={"lang"=>"en", "value"=>"Hey there"}>]
|
214
|
+
m.save!
|
215
|
+
m.attr_jsons_before_type_cast
|
216
|
+
# => string containing: {"lang_and_value_array":[{"lang":"fr","value":"S'il vous plaît"},{"lang":"en","value":"Hey there"}]}
|
217
|
+
```
|
218
|
+
|
219
|
+
You can nest AttrJson::Model objects inside each other, as deeply as you like.
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
class SomeLabels
|
223
|
+
include AttrJson::Model
|
224
|
+
|
225
|
+
attr_json :hello, LangAndValue.to_type, array: true
|
226
|
+
attr_json :goodbye, LangAndValue.to_type, array: true
|
227
|
+
end
|
228
|
+
class MyModel < ActiveRecord::Base
|
229
|
+
include AttrJson::Record
|
230
|
+
include AttrJson::Record::QueryScopes
|
231
|
+
|
232
|
+
attr_json :my_labels, SomeLabels.to_type
|
233
|
+
end
|
234
|
+
|
235
|
+
m = MyModel.new
|
236
|
+
m.my_labels = {}
|
237
|
+
m.my_labels
|
238
|
+
# => #<SomeLabels:0x007fed2a3b1a18>
|
239
|
+
m.my_labels.hello = [{lang: 'en', value: 'hello'}, {lang: 'es', value: 'hola'}]
|
240
|
+
m.my_labels
|
241
|
+
# => #<SomeLabels:0x007fed2a3b1a18 @attributes={"hello"=>[#<LangAndValue:0x007fed2a0eafc8 @attributes={"lang"=>"en", "value"=>"hello"}>, #<LangAndValue:0x007fed2a0bb4d0 @attributes={"lang"=>"es", "value"=>"hola"}>]}>
|
242
|
+
m.my_labels.hello.find { |l| l.lang == "en" }.value = "Howdy"
|
243
|
+
m.save!
|
244
|
+
m.attr_jsons
|
245
|
+
# => {"my_labels"=>#<SomeLabels:0x007fed2a714e80 @attributes={"hello"=>[#<LangAndValue:0x007fed2a714cf0 @attributes={"lang"=>"en", "value"=>"Howdy"}>, #<LangAndValue:0x007fed2a714ac0 @attributes={"lang"=>"es", "value"=>"hola"}>]}>}
|
246
|
+
m.attr_jsons_before_type_cast
|
247
|
+
# => string containing: {"my_labels":{"hello":[{"lang":"en","value":"Howdy"},{"lang":"es","value":"hola"}]}}
|
248
|
+
```
|
249
|
+
|
250
|
+
**GUESS WHAT?** You can **QUERY** nested structures with `jsonb_contains`,
|
251
|
+
using a dot-keypath notation, even through arrays as in this case. Your specific
|
252
|
+
defined `attr_json` types determine the query and type-casting.
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
MyModel.jsonb_contains("my_labels.hello.lang" => "en").to_sql
|
256
|
+
# => SELECT "products".* FROM "products" WHERE (products.json_attributes @> ('{"my_labels":{"hello":[{"lang":"en"}]}}')::jsonb)
|
257
|
+
MyModel.jsonb_contains("my_labels.hello.lang" => "en").first
|
258
|
+
|
259
|
+
|
260
|
+
# also can give hashes, at any level, or models themselves. They will
|
261
|
+
# be cast. Trying to make everything super consistent with no surprises.
|
262
|
+
|
263
|
+
MyModel.jsonb_contains("my_labels.hello" => LangAndValue.new(lang: 'en')).to_sql
|
264
|
+
# => SELECT "products".* FROM "products" WHERE (products.json_attributes @> ('{"my_labels":{"hello":[{"lang":"en"}]}}')::jsonb)
|
265
|
+
|
266
|
+
MyModel.jsonb_contains("my_labels.hello" => {"lang" => "en"}).to_sql
|
267
|
+
# => SELECT "products".* FROM "products" WHERE (products.json_attributes @> ('{"my_labels":{"hello":[{"lang":"en"}]}}')::jsonb)
|
268
|
+
|
269
|
+
```
|
270
|
+
|
271
|
+
Remember, we're using a postgres containment (`@>`) operator, so queries
|
272
|
+
always mean 'contains' -- the previous query needs a `my_labels.hello`
|
273
|
+
which is a hash that includes the key/value, `lang: en`, it can have
|
274
|
+
other key/values in it too. String values will need to match exactly.
|
275
|
+
|
276
|
+
|
277
|
+
<a name="forms"></a>
|
278
|
+
## Forms and Form Builders
|
279
|
+
|
280
|
+
Use with Rails form builders is supported pretty painlessly. Including with [simple_form](https://github.com/plataformatec/simple_form) and [cocoon](https://github.com/nathanvda/cocoon) (integration-tested in CI).
|
281
|
+
|
282
|
+
If you have nested AttrJson::Models you'd like to use in your forms much like Rails associated records: Where you would use Rails `accept_nested_attributes_for`, instead `include AttrJson::NestedAttributes` and use `attr_json_accepts_nested_attributes_for`. Multiple levels of nesting are supported.
|
283
|
+
|
284
|
+
To get simple_form to properly detect your attribute types, define your attributes with `rails_attribute: true`.
|
285
|
+
|
286
|
+
For more info, see doc page on [Use with Forms and Form Builders](doc_src/forms.md).
|
287
|
+
|
288
|
+
<a name="dirty"></a>
|
289
|
+
## Dirty tracking
|
290
|
+
|
291
|
+
Full change-tracking, ActiveRecord::Attributes::Dirty-style, is available in
|
292
|
+
Rails 5.1+ on `attr_json`s on your ActiveRecord classes that include
|
293
|
+
`AttrJson::Record`, by including `AttrJson::Record::Dirty`.
|
294
|
+
Change-tracking methods are available off the `attr_json_changes` method.
|
295
|
+
|
296
|
+
class MyModel < ActiveRecord::Base
|
297
|
+
include AttrJson::Record
|
298
|
+
include AttrJson::Record::Dirty
|
299
|
+
|
300
|
+
attr_json :str, :string
|
301
|
+
end
|
302
|
+
|
303
|
+
model = MyModel.new
|
304
|
+
model.str = "old"
|
305
|
+
model.save
|
306
|
+
model.str = "new"
|
307
|
+
|
308
|
+
# All and only "new" style dirty tracking methods (Raisl 5.1+)
|
309
|
+
# are available:
|
310
|
+
|
311
|
+
model.attr_json_changes.saved_changes
|
312
|
+
model.attr_json_changes.changes_to_save
|
313
|
+
model.attr_json_changes.saved_change_to_str?
|
314
|
+
model.attr_json_changes.saved_change_to_str
|
315
|
+
model.attr_json_changes.will_save_change_to_str?
|
316
|
+
# etc
|
317
|
+
|
318
|
+
More options are available, including merging changes from 'ordinary'
|
319
|
+
ActiveRecord attributes in. See docs on [Dirty Tracking](./doc_src/dirty_tracking.md)
|
320
|
+
|
321
|
+
## Do you want this?
|
322
|
+
|
323
|
+
Why might you want this?
|
324
|
+
|
325
|
+
* You have complicated data, which you want to access in object-oriented
|
326
|
+
fashion, but want to avoid very complicated normalized rdbms schema --
|
327
|
+
and are willing to trade the powerful complex querying support normalized rdbms
|
328
|
+
schema gives you.
|
329
|
+
|
330
|
+
* Single-Table Inheritance, with sub-classes that have non-shared
|
331
|
+
data fields. You rather not make all those columns, some of which will then also appear
|
332
|
+
to inapplicable sub-classes.
|
333
|
+
|
334
|
+
* A "content management system" type project, where you need complex
|
335
|
+
structured data of various types, maybe needs to be vary depending
|
336
|
+
on plugins or configuration, or for different article types -- but
|
337
|
+
doesn't need to be very queryable generally.
|
338
|
+
|
339
|
+
* You want to version your models, which is tricky with associations between models.
|
340
|
+
Minimize associations by inlining the complex data into one table row.
|
341
|
+
|
342
|
+
* Generally, we're turning postgres into a _simple_ object-oriented
|
343
|
+
document store. That can be mixed with an rdbms. The very same
|
344
|
+
row in a table in your db can have document-oriented json data _and_ foreign keys
|
345
|
+
and real rdbms associations to other rows. And it all just
|
346
|
+
feels like ActiveRecord, mostly.
|
347
|
+
|
348
|
+
Why might you _not_ want this?
|
349
|
+
|
350
|
+
* An rdbms and SQL is a wonderful thing, if you need sophisticated
|
351
|
+
querying and reporting with reasonable performance, complex data
|
352
|
+
in a single jsonb probably isn't gonna be the best.
|
353
|
+
|
354
|
+
* This is pretty well-designed code that _mostly_ only uses
|
355
|
+
fairly stable and public Rails API, but there is still some
|
356
|
+
risk of tying your boat to it, it's not Rails itself, and there is
|
357
|
+
some risk it won't keep up with Rails in the future.
|
358
|
+
|
359
|
+
|
360
|
+
## Note on Optimistic Locking
|
361
|
+
|
362
|
+
When you save a record with any changes to any attr_jsons, it will
|
363
|
+
overwrite the _whole json structure_ in the relevant column for that row.
|
364
|
+
Unlike ordinary AR attributes where updates just touch changed attributes.
|
365
|
+
|
366
|
+
Becuase of this, you probably want to seriously consider using ActiveRecord
|
367
|
+
[Optimistic Locking](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html)
|
368
|
+
to prevent overwriting other updates from processes.
|
369
|
+
|
370
|
+
## State of Code, and To Be Done
|
371
|
+
|
372
|
+
This is a pre-1.0 work in progress. But the functionality that is here seems pretty solid.
|
373
|
+
|
374
|
+
Backwards incompatible changes are possible before 1.0. Once I tag something 1.0, I'm pretty serious about minimizing backwards incompats.
|
375
|
+
|
376
|
+
I do not yet use this myself in production, and may not for a while. I generally am reluctant to release something as 1.0 with implied suitable for production when I'm not yet using it in production myself, but may with enough feedback. A couple others are already using in production.
|
377
|
+
|
378
|
+
Feedback of any kind of _very welcome_, please feel free to use the issue tracker.
|
379
|
+
|
380
|
+
Except for the jsonb_contains stuff using postgres jsonb contains operator, I don't believe any postgres-specific features are used. It ought to work with MySQL, testing and feedback welcome. (Or a PR to test on MySQL?). My own interest is postgres.
|
381
|
+
|
382
|
+
### Possible future features:
|
383
|
+
|
384
|
+
* Polymorphic JSON attributes.
|
385
|
+
|
386
|
+
* partial updates for json hashes would be really nice: Using postgres jsonb merge operators to only overwrite what changed. In my initial attempts, AR doesn't make it easy to customize this.
|
387
|
+
|
388
|
+
* seamless compatibility with ransack
|
389
|
+
|
390
|
+
* Should we give AttrJson::Model a before_serialize hook that you might
|
391
|
+
want to use similar to AR before_save? Should AttrJson::Models
|
392
|
+
raise on trying to serialize an invalid model?
|
393
|
+
|
394
|
+
* There are limits to what you can do with just jsonb_contains
|
395
|
+
queries. We could support operations like `>`, `<`, `<>`
|
396
|
+
as [jsonb_accessor](https://github.com/devmynd/jsonb_accessor),
|
397
|
+
even accross keypaths. (At present, you could use a
|
398
|
+
before_savee to denormalize/renormalize copy your data into
|
399
|
+
ordinary AR columns/associations for searching. Or perhaps a postgres ts_vector for text searching. Needs to be worked out.)
|
400
|
+
|
401
|
+
* We could/should probably support `jsonb_order` clauses, even
|
402
|
+
accross key paths, like jsonb_accessor.
|
403
|
+
|
404
|
+
* Could we make these attributes work in ordinary AR where, same
|
405
|
+
as they do in jsonb_contains? Maybe.
|
406
|
+
|
407
|
+
## Acknowledements and Prior Art
|
408
|
+
|
409
|
+
* The excellent work [Sean Griffin](https://twitter.com/sgrif) did on ActiveModel::Type
|
410
|
+
really lays the groundwork and makes this possible. Plus many other Rails developers.
|
411
|
+
Rails has a reputation for being composed of messy or poorly designed code, but
|
412
|
+
it's some really nice design in Rails that allows us to do some pretty powerful
|
413
|
+
stuff here, in surprisingly few lines of code.
|
414
|
+
|
415
|
+
* The existing [jsonb_accessor](https://github.com/devmynd/jsonb_accessor) was
|
416
|
+
an inspiration, and provided some good examples of how to do some things
|
417
|
+
with AR and ActiveModel::Types. I [started out trying to figure out](https://github.com/devmynd/jsonb_accessor/issues/69#issuecomment-294081059)
|
418
|
+
how to fit in nested hashes to jsonb_accessor... but ended up pretty much rewriting it entirely,
|
419
|
+
to lean on object-oriented polymorphism and ActiveModel::Type a lot heavier and have
|
420
|
+
the API and internals I wanted/imagined.
|
421
|
+
|
422
|
+
* Took a look at existing [active_model_attributes](https://github.com/Azdaroth/active_model_attributes) too.
|
423
|
+
|
424
|
+
* Didn't actually notice existing [json_attributes](https://github.com/joel/json_attributes)
|
425
|
+
until I was well on my way here. I think it's not updated for Rails5 or type-aware,
|
426
|
+
haven't looked at it too much.
|