occams-record 0.14.0 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +23 -0
- data/lib/occams-record/eager_loaders.rb +65 -4
- data/lib/occams-record/eager_loaders/ad_hoc_base.rb +61 -0
- data/lib/occams-record/eager_loaders/ad_hoc_many.rb +21 -0
- data/lib/occams-record/eager_loaders/ad_hoc_one.rb +21 -0
- data/lib/occams-record/eager_loaders/base.rb +15 -4
- data/lib/occams-record/eager_loaders/belongs_to.rb +2 -0
- data/lib/occams-record/eager_loaders/habtm.rb +2 -0
- data/lib/occams-record/eager_loaders/has_many.rb +2 -0
- data/lib/occams-record/eager_loaders/has_one.rb +2 -0
- data/lib/occams-record/eager_loaders/polymorphic_belongs_to.rb +15 -4
- data/lib/occams-record/raw_query.rb +6 -3
- data/lib/occams-record/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8d0dd44c9dd065285ae2a1dfc0c5cd05f27d70b2b611011f128a7692c7afc31
|
4
|
+
data.tar.gz: 0b08d794ce884b340d00b72d531cc3e7e1c0ee4e200eed585c65edf5066e1a4d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7587c4c1d9464ac8a078b417afb4e9f2603419b9ef13ecc05da8496854fcf787bf93330756118e5e9b4696729f47c4c676bc9ad36b6ae4da0222c931c7bace0c
|
7
|
+
data.tar.gz: b897b931d76a8e87eb97a07319975cd57c6cfa355842f0097830dd75db09f22c158edcb40d476b7be2d54d8c3b489e87294bf66bf0147e234f1dbeba18c2b590
|
data/README.md
CHANGED
@@ -11,6 +11,7 @@ Occam's Record is a high-efficiency query API for ActiveRecord. It is **not** an
|
|
11
11
|
* `find_each`/`find_in_batches` respects `order` and `limit`.
|
12
12
|
* Allows eager loading of associations when using raw SQL.
|
13
13
|
* Allows `find_each`/`find_in_batches` when using raw SQL.
|
14
|
+
* Eager load data from arbitrary SQL (no association required).
|
14
15
|
|
15
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:
|
16
17
|
|
@@ -86,6 +87,28 @@ widgets = OccamsRecord.
|
|
86
87
|
run
|
87
88
|
```
|
88
89
|
|
90
|
+
**Eager load using raw SQL without a predefined association**
|
91
|
+
|
92
|
+
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 bunch of stuff we don't care about. What if we could define an ad hoc association using raw SQL? Enter `eager_load_one` and `eager_load_many`! See the full documentation for a full description of all options.
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
widgets = OccamsRecord.
|
96
|
+
query(Widget.order("name")).
|
97
|
+
|
98
|
+
# load the results of the query into "customers", matching "widget_id"
|
99
|
+
# in the results to the "id" field of the widgets
|
100
|
+
eager_load_many(:customers, {:widget_id => :id}, %(
|
101
|
+
SELECT DISTINCT customers.id, customers.name, order_items.widget_id
|
102
|
+
FROM customers
|
103
|
+
INNER JOIN orders ON orders.customer_id = customers.id
|
104
|
+
INNER JOIN order_items ON order_items.order_id = orders.id
|
105
|
+
WHERE order_items.widget_id IN (%{ids})
|
106
|
+
), binds: {
|
107
|
+
# additional bind values (ids will be passed in for you)
|
108
|
+
}).
|
109
|
+
run
|
110
|
+
```
|
111
|
+
|
89
112
|
## Injecting instance methods
|
90
113
|
|
91
114
|
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.
|
@@ -10,6 +10,10 @@ module OccamsRecord
|
|
10
10
|
autoload :HasMany, 'occams-record/eager_loaders/has_many'
|
11
11
|
autoload :Habtm, 'occams-record/eager_loaders/habtm'
|
12
12
|
|
13
|
+
autoload :AdHocBase, 'occams-record/eager_loaders/ad_hoc_base'
|
14
|
+
autoload :AdHocOne, 'occams-record/eager_loaders/ad_hoc_one'
|
15
|
+
autoload :AdHocMany, 'occams-record/eager_loaders/ad_hoc_many'
|
16
|
+
|
13
17
|
# Methods for adding eager loading to a query.
|
14
18
|
module Builder
|
15
19
|
#
|
@@ -34,15 +38,72 @@ module OccamsRecord
|
|
34
38
|
self
|
35
39
|
end
|
36
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
|
+
#
|
66
|
+
def eager_load_one(*args, &eval_block)
|
67
|
+
@eager_loaders << EagerLoaders::AdHocOne.new(*args, &eval_block)
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Specify some arbitrary SQL to be loaded into some arbitrary attribute ("name"). The attribute will
|
73
|
+
# hold an array of 0 or more associated records.
|
74
|
+
#
|
75
|
+
# In the example below, :parts is NOT an association on Widget. Though if it where it would be a has_many. The
|
76
|
+
# mapping argument says "The widget_id column in this table (parts) maps to the id column in the other table (widgets)".
|
77
|
+
# The %{ids} bind param will be provided for you, and in this case will be all the id values from the main
|
78
|
+
# query.
|
79
|
+
#
|
80
|
+
# res = OccamsRecord.
|
81
|
+
# query(Widget.order("name")).
|
82
|
+
# eager_load_many(:parts, {:widget_id => :id}, %(
|
83
|
+
# SELECT * FROM parts WHERE widget_id IN (%{ids}) AND sku NOT IN (%{bad_skus})
|
84
|
+
# ), binds: {
|
85
|
+
# bad_skus: ["G90023ASDf0"]
|
86
|
+
# }).
|
87
|
+
# run
|
88
|
+
#
|
89
|
+
# @param name [Symbol] name of attribute to load records into
|
90
|
+
# @param mapping [Hash] a one element Hash with the key being the local/child id and the value being the foreign/parent id
|
91
|
+
# @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
|
92
|
+
# @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
|
93
|
+
# @param binds [Hash] any additional binds for your query.
|
94
|
+
# @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
|
95
|
+
#
|
96
|
+
def eager_load_many(*args, &eval_block)
|
97
|
+
@eager_loaders << EagerLoaders::AdHocMany.new(*args, &eval_block)
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
37
101
|
private
|
38
102
|
|
39
103
|
# Run all defined eager loaders into the given result rows
|
40
104
|
def eager_load!(rows)
|
41
105
|
@eager_loaders.each { |loader|
|
42
|
-
loader.
|
43
|
-
assoc_rows = Query.new(scope, use: loader.use, query_logger: @query_logger, &loader.eval_block).run
|
44
|
-
loader.merge! assoc_rows, rows
|
45
|
-
}
|
106
|
+
loader.run(rows, query_logger: @query_logger)
|
46
107
|
}
|
47
108
|
end
|
48
109
|
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module OccamsRecord
|
2
|
+
module EagerLoaders
|
3
|
+
#
|
4
|
+
# Base class for eager loading ad hoc associations.
|
5
|
+
#
|
6
|
+
class AdHocBase
|
7
|
+
# @return [String] association name
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
#
|
11
|
+
# Initialize a new add hoc association.
|
12
|
+
#
|
13
|
+
# @param name [Symbol] name of attribute to load records into
|
14
|
+
# @param mapping [Hash] a one element Hash with the key being the local/child id and the value being the foreign/parent id
|
15
|
+
# @param sql [String] the SQL to query the associated records. Include a bind params called '%{ids}' for the foreign/parent ids.
|
16
|
+
# @param binds [Hash] any additional binds for your query.
|
17
|
+
# @param model [ActiveRecord::Base] optional - ActiveRecord model that represents what you're loading. required when using Sqlite.
|
18
|
+
# @param use [Array<Module>] optional - Ruby modules to include in the result objects (single or array)
|
19
|
+
# @yield
|
20
|
+
#
|
21
|
+
def initialize(name, mapping, sql, binds: {}, model: nil, use: nil, &eval_block)
|
22
|
+
@name = name.to_s
|
23
|
+
@sql, @binds, @use, @model, @eval_block = sql, binds, use, model, eval_block
|
24
|
+
raise ArgumentError, "Add-hoc eager loading mapping must contain exactly one key-value pair" unless mapping.size == 1
|
25
|
+
@local_key = mapping.keys.first
|
26
|
+
@foreign_key = mapping.fetch(@local_key)
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Run the query and merge the results into the given rows.
|
31
|
+
#
|
32
|
+
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
33
|
+
# @param query_logger [Array<String>]
|
34
|
+
#
|
35
|
+
def run(rows, query_logger: nil)
|
36
|
+
calc_ids(rows) { |ids|
|
37
|
+
binds = @binds.merge({:ids => ids})
|
38
|
+
assoc_rows = RawQuery.new(@sql, binds, use: @use, query_logger: query_logger, &@eval_block).model(@model).run
|
39
|
+
merge! assoc_rows, rows
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
#
|
46
|
+
# Yield ids from the parent association to a block.
|
47
|
+
#
|
48
|
+
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
49
|
+
# @yield
|
50
|
+
#
|
51
|
+
def calc_ids(rows)
|
52
|
+
ids = rows.map { |r| r.send @foreign_key }.compact.uniq
|
53
|
+
yield ids
|
54
|
+
end
|
55
|
+
|
56
|
+
def merge!(assoc_rows, rows)
|
57
|
+
raise 'Not Implemented'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module OccamsRecord
|
2
|
+
module EagerLoaders
|
3
|
+
#
|
4
|
+
# Eager loader for an ad hoc association of 0 or many records (like has_many).
|
5
|
+
#
|
6
|
+
class AdHocMany < AdHocBase
|
7
|
+
private
|
8
|
+
|
9
|
+
#
|
10
|
+
# Merge the association rows into the given rows.
|
11
|
+
#
|
12
|
+
# @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the associated table
|
13
|
+
# @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main table
|
14
|
+
#
|
15
|
+
def merge!(assoc_rows, rows)
|
16
|
+
Merge.new(rows, name).
|
17
|
+
many!(assoc_rows, @foreign_key.to_s, @local_key.to_s)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module OccamsRecord
|
2
|
+
module EagerLoaders
|
3
|
+
#
|
4
|
+
# Eager loader for an ad hoc association of 0 or 1 records (like belongs_to or has_one).
|
5
|
+
#
|
6
|
+
class AdHocOne < AdHocBase
|
7
|
+
private
|
8
|
+
|
9
|
+
#
|
10
|
+
# Merge the association rows into the given rows.
|
11
|
+
#
|
12
|
+
# @param assoc_rows [Array<OccamsRecord::Results::Row>] rows loaded from the associated table
|
13
|
+
# @param rows [Array<OccamsRecord::Results::Row>] rows loaded from the main table
|
14
|
+
#
|
15
|
+
def merge!(assoc_rows, rows)
|
16
|
+
Merge.new(rows, name).
|
17
|
+
single!(assoc_rows, @foreign_key.to_s, @local_key.to_s)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -6,10 +6,6 @@ module OccamsRecord
|
|
6
6
|
class Base
|
7
7
|
# @return [String] association name
|
8
8
|
attr_reader :name
|
9
|
-
# @return [Array<Module>] optional Module to include in the result class (single or array)
|
10
|
-
attr_reader :use
|
11
|
-
# @return [Proc] optional Proc for eager loading things on this association
|
12
|
-
attr_reader :eval_block
|
13
9
|
|
14
10
|
#
|
15
11
|
# @param ref [ActiveRecord::Association] the ActiveRecord association
|
@@ -25,6 +21,21 @@ module OccamsRecord
|
|
25
21
|
@name = (as || ref.name).to_s
|
26
22
|
end
|
27
23
|
|
24
|
+
#
|
25
|
+
# Run the query and merge the results into the given rows.
|
26
|
+
#
|
27
|
+
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
28
|
+
# @param query_logger [Array<String>]
|
29
|
+
#
|
30
|
+
def run(rows, query_logger: nil)
|
31
|
+
query(rows) { |scope|
|
32
|
+
assoc_rows = Query.new(scope, use: @use, query_logger: query_logger, &@eval_block).run
|
33
|
+
merge! assoc_rows, rows
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
28
39
|
#
|
29
40
|
# Yield one or more ActiveRecord::Relation objects to a given block.
|
30
41
|
#
|
@@ -4,10 +4,6 @@ module OccamsRecord
|
|
4
4
|
class PolymorphicBelongsTo
|
5
5
|
# @return [String] association name
|
6
6
|
attr_reader :name
|
7
|
-
# @return [Array<Module>] optional Module to include in the result class (single or array)
|
8
|
-
attr_reader :use
|
9
|
-
# @return [Proc] optional Proc for eager loading things on this association
|
10
|
-
attr_reader :eval_block
|
11
7
|
|
12
8
|
#
|
13
9
|
# @param ref [ActiveRecord::Association] the ActiveRecord association
|
@@ -24,6 +20,21 @@ module OccamsRecord
|
|
24
20
|
@foreign_key = @ref.foreign_key.to_sym
|
25
21
|
end
|
26
22
|
|
23
|
+
#
|
24
|
+
# Run the query and merge the results into the given rows.
|
25
|
+
#
|
26
|
+
# @param rows [Array<OccamsRecord::Results::Row>] Array of rows used to calculate the query.
|
27
|
+
# @param query_logger [Array<String>]
|
28
|
+
#
|
29
|
+
def run(rows, query_logger: nil)
|
30
|
+
query(rows) { |scope|
|
31
|
+
assoc_rows = Query.new(scope, use: @use, query_logger: query_logger, &@eval_block).run
|
32
|
+
merge! assoc_rows, rows
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
27
38
|
#
|
28
39
|
# Yield ActiveRecord::Relations to the given block, one for every "type" represented in the given rows.
|
29
40
|
#
|
@@ -49,8 +49,8 @@ module OccamsRecord
|
|
49
49
|
# @return [Hash]
|
50
50
|
attr_reader :binds
|
51
51
|
|
52
|
-
include EagerLoaders::Builder
|
53
52
|
include Batches
|
53
|
+
include EagerLoaders::Builder
|
54
54
|
|
55
55
|
#
|
56
56
|
# Initialize a new query.
|
@@ -61,7 +61,7 @@ module OccamsRecord
|
|
61
61
|
# @param eager_loaders [OccamsRecord::EagerLoaders::Base]
|
62
62
|
# @param query_logger [Array] (optional) an array into which all queries will be inserted for logging/debug purposes
|
63
63
|
#
|
64
|
-
def initialize(sql, binds, use: nil, eager_loaders: [], query_logger: nil)
|
64
|
+
def initialize(sql, binds, use: nil, eager_loaders: [], query_logger: nil, &eval_block)
|
65
65
|
@sql = sql
|
66
66
|
@binds = binds
|
67
67
|
@use = use
|
@@ -69,6 +69,7 @@ module OccamsRecord
|
|
69
69
|
@query_logger = query_logger
|
70
70
|
@model = nil
|
71
71
|
@conn = @model&.connection || ActiveRecord::Base.connection
|
72
|
+
instance_eval(&eval_block) if eval_block
|
72
73
|
end
|
73
74
|
|
74
75
|
#
|
@@ -92,7 +93,9 @@ module OccamsRecord
|
|
92
93
|
# @return [Array<OccamsRecord::Results::Row>]
|
93
94
|
#
|
94
95
|
def run
|
95
|
-
|
96
|
+
_escaped_sql = escaped_sql
|
97
|
+
@query_logger << _escaped_sql if @query_logger
|
98
|
+
result = @conn.exec_query _escaped_sql
|
96
99
|
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.map(&:name), model: @model, modules: @use)
|
97
100
|
rows = result.rows.map { |row| row_class.new row }
|
98
101
|
eager_load! rows
|
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.15.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-03-
|
11
|
+
date: 2018-03-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -41,6 +41,9 @@ files:
|
|
41
41
|
- lib/occams-record.rb
|
42
42
|
- lib/occams-record/batches.rb
|
43
43
|
- lib/occams-record/eager_loaders.rb
|
44
|
+
- lib/occams-record/eager_loaders/ad_hoc_base.rb
|
45
|
+
- lib/occams-record/eager_loaders/ad_hoc_many.rb
|
46
|
+
- lib/occams-record/eager_loaders/ad_hoc_one.rb
|
44
47
|
- lib/occams-record/eager_loaders/base.rb
|
45
48
|
- lib/occams-record/eager_loaders/belongs_to.rb
|
46
49
|
- lib/occams-record/eager_loaders/habtm.rb
|