occams-record 0.14.0 → 0.15.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 +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
|