occams-record 1.1.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +68 -11
- data/lib/occams-record.rb +1 -1
- data/lib/occams-record/{eager_loaders.rb → eager_loaders/eager_loaders.rb} +0 -0
- data/lib/occams-record/raw_query.rb +10 -7
- data/lib/occams-record/results/results.rb +3 -16
- data/lib/occams-record/results/row.rb +37 -4
- data/lib/occams-record/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 214f1fb4b74fb65d13461419473fdf865ece2fae7797dc5d9c47ba4d1ad14b45
|
4
|
+
data.tar.gz: 4a836cb9bbc0e5a694aaf7f5220628d1b9ff4afbd11c4e6c5e1e9af1c6f3e3e9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 112fff206d8d9ea868351b91707b100f0eba99d81f7bb3fcfab9706bb71d611328e7f977796dc36c40c82b029ed1f4df0f38013465c0afeead3ba588b77ca3a9
|
7
|
+
data.tar.gz: f938b969b6688b3203d0b98abf58c18835f7b6eba76525794495778bfcbbad970a2f7cfe161a010dadf329dcd0ee00a57ddb27f0cafdd89114dd63be542f7567
|
data/README.md
CHANGED
@@ -2,23 +2,80 @@
|
|
2
2
|
|
3
3
|
> Do not multiply entities beyond necessity. -- Occam's Razor
|
4
4
|
|
5
|
-
|
5
|
+
OccamsRecord is a high-efficiency, advanced query library for use alongside ActiveRecord. It is **not** an ORM or an ActiveRecord replacement. OccamsRecord can breathe fresh life into your ActiveRecord app by giving it two things:
|
6
6
|
|
7
|
-
|
7
|
+
### 1) Huge performance gains
|
8
8
|
|
9
9
|
* 3x-5x faster than ActiveRecord queries, *minimum*.
|
10
10
|
* Uses 1/3 the memory of ActiveRecord query results.
|
11
|
-
* Eliminates the N+1 query problem.
|
11
|
+
* Eliminates the N+1 query problem. (This often exceeds the baseline 3x-5x gain.)
|
12
12
|
|
13
|
-
|
13
|
+
### 2) Supercharged querying & eager loading
|
14
14
|
|
15
|
-
|
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
|
+
Continue using ActiveRecord's query builder, but let Occams take over eager loading and raw SQL calls. None of the examples below are possible with ActiveRecord, but OccamsRecord won't limit you. (More complete examples are shown later, but these should whet your appetite.)
|
20
16
|
|
21
|
-
|
17
|
+
**Customize the SQL used to eager load associations**
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
OccamsRecord.
|
21
|
+
query(User.active).
|
22
|
+
eager_load(:orders, ->(q) { q.where("created_at >= ?", date })
|
23
|
+
```
|
24
|
+
|
25
|
+
**Use `ORDER BY` with `find_each`/`find_in_batches`**
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
OccamsRecord.
|
29
|
+
query(Order.order("created_at DESC")).
|
30
|
+
find_each { |order|
|
31
|
+
...
|
32
|
+
}
|
33
|
+
```
|
34
|
+
|
35
|
+
**Use `find_each`/`find_in_batches` with raw SQL**
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
OccamsRecord.
|
39
|
+
sql("
|
40
|
+
SELECT * FROM orders
|
41
|
+
WHERE created_at >= %{date}
|
42
|
+
LIMIT %{batch_limit}
|
43
|
+
OFFSET %{batch_offset}",
|
44
|
+
{date: 10.years.ago}
|
45
|
+
).
|
46
|
+
find_each { |order|
|
47
|
+
...
|
48
|
+
}
|
49
|
+
```
|
50
|
+
|
51
|
+
**Eager load associations when you're writing raw SQL**
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
OccamsRecord.
|
55
|
+
sql("
|
56
|
+
SELECT * FROM users
|
57
|
+
LEFT OUTER JOIN ...
|
58
|
+
").
|
59
|
+
model(User).
|
60
|
+
eager_load(:orders)
|
61
|
+
```
|
62
|
+
|
63
|
+
**Eager load "ad hoc associations" using raw SQL**
|
64
|
+
|
65
|
+
Relationships are complicated, and sometimes they can't be expressed in ActiveRecord models. Define your relationship on the fly!
|
66
|
+
(Don't worry, there's a full explanation later on.)
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
OccamsRecord.
|
70
|
+
query(User.all).
|
71
|
+
eager_load_many(:orders, {:id => :user_id}, "
|
72
|
+
SELECT user_id, orders.*
|
73
|
+
FROM orders INNER JOIN ...
|
74
|
+
WHERE user_id IN (%{ids})
|
75
|
+
")
|
76
|
+
```
|
77
|
+
|
78
|
+
[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:**
|
22
79
|
|
23
80
|
* OccamsRecord results are *read-only*.
|
24
81
|
* OccamsRecord results are *purely database rows* - they don't have any instance methods from your Rails models.
|
@@ -116,7 +173,7 @@ OccamsRecord.
|
|
116
173
|
LIMIT %{batch_limit}
|
117
174
|
OFFSET %{batch_offset}
|
118
175
|
", {
|
119
|
-
date:
|
176
|
+
date: 10.years.ago
|
120
177
|
}).
|
121
178
|
find_each(batch_size: 1000) do |order|
|
122
179
|
...
|
data/lib/occams-record.rb
CHANGED
@@ -2,7 +2,7 @@ require 'active_record'
|
|
2
2
|
require 'occams-record/version'
|
3
3
|
require 'occams-record/merge'
|
4
4
|
require 'occams-record/measureable'
|
5
|
-
require 'occams-record/eager_loaders'
|
5
|
+
require 'occams-record/eager_loaders/eager_loaders'
|
6
6
|
require 'occams-record/results/results'
|
7
7
|
require 'occams-record/results/row'
|
8
8
|
require 'occams-record/query'
|
File without changes
|
@@ -71,7 +71,6 @@ module OccamsRecord
|
|
71
71
|
@use = use
|
72
72
|
@eager_loaders = eager_loaders || EagerLoaders::Context.new
|
73
73
|
@query_logger, @measurements = query_logger, measurements
|
74
|
-
@conn = @eager_loaders.model&.connection || ActiveRecord::Base.connection
|
75
74
|
end
|
76
75
|
|
77
76
|
#
|
@@ -100,10 +99,10 @@ module OccamsRecord
|
|
100
99
|
result = if measure?
|
101
100
|
record_start_time!
|
102
101
|
measure!(table_name, _escaped_sql) {
|
103
|
-
|
102
|
+
conn.exec_query _escaped_sql
|
104
103
|
}
|
105
104
|
else
|
106
|
-
|
105
|
+
conn.exec_query _escaped_sql
|
107
106
|
end
|
108
107
|
row_class = OccamsRecord::Results.klass(result.columns, result.column_types, @eager_loaders.names, model: @eager_loaders.model, modules: @use)
|
109
108
|
rows = result.rows.map { |row| row_class.new row }
|
@@ -136,9 +135,9 @@ module OccamsRecord
|
|
136
135
|
return sql if binds.empty?
|
137
136
|
sql % binds.reduce({}) { |a, (col, val)|
|
138
137
|
a[col.to_sym] = if val.is_a? Array
|
139
|
-
val.map { |x|
|
138
|
+
val.map { |x| conn.quote x }.join(', ')
|
140
139
|
else
|
141
|
-
|
140
|
+
conn.quote val
|
142
141
|
end
|
143
142
|
a
|
144
143
|
}
|
@@ -163,8 +162,8 @@ module OccamsRecord
|
|
163
162
|
end
|
164
163
|
|
165
164
|
Enumerator.new do |y|
|
166
|
-
if use_transaction and
|
167
|
-
|
165
|
+
if use_transaction and conn.open_transactions == 0
|
166
|
+
conn.transaction {
|
168
167
|
run_batches y, of
|
169
168
|
}
|
170
169
|
else
|
@@ -186,5 +185,9 @@ module OccamsRecord
|
|
186
185
|
offset += results.size
|
187
186
|
end
|
188
187
|
end
|
188
|
+
|
189
|
+
def conn
|
190
|
+
@conn ||= @eager_loaders.model&.connection || ActiveRecord::Base.connection
|
191
|
+
end
|
189
192
|
end
|
190
193
|
end
|
@@ -9,8 +9,8 @@ module OccamsRecord
|
|
9
9
|
end
|
10
10
|
|
11
11
|
#
|
12
|
-
# Dynamically build a class for a specific set of result rows. It inherits from OccamsRecord::Results::Row, and optionall
|
13
|
-
#
|
12
|
+
# Dynamically build a class for a specific set of result rows. It inherits from OccamsRecord::Results::Row, and optionall prepends
|
13
|
+
# user-defined modules.
|
14
14
|
#
|
15
15
|
# @param column_names [Array<String>] the column names in the result set. The order MUST match the order returned by the query.
|
16
16
|
# @param column_types [Hash] Column name => type from an ActiveRecord::Result
|
@@ -25,6 +25,7 @@ module OccamsRecord
|
|
25
25
|
|
26
26
|
self.columns = column_names.map(&:to_s)
|
27
27
|
self.associations = association_names.map(&:to_s)
|
28
|
+
self._model = model
|
28
29
|
self.model_name = model ? model.name : nil
|
29
30
|
self.table_name = model ? model.table_name : nil
|
30
31
|
self.primary_key = if model&.primary_key and (pkey = model.primary_key.to_s) and columns.include?(pkey)
|
@@ -34,20 +35,6 @@ module OccamsRecord
|
|
34
35
|
# Build getters & setters for associations. (We need setters b/c they're set AFTER the row is initialized
|
35
36
|
attr_accessor(*association_names)
|
36
37
|
|
37
|
-
# Build id getters for associations, e.g. "widget_ids" for "widgets"
|
38
|
-
self.associations.each do |assoc|
|
39
|
-
if (ref = model.reflections[assoc]) and !ref.polymorphic? and (ref.macro == :has_many or ref.macro == :has_and_belongs_to_many)
|
40
|
-
pkey = ref.association_primary_key.to_sym
|
41
|
-
define_method "#{assoc.singularize}_ids" do
|
42
|
-
begin
|
43
|
-
self.send(assoc).map(&pkey).uniq
|
44
|
-
rescue NoMethodError => e
|
45
|
-
raise MissingColumnError.new(self, e.name)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end if model
|
50
|
-
|
51
38
|
# Build a getter for each attribute returned by the query. The values will be type converted on demand.
|
52
39
|
model_column_types = model ? model.attributes_builder.types : {}
|
53
40
|
self.columns.each_with_index do |col, idx|
|
@@ -12,6 +12,7 @@ module OccamsRecord
|
|
12
12
|
attr_accessor :columns
|
13
13
|
# Array of associations names
|
14
14
|
attr_accessor :associations
|
15
|
+
attr_accessor :_model
|
15
16
|
# Name of Rails model
|
16
17
|
attr_accessor :model_name
|
17
18
|
# Name of originating database table
|
@@ -106,18 +107,50 @@ module OccamsRecord
|
|
106
107
|
"#<#{self.class.model_name || "Anonymous"} #{self.class.primary_key}: #{id}>"
|
107
108
|
end
|
108
109
|
|
110
|
+
IDS_SUFFIX = /_ids$/
|
109
111
|
def method_missing(name, *args, &block)
|
110
|
-
|
111
|
-
|
112
|
+
model = self.class._model
|
113
|
+
return super if args.any? or !block.nil? or model.nil?
|
112
114
|
|
113
|
-
|
115
|
+
name_str = name.to_s
|
116
|
+
assoc = name_str.sub(IDS_SUFFIX, "").pluralize
|
117
|
+
if name_str =~ IDS_SUFFIX and can_define_ids_reader? assoc
|
118
|
+
define_ids_reader! assoc
|
119
|
+
send name
|
120
|
+
elsif model.reflections.has_key? name_str
|
114
121
|
raise MissingEagerLoadError.new(self, name)
|
115
|
-
elsif model.columns_hash.has_key?
|
122
|
+
elsif model.columns_hash.has_key? name_str
|
116
123
|
raise MissingColumnError.new(self, name)
|
117
124
|
else
|
118
125
|
super
|
119
126
|
end
|
120
127
|
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def can_define_ids_reader?(assoc)
|
132
|
+
model = self.class._model
|
133
|
+
self.class.associations.include?(assoc) &&
|
134
|
+
(ref = model.reflections[assoc]) &&
|
135
|
+
!ref.polymorphic? &&
|
136
|
+
(ref.macro == :has_many || ref.macro == :has_and_belongs_to_many)
|
137
|
+
end
|
138
|
+
|
139
|
+
def define_ids_reader!(assoc)
|
140
|
+
model = self.class._model
|
141
|
+
ref = model.reflections[assoc]
|
142
|
+
pkey = ref.association_primary_key.to_sym
|
143
|
+
|
144
|
+
self.class.class_eval do
|
145
|
+
define_method "#{assoc.singularize}_ids" do
|
146
|
+
begin
|
147
|
+
self.send(assoc).map(&pkey).uniq
|
148
|
+
rescue NoMethodError => e
|
149
|
+
raise MissingColumnError.new(self, e.name)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
121
154
|
end
|
122
155
|
end
|
123
156
|
end
|
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: 1.1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jordan Hollinger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -40,7 +40,6 @@ files:
|
|
40
40
|
- README.md
|
41
41
|
- lib/occams-record.rb
|
42
42
|
- lib/occams-record/batches.rb
|
43
|
-
- lib/occams-record/eager_loaders.rb
|
44
43
|
- lib/occams-record/eager_loaders/ad_hoc_base.rb
|
45
44
|
- lib/occams-record/eager_loaders/ad_hoc_many.rb
|
46
45
|
- lib/occams-record/eager_loaders/ad_hoc_one.rb
|
@@ -48,6 +47,7 @@ files:
|
|
48
47
|
- lib/occams-record/eager_loaders/belongs_to.rb
|
49
48
|
- lib/occams-record/eager_loaders/builder.rb
|
50
49
|
- lib/occams-record/eager_loaders/context.rb
|
50
|
+
- lib/occams-record/eager_loaders/eager_loaders.rb
|
51
51
|
- lib/occams-record/eager_loaders/habtm.rb
|
52
52
|
- lib/occams-record/eager_loaders/has_many.rb
|
53
53
|
- lib/occams-record/eager_loaders/has_one.rb
|