occams-record 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/README.md +138 -129
- data/lib/occams-record/results.rb +46 -12
- data/lib/occams-record/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6122accb864f31c103a6036c8c3b9e08b457b52f
|
4
|
+
data.tar.gz: f8ed00235fc026d24a56a9f44065a45142472603
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1f6a942bc993990b22448507ed25723a12a3a2a7d06d5684008b3a78a51f676b973cb11297f2a6818a9becd3164db5079181ae0566251cd8b8a2eee1b99fc763
|
7
|
+
data.tar.gz: 40f46acd684737a8214f7b06ce1dce31de0fc76bcc1f8ab4ee152973d6223f8089e416cec4536c9e567ac3c2ea292429a87cef898bcb059f05210d84285ff55a
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
### Occams Record [](https://travis-ci.org/jhollinger/occams-record)
|
2
2
|
|
3
3
|
> Do not multiply entities beyond necessity. -- Occam's Razor
|
4
4
|
|
@@ -11,203 +11,212 @@ Occam's Record is a high-efficiency, advanced query library for ActiveRecord app
|
|
11
11
|
* `find_each`/`find_in_batches` respects `order` and `limit`.
|
12
12
|
* Allows eager loading of associations when querying with raw SQL.
|
13
13
|
* Allows `find_each`/`find_in_batches` when querying with raw SQL.
|
14
|
-
* Eager load
|
14
|
+
* Eager load an ad hoc assocation using arbitrary SQL.
|
15
15
|
|
16
16
|
[Look over the speed and memory measurements yourself!](https://github.com/jhollinger/occams-record/wiki/Measurements) OccamsRecord achieves all of this by making some very specific trade-offs:
|
17
17
|
|
18
18
|
* OccamsRecord results are **read-only**.
|
19
19
|
* OccamsRecord results are **purely database rows** - they don't have any instance methods from your Rails models.
|
20
|
-
*
|
20
|
+
* You **must eager load** each assocation you intend to use. If you forget one, an exception will be raised.
|
21
21
|
|
22
|
-
|
22
|
+
---
|
23
23
|
|
24
|
-
|
24
|
+
# Installation
|
25
25
|
|
26
|
-
|
26
|
+
Simply add it to your `Gemfile`:
|
27
27
|
|
28
28
|
```ruby
|
29
29
|
gem 'occams-record'
|
30
30
|
```
|
31
31
|
|
32
|
-
|
32
|
+
---
|
33
|
+
|
34
|
+
# Overview
|
33
35
|
|
34
|
-
|
36
|
+
Full documentation is available at [rubydoc.info/gems/occams-record](http://www.rubydoc.info/gems/occams-record).
|
37
|
+
|
38
|
+
Build your queries like normal, using ActiveRecord's excellent query builder. Then pass them off to Occams Record.
|
35
39
|
|
36
40
|
```ruby
|
37
|
-
|
38
|
-
|
39
|
-
|
41
|
+
q = Order.
|
42
|
+
completed.
|
43
|
+
where("order_date > ?", 30.days.ago).
|
44
|
+
order("order_date DESC")
|
45
|
+
|
46
|
+
orders = OccamsRecord.
|
47
|
+
query(q).
|
40
48
|
run
|
49
|
+
````
|
41
50
|
|
42
|
-
|
43
|
-
=> 1000
|
51
|
+
`each`, `map`, `reduce`, and all other Enumerable methods are supported. `find_each` and `find_in_batches` are also supported, and unlike their ActiveRecord counterparts they respect *ORDER BY*. Occams Record has great support for raw SQL queries too, but we'll get to those later.
|
44
52
|
|
45
|
-
|
46
|
-
=> "Widget 1000"
|
53
|
+
## Basic eager loading
|
47
54
|
|
48
|
-
|
49
|
-
|
55
|
+
Eager loading is similiar to ActiveRecord's `preload` (each association is loaded in a separate query). Nested associations use blocks instead of Hashes.
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
orders = OccamsRecord.
|
59
|
+
query(q).
|
60
|
+
eager_load(:customer).
|
61
|
+
eager_load(:line_items) {
|
62
|
+
eager_load(:product)
|
63
|
+
}.
|
64
|
+
run
|
65
|
+
|
66
|
+
order = orders[0]
|
67
|
+
puts order.customer.name
|
68
|
+
|
69
|
+
order.line_items.each { |line_item|
|
70
|
+
puts line_item.product.name
|
71
|
+
puts line_item.product.category.name
|
72
|
+
OccamsRecord::MissingEagerLoadError: Association 'category' is unavailable on Product because it was not eager loaded!
|
73
|
+
}
|
50
74
|
```
|
51
75
|
|
52
|
-
|
76
|
+
## Advanced eager loading
|
53
77
|
|
54
|
-
|
78
|
+
Occams Record allows you to customize each eager load query using the full power of ActiveRecord's query builder.
|
55
79
|
|
56
80
|
```ruby
|
57
|
-
|
58
|
-
query(
|
59
|
-
|
60
|
-
eager_load(:
|
81
|
+
orders = OccamsRecord.
|
82
|
+
query(q).
|
83
|
+
# Only SELECT these two columns. Your DBA will thank you, esp. on "wide" tables.
|
84
|
+
eager_load(:customer, select: "id, name").
|
85
|
+
|
86
|
+
# A Proc can customize the query using any of ActiveRecord's query builders and
|
87
|
+
# any scopes you've defined on the LineItem model.
|
88
|
+
eager_load(:line_items, ->(q) { q.active.order("created_at") }) {
|
89
|
+
eager_load(:product)
|
90
|
+
}.
|
61
91
|
run
|
92
|
+
```
|
93
|
+
|
94
|
+
Occams Record also supports creating ad hoc associations using raw SQL. We'll get to that in the next section.
|
95
|
+
|
96
|
+
## Raw SQL queries
|
97
|
+
|
98
|
+
ActiveRecord has raw SQL "escape hatches" like `find_by_sql` or `exec_query`, but they both give up critical features like eager loading and `find_each`/`find_in_batches`. Not so with Occams Record!
|
99
|
+
|
100
|
+
**Batched loading**
|
62
101
|
|
63
|
-
|
64
|
-
=> ["Spline 1", "Spline 2", "Spline 3"]
|
102
|
+
To use `find_each`/`find_in_batches` you must provide the limit and offset statements yourself. OccamsRecord will fill in the values for you. Also, notice that the binding syntax is a bit different (it uses Ruby's built-in named string substitution).
|
65
103
|
|
66
|
-
|
67
|
-
|
104
|
+
```ruby
|
105
|
+
OccamsRecord.
|
106
|
+
sql(%(
|
107
|
+
SELECT * FROM orders
|
108
|
+
WHERE order_date > %{date}
|
109
|
+
ORDER BY order_date DESC
|
110
|
+
LIMIT %{batch_limit}
|
111
|
+
OFFSET %{batch_offset}
|
112
|
+
), {
|
113
|
+
date: 30.days.ago
|
114
|
+
}).
|
115
|
+
find_each(batch_size: 1000) do |order|
|
116
|
+
...
|
117
|
+
end
|
68
118
|
```
|
69
119
|
|
70
|
-
**
|
120
|
+
**Eager loading**
|
71
121
|
|
72
|
-
|
122
|
+
To use `eager_load` with a raw SQL query you must tell Occams what the base model is. (That doesn't apply if you're loading an ad hoc, raw SQL association. We'll get to those later).
|
73
123
|
|
74
124
|
```ruby
|
75
|
-
|
76
|
-
|
77
|
-
|
125
|
+
orders = OccamsRecord.
|
126
|
+
sql(%(
|
127
|
+
SELECT * FROM orders
|
128
|
+
WHERE order_date > %{date}
|
129
|
+
ORDER BY order_date DESC
|
130
|
+
), {
|
131
|
+
date: 30.days.ago
|
132
|
+
}).
|
133
|
+
model(Order).
|
134
|
+
eager_load(:customer).
|
135
|
+
run
|
136
|
+
```
|
78
137
|
|
79
|
-
|
80
|
-
eager_load(:order_items, select: "widget_id, order_id") {
|
138
|
+
## Raw SQL eager loading
|
81
139
|
|
82
|
-
|
83
|
-
eager_load(:orders, ->(q) { q.select("id, customer_id").order("order_date DESC") }) {
|
140
|
+
Let's say we want to load each product with an array of all customers who've ordered it. We *could* do that by loading various nested associations:
|
84
141
|
|
85
|
-
|
86
|
-
|
142
|
+
```ruby
|
143
|
+
products_with_orders = OccamsRecord.
|
144
|
+
query(Product.all).
|
145
|
+
eager_load(:line_items) {
|
146
|
+
eager_load(:order) {
|
147
|
+
eager_load(:customer)
|
87
148
|
}
|
88
149
|
}.
|
89
|
-
|
150
|
+
map { |product|
|
151
|
+
customers = product.line_items.map(&:order).map(&:customer).uniq
|
152
|
+
[product, customers]
|
153
|
+
}
|
90
154
|
```
|
91
155
|
|
92
|
-
|
93
|
-
|
94
|
-
Let's say we want to load each widget and eager load all the customers who've ever ordered it. We could do that using the above example, but we end up loading a lot of useless intermediate records. What if we could define an ad hoc association, using raw SQL, to load exactly what we need? Enter `eager_load_one` and `eager_load_many`! See the full documentation for a full description of all options.
|
156
|
+
But that's very wasteful. Occams gives us a better options: `eager_load_many` and `eager_load_one`.
|
95
157
|
|
96
158
|
```ruby
|
97
|
-
|
98
|
-
query(
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
INNER JOIN orders ON orders.customer_id = customers.id
|
106
|
-
INNER JOIN order_items ON order_items.order_id = orders.id
|
107
|
-
WHERE order_items.widget_id IN (%{ids})
|
159
|
+
products = OccamsRecord.
|
160
|
+
query(Product.all).
|
161
|
+
eager_load_many(:customers, {:product_id => :id}, %w(
|
162
|
+
SELECT DISTINCT product_id, customers.*
|
163
|
+
FROM line_items
|
164
|
+
INNER JOIN orders ON line_items.order_id = orders.id
|
165
|
+
INNER JOIN customers on orders.customer_id = customers.id
|
166
|
+
WHERE line_items.product_id IN (%{ids})
|
108
167
|
), binds: {
|
109
168
|
# additional bind values (ids will be passed in for you)
|
110
169
|
}).
|
111
170
|
run
|
112
171
|
```
|
113
172
|
|
114
|
-
|
173
|
+
`eager_load_many` allows us to declare an ad hoc *has_many* association called *customers*. The `{:product_id => :id}` Hash defines the mapping: *product_id* in these results maps to *id* in the parent Product. The SQL string and binds should be familiar by now. The `%{ids}` value will be provided for you - just stick it in the right place.
|
115
174
|
|
116
|
-
|
175
|
+
`eager_load_one` defines an ad hoc `has_one`/`belongs_to` association.
|
117
176
|
|
118
|
-
|
119
|
-
module MyWidgetMethods
|
120
|
-
def to_s
|
121
|
-
name
|
122
|
-
end
|
177
|
+
These ad hoc eager loaders are available on both `OccamsRecord.query` and `OccamsRecord.sql`. While eager loading with `OccamsRecord.sql` normallly requires you to declare the model, that is not necessary with `eager_load_one`/`eager_load_many`.
|
123
178
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
end
|
179
|
+
## Injecting instance methods
|
180
|
+
|
181
|
+
Occams Records results are just plain rows; there are no methods from your Rails models. (Separating your persistence layer from your domain is good thing!) But sometimes you need a few methods. Occams Record allows you to specify modules to be included in your results.
|
128
182
|
|
183
|
+
```ruby
|
129
184
|
module MyOrderMethods
|
130
185
|
def description
|
131
186
|
"#{order_number} - #{date}"
|
132
187
|
end
|
133
188
|
end
|
134
189
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
widgets[0].to_s
|
141
|
-
=> "Widget A"
|
142
|
-
|
143
|
-
widgets[0].price_per_unit
|
144
|
-
=> 57.23
|
145
|
-
|
146
|
-
widgets[0].expensive?
|
147
|
-
=> false
|
148
|
-
|
149
|
-
widgets[0].orders[0].description
|
150
|
-
=> "O839SJZ98B - 1/8/2017"
|
151
|
-
```
|
152
|
-
|
153
|
-
## Raw SQL queries
|
154
|
-
|
155
|
-
If you have a complicated query to run, you may drop down to hand-written SQL while still taking advantage of eager loading and variable escaping (not possible in ActiveRecord). Note the slightly different syntax for binding variables.
|
156
|
-
|
157
|
-
NOTE this feature is quite new and might have some bugs. Since we are not yet at 1.0, breaking changes may occur. Issues and Pull Requests welcome.
|
158
|
-
|
159
|
-
```ruby
|
160
|
-
widgets = OccamsRecord.sql(%(
|
161
|
-
SELECT * FROM widgets
|
162
|
-
WHERE category_id = %{cat_id}
|
163
|
-
), {
|
164
|
-
cat_id: 5
|
165
|
-
}).run
|
166
|
-
```
|
167
|
-
|
168
|
-
**Performing eager loading with raw SQL**
|
169
|
-
|
170
|
-
To perform eager loading with raw SQL you must specify the base model (unless you use the raw SQL eager loaders `eager_load_one` or `eager_load_many`). NOTE some database adapters, notably SQLite, require you to always specify the model.
|
190
|
+
module MyProductMethods
|
191
|
+
def expensive?
|
192
|
+
price > 100
|
193
|
+
end
|
194
|
+
end
|
171
195
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
), {
|
178
|
-
cat_ids: [5, 10]
|
179
|
-
}).
|
180
|
-
model(Widget).
|
181
|
-
eager_load(:category).
|
196
|
+
orders = OccamsRecord.
|
197
|
+
query(Order.all, use: MyOrderMethods).
|
198
|
+
eager_load(:line_items) {
|
199
|
+
eager_load(:product, use: [MyProductMethods, OtherMethods])
|
200
|
+
}.
|
182
201
|
run
|
183
202
|
```
|
184
203
|
|
185
|
-
|
204
|
+
---
|
186
205
|
|
187
|
-
|
206
|
+
# Unsupported features
|
188
207
|
|
189
|
-
|
190
|
-
widgets = OccamsRecord.sql(%(
|
191
|
-
SELECT * FROM widgets
|
192
|
-
WHERE category_id = %{cat_id}
|
193
|
-
ORDER BY name, id
|
194
|
-
LIMIT %{batch_limit}
|
195
|
-
OFFSET %{batch_offset}
|
196
|
-
), {
|
197
|
-
cat_id: 5
|
198
|
-
}).find_each { |widget|
|
199
|
-
puts widget.name
|
200
|
-
}
|
201
|
-
```
|
208
|
+
The following ActiveRecord features are under consideration, but not high priority. Pull requests welcome!
|
202
209
|
|
203
|
-
|
210
|
+
* `:through` associations.
|
204
211
|
|
205
|
-
The following
|
212
|
+
The following ActiveRecord features are not supported, and likely never will be. Pull requests are still welcome, though.
|
206
213
|
|
207
214
|
* ActiveRecord enum types
|
208
215
|
* ActiveRecord serialized types
|
209
216
|
|
210
|
-
|
217
|
+
---
|
218
|
+
|
219
|
+
# Testing
|
211
220
|
|
212
221
|
To run the tests, simply run:
|
213
222
|
|
@@ -219,7 +228,7 @@ bundle exec rake test
|
|
219
228
|
By default, bundler will install the latest (supported) version of ActiveRecord. To specify a version to test against, run:
|
220
229
|
|
221
230
|
```bash
|
222
|
-
AR=
|
231
|
+
AR=5.2 bundle update activerecord
|
223
232
|
bundle exec rake test
|
224
233
|
```
|
225
234
|
|
@@ -25,6 +25,8 @@ module OccamsRecord
|
|
25
25
|
self.columns = column_names.map(&:to_s)
|
26
26
|
self.associations = association_names.map(&:to_s)
|
27
27
|
self.model_name = model ? model.name : nil
|
28
|
+
self.table_name = model ? model.table_name : nil
|
29
|
+
self.primary_key = model&.primary_key&.to_s
|
28
30
|
|
29
31
|
# Build getters & setters for associations. (We need setters b/c they're set AFTER the row is initialized
|
30
32
|
attr_accessor(*association_names)
|
@@ -64,6 +66,10 @@ module OccamsRecord
|
|
64
66
|
attr_accessor :associations
|
65
67
|
# Name of Rails model
|
66
68
|
attr_accessor :model_name
|
69
|
+
# Name of originating database table
|
70
|
+
attr_accessor :table_name
|
71
|
+
# Name of primary key column (nil if column wasn't in the SELECT)
|
72
|
+
attr_accessor :primary_key
|
67
73
|
end
|
68
74
|
self.columns = []
|
69
75
|
self.associations = []
|
@@ -78,20 +84,35 @@ module OccamsRecord
|
|
78
84
|
@cast_values = {}
|
79
85
|
end
|
80
86
|
|
87
|
+
#
|
88
|
+
# Returns true if the two objects are from the same table and have the same primary key.
|
89
|
+
#
|
90
|
+
# @param obj [OccamsRecord::Results::Row]
|
91
|
+
# @return [Boolean]
|
92
|
+
#
|
93
|
+
def ==(obj)
|
94
|
+
super ||
|
95
|
+
obj.is_a?(OccamsRecord::Results::Row) &&
|
96
|
+
obj.class.table_name && obj.class.table_name == self.class.table_name &&
|
97
|
+
(pkey1 = obj.class.primary_key) && (pkey2 = self.class.primary_key) &&
|
98
|
+
obj.send(pkey1) == self.send(pkey2)
|
99
|
+
end
|
100
|
+
|
81
101
|
#
|
82
102
|
# Return row as a Hash (recursive).
|
83
103
|
#
|
84
104
|
# @param symbolize_names [Boolean] if true, make Hash keys Symbols instead of Strings
|
105
|
+
# @param recursive [Boolean] if true, convert all associated records to Hashes too
|
85
106
|
# @return [Hash] a Hash with String or Symbol keys
|
86
107
|
#
|
87
|
-
def to_h(symbolize_names: false)
|
108
|
+
def to_h(symbolize_names: false, recursive: true)
|
88
109
|
hash = self.class.columns.reduce({}) { |a, col_name|
|
89
110
|
key = symbolize_names ? col_name.to_sym : col_name
|
90
111
|
a[key] = send col_name
|
91
112
|
a
|
92
113
|
}
|
93
114
|
|
94
|
-
self.class.associations.reduce(hash) { |a, assoc_name|
|
115
|
+
recursive ? self.class.associations.reduce(hash) { |a, assoc_name|
|
95
116
|
key = symbolize_names ? assoc_name.to_sym : assoc_name
|
96
117
|
assoc = send assoc_name
|
97
118
|
a[key] = if assoc.is_a? Array
|
@@ -100,11 +121,33 @@ module OccamsRecord
|
|
100
121
|
assoc.to_h(symbolize_names: symbolize_names)
|
101
122
|
end
|
102
123
|
a
|
103
|
-
}
|
124
|
+
} : hash
|
104
125
|
end
|
105
126
|
|
106
127
|
alias_method :to_hash, :to_h
|
107
128
|
|
129
|
+
#
|
130
|
+
# Returns the name of the model and the attributes.
|
131
|
+
#
|
132
|
+
# @return [String]
|
133
|
+
#
|
134
|
+
def to_s
|
135
|
+
"#{self.class.model_name || "Anonymous"}#{to_h(symbolize_names: true, recursive: false)}"
|
136
|
+
end
|
137
|
+
|
138
|
+
#
|
139
|
+
# Returns a string with the "real" model name and raw result values.
|
140
|
+
#
|
141
|
+
# Weird note - if this string is longer than 65 chars it won't be used in exception messages.
|
142
|
+
# https://bugs.ruby-lang.org/issues/8982
|
143
|
+
#
|
144
|
+
# @return [String]
|
145
|
+
#
|
146
|
+
def inspect
|
147
|
+
id = self.class.primary_key ? send(self.class.primary_key) : "none"
|
148
|
+
"#<#{self.class.model_name || "Anonymous"} #{self.class.primary_key}: #{id}>"
|
149
|
+
end
|
150
|
+
|
108
151
|
def method_missing(name, *args, &block)
|
109
152
|
return super if args.any? or !block.nil?
|
110
153
|
model = self.class.model_name.constantize
|
@@ -117,15 +160,6 @@ module OccamsRecord
|
|
117
160
|
super
|
118
161
|
end
|
119
162
|
end
|
120
|
-
|
121
|
-
#
|
122
|
-
# Returns a string with the "real" model name and raw result values.
|
123
|
-
#
|
124
|
-
# @return [String]
|
125
|
-
#
|
126
|
-
def inspect
|
127
|
-
"#<OccamsRecord::Results::Row @model_name=#{self.class.model_name} @raw_values=#{@raw_values}>"
|
128
|
-
end
|
129
163
|
end
|
130
164
|
end
|
131
165
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: occams-record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.28.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jordan Hollinger
|
@@ -56,7 +56,7 @@ files:
|
|
56
56
|
- lib/occams-record/raw_query.rb
|
57
57
|
- lib/occams-record/results.rb
|
58
58
|
- lib/occams-record/version.rb
|
59
|
-
homepage: https://github.
|
59
|
+
homepage: https://jhollinger.github.io/occams-record/
|
60
60
|
licenses:
|
61
61
|
- MIT
|
62
62
|
metadata: {}
|
@@ -76,7 +76,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
76
|
version: '0'
|
77
77
|
requirements: []
|
78
78
|
rubyforge_project:
|
79
|
-
rubygems_version: 2.
|
79
|
+
rubygems_version: 2.5.2.2
|
80
80
|
signing_key:
|
81
81
|
specification_version: 4
|
82
82
|
summary: The missing high-efficiency query API for ActiveRecord
|