jsonb_accessor 1.0.0.beta → 1.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +144 -43
- data/UPGRADE_GUIDE.md +67 -0
- data/gemfiles/activerecord_5.0.0.gemfile.lock +1 -1
- data/lib/jsonb_accessor/query_builder.rb +4 -4
- data/lib/jsonb_accessor/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5a63babf736e4f8208d2aef0238f2f2ec82fc7b
|
4
|
+
data.tar.gz: 72f1ef3b5c23839cd7fac5c016b3f5ab228b9ba6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82287481f107b50bb00aa74a2e750010db9a0961110107604538ed976500bfec54fab7b5ee0f9ef4f013b3bb742e5c44d8dcfb2c5371a3286807c580705686ad
|
7
|
+
data.tar.gz: 71eab6fd8aa74450a93858d1d1970af547a13a5e9da49e43cdff6f55d0c78b5c164ef00f7ce36532ec2ce1caeeb3cff05dc0a1624f1c1c002623ee697e2782b2
|
data/README.md
CHANGED
@@ -4,13 +4,20 @@
|
|
4
4
|
|
5
5
|
Adds typed `jsonb` backed fields as first class citizens to your `ActiveRecord` models. This gem is similar in spirit to [HstoreAccessor](https://github.com/devmynd/hstore_accessor), but the `jsonb` column in PostgreSQL has a few distinct advantages, mostly around nested documents and support for collections.
|
6
6
|
|
7
|
+
It also adds generic scopes for querying `jsonb` columns.
|
8
|
+
|
9
|
+
## 1.0 Beta
|
10
|
+
|
11
|
+
This README reflects the 1.0 beta. Method names and interfaces may still change.
|
12
|
+
|
7
13
|
## Table of Contents
|
8
14
|
|
9
15
|
* [Installation](#installation)
|
10
16
|
* [Usage](#usage)
|
11
|
-
* [
|
17
|
+
* [Scopes](#scopes)
|
12
18
|
* [Single-Table Inheritance](#single-table-inheritance)
|
13
19
|
* [Dependencies](#dependencies)
|
20
|
+
* [Validations](#validations)
|
14
21
|
* [Development](#development)
|
15
22
|
* [Contributing](#contributing)
|
16
23
|
|
@@ -31,10 +38,10 @@ And then execute:
|
|
31
38
|
First we must create a model which has a `jsonb` column available to store data into it:
|
32
39
|
|
33
40
|
```ruby
|
34
|
-
class
|
41
|
+
class CreateProducts < ActiveRecord::Migration
|
35
42
|
def change
|
36
43
|
create_table :products do |t|
|
37
|
-
t.jsonb :
|
44
|
+
t.jsonb :data
|
38
45
|
end
|
39
46
|
end
|
40
47
|
end
|
@@ -44,19 +51,141 @@ We can then declare the `jsonb` fields we wish to expose via the accessor:
|
|
44
51
|
|
45
52
|
```ruby
|
46
53
|
class Product < ActiveRecord::Base
|
47
|
-
jsonb_accessor
|
48
|
-
:options,
|
54
|
+
jsonb_accessor :data,
|
49
55
|
title: :string,
|
50
|
-
id_value: :value,
|
51
56
|
external_id: :integer,
|
52
57
|
reviewed_at: :datetime
|
53
|
-
)
|
54
58
|
end
|
55
59
|
```
|
56
60
|
|
57
|
-
|
61
|
+
Any type the [`attribute` API](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute) supports. You can also implement your own type by following the example in the `attribute` documentation.
|
62
|
+
|
63
|
+
To pass through options like `default` and `array` to the `attribute` API, just put them in an array.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class Product < ActiveRecord::Base
|
67
|
+
jsonb_accessor :data,
|
68
|
+
title: [:string, default: "Untitled"],
|
69
|
+
previous_titles: [:string, array: true, default: []]
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
You can also pass in a `store_key` option.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
class Product < ActiveRecord::Base
|
77
|
+
jsonb_accessor :data, title: [:string, store_key: :t]
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
81
|
+
This allows you to use `title` for your getters and setters, but use `t` as the key in the `jsonb` column.
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
product = Product.new(title: "Foo")
|
85
|
+
product.title #=> "Foo"
|
86
|
+
product.data #=> { "t" => "Foo" }
|
87
|
+
```
|
88
|
+
|
89
|
+
## Scopes
|
90
|
+
|
91
|
+
Jsonb Accessor provides several scopes to make it easier to query `jsonb` columns. `jsonb_contains`, `jsonb_number_where`, `jsonb_time_where`, and `jsonb_where` are available on all `ActiveRecord::Base` subclasses and don't require that you make use of the `jsonb_accessor` declaration.
|
92
|
+
|
93
|
+
If a class does have a `jsonb_accessor` declaration, then we define one custom scope. So, let's say we have a class that looks like this:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
class Product < ActiveRecord::Base
|
97
|
+
jsonb_accessor :data,
|
98
|
+
name: :string,
|
99
|
+
price: [:integer, store_key: :p],
|
100
|
+
price_in_cents: :integer,
|
101
|
+
reviewed_at: :datetime
|
102
|
+
end
|
103
|
+
```
|
104
|
+
|
105
|
+
Jsonb Accessor will add a `scope` to `Product` called `data_where`.
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
Product.all.data_where(name: "Granite Towel", price: 17)
|
109
|
+
```
|
110
|
+
|
111
|
+
For number fields you can query using `<` or `>`or use plain english if that's what you prefer.
|
58
112
|
|
59
|
-
|
113
|
+
```ruby
|
114
|
+
Product.all.data_where(price: { <: 15 })
|
115
|
+
Product.all.data_where(price: { <=: 15 })
|
116
|
+
Product.all.data_where(price: { less_than: 15 })
|
117
|
+
Product.all.data_where(price: { less_than_or_equal_to: 15 })
|
118
|
+
|
119
|
+
Product.all.data_where(price: { >: 15 })
|
120
|
+
Product.all.data_where(price: { >=: 15 })
|
121
|
+
Product.all.data_where(price: { greater_than: 15 })
|
122
|
+
Product.all.data_where(price: { greater_than_or_equal_to: 15 })
|
123
|
+
|
124
|
+
Product.all.data_where(price: { greater_than: 15, less_than: 30 })
|
125
|
+
```
|
126
|
+
|
127
|
+
For time related fields you can query using `before` and `after`.
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
Product.all.data_where(reviewed_at: { before: Time.current.beginning_of_week, after: 4.weeks.ago })
|
131
|
+
```
|
132
|
+
|
133
|
+
This scope is a convenient wrapper around the `jsonb_where` `scope` that saves you from having to convert the given keys to the store keys and from specifying the column.
|
134
|
+
|
135
|
+
### `jsonb_where`
|
136
|
+
|
137
|
+
Works just like the [`scope` above](#scopes) except that it does not convert the given keys to store keys and you must specify the column name. For example:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
Product.all.jsonb_where(:data, reviewed_at: { before: Time.current }, p: { greater_than: 5 })
|
141
|
+
|
142
|
+
# instead of
|
143
|
+
|
144
|
+
Product.all.data_where(reviewed_at: { before: Time.current }, price: { greater_than: 5 })
|
145
|
+
```
|
146
|
+
This scope makes use of the `jsonb_contains`, `jsonb_number_where`, and `jsonb_time_where` `scope`s.
|
147
|
+
|
148
|
+
### `jsonb_contains`
|
149
|
+
|
150
|
+
Returns all records that contain the given JSON paths.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
Product.all.jsonb_contains(:data, title: "foo")
|
154
|
+
Product.all.jsonb_contains(:data, reviewed_at: 10.minutes.ago, p: 12) # Using the store key
|
155
|
+
```
|
156
|
+
|
157
|
+
**Note:** Under the hood, `jsonb_contains` uses the [`@>` operator in Postgres](https://www.postgresql.org/docs/9.5/static/functions-json.html) so when you include an array query, the stored array and the array used for the query do not need to match exactly. For example, when queried with `[1, 2]`, records that have arrays of `[2, 1, 3]` will be returned.
|
158
|
+
|
159
|
+
### `jsonb_number_where`
|
160
|
+
|
161
|
+
Returns all records that match the given criteria.
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
Product.all.jsonb_number_where(:data, :price_in_cents, :greater_than, 300)
|
165
|
+
```
|
166
|
+
|
167
|
+
It supports:
|
168
|
+
|
169
|
+
* `>`
|
170
|
+
* `>=`
|
171
|
+
* `greater_than`
|
172
|
+
* `greater_than_or_equal_to`
|
173
|
+
* `<`
|
174
|
+
* `<=`
|
175
|
+
* `less_than`
|
176
|
+
* `less_than_or_equal_to`
|
177
|
+
|
178
|
+
and it is indifferent to strings/symbols.
|
179
|
+
|
180
|
+
### `jsonb_time_where`
|
181
|
+
|
182
|
+
Returns all records that match the given criteria.
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
Product.all.jsonb_time_where(:data, :reviewed_at, :before, 2.days.ago)
|
186
|
+
```
|
187
|
+
|
188
|
+
It supports `before` and `after` and is indifferent to strings/symbols.
|
60
189
|
|
61
190
|
## Single-Table Inheritance
|
62
191
|
|
@@ -70,8 +199,8 @@ rows can have different values.
|
|
70
199
|
We set up our table with an `jsonb` field:
|
71
200
|
|
72
201
|
```ruby
|
73
|
-
# db/migration/<timestamp>
|
74
|
-
class
|
202
|
+
# db/migration/<timestamp>_create_players.rb
|
203
|
+
class CreateVehicles < ActiveRecord::Migration
|
75
204
|
def change
|
76
205
|
create_table :vehicles do |t|
|
77
206
|
t.string :make
|
@@ -110,44 +239,16 @@ From here any attributes specific to any sub-class can be stored in the
|
|
110
239
|
`jsonb` column avoiding sparse data. Indices can also be created on
|
111
240
|
individual fields in an `jsonb` column.
|
112
241
|
|
113
|
-
This approach was originally
|
242
|
+
This approach was originally conceived by Joe Hirn in [this blog
|
114
243
|
post](http://www.devmynd.com/blog/2013-3-single-table-inheritance-hstore-lovely-combination).
|
115
244
|
|
116
|
-
##
|
117
|
-
|
118
|
-
JsonbAccessor currently supports several scopes. Let's say we have a class that looks like this:
|
119
|
-
|
120
|
-
```ruby
|
121
|
-
class Product < ActiveRecord::Base
|
122
|
-
jsonb_accessor :data,
|
123
|
-
approved: :boolean,
|
124
|
-
name: :string,
|
125
|
-
price: :integer,
|
126
|
-
previous_prices: :integer_array,
|
127
|
-
reviewed_at: :date_time
|
128
|
-
end
|
129
|
-
```
|
130
|
-
|
131
|
-
### General Scopes
|
132
|
-
|
133
|
-
#### `<jsonb_field>_contains`
|
134
|
-
|
135
|
-
**Description:** returns all records that contain matching attributes in the specified `jsonb` field.
|
136
|
-
|
137
|
-
```ruby
|
138
|
-
product_1 = Product.create!(name: "foo", approved: true, reviewed_at: 3.days.ago)
|
139
|
-
product_2 = Product.create!(name: "bar", approved: true)
|
140
|
-
product_3 = Product.create!(name: "foo", approved: false)
|
141
|
-
|
142
|
-
Product.data_contains(name: "foo", approved: true) # => [product_1]
|
143
|
-
```
|
245
|
+
## Validations
|
144
246
|
|
145
|
-
|
247
|
+
Because this gem promotes attributes nested into the JSON column to first level attributes, most validations should just work. Please leave us feedback if they're not working as expected.
|
146
248
|
|
147
249
|
## Dependencies
|
148
250
|
|
149
|
-
- ActiveRecord 5.0
|
150
|
-
- Ruby >= 2.2.2
|
251
|
+
- ActiveRecord >= 5.0
|
151
252
|
- Postgres >= 9.4 (in order to use the [jsonb column type](http://www.postgresql.org/docs/9.4/static/datatype-json.html)).
|
152
253
|
|
153
254
|
## Development
|
data/UPGRADE_GUIDE.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Upgrading from 0.X.X to 1.0.0
|
2
|
+
|
3
|
+
## Jsonb Accessor declaration
|
4
|
+
|
5
|
+
In 0.X.X you would write:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class Product < ActiveRecord::Base
|
9
|
+
jsonb_accessor :data,
|
10
|
+
:count, # doesn't specify a type
|
11
|
+
title: :string,
|
12
|
+
external_id: :integer,
|
13
|
+
reviewed_at: :date_time, # snake cased
|
14
|
+
previous_rankings: :integer_array, # `:type_array` key
|
15
|
+
external_rankings: :array # plain array
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
In 1.0.0 you would write:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class Product < ActiveRecord::Base
|
23
|
+
jsonb_accessor :data,
|
24
|
+
count: :value, # all fields must specify a type
|
25
|
+
title: :string,
|
26
|
+
external_id: :integer,
|
27
|
+
reviewed_at: :datetime, # `:date_time` is now `:datetime`
|
28
|
+
previous_rankings: [:integer, array: true], # now just the type followed by `array: true`
|
29
|
+
external_rankings: [:value, array: true] # now the value type is specified as well as `array: true`
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
There are several important differences. All fields must now specify a type, `:date_time` is now `:datetime`, and arrays are specified using a type and `array: true` instead of `type_array`.
|
34
|
+
|
35
|
+
Also, in order to use the `value` type you need to register it:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
# in an initializer
|
39
|
+
ActiveRecord::Type.register(:value, ActiveRecord::Type::Value)
|
40
|
+
```
|
41
|
+
|
42
|
+
### Deeply nested objects
|
43
|
+
|
44
|
+
In 0.X.X you could write:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
class Product < ActiveRecord::Base
|
48
|
+
jsonb_accessor :data,
|
49
|
+
ranking_info: {
|
50
|
+
original_rank: :integer,
|
51
|
+
current_rank: :integer,
|
52
|
+
metadata: {
|
53
|
+
ranked_on: :date
|
54
|
+
}
|
55
|
+
}
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
Which would allow you to use getter and setter methods at any point in the structure.
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
Product.new(ranking_info: { original_rank: 3, current_rank: 5, metadata: { ranked_on: Date.today } })
|
63
|
+
product.ranking_info.original_rank # 3
|
64
|
+
product.ranking_info.metadata.ranked_on # Date.today
|
65
|
+
```
|
66
|
+
|
67
|
+
1.0.0 does not support this syntax. If you need these sort of methods, you can create your own type `class` and register it with `ActiveRecord::Type`. [Here's an example](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute).
|
@@ -46,12 +46,12 @@ module JsonbAccessor
|
|
46
46
|
scope(:jsonb_contains,
|
47
47
|
-> (column_name, attributes) { where("#{table_name}.#{column_name} @> (?)::jsonb", attributes.to_json) })
|
48
48
|
|
49
|
-
scope(:
|
49
|
+
scope(:jsonb_number_where, lambda do |column_name, field_name, given_operator, value|
|
50
50
|
operator = JsonbAccessor::NUMBER_OPERATORS_MAP.fetch(given_operator.to_s)
|
51
51
|
where("(#{table_name}.#{column_name} ->> ?)::float #{operator} ?", field_name, value)
|
52
52
|
end)
|
53
53
|
|
54
|
-
scope(:
|
54
|
+
scope(:jsonb_time_where, lambda do |column_name, field_name, given_operator, value|
|
55
55
|
operator = JsonbAccessor::TIME_OPERATORS_MAP.fetch(given_operator.to_s)
|
56
56
|
where("(#{table_name}.#{column_name} ->> ?)::timestamp #{operator} ?", field_name, value)
|
57
57
|
end)
|
@@ -63,9 +63,9 @@ module JsonbAccessor
|
|
63
63
|
attributes.each do |name, value|
|
64
64
|
case value
|
65
65
|
when IS_NUMBER_QUERY_ARGUMENTS
|
66
|
-
value.each { |operator, query_value| query = query.
|
66
|
+
value.each { |operator, query_value| query = query.jsonb_number_where(column_name, name, operator, query_value) }
|
67
67
|
when IS_TIME_QUERY_ARGUMENTS
|
68
|
-
value.each { |operator, query_value| query = query.
|
68
|
+
value.each { |operator, query_value| query = query.jsonb_time_where(column_name, name, operator, query_value) }
|
69
69
|
else
|
70
70
|
contains_attributes[name] = value
|
71
71
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonb_accessor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.beta
|
4
|
+
version: 1.0.0.beta.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Crismali
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: exe
|
12
12
|
cert_chain: []
|
13
|
-
date: 2016-10-
|
13
|
+
date: 2016-10-19 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -228,6 +228,7 @@ files:
|
|
228
228
|
- LICENSE.txt
|
229
229
|
- README.md
|
230
230
|
- Rakefile
|
231
|
+
- UPGRADE_GUIDE.md
|
231
232
|
- bin/console
|
232
233
|
- bin/setup
|
233
234
|
- db/config.yml
|