occams-record 0.29.0 → 0.31.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 +4 -4
- data/README.md +31 -23
- data/lib/occams-record/eager_loaders.rb +17 -109
- data/lib/occams-record/eager_loaders/ad_hoc_base.rb +7 -3
- data/lib/occams-record/eager_loaders/base.rb +9 -3
- data/lib/occams-record/eager_loaders/builder.rb +112 -0
- data/lib/occams-record/eager_loaders/context.rb +113 -0
- data/lib/occams-record/eager_loaders/polymorphic_belongs_to.rb +10 -3
- data/lib/occams-record/eager_loaders/through.rb +107 -0
- data/lib/occams-record/query.rb +5 -7
- data/lib/occams-record/raw_query.rb +8 -10
- data/lib/occams-record/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c2c1004e553179f934744fe17227319b53e6fe663d4b6e5c246017c0a7728808
|
|
4
|
+
data.tar.gz: 502a1fe403a0ccf1980447d7f2b6f960cc3988a72b574df7d30a1d3db3ab69a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49f84fc3ef76a06740b9c9702d49a2370d3ffaac28f5af2ec7747a911fe14031c477a53eae7b8ca50fb084c8b994186068fad1c272ddab32ed34280ab8ee1a04
|
|
7
|
+
data.tar.gz: 4615260b822144d68c8a7c858de3f02820fbaffd5c490ae8cdf33eed96bb89529721cc527c435f90f00618c85e88992b02b46bfce2094d96187ee985f61042d8
|
data/README.md
CHANGED
|
@@ -2,22 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
> Do not multiply entities beyond necessity. -- Occam's Razor
|
|
4
4
|
|
|
5
|
-
Occam's Record is a high-efficiency, advanced query library for ActiveRecord apps. It is **not** an ORM or an ActiveRecord replacement. Use it to solve pain points in your existing ActiveRecord app.
|
|
5
|
+
Occam's Record is a high-efficiency, advanced query library for ActiveRecord apps. It is **not** an ORM or an ActiveRecord replacement. Use it to solve pain points in your existing ActiveRecord app. Occams Record gives you two things:
|
|
6
|
+
|
|
7
|
+
**Performance**
|
|
6
8
|
|
|
7
9
|
* 3x-5x faster than ActiveRecord queries.
|
|
8
10
|
* Uses 1/3 the memory of ActiveRecord query results.
|
|
9
11
|
* Eliminates the N+1 query problem.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
|
|
13
|
+
**More powerful queries & eager loading**
|
|
14
|
+
|
|
15
|
+
* Customize the SQL used to eager load associations.
|
|
16
|
+
* Use `ORDER BY` with `find_each`/`find_in_batches`.
|
|
17
|
+
* Use `find_each`/`find_in_batches` with raw SQL.
|
|
18
|
+
* Eager load associations when you're writing raw SQL.
|
|
19
|
+
* Eager load "ad hoc associations" using raw SQL.
|
|
15
20
|
|
|
16
21
|
[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
22
|
|
|
18
|
-
* OccamsRecord results are
|
|
19
|
-
* OccamsRecord results are
|
|
20
|
-
* You
|
|
23
|
+
* OccamsRecord results are *read-only*.
|
|
24
|
+
* OccamsRecord results are *purely database rows* - they don't have any instance methods from your Rails models.
|
|
25
|
+
* You *must eager load* each assocation you intend to use. If you forget one, an exception will be raised.
|
|
21
26
|
|
|
22
27
|
---
|
|
23
28
|
|
|
@@ -48,11 +53,11 @@ orders = OccamsRecord.
|
|
|
48
53
|
run
|
|
49
54
|
````
|
|
50
55
|
|
|
51
|
-
`each`, `map`, `reduce`, and other Enumerable methods may be used instead of *run*. `find_each` and `find_in_batches` are also supported
|
|
56
|
+
`each`, `map`, `reduce`, and other Enumerable methods may be used instead of *run*. `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.
|
|
52
57
|
|
|
53
58
|
## Basic eager loading
|
|
54
59
|
|
|
55
|
-
Eager loading is similiar to ActiveRecord's `preload` (each association is loaded in a separate query). Nested associations use blocks instead of Hashes.
|
|
60
|
+
Eager loading is similiar to ActiveRecord's `preload` (each association is loaded in a separate query). Nested associations use blocks instead of Hashes. If you try to use an association you didn't eager load an exception will be raised.
|
|
56
61
|
|
|
57
62
|
```ruby
|
|
58
63
|
orders = OccamsRecord.
|
|
@@ -60,6 +65,7 @@ orders = OccamsRecord.
|
|
|
60
65
|
eager_load(:customer).
|
|
61
66
|
eager_load(:line_items) {
|
|
62
67
|
eager_load(:product)
|
|
68
|
+
eager_load(:something_else)
|
|
63
69
|
}.
|
|
64
70
|
run
|
|
65
71
|
|
|
@@ -80,33 +86,34 @@ Occams Record allows you to customize each eager load query using the full power
|
|
|
80
86
|
```ruby
|
|
81
87
|
orders = OccamsRecord.
|
|
82
88
|
query(q).
|
|
83
|
-
# Only SELECT
|
|
89
|
+
# Only SELECT the columns you need. Your DBA will thank you.
|
|
84
90
|
eager_load(:customer, select: "id, name").
|
|
85
91
|
|
|
86
|
-
# A Proc can customize the query using any of ActiveRecord's query
|
|
87
|
-
# any scopes you've defined on the LineItem model.
|
|
92
|
+
# A Proc can customize the query using any of ActiveRecord's query
|
|
93
|
+
# builders and any scopes you've defined on the LineItem model.
|
|
88
94
|
eager_load(:line_items, ->(q) { q.active.order("created_at") }) {
|
|
89
95
|
eager_load(:product)
|
|
96
|
+
eager_load(:something_else)
|
|
90
97
|
}.
|
|
91
98
|
run
|
|
92
99
|
```
|
|
93
100
|
|
|
94
|
-
Occams Record also supports
|
|
101
|
+
Occams Record also supports loading ad hoc associations using raw SQL. We'll get to that in the next section.
|
|
95
102
|
|
|
96
103
|
## Raw SQL queries
|
|
97
104
|
|
|
98
|
-
ActiveRecord has raw SQL
|
|
105
|
+
ActiveRecord has raw SQL escape hatches like `find_by_sql` and `exec_query`, but they give up critical features like eager loading and `find_each`/`find_in_batches`. Occams Record's escape hatches don't make you give up anything.
|
|
99
106
|
|
|
100
107
|
**Batched loading**
|
|
101
108
|
|
|
102
|
-
To use `find_each`/`find_in_batches` you must provide the limit and offset statements yourself
|
|
109
|
+
To use `find_each`/`find_in_batches` you must provide the limit and offset statements yourself; Occams will provide the values. Also, notice that the binding syntax is a bit different (it uses Ruby's built-in named string substitution).
|
|
103
110
|
|
|
104
111
|
```ruby
|
|
105
112
|
OccamsRecord.
|
|
106
113
|
sql(%(
|
|
107
114
|
SELECT * FROM orders
|
|
108
115
|
WHERE order_date > %{date}
|
|
109
|
-
ORDER BY order_date DESC
|
|
116
|
+
ORDER BY order_date DESC, id
|
|
110
117
|
LIMIT %{batch_limit}
|
|
111
118
|
OFFSET %{batch_offset}
|
|
112
119
|
), {
|
|
@@ -119,14 +126,14 @@ OccamsRecord.
|
|
|
119
126
|
|
|
120
127
|
**Eager loading**
|
|
121
128
|
|
|
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)
|
|
129
|
+
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.)
|
|
123
130
|
|
|
124
131
|
```ruby
|
|
125
132
|
orders = OccamsRecord.
|
|
126
133
|
sql(%(
|
|
127
134
|
SELECT * FROM orders
|
|
128
135
|
WHERE order_date > %{date}
|
|
129
|
-
ORDER BY order_date DESC
|
|
136
|
+
ORDER BY order_date DESC, id
|
|
130
137
|
), {
|
|
131
138
|
date: 30.days.ago
|
|
132
139
|
}).
|
|
@@ -153,7 +160,7 @@ products_with_orders = OccamsRecord.
|
|
|
153
160
|
}
|
|
154
161
|
```
|
|
155
162
|
|
|
156
|
-
But that's very wasteful. Occams gives us
|
|
163
|
+
But that's very wasteful. Occams gives us better options: `eager_load_many` and `eager_load_one`.
|
|
157
164
|
|
|
158
165
|
```ruby
|
|
159
166
|
products = OccamsRecord.
|
|
@@ -174,7 +181,7 @@ products = OccamsRecord.
|
|
|
174
181
|
|
|
175
182
|
`eager_load_one` defines an ad hoc `has_one`/`belongs_to` association.
|
|
176
183
|
|
|
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
|
|
184
|
+
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 isn't necessary when using these methods.
|
|
178
185
|
|
|
179
186
|
## Injecting instance methods
|
|
180
187
|
|
|
@@ -207,10 +214,11 @@ orders = OccamsRecord.
|
|
|
207
214
|
|
|
208
215
|
The following ActiveRecord features are under consideration, but not high priority. Pull requests welcome!
|
|
209
216
|
|
|
210
|
-
*
|
|
217
|
+
* Eager loading `through` associations that involve a `has_and_belongs_to_many`.
|
|
211
218
|
|
|
212
219
|
The following ActiveRecord features are not supported, and likely never will be. Pull requests are still welcome, though.
|
|
213
220
|
|
|
221
|
+
* Eager loading `through` associations that involve a polymorphic association.
|
|
214
222
|
* ActiveRecord enum types
|
|
215
223
|
* ActiveRecord serialized types
|
|
216
224
|
|
|
@@ -3,126 +3,34 @@ module OccamsRecord
|
|
|
3
3
|
# Contains eager loaders for various kinds of associations.
|
|
4
4
|
#
|
|
5
5
|
module EagerLoaders
|
|
6
|
+
autoload :Builder, 'occams-record/eager_loaders/builder'
|
|
7
|
+
autoload :Context, 'occams-record/eager_loaders/context'
|
|
8
|
+
|
|
6
9
|
autoload :Base, 'occams-record/eager_loaders/base'
|
|
7
10
|
autoload :BelongsTo, 'occams-record/eager_loaders/belongs_to'
|
|
8
11
|
autoload :PolymorphicBelongsTo, 'occams-record/eager_loaders/polymorphic_belongs_to'
|
|
9
12
|
autoload :HasOne, 'occams-record/eager_loaders/has_one'
|
|
10
13
|
autoload :HasMany, 'occams-record/eager_loaders/has_many'
|
|
11
14
|
autoload :Habtm, 'occams-record/eager_loaders/habtm'
|
|
15
|
+
autoload :Through, 'occams-record/eager_loaders/through'
|
|
12
16
|
|
|
13
17
|
autoload :AdHocBase, 'occams-record/eager_loaders/ad_hoc_base'
|
|
14
18
|
autoload :AdHocOne, 'occams-record/eager_loaders/ad_hoc_one'
|
|
15
19
|
autoload :AdHocMany, 'occams-record/eager_loaders/ad_hoc_many'
|
|
16
20
|
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# @return [OccamsRecord::Query] returns self
|
|
31
|
-
#
|
|
32
|
-
def eager_load(assoc, scope = nil, select: nil, use: nil, as: nil, &eval_block)
|
|
33
|
-
ref = @model ? @model.reflections[assoc.to_s] : nil
|
|
34
|
-
ref ||= @model.subclasses.map(&:reflections).detect { |x| x.has_key? assoc.to_s }&.[](assoc.to_s) if @model
|
|
35
|
-
raise "OccamsRecord: No assocation `:#{assoc}` on `#{@model&.name || '<model missing>'}` or subclasses" if ref.nil?
|
|
36
|
-
scope ||= ->(q) { q.select select } if select
|
|
37
|
-
@eager_loaders << eager_loader_for_association(ref).new(ref, scope, use: use, as: as, &eval_block)
|
|
38
|
-
self
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
#
|
|
42
|
-
# Specify some arbitrary SQL to be loaded into some arbitrary attribute ("name"). The attribute will
|
|
43
|
-
# hold either one record or none.
|
|
44
|
-
#
|
|
45
|
-
# In the example below, :category is NOT an association on Widget. Though if it where it would be a belongs_to. The
|
|
46
|
-
# mapping argument says "The id column in this table (categories) maps to the category_id column in the other table (widgets)".
|
|
47
|
-
# The %{ids} bind param will be provided for you, and in this case will be all the category_id values from the main
|
|
48
|
-
# query.
|
|
49
|
-
#
|
|
50
|
-
# res = OccamsRecord.
|
|
51
|
-
# query(Widget.order("name")).
|
|
52
|
-
# eager_load_one(:category, {:id => :category_id}, %(
|
|
53
|
-
# SELECT * FROM categories WHERE id IN (%{ids}) AND name != %{bad_name}
|
|
54
|
-
# ), binds: {
|
|
55
|
-
# bad_name: "Bad Category"
|
|
56
|
-
# }).
|
|
57
|
-
# run
|
|
58
|
-
#
|
|
59
|
-
# @param name [Symbol] name of attribute to load records into
|
|
60
|
-
# @param mapping [Hash] a one element Hash with the key being the local/child id and the value being the foreign/parent id
|
|
61
|
-
# @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
|
|
62
|
-
# @param binds [Hash] any additional binds for your query.
|
|
63
|
-
# @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
|
|
64
|
-
# @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
|
|
65
|
-
# @yield eager load associations nested under this one
|
|
66
|
-
#
|
|
67
|
-
def eager_load_one(name, mapping, sql, binds: {}, model: nil, use: nil, &eval_block)
|
|
68
|
-
@eager_loaders << EagerLoaders::AdHocOne.new(name, mapping, sql, binds: binds, model: model, use: use, &eval_block)
|
|
69
|
-
self
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
#
|
|
73
|
-
# Specify some arbitrary SQL to be loaded into some arbitrary attribute ("name"). The attribute will
|
|
74
|
-
# hold an array of 0 or more associated records.
|
|
75
|
-
#
|
|
76
|
-
# In the example below, :parts is NOT an association on Widget. Though if it where it would be a has_many. The
|
|
77
|
-
# mapping argument says "The widget_id column in this table (parts) maps to the id column in the other table (widgets)".
|
|
78
|
-
# The %{ids} bind param will be provided for you, and in this case will be all the id values from the main
|
|
79
|
-
# query.
|
|
80
|
-
#
|
|
81
|
-
# res = OccamsRecord.
|
|
82
|
-
# query(Widget.order("name")).
|
|
83
|
-
# eager_load_many(:parts, {:widget_id => :id}, %(
|
|
84
|
-
# SELECT * FROM parts WHERE widget_id IN (%{ids}) AND sku NOT IN (%{bad_skus})
|
|
85
|
-
# ), binds: {
|
|
86
|
-
# bad_skus: ["G90023ASDf0"]
|
|
87
|
-
# }).
|
|
88
|
-
# run
|
|
89
|
-
#
|
|
90
|
-
# @param name [Symbol] name of attribute to load records into
|
|
91
|
-
# @param mapping [Hash] a one element Hash with the key being the local/child id and the value being the foreign/parent id
|
|
92
|
-
# @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
|
|
93
|
-
# @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
|
|
94
|
-
# @param binds [Hash] any additional binds for your query.
|
|
95
|
-
# @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
|
|
96
|
-
# @yield eager load associations nested under this one
|
|
97
|
-
#
|
|
98
|
-
def eager_load_many(name, mapping, sql, binds: {}, model: nil, use: nil, &eval_block)
|
|
99
|
-
@eager_loaders << EagerLoaders::AdHocMany.new(name, mapping, sql, binds: binds, model: model, use: use, &eval_block)
|
|
100
|
-
self
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
private
|
|
104
|
-
|
|
105
|
-
# Run all defined eager loaders into the given result rows
|
|
106
|
-
def eager_load!(rows)
|
|
107
|
-
@eager_loaders.each { |loader|
|
|
108
|
-
loader.run(rows, query_logger: @query_logger)
|
|
109
|
-
}
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Fetch the appropriate eager loader for the given association type.
|
|
113
|
-
def eager_loader_for_association(ref)
|
|
114
|
-
case ref.macro
|
|
115
|
-
when :belongs_to
|
|
116
|
-
ref.options[:polymorphic] ? PolymorphicBelongsTo : BelongsTo
|
|
117
|
-
when :has_one
|
|
118
|
-
HasOne
|
|
119
|
-
when :has_many
|
|
120
|
-
HasMany
|
|
121
|
-
when :has_and_belongs_to_many
|
|
122
|
-
Habtm
|
|
123
|
-
else
|
|
124
|
-
raise "Unsupported association type `#{macro}`"
|
|
125
|
-
end
|
|
21
|
+
# Fetch the appropriate eager loader for the given association type.
|
|
22
|
+
def self.fetch!(ref)
|
|
23
|
+
case ref.macro
|
|
24
|
+
when :belongs_to
|
|
25
|
+
ref.polymorphic? ? PolymorphicBelongsTo : BelongsTo
|
|
26
|
+
when :has_one
|
|
27
|
+
HasOne
|
|
28
|
+
when :has_many
|
|
29
|
+
HasMany
|
|
30
|
+
when :has_and_belongs_to_many
|
|
31
|
+
Habtm
|
|
32
|
+
else
|
|
33
|
+
raise "Unsupported association type `#{macro}`"
|
|
126
34
|
end
|
|
127
35
|
end
|
|
128
36
|
end
|
|
@@ -4,6 +4,8 @@ module OccamsRecord
|
|
|
4
4
|
# Base class for eager loading ad hoc associations.
|
|
5
5
|
#
|
|
6
6
|
class AdHocBase
|
|
7
|
+
include EagerLoaders::Builder
|
|
8
|
+
|
|
7
9
|
# @return [String] association name
|
|
8
10
|
attr_reader :name
|
|
9
11
|
|
|
@@ -18,12 +20,14 @@ module OccamsRecord
|
|
|
18
20
|
# @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
|
|
19
21
|
# @yield eager load associations nested under this one
|
|
20
22
|
#
|
|
21
|
-
def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, &
|
|
23
|
+
def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, &builder)
|
|
22
24
|
@name = name.to_s
|
|
23
|
-
@sql, @binds, @use, @model
|
|
25
|
+
@sql, @binds, @use, @model = sql, binds, use, model
|
|
24
26
|
raise ArgumentError, "Add-hoc eager loading mapping must contain exactly one key-value pair" unless mapping.size == 1
|
|
25
27
|
@local_key = mapping.keys.first
|
|
26
28
|
@foreign_key = mapping.fetch(@local_key)
|
|
29
|
+
@eager_loaders = EagerLoaders::Context.new(@model)
|
|
30
|
+
instance_eval(&builder) if builder
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
#
|
|
@@ -36,7 +40,7 @@ module OccamsRecord
|
|
|
36
40
|
calc_ids(rows) { |ids|
|
|
37
41
|
assoc = if ids.any?
|
|
38
42
|
binds = @binds.merge({:ids => ids})
|
|
39
|
-
RawQuery.new(@sql, binds, use: @use, query_logger: query_logger
|
|
43
|
+
RawQuery.new(@sql, binds, use: @use, eager_loaders: @eager_loaders, query_logger: query_logger).run
|
|
40
44
|
else
|
|
41
45
|
[]
|
|
42
46
|
end
|
|
@@ -4,6 +4,8 @@ module OccamsRecord
|
|
|
4
4
|
# Base class for eagoer loading an association. IMPORTANT eager loaders MUST remain stateless after initialization!
|
|
5
5
|
#
|
|
6
6
|
class Base
|
|
7
|
+
include EagerLoaders::Builder
|
|
8
|
+
|
|
7
9
|
# @return [String] association name
|
|
8
10
|
attr_reader :name
|
|
9
11
|
|
|
@@ -13,12 +15,16 @@ module OccamsRecord
|
|
|
13
15
|
# ActiveRecord::Relation on which you may call all the normal query hethods (select, where, etc) as well as any scopes you've defined on the model.
|
|
14
16
|
# @param use [Array(Module)] optional Module to include in the result class (single or array)
|
|
15
17
|
# @param as [Symbol] Load the association usign a different attribute name
|
|
18
|
+
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
|
16
19
|
# @yield perform eager loading on *this* association (optional)
|
|
17
20
|
#
|
|
18
|
-
def initialize(ref, scope = nil, use: nil, as: nil, &
|
|
19
|
-
@ref, @scope, @use, @as
|
|
21
|
+
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: :select, &builder)
|
|
22
|
+
@ref, @scope, @use, @as = ref, scope, use, as
|
|
20
23
|
@model = ref.klass
|
|
21
24
|
@name = (as || ref.name).to_s
|
|
25
|
+
@eager_loaders = EagerLoaders::Context.new(@model)
|
|
26
|
+
@optimizer = optimizer
|
|
27
|
+
instance_eval(&builder) if builder
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
#
|
|
@@ -29,7 +35,7 @@ module OccamsRecord
|
|
|
29
35
|
#
|
|
30
36
|
def run(rows, query_logger: nil)
|
|
31
37
|
query(rows) { |*args|
|
|
32
|
-
assoc_rows = args[0] ? Query.new(args[0], use: @use, query_logger: query_logger
|
|
38
|
+
assoc_rows = args[0] ? Query.new(args[0], use: @use, eager_loaders: @eager_loaders, query_logger: query_logger).run : []
|
|
33
39
|
merge! assoc_rows, rows, *args[1..-1]
|
|
34
40
|
}
|
|
35
41
|
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module OccamsRecord
|
|
2
|
+
module EagerLoaders
|
|
3
|
+
#
|
|
4
|
+
# Methods for adding eager loading to a query.
|
|
5
|
+
#
|
|
6
|
+
# Users MUST have an OccamsRecord::EagerLoaders::Context at @eager_loaders.
|
|
7
|
+
#
|
|
8
|
+
module Builder
|
|
9
|
+
#
|
|
10
|
+
# Specify an association to be eager-loaded. For maximum memory savings, only SELECT the
|
|
11
|
+
# colums you actually need.
|
|
12
|
+
#
|
|
13
|
+
# @param assoc [Symbol] name of association
|
|
14
|
+
# @param scope [Proc] a scope to apply to the query (optional). It will be passed an
|
|
15
|
+
# ActiveRecord::Relation on which you may call all the normal query hethods (select, where, etc) as well as any scopes you've defined on the model.
|
|
16
|
+
# @param select [String] a custom SELECT statement, minus the SELECT (optional)
|
|
17
|
+
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
|
18
|
+
# @param as [Symbol] Load the association usign a different attribute name
|
|
19
|
+
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
|
20
|
+
# @yield a block where you may perform eager loading on *this* association (optional)
|
|
21
|
+
# @return [OccamsRecord::Query] returns self
|
|
22
|
+
#
|
|
23
|
+
def eager_load(assoc, scope = nil, select: nil, use: nil, as: nil, optimizer: :select, &builder)
|
|
24
|
+
@eager_loaders.add(assoc, scope, select: select, use: use, as: as, optimizer: optimizer, &builder)
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# Same as eager_load, except it returns the new eager loader object instead of self. You can use the
|
|
30
|
+
# new object to call "nest" again, programtically building up nested eager loads instead of passing
|
|
31
|
+
# nested blocks.
|
|
32
|
+
#
|
|
33
|
+
# @param assoc [Symbol] name of association
|
|
34
|
+
# @param scope [Proc] a scope to apply to the query (optional). It will be passed an
|
|
35
|
+
# ActiveRecord::Relation on which you may call all the normal query hethods (select, where, etc) as well as any scopes you've defined on the model.
|
|
36
|
+
# @param select [String] a custom SELECT statement, minus the SELECT (optional)
|
|
37
|
+
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
|
38
|
+
# @param as [Symbol] Load the association usign a different attribute name
|
|
39
|
+
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
|
40
|
+
# @return [OccamsRecord::EagerLoaders::Base] returns self
|
|
41
|
+
#
|
|
42
|
+
#
|
|
43
|
+
def nest(assoc, scope = nil, select: nil, use: nil, as: nil, optimizer: :select)
|
|
44
|
+
raise ArgumentError, "OccamsRecord::EagerLoaders::Builder#nest does not accept a block!" if block_given?
|
|
45
|
+
@eager_loaders.add(assoc, scope, select: select, use: use, as: as, optimizer: optimizer) ||
|
|
46
|
+
raise("OccamsRecord::EagerLoaders::Builder#nest may not be called under a polymorphic association")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
#
|
|
50
|
+
# Specify some arbitrary SQL to be loaded into some arbitrary attribute ("name"). The attribute will
|
|
51
|
+
# hold either one record or none.
|
|
52
|
+
#
|
|
53
|
+
# In the example below, :category is NOT an association on Widget. Though if it where it would be a belongs_to. The
|
|
54
|
+
# mapping argument says "The id column in this table (categories) maps to the category_id column in the other table (widgets)".
|
|
55
|
+
# The %{ids} bind param will be provided for you, and in this case will be all the category_id values from the main
|
|
56
|
+
# query.
|
|
57
|
+
#
|
|
58
|
+
# res = OccamsRecord.
|
|
59
|
+
# query(Widget.order("name")).
|
|
60
|
+
# eager_load_one(:category, {:id => :category_id}, %(
|
|
61
|
+
# SELECT * FROM categories WHERE id IN (%{ids}) AND name != %{bad_name}
|
|
62
|
+
# ), binds: {
|
|
63
|
+
# bad_name: "Bad Category"
|
|
64
|
+
# }).
|
|
65
|
+
# run
|
|
66
|
+
#
|
|
67
|
+
# @param name [Symbol] name of attribute to load records into
|
|
68
|
+
# @param mapping [Hash] a one element Hash with the key being the local/child id and the value being the foreign/parent id
|
|
69
|
+
# @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
|
|
70
|
+
# @param binds [Hash] any additional binds for your query.
|
|
71
|
+
# @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
|
|
72
|
+
# @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
|
|
73
|
+
# @yield eager load associations nested under this one
|
|
74
|
+
#
|
|
75
|
+
def eager_load_one(name, mapping, sql, binds: {}, model: nil, use: nil, &builder)
|
|
76
|
+
@eager_loaders << EagerLoaders::AdHocOne.new(name, mapping, sql, binds: binds, model: model, use: use, &builder)
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#
|
|
81
|
+
# Specify some arbitrary SQL to be loaded into some arbitrary attribute ("name"). The attribute will
|
|
82
|
+
# hold an array of 0 or more associated records.
|
|
83
|
+
#
|
|
84
|
+
# In the example below, :parts is NOT an association on Widget. Though if it where it would be a has_many. The
|
|
85
|
+
# mapping argument says "The widget_id column in this table (parts) maps to the id column in the other table (widgets)".
|
|
86
|
+
# The %{ids} bind param will be provided for you, and in this case will be all the id values from the main
|
|
87
|
+
# query.
|
|
88
|
+
#
|
|
89
|
+
# res = OccamsRecord.
|
|
90
|
+
# query(Widget.order("name")).
|
|
91
|
+
# eager_load_many(:parts, {:widget_id => :id}, %(
|
|
92
|
+
# SELECT * FROM parts WHERE widget_id IN (%{ids}) AND sku NOT IN (%{bad_skus})
|
|
93
|
+
# ), binds: {
|
|
94
|
+
# bad_skus: ["G90023ASDf0"]
|
|
95
|
+
# }).
|
|
96
|
+
# run
|
|
97
|
+
#
|
|
98
|
+
# @param name [Symbol] name of attribute to load records into
|
|
99
|
+
# @param mapping [Hash] a one element Hash with the key being the local/child id and the value being the foreign/parent id
|
|
100
|
+
# @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
|
|
101
|
+
# @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
|
|
102
|
+
# @param binds [Hash] any additional binds for your query.
|
|
103
|
+
# @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
|
|
104
|
+
# @yield eager load associations nested under this one
|
|
105
|
+
#
|
|
106
|
+
def eager_load_many(name, mapping, sql, binds: {}, model: nil, use: nil, &builder)
|
|
107
|
+
@eager_loaders << EagerLoaders::AdHocMany.new(name, mapping, sql, binds: binds, model: model, use: use, &builder)
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module OccamsRecord
|
|
2
|
+
module EagerLoaders
|
|
3
|
+
#
|
|
4
|
+
# A container for all eager loading on a particular Active Record model. Usually the context is initialized
|
|
5
|
+
# with the model, and all eager loaders are immediately initialized. Any errors (like a wrong association name
|
|
6
|
+
# ) will be thrown immediately and before any queries are run.
|
|
7
|
+
#
|
|
8
|
+
# However, in certain situations the model cannot be known until runtime (e.g. eager loading off of a
|
|
9
|
+
# polymorphic association). In these cases the model won't be set, or the eager loaders fully initialized,
|
|
10
|
+
# until the parent queries have run. This means that certain errors (like a wrong association name) won't be
|
|
11
|
+
# noticed until very late, after queries have started running.
|
|
12
|
+
#
|
|
13
|
+
class Context
|
|
14
|
+
# @return [ActiveRecord::Base]
|
|
15
|
+
attr_reader :model
|
|
16
|
+
|
|
17
|
+
#
|
|
18
|
+
# Initialize a new eager loading context.
|
|
19
|
+
#
|
|
20
|
+
# @param mode [ActiveRecord::Base] the model that contains the associations that will be referenced.
|
|
21
|
+
#
|
|
22
|
+
def initialize(model = nil)
|
|
23
|
+
@model = model
|
|
24
|
+
@loaders = []
|
|
25
|
+
@dynamic_loaders = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#
|
|
29
|
+
# Set the model.
|
|
30
|
+
#
|
|
31
|
+
# @param model [ActiveRecord::Base]
|
|
32
|
+
#
|
|
33
|
+
def model=(model)
|
|
34
|
+
@model = model
|
|
35
|
+
@loaders = @loaders + @dynamic_loaders.map { |args|
|
|
36
|
+
build_loader(*args)
|
|
37
|
+
}
|
|
38
|
+
@dynamic_loaders = []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#
|
|
42
|
+
# Return the names of the associations being loaded.
|
|
43
|
+
#
|
|
44
|
+
# @return [Array<String>]
|
|
45
|
+
#
|
|
46
|
+
def names
|
|
47
|
+
@loaders.map(&:name) +
|
|
48
|
+
@loaders.select { |l| l.respond_to? :through_name }.map(&:through_name) # TODO make not hacky
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#
|
|
52
|
+
# Append an already-initialized eager loader.
|
|
53
|
+
#
|
|
54
|
+
# @param loader [OccamsRecord::EagerLoaders::Base]
|
|
55
|
+
# @return [OccamsRecord::EagerLoaders::Base] the added loader
|
|
56
|
+
#
|
|
57
|
+
def <<(loader)
|
|
58
|
+
@loaders << loader
|
|
59
|
+
loader
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#
|
|
63
|
+
# Specify an association to be eager-loaded. For maximum memory savings, only SELECT the
|
|
64
|
+
# colums you actually need.
|
|
65
|
+
#
|
|
66
|
+
# @param assoc [Symbol] name of association
|
|
67
|
+
# @param scope [Proc] a scope to apply to the query (optional). It will be passed an
|
|
68
|
+
# ActiveRecord::Relation on which you may call all the normal query hethods (select, where, etc) as well as any scopes you've defined on the model.
|
|
69
|
+
# @param select [String] a custom SELECT statement, minus the SELECT (optional)
|
|
70
|
+
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
|
71
|
+
# @param as [Symbol] Load the association usign a different attribute name
|
|
72
|
+
# @param optimizer [Symbol] Only used for `through` associations. Options are :none (load all intermediate records) | :select (load all intermediate records but only SELECT the necessary columns)
|
|
73
|
+
# @yield a block where you may perform eager loading on *this* association (optional)
|
|
74
|
+
# @return [OccamsRecord::EagerLoaders::Base] the new loader. if @model is nil, nil will be returned.
|
|
75
|
+
#
|
|
76
|
+
def add(assoc, scope = nil, select: nil, use: nil, as: nil, optimizer: :select, &builder)
|
|
77
|
+
if @model
|
|
78
|
+
loader = build_loader(assoc, scope, select, use, as, optimizer, builder)
|
|
79
|
+
@loaders << loader
|
|
80
|
+
loader
|
|
81
|
+
else
|
|
82
|
+
@dynamic_loaders << [assoc, scope, select, use, as, optimizer, builder]
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
#
|
|
88
|
+
# Performs all eager loading in this context (and in any nested ones).
|
|
89
|
+
#
|
|
90
|
+
# @param rows [Array<ActiveRecord::Base>] the parent rows to load child rows into
|
|
91
|
+
# @param query_logger [Array] optional query logger
|
|
92
|
+
#
|
|
93
|
+
def run!(rows, query_logger: nil)
|
|
94
|
+
raise "Cannot run eager loaders when @model has not been set!" if @dynamic_loaders.any? and @model.nil?
|
|
95
|
+
@loaders.each { |loader|
|
|
96
|
+
loader.run(rows, query_logger: query_logger)
|
|
97
|
+
}
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def build_loader(assoc, scope, select, use, as, optimizer, builder)
|
|
104
|
+
ref = @model.reflections[assoc.to_s]
|
|
105
|
+
ref ||= @model.subclasses.map(&:reflections).detect { |x| x.has_key? assoc.to_s }&.[](assoc.to_s)
|
|
106
|
+
raise "OccamsRecord: No assocation `:#{assoc}` on `#{@model.name}` or subclasses" if ref.nil?
|
|
107
|
+
scope ||= ->(q) { q.select select } if select
|
|
108
|
+
loader_class = !!ref.through_reflection ? EagerLoaders::Through : EagerLoaders.fetch!(ref)
|
|
109
|
+
loader_class.new(ref, scope, use: use, as: as, optimizer: optimizer, &builder)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -2,6 +2,8 @@ module OccamsRecord
|
|
|
2
2
|
module EagerLoaders
|
|
3
3
|
# Eager loader for polymorphic belongs tos
|
|
4
4
|
class PolymorphicBelongsTo
|
|
5
|
+
include EagerLoaders::Builder
|
|
6
|
+
|
|
5
7
|
# @return [String] association name
|
|
6
8
|
attr_reader :name
|
|
7
9
|
|
|
@@ -11,13 +13,16 @@ module OccamsRecord
|
|
|
11
13
|
# ActiveRecord::Relation on which you may call all the normal query hethods (select, where, etc) as well as any scopes you've defined on the model.
|
|
12
14
|
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
|
13
15
|
# @param as [Symbol] Load the association usign a different attribute name
|
|
16
|
+
# @param optimizer [Symbol] Only used for `through` associations. A no op here.
|
|
14
17
|
# @yield perform eager loading on *this* association (optional)
|
|
15
18
|
#
|
|
16
|
-
def initialize(ref, scope = nil, use: nil, as: nil, &
|
|
17
|
-
@ref, @scope, @use
|
|
19
|
+
def initialize(ref, scope = nil, use: nil, as: nil, optimizer: nil, &builder)
|
|
20
|
+
@ref, @scope, @use = ref, scope, use
|
|
18
21
|
@name = (as || ref.name).to_s
|
|
19
22
|
@foreign_type = @ref.foreign_type.to_sym
|
|
20
23
|
@foreign_key = @ref.foreign_key.to_sym
|
|
24
|
+
@eager_loaders = EagerLoaders::Context.new
|
|
25
|
+
instance_eval(&builder) if builder
|
|
21
26
|
end
|
|
22
27
|
|
|
23
28
|
#
|
|
@@ -28,7 +33,9 @@ module OccamsRecord
|
|
|
28
33
|
#
|
|
29
34
|
def run(rows, query_logger: nil)
|
|
30
35
|
query(rows) { |scope|
|
|
31
|
-
|
|
36
|
+
eager_loaders = @eager_loaders.dup
|
|
37
|
+
eager_loaders.model = scope.klass
|
|
38
|
+
assoc_rows = Query.new(scope, use: @use, eager_loaders: eager_loaders, query_logger: query_logger).run
|
|
32
39
|
merge! assoc_rows, rows
|
|
33
40
|
}
|
|
34
41
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module OccamsRecord
|
|
2
|
+
module EagerLoaders
|
|
3
|
+
#
|
|
4
|
+
# Handles :through associations for has_many and has_one. Polymorphic associations are not supported.
|
|
5
|
+
#
|
|
6
|
+
class Through < Base
|
|
7
|
+
Link = Struct.new(:name, :macro, :ref, :next_ref)
|
|
8
|
+
|
|
9
|
+
#
|
|
10
|
+
# See documentation for OccamsRecord::EagerLoaders::Base.
|
|
11
|
+
#
|
|
12
|
+
def initialize(*args)
|
|
13
|
+
super
|
|
14
|
+
|
|
15
|
+
unless @ref.macro == :has_one or @ref.macro == :has_many
|
|
16
|
+
raise ArgumentError, "#{@ref.active_record.name}##{@ref.name} cannot be eager loaded because only `has_one` and `has_many` are supported for `through` associations"
|
|
17
|
+
end
|
|
18
|
+
if (polys = @ref.chain.select(&:polymorphic?)).any?
|
|
19
|
+
names = polys.map { |r| "#{r.active_record.name}##{r.name}" }
|
|
20
|
+
raise ArgumentError, "#{@ref.active_record.name}##{@ref.name} cannot be eager loaded because these `through` associations are polymorphic: #{names.join ', '}"
|
|
21
|
+
end
|
|
22
|
+
unless @optimizer == :none or @optimizer == :select
|
|
23
|
+
raise ArgumentError, "Unrecognized optimizer '#{@optimizer}'"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
chain = @ref.chain.reverse
|
|
27
|
+
@chain = chain.each_with_index.map { |x, i|
|
|
28
|
+
Link.new(x.source_reflection.name, x.source_reflection.macro, x, chain[i + 1])
|
|
29
|
+
}
|
|
30
|
+
@loader = build_loader
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# TODO make not hacky
|
|
34
|
+
def through_name
|
|
35
|
+
@loader.name
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def run(rows, query_logger: nil)
|
|
39
|
+
results = @loader.run(rows, query_logger: query_logger)
|
|
40
|
+
attr_set = "#{name}="
|
|
41
|
+
results.each do |row|
|
|
42
|
+
row.send(attr_set, reduce(row))
|
|
43
|
+
end
|
|
44
|
+
results
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def reduce(node, depth = 0)
|
|
50
|
+
link = @chain[depth]
|
|
51
|
+
case link&.macro
|
|
52
|
+
when nil
|
|
53
|
+
node
|
|
54
|
+
when :has_many
|
|
55
|
+
node.send(link.name).reduce(Set.new) { |a, child|
|
|
56
|
+
result = reduce(child, depth + 1)
|
|
57
|
+
case result
|
|
58
|
+
when Array then a + result
|
|
59
|
+
else a << result
|
|
60
|
+
end
|
|
61
|
+
}.to_a
|
|
62
|
+
when :has_one, :belongs_to
|
|
63
|
+
child = node.send(link.name)
|
|
64
|
+
reduce(child, depth + 1)
|
|
65
|
+
else
|
|
66
|
+
raise "Unsupported through chain link type '#{link.macro}'"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_loader
|
|
71
|
+
head = @chain[0]
|
|
72
|
+
links = @chain[1..-2]
|
|
73
|
+
tail = @chain[-1]
|
|
74
|
+
|
|
75
|
+
outer_loader = EagerLoaders.fetch!(head.ref).new(head.ref, optimized_select(head))
|
|
76
|
+
|
|
77
|
+
links.
|
|
78
|
+
reduce(outer_loader) { |loader, link|
|
|
79
|
+
loader.nest(link.ref.source_reflection.name, optimized_select(link))
|
|
80
|
+
}.
|
|
81
|
+
nest(tail.ref.source_reflection.name, @scope, use: @use, as: @as)
|
|
82
|
+
|
|
83
|
+
outer_loader
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def optimized_select(link)
|
|
87
|
+
return nil unless @optimizer == :select
|
|
88
|
+
|
|
89
|
+
cols = case link.macro
|
|
90
|
+
when :belongs_to
|
|
91
|
+
[link.ref.association_primary_key]
|
|
92
|
+
when :has_one, :has_many
|
|
93
|
+
[link.ref.association_primary_key, link.ref.foreign_key]
|
|
94
|
+
else
|
|
95
|
+
raise "Unsupported through chain link type '#{link.macro}'"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
case link.next_ref.source_reflection.macro
|
|
99
|
+
when :belongs_to
|
|
100
|
+
cols << link.next_ref.foreign_key
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
->(q) { q.select(cols.join(", ")) }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/occams-record/query.rb
CHANGED
|
@@ -46,16 +46,14 @@ module OccamsRecord
|
|
|
46
46
|
# @param scope [ActiveRecord::Relation]
|
|
47
47
|
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
|
48
48
|
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
|
49
|
-
# @param eager_loaders [OccamsRecord::EagerLoaders::
|
|
50
|
-
# @yield will be eval'd on this instance. Can be used for eager loading. (optional)
|
|
49
|
+
# @param eager_loaders [OccamsRecord::EagerLoaders::Context]
|
|
51
50
|
#
|
|
52
|
-
def initialize(scope, use: nil,
|
|
51
|
+
def initialize(scope, use: nil, eager_loaders: nil, query_logger: nil)
|
|
53
52
|
@model = scope.klass
|
|
54
53
|
@scope = scope
|
|
55
|
-
@eager_loaders = eager_loaders
|
|
54
|
+
@eager_loaders = eager_loaders || EagerLoaders::Context.new(@model)
|
|
56
55
|
@use = use
|
|
57
56
|
@query_logger = query_logger
|
|
58
|
-
instance_eval(&eval_block) if eval_block
|
|
59
57
|
end
|
|
60
58
|
|
|
61
59
|
#
|
|
@@ -82,9 +80,9 @@ module OccamsRecord
|
|
|
82
80
|
sql = block_given? ? yield(scope).to_sql : scope.to_sql
|
|
83
81
|
@query_logger << sql if @query_logger
|
|
84
82
|
result = model.connection.exec_query sql
|
|
85
|
-
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.
|
|
83
|
+
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: model, modules: @use)
|
|
86
84
|
rows = result.rows.map { |row| row_class.new row }
|
|
87
|
-
|
|
85
|
+
@eager_loaders.run!(rows, query_logger: @query_logger)
|
|
88
86
|
rows
|
|
89
87
|
end
|
|
90
88
|
|
|
@@ -60,18 +60,16 @@ module OccamsRecord
|
|
|
60
60
|
# @param sql [String] The SELECT statement to run. Binds should use Ruby's named string substitution.
|
|
61
61
|
# @param binds [Hash] Bind values (Symbol keys)
|
|
62
62
|
# @param use [Array<Module>] optional Module to include in the result class (single or array)
|
|
63
|
-
# @param eager_loaders [OccamsRecord::EagerLoaders::
|
|
63
|
+
# @param eager_loaders [OccamsRecord::EagerLoaders::Context]
|
|
64
64
|
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
|
65
65
|
#
|
|
66
|
-
def initialize(sql, binds, use: nil, eager_loaders:
|
|
66
|
+
def initialize(sql, binds, use: nil, eager_loaders: nil, query_logger: nil)
|
|
67
67
|
@sql = sql
|
|
68
68
|
@binds = binds
|
|
69
69
|
@use = use
|
|
70
|
-
@eager_loaders = eager_loaders
|
|
70
|
+
@eager_loaders = eager_loaders || EagerLoaders::Context.new
|
|
71
71
|
@query_logger = query_logger
|
|
72
|
-
@
|
|
73
|
-
@conn = @model&.connection || ActiveRecord::Base.connection
|
|
74
|
-
instance_eval(&eval_block) if eval_block
|
|
72
|
+
@conn = @eager_loaders.model&.connection || ActiveRecord::Base.connection
|
|
75
73
|
end
|
|
76
74
|
|
|
77
75
|
#
|
|
@@ -85,7 +83,7 @@ module OccamsRecord
|
|
|
85
83
|
# @return [OccamsRecord::RawQuery] self
|
|
86
84
|
#
|
|
87
85
|
def model(klass)
|
|
88
|
-
@model = klass
|
|
86
|
+
@eager_loaders.model = klass
|
|
89
87
|
self
|
|
90
88
|
end
|
|
91
89
|
|
|
@@ -98,9 +96,9 @@ module OccamsRecord
|
|
|
98
96
|
_escaped_sql = escaped_sql
|
|
99
97
|
@query_logger << _escaped_sql if @query_logger
|
|
100
98
|
result = @conn.exec_query _escaped_sql
|
|
101
|
-
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.
|
|
99
|
+
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: @eager_loaders.model, modules: @use)
|
|
102
100
|
rows = result.rows.map { |row| row_class.new row }
|
|
103
|
-
|
|
101
|
+
@eager_loaders.run!(rows, query_logger: @query_logger)
|
|
104
102
|
rows
|
|
105
103
|
end
|
|
106
104
|
|
|
@@ -154,7 +152,7 @@ module OccamsRecord
|
|
|
154
152
|
results = RawQuery.new(@sql, @binds.merge({
|
|
155
153
|
batch_limit: of,
|
|
156
154
|
batch_offset: offset,
|
|
157
|
-
}), use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders).
|
|
155
|
+
}), use: @use, query_logger: @query_logger, eager_loaders: @eager_loaders).run
|
|
158
156
|
|
|
159
157
|
y.yield results if results.any?
|
|
160
158
|
break if results.size < of
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: occams-record
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.31.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jordan Hollinger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2018-
|
|
11
|
+
date: 2018-08-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -46,10 +46,13 @@ files:
|
|
|
46
46
|
- lib/occams-record/eager_loaders/ad_hoc_one.rb
|
|
47
47
|
- lib/occams-record/eager_loaders/base.rb
|
|
48
48
|
- lib/occams-record/eager_loaders/belongs_to.rb
|
|
49
|
+
- lib/occams-record/eager_loaders/builder.rb
|
|
50
|
+
- lib/occams-record/eager_loaders/context.rb
|
|
49
51
|
- lib/occams-record/eager_loaders/habtm.rb
|
|
50
52
|
- lib/occams-record/eager_loaders/has_many.rb
|
|
51
53
|
- lib/occams-record/eager_loaders/has_one.rb
|
|
52
54
|
- lib/occams-record/eager_loaders/polymorphic_belongs_to.rb
|
|
55
|
+
- lib/occams-record/eager_loaders/through.rb
|
|
53
56
|
- lib/occams-record/errors.rb
|
|
54
57
|
- lib/occams-record/merge.rb
|
|
55
58
|
- lib/occams-record/query.rb
|
|
@@ -76,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
76
79
|
version: '0'
|
|
77
80
|
requirements: []
|
|
78
81
|
rubyforge_project:
|
|
79
|
-
rubygems_version: 2.7.
|
|
82
|
+
rubygems_version: 2.7.6
|
|
80
83
|
signing_key:
|
|
81
84
|
specification_version: 4
|
|
82
85
|
summary: The missing high-efficiency query API for ActiveRecord
|