occams-record 0.27.0 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 9c718e8ed411d6d2a139dcc27c2c0f7fa788fc6dafb8c534d3332682c85502f6
4
- data.tar.gz: 464d6e322e1a478f861cb576b4e506589da318485c29fe5ec852726fcba2a692
2
+ SHA1:
3
+ metadata.gz: 6122accb864f31c103a6036c8c3b9e08b457b52f
4
+ data.tar.gz: f8ed00235fc026d24a56a9f44065a45142472603
5
5
  SHA512:
6
- metadata.gz: ed85408318c87e662a14cc863fe43cb2b2e1f1a5aae739ff1a1b22968957a455e771e3ac6fcb3b902b0febe9585bddad6b4c02640bfaa077a6e6a98f3a3c148d
7
- data.tar.gz: 78f981128bd9fad0d6a670ed21426366cf3ac1b74169f3571a6335ae9cf1d5a5c2c790fa7f4bc1c09d4b5756a0a7850998467a779c7bef4db2dc50b1553fb330
6
+ metadata.gz: 1f6a942bc993990b22448507ed25723a12a3a2a7d06d5684008b3a78a51f676b973cb11297f2a6818a9becd3164db5079181ae0566251cd8b8a2eee1b99fc763
7
+ data.tar.gz: 40f46acd684737a8214f7b06ce1dce31de0fc76bcc1f8ab4ee152973d6223f8089e416cec4536c9e567ac3c2ea292429a87cef898bcb059f05210d84285ff55a
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Occam's Record [![Build Status](https://travis-ci.org/jhollinger/occams-record.svg?branch=master)](https://travis-ci.org/jhollinger/occams-record)
1
+ ### Occams Record [![Build Status](https://travis-ci.org/jhollinger/occams-record.svg?branch=master)](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 data from arbitrary SQL (no association required).
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
- * OccamsRecord queries must eager load each association that will be used. Otherwise they simply won't be availble.
20
+ * You **must eager load** each assocation you intend to use. If you forget one, an exception will be raised.
21
21
 
22
- ## Usage
22
+ ---
23
23
 
24
- Full documentation is available at [rubydoc.info/gems/occams-record](http://www.rubydoc.info/gems/occams-record).
24
+ # Installation
25
25
 
26
- **Add to your Gemfile**
26
+ Simply add it to your `Gemfile`:
27
27
 
28
28
  ```ruby
29
29
  gem 'occams-record'
30
30
  ```
31
31
 
32
- **Simple example**
32
+ ---
33
+
34
+ # Overview
33
35
 
34
- Build your query the same as before, using `ActiveRecord`'s excellent query builder. Just hand it off to `OccamsRecord.query` before running it. Be sure to use `OccamsRecord`'s eager loading helpers instead of `ActiveRecord`'s.
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
- widgets = OccamsRecord.
38
- query(Widget.order("name")).
39
- eager_load(:category).
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
- widgets[0].id
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
- widgets[0].name
46
- => "Widget 1000"
53
+ ## Basic eager loading
47
54
 
48
- widgets[0].category.name
49
- => "Category 1"
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
- **More complicated example**
76
+ ## Advanced eager loading
53
77
 
54
- Notice that we're eager loading splines, but *only the fields that we need*. If that's a wide table, your DBA will thank you.
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
- widgets = OccamsRecord.
58
- query(Widget.order("name")).
59
- eager_load(:category).
60
- eager_load(:splines, select: "widget_id, description").
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
- widgets[0].splines.map { |s| s.description }
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
- widgets[1].splines.map { |s| s.description }
67
- => ["Spline 4", "Spline 5"]
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
- **Even more complicated example**
120
+ **Eager loading**
71
121
 
72
- Here we're eager loading several levels down. Notice the `Proc` given to `eager_load(:orders)`. The `select:` option is just for convenience; you may instead pass a `Proc` and customize the query with any of ActiveRecord's query builder helpers (`select`, `where`, `order`, `limit`, etc).
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
- widgets = OccamsRecord.
76
- query(Widget.order("name")).
77
- eager_load(:category).
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
- # load order_items, but only the fields needed to identify which orders go with which widgets
80
- eager_load(:order_items, select: "widget_id, order_id") {
138
+ ## Raw SQL eager loading
81
139
 
82
- # load the orders ("q" has all the normal query methods and any scopes defined on Order)
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
- # load the customers who made the orders, but only their names
86
- eager_load(:customer, select: "id, name")
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
- run
150
+ map { |product|
151
+ customers = product.line_items.map(&:order).map(&:customer).uniq
152
+ [product, customers]
153
+ }
90
154
  ```
91
155
 
92
- **Eager load using raw SQL without a predefined association**
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
- widgets = OccamsRecord.
98
- query(Widget.order("name")).
99
-
100
- # load the results of the query into "customers", matching "widget_id"
101
- # in the results to the "id" field of the widgets
102
- eager_load_many(:customers, {:widget_id => :id}, %(
103
- SELECT DISTINCT customers.id, customers.name, order_items.widget_id
104
- FROM customers
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
- ## Injecting instance methods
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
- By default your results will only have getters for selected columns and eager-loaded associations. If you must, you *can* inject extra methods into your results by putting those methods into a Module. NOTE this is discouraged, as you should try to maintain a clear separation between your persistence layer and your domain.
175
+ `eager_load_one` defines an ad hoc `has_one`/`belongs_to` association.
117
176
 
118
- ```ruby
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
- def expensive?
125
- price_per_unit > 100
126
- end
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
- widgets = OccamsRecord.
136
- query(Widget.order("name"), use: MyWidgetMethods).
137
- eager_load(:orders, use: [MyOrderMethods, SomeAdditionalMethods]).
138
- run
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
- ```ruby
173
- widgets = OccamsRecord.
174
- sql(%(
175
- SELECT * FROM widgets
176
- WHERE category_id IN (%{cat_ids})
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
- **Using find_each/find_in_batches with raw SQL**
204
+ ---
186
205
 
187
- To use `find_each` or `find_in_batches` with raw SQL you must provide the `LIMIT` and `OFFSET` clauses yourself. The bind values for these will be filled in by OccamsRecord. Remember to always specific a consitent `ORDER BY` clause.
206
+ # Unsupported features
188
207
 
189
- ```ruby
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
- ## Unsupported features
210
+ * `:through` associations.
204
211
 
205
- The following `ActiveRecord` are not supported, and I have no plans to do so. However, I'd be glad to accept pull requests.
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
- ## Testing
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=4.2 bundle update activerecord
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
@@ -3,5 +3,5 @@
3
3
  #
4
4
  module OccamsRecord
5
5
  # Library version
6
- VERSION = '0.27.0'.freeze
6
+ VERSION = '0.28.0'.freeze
7
7
  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.27.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.com/jhollinger/occams-record
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.7.3
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