post_json 1.0.11 → 1.0.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +87 -71
- data/lib/post_json/base.rb +67 -8
- data/lib/post_json/version.rb +1 -1
- data/spec/models/base_spec.rb +49 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 52a70dfcec7f3478c9b2fab7e465c5b33c97229b
|
4
|
+
data.tar.gz: 7d302f9d087bc00360e203320e0afd5b64e3eb1d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b17682fa4780f53680fc7c7c55bb423974208d79356ba6ec70a9ab2ebc16ce9c83a7cc60b6101b71b507797434d84ef33cf9c818b40bbeed268c4b7045d69782
|
7
|
+
data.tar.gz: 99bbe826b5aab7659aaa74fa2e8413023b398940b4bf50aa98ed540989d339a2488c9e7300fb0af0c35cebcbcb4c37dc7d54d7180f41c5f4509e486ce476d4b2
|
data/README.md
CHANGED
@@ -1,41 +1,38 @@
|
|
1
1
|
# Welcome to PostJson
|
2
2
|
|
3
|
-
PostJson is everything you expect of ActiveRecord and PostgreSQL,
|
4
|
-
(free as a bird - no schemas).
|
3
|
+
PostJson is everything you expect of ActiveRecord and PostgreSQL, with the added power and dynamic nature of a document database (Free as a bird! No schemas!).
|
5
4
|
|
6
|
-
PostJson
|
7
|
-
PostJson, because we love document databases and PostgreSQL. PostJson combine features of Ruby, ActiveRecord and
|
8
|
-
PostgreSQL to provide a great document database.
|
5
|
+
PostJson combines features of Ruby, ActiveRecord and PostgreSQL to provide a great document database by taking advantage of PostgreSQL 9.2+ support for JavaScript (Google's V8 engine). We started the work on PostJson, because we love document databases **and** PostgreSQL.
|
9
6
|
|
10
|
-
See example of how we use PostJson as part of
|
7
|
+
See the example of how we use PostJson as part of [Jumpstarter](https://github.com/webnuts/jumpstarter).
|
11
8
|
|
12
9
|
|
13
|
-
##
|
10
|
+
## Installation
|
14
11
|
|
15
|
-
|
12
|
+
Add the gem to your `Gemfile`:
|
16
13
|
|
17
14
|
gem 'post_json'
|
18
|
-
|
19
|
-
### At the command prompt, install the gem, run the generator, and migrate the db:
|
20
15
|
|
21
|
-
|
22
|
-
|
23
|
-
|
16
|
+
Then:
|
17
|
+
|
18
|
+
$ bundle install
|
19
|
+
|
20
|
+
Run the generator and migrate the db:
|
21
|
+
|
22
|
+
$ rails g post_json:install
|
23
|
+
$ rake db:migrate
|
24
24
|
|
25
25
|
That's it!
|
26
26
|
|
27
27
|
(See POSTGRESQL_INSTALL.md if you need the install instructions for PostgreSQL with PLV8)
|
28
28
|
|
29
|
-
##
|
30
|
-
|
31
|
-
You should feel home right away, if you already know ActiveRecord. PostJson try hard to respect the ActiveRecord
|
32
|
-
API, so methods work and do as you would expect from ActiveRecord.
|
29
|
+
## Usage
|
33
30
|
|
34
|
-
PostJson
|
31
|
+
PostJson also tries hard to respect the ActiveRecord API, so, if you have experience with ActiveRecord, the model methods work as you would expect.
|
35
32
|
|
36
|
-
|
33
|
+
### Model
|
37
34
|
|
38
|
-
|
35
|
+
All PostJson models represent a collection.
|
39
36
|
|
40
37
|
```ruby
|
41
38
|
class Person < PostJson::Collection["people"]
|
@@ -43,12 +40,11 @@ end
|
|
43
40
|
|
44
41
|
me = Person.create(name: "Jacob")
|
45
42
|
```
|
43
|
+
|
44
|
+
__Notice you don't have to define model attributes anywhere!__
|
46
45
|
|
47
|
-
As you can see
|
48
|
-
|
49
|
-
|
50
|
-
`Person` can do the same as any model class inheriting `ActiveRecord::Base`.
|
51
|
-
|
46
|
+
As you can see, this is very similar to a standard ActiveRecord model. `PostJson::Collection["people"]` inherits from `PostJson::Base`, which, in turn, inherits from `ActiveRecord::Base`. This is part of the reason the `Person` model will seem so familiar.
|
47
|
+
|
52
48
|
You can also skip the creation of a class:
|
53
49
|
|
54
50
|
```ruby
|
@@ -56,7 +52,9 @@ people = PostJson::Collection["people"]
|
|
56
52
|
me = people.create(name: "Jacob")
|
57
53
|
```
|
58
54
|
|
59
|
-
###
|
55
|
+
### Validations
|
56
|
+
|
57
|
+
Use standard `ActiveRecord` validations in your models:
|
60
58
|
|
61
59
|
```ruby
|
62
60
|
class Person < PostJson::Collection["people"]
|
@@ -64,20 +62,13 @@ class Person < PostJson::Collection["people"]
|
|
64
62
|
end
|
65
63
|
```
|
66
64
|
|
67
|
-
PostJson::Collection["people"] returns a class, which is based on `PostJson::Base`, which is based on
|
68
|
-
`ActiveRecord::Base`. So its the exact same validation as you may know.
|
69
|
-
|
70
65
|
Read the <a href="http://guides.rubyonrails.org/active_record_validations.html" target="_blank">Rails guide about validation</a> if you need more information.
|
71
66
|
|
72
|
-
###
|
67
|
+
### Querying
|
73
68
|
|
74
69
|
```ruby
|
75
70
|
me = Person.create(name: "Jacob", details: {age: 33})
|
76
|
-
```
|
77
71
|
|
78
|
-
Now we can make a query and get the document:
|
79
|
-
|
80
|
-
```ruby
|
81
72
|
# PostJson supports filtering on nested attributes
|
82
73
|
also_me_1 = Person.where(details: {age: 33}).first
|
83
74
|
also_me_2 = Person.where("details.age" => 33).first
|
@@ -89,9 +80,7 @@ also_me_3 = Person.where("function(doc) { return doc.details.age == 33; }").firs
|
|
89
80
|
also_me_4 = Person.where("json_details.age = ?", 33).first
|
90
81
|
```
|
91
82
|
|
92
|
-
### Accessing attributes
|
93
|
-
|
94
|
-
Like you would expect with ActiveRecord:
|
83
|
+
### Accessing attributes
|
95
84
|
|
96
85
|
```ruby
|
97
86
|
person = Person.create(name: "Jacob")
|
@@ -115,22 +104,55 @@ puts person.name_changed? # => false
|
|
115
104
|
puts person.name_change # => nil
|
116
105
|
```
|
117
106
|
|
118
|
-
###
|
107
|
+
### Transformation with `select`
|
119
108
|
|
120
|
-
|
109
|
+
The `select` method allows you to transform a collection of documents into an array of hashes that contain only the attributes you want. The hash passed to `select` maps keys to selectors of arbitrary depth.
|
110
|
+
|
111
|
+
> In this example we only want the 'name' and 'age' attributes from the `Person` but 'age' is nested under 'details'.
|
121
112
|
|
122
113
|
```ruby
|
114
|
+
# create a person with age nested under details
|
123
115
|
me = Person.create(name: "Jacob", details: {age: 33})
|
124
116
|
|
117
|
+
# the dot (.) signifies that the selector is looking for a nested attribute
|
125
118
|
other_me = Person.limit(1).select({name: "name", age: "details.age"}).first
|
126
119
|
|
127
120
|
puts other_me
|
128
121
|
# => {name: "Jacob", age: 33}
|
122
|
+
```
|
123
|
+
|
124
|
+
### Dates
|
125
|
+
|
126
|
+
Dates are not natively supported by JSON. This is why dates are persisted as strings.
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
me = Person.create(name: "Jacob", nested: {now: Time.now})
|
130
|
+
puts me.attributes
|
131
|
+
# => {"name"=>"Jacob", "nested"=>{"now"=>2013-10-24 16:15:05 +0200}, "id"=>"fb9ef4bb-1441-4392-a95d-6402f72829db", "version"=>1, "created_at"=>Thu, 24 Oct 2013 14:15:05 UTC +00:00, "updated_at"=>Thu, 24 Oct 2013 14:15:05 UTC +00:00}
|
132
|
+
```
|
133
|
+
|
134
|
+
Lets reload it and see how it is stored:
|
129
135
|
|
136
|
+
```ruby
|
137
|
+
me.reload
|
138
|
+
puts me.attributes
|
139
|
+
# => {"name"=>"Jacob", "nested"=>{"now"=>"2013-10-24T14:15:05.783Z"}, "id"=>"fb9ef4bb-1441-4392-a95d-6402f72829db", "version"=>1, "created_at"=>"2013-10-24T14:15:05.831Z", "updated_at"=>"2013-10-24T14:15:05.831Z"}
|
130
140
|
```
|
131
|
-
`select` takes a hash as argument and return an array of hashes. The value of each key/value pair in the hash argument is a selector. Selectors can point at attributes at root level, but also nested attributes. Each level of attributes is seperated with a dot (.).
|
132
141
|
|
133
|
-
|
142
|
+
PostJson will serialize Time and DateTime to format `strftime('%Y-%m-%dT%H:%M:%S.%LZ')` when persisting documents.
|
143
|
+
|
144
|
+
PostJson will also parse an attribute's value to a `Time` object, if the value is a string and matches the format.
|
145
|
+
|
146
|
+
### Supported methods
|
147
|
+
|
148
|
+
all, any?, blank?, count, delete, delete_all, destroy, destroy_all, each, empty?, except, exists?, find, find_by,
|
149
|
+
find_by!, find_each, find_in_batches, first, first!, first_or_create, first_or_initialize, ids, last, limit, load,
|
150
|
+
many?, offset, only, order, pluck, reorder, reverse_order, select, size, take, take!, to_a, to_sql, and where.
|
151
|
+
|
152
|
+
We also added `page(page, per_page)`, which translate into `offset((page-1)*per_page).limit(per_page)`.
|
153
|
+
|
154
|
+
|
155
|
+
## Configuration Options
|
134
156
|
|
135
157
|
```ruby
|
136
158
|
PostJson.setup "people" do |collection|
|
@@ -144,14 +166,7 @@ PostJson.setup "people" do |collection|
|
|
144
166
|
end
|
145
167
|
```
|
146
168
|
|
147
|
-
|
148
|
-
|
149
|
-
all, any?, blank?, count, delete, delete_all, destroy, destroy_all, each, empty?, except, exists?, find, find_by,
|
150
|
-
find_by!, find_each, find_in_batches, first, first!, first_or_create, first_or_initialize, ids, last, limit, load,
|
151
|
-
many?, offset, only, order, pluck, reorder, reverse_order, select, size, take, take!, to_a, to_sql, and where.
|
152
|
-
|
153
|
-
We also added `page(page, per_page)`, which translate into `offset((page-1)*per_page).limit(per_page)`.
|
154
|
-
|
169
|
+
For a Rails project this configuration could go in an initializer (`config/initializers/post_json.rb`).
|
155
170
|
|
156
171
|
## Performance
|
157
172
|
|
@@ -163,7 +178,7 @@ test_model = PostJson::Collection["test"]
|
|
163
178
|
content = test_model.last.content
|
164
179
|
|
165
180
|
result = test_model.where(content: content).count
|
166
|
-
# Rails debug
|
181
|
+
# Rails debug duration was 975.5ms
|
167
182
|
```
|
168
183
|
|
169
184
|
The duration was above 50ms as you can see.
|
@@ -174,7 +189,7 @@ Now lets see how the performance will be on the second and future queries using
|
|
174
189
|
|
175
190
|
```ruby
|
176
191
|
result = test_model.where(content: content).count
|
177
|
-
# Rails debug
|
192
|
+
# Rails debug duration was 1.5ms
|
178
193
|
```
|
179
194
|
|
180
195
|
It shows PostgreSQL as a document database combined with indexing has great performance out of the box.
|
@@ -183,45 +198,46 @@ See the next section about "Dynamic Indexes" for details.
|
|
183
198
|
|
184
199
|
## Dynamic Indexes
|
185
200
|
|
186
|
-
|
201
|
+
PostJson will measure the duration of each `SELECT` query and instruct PostgreSQL to create an index,
|
202
|
+
if the query duration is above a specified threshold. This feature is called `Dynamic Index`. Since most
|
203
|
+
applications perform the same queries over and over again we think you'll find this useful.
|
187
204
|
|
188
|
-
|
189
|
-
so queries speed up considerably.
|
205
|
+
Each collection (like `PostJson::Collection["people"]` above) has two index attributes:
|
190
206
|
|
191
|
-
|
192
|
-
|
207
|
+
* **use_dynamic_index** (default: true)
|
208
|
+
* **create_dynamic_index_milliseconds_threshold** (default: 50)
|
193
209
|
|
194
|
-
|
195
|
-
attribute `create_dynamic_index_milliseconds_threshold` (which is 50 by default).
|
210
|
+
### Example
|
196
211
|
|
197
|
-
|
212
|
+
```ruby
|
213
|
+
PostJson::Collection["people"].where(name: "Jacob").count
|
198
214
|
|
199
|
-
|
215
|
+
# => query duration > 50ms
|
216
|
+
```
|
200
217
|
|
201
|
-
PostJson will
|
202
|
-
you execute a query with `name` the performance will be much improved.
|
218
|
+
PostJson will check for an index on `name` and create it if it doesn't exist.
|
203
219
|
|
204
|
-
|
220
|
+
### Index configuration
|
205
221
|
|
206
222
|
```ruby
|
207
223
|
class Person < PostJson::Collection["people"]
|
208
224
|
self.create_dynamic_index_milliseconds_threshold = 75
|
209
225
|
end
|
226
|
+
```
|
210
227
|
|
211
|
-
|
228
|
+
or:
|
212
229
|
|
230
|
+
```ruby
|
213
231
|
PostJson::Collection["people"].create_dynamic_index_milliseconds_threshold = 75
|
214
|
-
|
215
|
-
# Now indexes are only created if queries are slower than 75 milliseconds.
|
216
232
|
```
|
217
233
|
|
218
|
-
|
234
|
+
### WARNING
|
219
235
|
|
220
|
-
Do not set
|
236
|
+
Do not set the dynamic index threshold too low as PostJson will try to create an index for every query. A threshold of 1 millisecond would be less than the duration of almost all queries.
|
221
237
|
|
222
238
|
## Primary Keys
|
223
239
|
|
224
|
-
PostJson
|
240
|
+
PostJson assigns UUID as primary key (id):
|
225
241
|
|
226
242
|
```ruby
|
227
243
|
me = Person.create(name: "Jacob")
|
@@ -230,13 +246,13 @@ puts me.id
|
|
230
246
|
# => "297a2500-a456-459b-b3e9-e876f59602c2"
|
231
247
|
```
|
232
248
|
|
233
|
-
|
249
|
+
or you can set it directly:
|
234
250
|
|
235
251
|
```ruby
|
236
252
|
john_doe = Person.create(id: "John Doe")
|
237
253
|
```
|
238
254
|
|
239
|
-
|
255
|
+
The primary key is downcased when doing a query or finding records:
|
240
256
|
|
241
257
|
```ruby
|
242
258
|
found = Person.where(id: "JOhN DoE").first
|
data/lib/post_json/base.rb
CHANGED
@@ -110,15 +110,15 @@ module PostJson
|
|
110
110
|
def __doc__body_convert_attribute_type(attribute_name, value)
|
111
111
|
case value
|
112
112
|
when /^[0-9]{4}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\.[0-9]{3}Z$/
|
113
|
-
Time.parse(value)
|
113
|
+
Time.zone.parse(value)
|
114
114
|
when Hash
|
115
115
|
value.inject(HashWithIndifferentAccess.new) do |result, (key, value)|
|
116
|
-
result[key] =
|
116
|
+
result[key] = __doc__body_convert_attribute_type("#{attribute_name}.#{key}", value)
|
117
117
|
result
|
118
118
|
end
|
119
119
|
when Array
|
120
120
|
value.map.with_index do |array_value, index|
|
121
|
-
|
121
|
+
__doc__body_convert_attribute_type("#{attribute_name}[#{index}]", array_value)
|
122
122
|
end
|
123
123
|
else
|
124
124
|
value
|
@@ -173,11 +173,11 @@ module PostJson
|
|
173
173
|
method_name
|
174
174
|
end
|
175
175
|
|
176
|
-
if attribute_name.in?(attribute_names)
|
176
|
+
if attribute_name.in?(attribute_names) || self.class.column_names.include?(attribute_name) || super_respond_to?(attribute_name.to_sym)
|
177
|
+
super
|
178
|
+
else
|
177
179
|
self.class.define_attribute_accessor(attribute_name)
|
178
180
|
send(method_symbol, *args)
|
179
|
-
else
|
180
|
-
super
|
181
181
|
end
|
182
182
|
end
|
183
183
|
|
@@ -253,6 +253,54 @@ module PostJson
|
|
253
253
|
end
|
254
254
|
RUBY
|
255
255
|
end
|
256
|
+
|
257
|
+
def convert_attribute_value_before_save(primary_key, selector, value)
|
258
|
+
case value
|
259
|
+
when Time
|
260
|
+
value.in_time_zone
|
261
|
+
when DateTime
|
262
|
+
value.to_time.in_time_zone
|
263
|
+
else
|
264
|
+
value
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def convert_document_hash_before_save(primary_key, document_hash, prefix = nil)
|
269
|
+
if document_hash
|
270
|
+
document_hash.inject(HashWithIndifferentAccess.new) do |result_hash, (key, value)|
|
271
|
+
selector = if prefix
|
272
|
+
"#{prefix}.#{key}"
|
273
|
+
else
|
274
|
+
key
|
275
|
+
end
|
276
|
+
case value
|
277
|
+
when Hash
|
278
|
+
result_hash[key] = convert_document_hash_before_save(primary_key, value, selector)
|
279
|
+
when Array
|
280
|
+
result_hash[key] = convert_document_array_before_save(primary_key, value, selector)
|
281
|
+
else
|
282
|
+
result_hash[key] = convert_attribute_value_before_save(primary_key, selector, value)
|
283
|
+
end
|
284
|
+
result_hash
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def convert_document_array_before_save(primary_key, document_array, prefix = nil)
|
290
|
+
if document_array
|
291
|
+
document_array.map.with_index do |value, index|
|
292
|
+
selector = "#{prefix}[#{index}]"
|
293
|
+
case value
|
294
|
+
when Hash
|
295
|
+
convert_document_hash_before_save(primary_key, value, selector)
|
296
|
+
when Array
|
297
|
+
convert_document_array_before_save(primary_key, value, selector)
|
298
|
+
else
|
299
|
+
convert_attribute_value_before_save(primary_key, selector, value)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
256
304
|
end
|
257
305
|
|
258
306
|
protected
|
@@ -280,10 +328,11 @@ module PostJson
|
|
280
328
|
end
|
281
329
|
|
282
330
|
if self.class.persisted_settings.use_timestamps
|
283
|
-
__local__current_time = Time.zone.now
|
331
|
+
__local__current_time = Time.zone.now
|
284
332
|
__doc__body_write_attribute(self.class.persisted_settings.created_at_attribute_name, __local__current_time)
|
285
333
|
__doc__body_write_attribute(self.class.persisted_settings.updated_at_attribute_name, __local__current_time)
|
286
334
|
end
|
335
|
+
|
287
336
|
super
|
288
337
|
end
|
289
338
|
|
@@ -302,10 +351,20 @@ module PostJson
|
|
302
351
|
end
|
303
352
|
|
304
353
|
if self.class.persisted_settings.use_timestamps && __doc__body_attribute_changed?(self.class.persisted_settings.updated_at_attribute_name)
|
305
|
-
__local__current_time = Time.zone.now
|
354
|
+
__local__current_time = Time.zone.now
|
306
355
|
__doc__body_write_attribute(self.class.persisted_settings.updated_at_attribute_name, __local__current_time)
|
307
356
|
end
|
308
357
|
super
|
309
358
|
end
|
359
|
+
|
360
|
+
def typecasted_attribute_value(name)
|
361
|
+
result = super
|
362
|
+
name = name.to_s
|
363
|
+
if name == '__doc__body'
|
364
|
+
self.class.convert_document_hash_before_save(self[self.primary_key], result)
|
365
|
+
else
|
366
|
+
result
|
367
|
+
end
|
368
|
+
end
|
310
369
|
end
|
311
370
|
end
|
data/lib/post_json/version.rb
CHANGED
data/spec/models/base_spec.rb
CHANGED
@@ -433,4 +433,52 @@ describe "Base model" do
|
|
433
433
|
it { subject.find("John").age.should == 25 }
|
434
434
|
end
|
435
435
|
end
|
436
|
-
|
436
|
+
|
437
|
+
context "dates" do
|
438
|
+
let(:time) { Time.now }
|
439
|
+
let(:time_in_zone) { time.in_time_zone }
|
440
|
+
let(:date_time) { time.to_datetime }
|
441
|
+
let(:time_result) { Time.parse(time_in_zone.strftime('%Y-%m-%dT%H:%M:%S.%LZ')) }
|
442
|
+
let(:time_hash) { { 'time' => time, 'time_in_zone' => time_in_zone, 'date_time' => date_time } }
|
443
|
+
let(:time_array) { [time, time_in_zone, date_time] }
|
444
|
+
let(:record) { PostJson::Collection["dates"].new(time: time, time_in_zone: time_in_zone, date_time: date_time, time_hash: time_hash, time_array: time_array) }
|
445
|
+
|
446
|
+
context "before save" do
|
447
|
+
subject { record }
|
448
|
+
|
449
|
+
its(:time) { should == time }
|
450
|
+
its(:time_in_zone) { should == time_in_zone }
|
451
|
+
its(:date_time) { should == date_time }
|
452
|
+
its(:time_hash) { should == time_hash }
|
453
|
+
its(:time_array) { should == time_array }
|
454
|
+
end
|
455
|
+
|
456
|
+
context "after save" do
|
457
|
+
subject { record }
|
458
|
+
|
459
|
+
before do
|
460
|
+
subject.save!
|
461
|
+
end
|
462
|
+
|
463
|
+
its(:time) { should == time }
|
464
|
+
its(:time_in_zone) { should == time_in_zone }
|
465
|
+
its(:date_time) { should == date_time }
|
466
|
+
its(:time_hash) { should == time_hash }
|
467
|
+
its(:time_array) { should == time_array }
|
468
|
+
|
469
|
+
context "and reload" do
|
470
|
+
|
471
|
+
|
472
|
+
before do
|
473
|
+
subject.reload
|
474
|
+
end
|
475
|
+
|
476
|
+
its(:time) { should == time_result }
|
477
|
+
its(:time_in_zone) { should == time_result }
|
478
|
+
its(:date_time) { should == time_result }
|
479
|
+
its(:time_hash) { should == { 'time' => time_result, 'time_in_zone' => time_result, 'date_time' => time_result } }
|
480
|
+
its(:time_array) { should == [time_result]*3 }
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|