activerecord-quick_read 1.1.0 → 1.2.1
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/.ruby-version +1 -1
- data/CHANGELOG.md +15 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +48 -15
- data/README.md +137 -18
- data/benchmarks/read_benchmark.rb +104 -0
- data/lib/activerecord/quick_read/lite_base.rb +1 -1
- data/lib/activerecord/quick_read/railtie.rb +1 -1
- data/lib/activerecord/quick_read/version.rb +1 -1
- data/lib/activerecord/quick_read.rb +17 -11
- metadata +10 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d2a22ddf1957e253d53140f7ede9c14b6e329d707f4e4be8c7c6752b9bedfc8
|
|
4
|
+
data.tar.gz: d7b7b455816cccc89bbe69a83a07ec448a4f84756469838f8ba614a2c84b0735
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 34771e716d9b34b9e9f56437960206462f92157bf562f8e2a71d09ca2b3ec069300abd1d7a5aad5bee5e0f4f1e9a9a2dfbe04fd2886539e72b00a920df14b1e1
|
|
7
|
+
data.tar.gz: a3dc6d0a256c59f0f83763fded7aa4fa2d8fbdf724b5accdcb80e949f9f419aca1827243f6d70b29227197fbd4659f82f1ae2c1549435216d76d48013370ac25
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.4.7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.2.1] - 2026-08-26
|
|
4
|
+
|
|
5
|
+
- Fixed a missing `require "benchmark"` that broke `quick_reads` on Rails 8.1
|
|
6
|
+
- Optimized `quick_build` (memoized the struct class and no longer mutates the input hash)
|
|
7
|
+
- Switched `quick_reads` to `select_rows` (positional) for full-column reads, avoiding per-row hash allocation
|
|
8
|
+
- Removed the runtime `benchmark` dependency in favor of `Process.clock_gettime`
|
|
9
|
+
- Added benchmarks and updated the README performance claims
|
|
10
|
+
|
|
11
|
+
## [1.2.0] - 2026-08-26
|
|
12
|
+
|
|
13
|
+
- Added support for older Rails 5 (`parent` fallback for `module_parent`)
|
|
14
|
+
- Removed the `activerecord` version bounds, enabling Rails 8.x (e.g. 8.0.5.1) that were previously excluded by `<= 8`
|
|
15
|
+
- Fixed `quick_read` and `quick_reads` when called directly on a model
|
|
16
|
+
- Expanded the README and test suite
|
|
17
|
+
|
|
3
18
|
## [0.1.0] - 2023-04-21
|
|
4
19
|
|
|
5
20
|
- Initial release
|
data/Gemfile
CHANGED
|
@@ -9,4 +9,17 @@ gem "rake", "~> 13.0"
|
|
|
9
9
|
|
|
10
10
|
gem "rspec", "~> 3.0"
|
|
11
11
|
|
|
12
|
+
gem "benchmark-ips"
|
|
13
|
+
|
|
14
|
+
gem "activerecord", "~> 8.1"
|
|
15
|
+
|
|
16
|
+
gem "sqlite3", "~> 2.1"
|
|
17
|
+
|
|
12
18
|
gem "standard", "~> 1.3"
|
|
19
|
+
|
|
20
|
+
# Gems extracted from Ruby's stdlib in Ruby 3.4+ that Rails 7.0 still requires
|
|
21
|
+
gem "mutex_m"
|
|
22
|
+
gem "base64"
|
|
23
|
+
gem "bigdecimal"
|
|
24
|
+
gem "racc"
|
|
25
|
+
gem "ostruct"
|
data/Gemfile.lock
CHANGED
|
@@ -1,33 +1,54 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
activerecord-quick_read (1.
|
|
5
|
-
activerecord
|
|
4
|
+
activerecord-quick_read (1.2.1)
|
|
5
|
+
activerecord
|
|
6
6
|
|
|
7
7
|
GEM
|
|
8
8
|
remote: https://rubygems.org/
|
|
9
9
|
specs:
|
|
10
|
-
activemodel (
|
|
11
|
-
activesupport (=
|
|
12
|
-
activerecord (
|
|
13
|
-
activemodel (=
|
|
14
|
-
activesupport (=
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
activemodel (8.1.3.1)
|
|
11
|
+
activesupport (= 8.1.3.1)
|
|
12
|
+
activerecord (8.1.3.1)
|
|
13
|
+
activemodel (= 8.1.3.1)
|
|
14
|
+
activesupport (= 8.1.3.1)
|
|
15
|
+
timeout (>= 0.4.0)
|
|
16
|
+
activesupport (8.1.3.1)
|
|
17
|
+
base64
|
|
18
|
+
bigdecimal
|
|
19
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
20
|
+
connection_pool (>= 2.2.5)
|
|
21
|
+
drb
|
|
17
22
|
i18n (>= 1.6, < 2)
|
|
23
|
+
json
|
|
24
|
+
logger (>= 1.4.2)
|
|
18
25
|
minitest (>= 5.1)
|
|
19
|
-
|
|
26
|
+
securerandom (>= 0.3)
|
|
27
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
|
28
|
+
uri (>= 0.13.1)
|
|
20
29
|
ast (2.4.2)
|
|
21
|
-
|
|
30
|
+
base64 (0.3.0)
|
|
31
|
+
benchmark-ips (2.15.1)
|
|
32
|
+
bigdecimal (4.1.2)
|
|
33
|
+
concurrent-ruby (1.3.8)
|
|
34
|
+
connection_pool (3.0.2)
|
|
22
35
|
diff-lcs (1.5.0)
|
|
23
|
-
|
|
36
|
+
drb (2.2.3)
|
|
37
|
+
i18n (1.15.2)
|
|
24
38
|
concurrent-ruby (~> 1.0)
|
|
25
39
|
json (2.6.3)
|
|
26
40
|
language_server-protocol (3.17.0.3)
|
|
27
|
-
|
|
41
|
+
logger (1.7.0)
|
|
42
|
+
minitest (6.0.6)
|
|
43
|
+
drb (~> 2.0)
|
|
44
|
+
prism (~> 1.5)
|
|
45
|
+
mutex_m (0.3.0)
|
|
46
|
+
ostruct (0.6.3)
|
|
28
47
|
parallel (1.23.0)
|
|
29
48
|
parser (3.2.2.0)
|
|
30
49
|
ast (~> 2.4.1)
|
|
50
|
+
prism (1.9.0)
|
|
51
|
+
racc (1.8.1)
|
|
31
52
|
rainbow (3.1.1)
|
|
32
53
|
rake (13.0.6)
|
|
33
54
|
regexp_parser (2.8.0)
|
|
@@ -61,23 +82,35 @@ GEM
|
|
|
61
82
|
rubocop (>= 1.7.0, < 2.0)
|
|
62
83
|
rubocop-ast (>= 0.4.0)
|
|
63
84
|
ruby-progressbar (1.13.0)
|
|
85
|
+
securerandom (0.4.1)
|
|
86
|
+
sqlite3 (2.9.6-arm64-darwin)
|
|
64
87
|
standard (1.27.0)
|
|
65
88
|
language_server-protocol (~> 3.17.0.2)
|
|
66
89
|
rubocop (~> 1.50.2)
|
|
67
90
|
rubocop-performance (~> 1.16.0)
|
|
91
|
+
timeout (0.6.1)
|
|
68
92
|
tzinfo (2.0.6)
|
|
69
93
|
concurrent-ruby (~> 1.0)
|
|
70
94
|
unicode-display_width (2.4.2)
|
|
95
|
+
uri (1.1.1)
|
|
71
96
|
|
|
72
97
|
PLATFORMS
|
|
73
98
|
arm64-darwin-22
|
|
74
|
-
|
|
99
|
+
arm64-darwin-23
|
|
75
100
|
|
|
76
101
|
DEPENDENCIES
|
|
102
|
+
activerecord (~> 8.1)
|
|
77
103
|
activerecord-quick_read!
|
|
104
|
+
base64
|
|
105
|
+
benchmark-ips
|
|
106
|
+
bigdecimal
|
|
107
|
+
mutex_m
|
|
108
|
+
ostruct
|
|
109
|
+
racc
|
|
78
110
|
rake (~> 13.0)
|
|
79
111
|
rspec (~> 3.0)
|
|
112
|
+
sqlite3 (~> 2.1)
|
|
80
113
|
standard (~> 1.3)
|
|
81
114
|
|
|
82
115
|
BUNDLED WITH
|
|
83
|
-
|
|
116
|
+
4.0.19
|
data/README.md
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
# ActiveRecord::QuickRead
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://badge.fury.io/rb/activerecord-quick_read)
|
|
4
|
+
|
|
5
|
+
Makes Rails go faster. Speedups scale with the size and complexity of your models, up to ~13x.
|
|
4
6
|
|
|
5
7
|
### How does it work?
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
A normal ActiveRecord query does more than run SQL. Once the rows come back, ActiveRecord
|
|
10
|
+
instantiates a model object for **every** row: allocating the object, running `initialize`,
|
|
11
|
+
typecasting each column, setting up dirty tracking, and evaluating any `after_initialize` callbacks.
|
|
12
|
+
For large result sets that per-row work adds up fast, and you end up paying for features you may not
|
|
13
|
+
even use (like attribute change tracking) when all you wanted was to *read* the data.
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
`quick_read` sidesteps all of that. It runs the exact same scoped SQL, but reads the raw rows with
|
|
16
|
+
`connection.select_rows`, which returns each row as a plain array of values instead of the
|
|
17
|
+
column-keyed hashes that `select_all` builds. Each array is then mapped positionally to a lightweight
|
|
18
|
+
`Struct` whose members mirror the model's columns:
|
|
12
19
|
|
|
20
|
+
```ruby
|
|
21
|
+
# the Lite class generated for your model, e.g.:
|
|
22
|
+
Report::Lite = Struct.new(:id, :message, :status, :created_at, :updated_at)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Most of the speed comes from what's *not* allocated: no model objects, no per-row hash, no
|
|
26
|
+
typecasting, no dirty tracking, and no `after_initialize` callbacks. The struct is built directly
|
|
27
|
+
from the raw row values. The result is the same data you asked for, returned faster when you read
|
|
28
|
+
and serialize it. The speedup grows with the size and complexity of your model.
|
|
29
|
+
|
|
30
|
+
The structs are *upgradeable*, not throwaway. Each `Lite` instance knows its source model and can
|
|
31
|
+
lazily materialize a full ActiveRecord object on demand (via `method_missing`). So when you call a
|
|
32
|
+
method the struct doesn't have (like `save`, `update`, or an association such as `report.author`) it
|
|
33
|
+
transparently builds the real model behind the scenes and delegates to it. You only pay the cost of
|
|
34
|
+
instantiation when you actually need to write, not when you're just reading.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
13
37
|
|
|
14
38
|
Install the gem and add to the application's Gemfile by executing:
|
|
15
39
|
|
|
@@ -21,36 +45,131 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
|
21
45
|
|
|
22
46
|
## Usage
|
|
23
47
|
|
|
24
|
-
|
|
25
|
-
Use `#quick_build` on your
|
|
48
|
+
Add quickness to your models, then call `#quick_read` on an ActiveRecord relation to get a single
|
|
49
|
+
record, or `#quick_reads` to get all scoped records. Use `#quick_build` on your models to build a
|
|
50
|
+
lite instance from a hash.
|
|
26
51
|
|
|
27
|
-
|
|
28
|
-
|
|
52
|
+
### Enable on a single model
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
29
55
|
class Report < ApplicationRecord
|
|
30
56
|
extend ActiveRecord::QuickRead
|
|
31
57
|
end
|
|
32
58
|
```
|
|
33
59
|
|
|
34
|
-
|
|
60
|
+
### Enable for every model via ApplicationRecord
|
|
35
61
|
|
|
36
|
-
|
|
62
|
+
Instead of extending each model individually, extend `ApplicationRecord` once and every subclass
|
|
63
|
+
inherits the quick read behavior:
|
|
37
64
|
|
|
38
65
|
```ruby
|
|
39
|
-
|
|
66
|
+
class ApplicationRecord < ActiveRecord::Base
|
|
67
|
+
self.abstract_class = true
|
|
68
|
+
|
|
69
|
+
extend ActiveRecord::QuickRead
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Because models aren't fully defined until boot, the `Lite` structs are built lazily after Rails
|
|
74
|
+
initializes (via the included railtie). Any class that inherits from `ApplicationRecord`, even ones
|
|
75
|
+
defined in engines or gems, automatically gets a `Lite` struct.
|
|
76
|
+
|
|
77
|
+
As a side benefit, defining a struct requires reading the model's `column_names`, which warms
|
|
78
|
+
ActiveRecord's schema cache and touches each model at boot. That in turn pre-warms the models, so
|
|
79
|
+
your first real web requests don't pay the one-time schema-loading cost.
|
|
80
|
+
|
|
81
|
+
### Reading
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# A single record
|
|
85
|
+
report = Report.where(id: params[:id]).quick_read
|
|
86
|
+
report.id # => 1
|
|
87
|
+
report.message # => "All done"
|
|
88
|
+
report.class # => Report::Lite
|
|
89
|
+
|
|
90
|
+
# All scoped records
|
|
91
|
+
reports = Report.where(status: "done").quick_reads
|
|
92
|
+
reports.first.title
|
|
40
93
|
```
|
|
41
94
|
|
|
42
|
-
|
|
95
|
+
The returned `Report::Lite` is a struct with the same attributes (and `#to_h`) as your model:
|
|
96
|
+
|
|
43
97
|
```ruby
|
|
44
|
-
Report.
|
|
98
|
+
Report.order(:created_at).quick_reads.map(&:to_h)
|
|
45
99
|
```
|
|
46
100
|
|
|
47
|
-
|
|
101
|
+
### Batching
|
|
102
|
+
|
|
103
|
+
Load your batches faster, with bigger wins on larger, more complex models:
|
|
104
|
+
|
|
48
105
|
```ruby
|
|
49
|
-
Report.
|
|
106
|
+
Report.in_batches do |batch|
|
|
107
|
+
batch.quick_reads.each do |report|
|
|
108
|
+
puts report.message
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Building
|
|
114
|
+
|
|
115
|
+
Build a lite instance from attributes without hitting the database:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
draft = Report.quick_build(id: 1, message: "hello")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Upgrading to a full Active Record object
|
|
122
|
+
|
|
123
|
+
Lite instances lazily upgrade to full-fledged ActiveRecord objects on demand. Calling any method the
|
|
124
|
+
struct doesn't have (like `save`, `update`, or association accessors) transparently materializes the
|
|
125
|
+
underlying model:
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
Report.where(status: "queued").quick_reads.each do |report|
|
|
129
|
+
report.update(status: "done")
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
report = Report.first.quick_read
|
|
135
|
+
report.author # => loads the belongs_to association on the full model
|
|
136
|
+
report.save!
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Reloading
|
|
140
|
+
|
|
141
|
+
Refresh a lite instance with fresh data from the database:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
report = Report.first.quick_read
|
|
145
|
+
# ... the underlying row changes elsewhere ...
|
|
146
|
+
report.reload
|
|
147
|
+
report.message # => updated value
|
|
50
148
|
```
|
|
51
149
|
|
|
52
150
|
It just works.
|
|
53
151
|
|
|
152
|
+
## Benchmarks
|
|
153
|
+
|
|
154
|
+
Measured against ActiveRecord 8.1 and SQLite with 10,000 records (see `benchmarks/read_benchmark.rb`).
|
|
155
|
+
The model includes string, integer, date, decimal, text, and JSON-serialized columns to exercise
|
|
156
|
+
typecasting:
|
|
157
|
+
|
|
158
|
+
| Scenario | ActiveRecord | quick_read | Speedup |
|
|
159
|
+
| --- | --- | --- | --- |
|
|
160
|
+
| Read fields | 7.6 i/s | 63.7 i/s | ~8.3x |
|
|
161
|
+
| Convert to hashes | 4.7 i/s | 56.4 i/s | ~13.7x |
|
|
162
|
+
| Access associations | 1.1 i/s | 1.0 i/s | ~same |
|
|
163
|
+
|
|
164
|
+
The win comes from skipping typecasting and instantiation when reading. The speedup depends on the
|
|
165
|
+
size and complexity of your models: more columns, and heavier typecasting (dates, decimals, JSON,
|
|
166
|
+
etc.), mean a bigger gap. When you access associations, each lite struct upgrades to a full
|
|
167
|
+
ActiveRecord object (via `method_missing`), so performance is comparable to a plain ActiveRecord read.
|
|
168
|
+
|
|
169
|
+
Run it yourself:
|
|
170
|
+
|
|
171
|
+
$ bundle exec ruby benchmarks/read_benchmark.rb
|
|
172
|
+
|
|
54
173
|
## Development
|
|
55
174
|
|
|
56
175
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
@@ -59,7 +178,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
59
178
|
|
|
60
179
|
## Contributing
|
|
61
180
|
|
|
62
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
181
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ridiculous/activerecord-quick_reads. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ridiculous/activerecord-quick_reads/blob/main/CODE_OF_CONDUCT.md).
|
|
63
182
|
|
|
64
183
|
## License
|
|
65
184
|
|
|
@@ -67,4 +186,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
67
186
|
|
|
68
187
|
## Code of Conduct
|
|
69
188
|
|
|
70
|
-
Everyone interacting in the ActiveRecord::QuickRead project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
|
189
|
+
Everyone interacting in the ActiveRecord::QuickRead project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ridiculous/activerecord-quick_reads/blob/main/CODE_OF_CONDUCT.md).
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "benchmark/ips"
|
|
5
|
+
require "active_record"
|
|
6
|
+
require "activerecord/quick_read"
|
|
7
|
+
|
|
8
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
|
9
|
+
ActiveRecord::Base.logger = nil
|
|
10
|
+
|
|
11
|
+
ActiveRecord::Schema.define do
|
|
12
|
+
create_table :authors do |t|
|
|
13
|
+
t.string :name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
create_table :reports do |t|
|
|
17
|
+
t.integer :author_id
|
|
18
|
+
t.string :message
|
|
19
|
+
t.string :status
|
|
20
|
+
t.integer :score
|
|
21
|
+
t.date :published_on
|
|
22
|
+
t.decimal :amount, precision: 10, scale: 2
|
|
23
|
+
t.text :body
|
|
24
|
+
t.text :payload
|
|
25
|
+
t.timestamps
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class Author < ActiveRecord::Base
|
|
30
|
+
has_many :reports
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Report < ActiveRecord::Base
|
|
34
|
+
belongs_to :author
|
|
35
|
+
|
|
36
|
+
serialize :payload, coder: JSON
|
|
37
|
+
|
|
38
|
+
extend ActiveRecord::QuickRead
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
ROWS = Integer(ENV.fetch("ROWS", "10_000"))
|
|
42
|
+
AUTHORS = Integer(ENV.fetch("AUTHORS", "100"))
|
|
43
|
+
now = Time.now
|
|
44
|
+
|
|
45
|
+
Author.insert_all(AUTHORS.times.map { |i| {name: "Author #{i}"} })
|
|
46
|
+
|
|
47
|
+
Report.insert_all(
|
|
48
|
+
ROWS.times.map do |i|
|
|
49
|
+
{
|
|
50
|
+
message: "message #{i}",
|
|
51
|
+
status: i.even? ? "done" : "queued",
|
|
52
|
+
score: i,
|
|
53
|
+
author_id: (i % AUTHORS) + 1,
|
|
54
|
+
published_on: "2026-08-26",
|
|
55
|
+
amount: 1234.56,
|
|
56
|
+
body: "lorem ipsum dolor sit amet, consectetur adipiscing elit #{i}",
|
|
57
|
+
payload: {foo: "bar", baz: i, nested: {a: [1, 2, 3]}},
|
|
58
|
+
created_at: now,
|
|
59
|
+
updated_at: now
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
puts "Benchmarking #{ROWS} records\n\n"
|
|
65
|
+
|
|
66
|
+
Benchmark.ips do |x|
|
|
67
|
+
x.config(time: 5, warmup: 2)
|
|
68
|
+
|
|
69
|
+
x.report("activerecord read") do
|
|
70
|
+
Report.all.each do |r|
|
|
71
|
+
r.message
|
|
72
|
+
r.status
|
|
73
|
+
r.score
|
|
74
|
+
r.published_on
|
|
75
|
+
r.amount
|
|
76
|
+
r.body
|
|
77
|
+
r.payload
|
|
78
|
+
r.created_at
|
|
79
|
+
r.updated_at
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
x.report("quick_reads read") do
|
|
84
|
+
Report.quick_reads.each do |r|
|
|
85
|
+
r.message
|
|
86
|
+
r.status
|
|
87
|
+
r.score
|
|
88
|
+
r.published_on
|
|
89
|
+
r.amount
|
|
90
|
+
r.body
|
|
91
|
+
r.payload
|
|
92
|
+
r.created_at
|
|
93
|
+
r.updated_at
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
x.report("activerecord attributes") { Report.all.map(&:attributes) }
|
|
98
|
+
x.report("quick_reads to_h") { Report.quick_reads.map(&:to_h) }
|
|
99
|
+
|
|
100
|
+
x.report("activerecord association") { Report.all.each { |r| r.author&.name } }
|
|
101
|
+
x.report("quick_reads association") { Report.quick_reads.each { |r| r.author&.name } }
|
|
102
|
+
|
|
103
|
+
x.compare!
|
|
104
|
+
end
|
|
@@ -28,7 +28,7 @@ module ActiveRecord
|
|
|
28
28
|
|
|
29
29
|
# Since the struct that includes this module is defined within the model's namespace
|
|
30
30
|
def _ar_model
|
|
31
|
-
self.class.module_parent
|
|
31
|
+
self.class.respond_to?(:module_parent) ? self.class.module_parent : self.class.parent
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -4,7 +4,7 @@ module ActiveRecord
|
|
|
4
4
|
config.after_initialize do
|
|
5
5
|
QuickRead.define_lite_structs
|
|
6
6
|
rescue => e
|
|
7
|
-
ActiveRecord::Base.logger.debug
|
|
7
|
+
ActiveRecord::Base.logger.debug { "QuickRead: Failed to define lite structs #{e.message}" }
|
|
8
8
|
end
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -23,7 +23,7 @@ module ActiveRecord
|
|
|
23
23
|
|
|
24
24
|
QuickRead.define_lite_struct(model)
|
|
25
25
|
rescue => e
|
|
26
|
-
ActiveRecord::Base.logger.debug
|
|
26
|
+
ActiveRecord::Base.logger.debug { "QuickRead: Failed to define quick models #{e.message}" }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def self.models
|
|
@@ -33,13 +33,13 @@ module ActiveRecord
|
|
|
33
33
|
def self.define_lite_structs
|
|
34
34
|
return if QuickRead.models.empty?
|
|
35
35
|
|
|
36
|
-
ActiveRecord::Base.logger.debug
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
end
|
|
36
|
+
ActiveRecord::Base.logger.debug { "QuickRead: Defining #{QuickRead.models.size} quick models" }
|
|
37
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
38
|
+
while (model = models.pop)
|
|
39
|
+
define_lite_struct(model)
|
|
41
40
|
end
|
|
42
|
-
|
|
41
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
42
|
+
ActiveRecord::Base.logger.debug { "QuickRead: Defining quick models took #{elapsed.round(4)}s" }
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def self.define_lite_struct(model)
|
|
@@ -59,16 +59,22 @@ module ActiveRecord
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def quick_build(attrs = {})
|
|
62
|
-
klass = const_get(:Lite)
|
|
63
|
-
klass.new(*attrs.
|
|
62
|
+
klass = @quick_read_lite ||= const_get(:Lite)
|
|
63
|
+
klass.new(*klass.members.map { |member| attrs.fetch(member) { attrs[member.to_s] } })
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def quick_reads
|
|
67
|
-
|
|
67
|
+
scope = current_scope || all
|
|
68
|
+
klass = @quick_read_lite ||= const_get(:Lite)
|
|
69
|
+
if scope.select_values.empty?
|
|
70
|
+
connection.select_rows(scope.to_sql).map { |values| klass.new(*values) }
|
|
71
|
+
else
|
|
72
|
+
connection.select_all(scope.to_sql).map { |attrs| quick_build(attrs) }
|
|
73
|
+
end
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
def quick_read
|
|
71
|
-
current_scope.limit(1).quick_reads.first
|
|
77
|
+
(current_scope || all).limit(1).quick_reads.first
|
|
72
78
|
end
|
|
73
79
|
|
|
74
80
|
# Instantiate a new ActiveRecord object from a plain hash, marked as persisted, no changes, typecast
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-quick_read
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1
|
|
4
|
+
version: 1.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Buckley
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activerecord
|
|
@@ -16,21 +15,17 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
20
|
-
- - "<="
|
|
21
|
-
- !ruby/object:Gem::Version
|
|
22
|
-
version: '8'
|
|
18
|
+
version: '0'
|
|
23
19
|
type: :runtime
|
|
24
20
|
prerelease: false
|
|
25
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
26
22
|
requirements:
|
|
27
23
|
- - ">="
|
|
28
24
|
- !ruby/object:Gem::Version
|
|
29
|
-
version: '
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
description: Makes rails go _faster_. Improve read times 4x!
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Faster ActiveRecord reads. Skips model instantiation and returns lightweight
|
|
27
|
+
objects (with untypecast values) just for reading. Fallback to full-fledged ActiveRecords
|
|
28
|
+
on method_missing.
|
|
34
29
|
email:
|
|
35
30
|
- ridiculous@hey.com
|
|
36
31
|
executables: []
|
|
@@ -48,6 +43,7 @@ files:
|
|
|
48
43
|
- LICENSE.txt
|
|
49
44
|
- README.md
|
|
50
45
|
- Rakefile
|
|
46
|
+
- benchmarks/read_benchmark.rb
|
|
51
47
|
- lib/activerecord/quick_read.rb
|
|
52
48
|
- lib/activerecord/quick_read/lite_base.rb
|
|
53
49
|
- lib/activerecord/quick_read/railtie.rb
|
|
@@ -59,7 +55,6 @@ metadata:
|
|
|
59
55
|
homepage_uri: https://github.com/ridiculous/activerecord-quick_reads
|
|
60
56
|
source_code_uri: https://github.com/ridiculous/activerecord-quick_reads
|
|
61
57
|
changelog_uri: https://github.com/ridiculous/activerecord-quick_reads/CHANGELOG.md
|
|
62
|
-
post_install_message:
|
|
63
58
|
rdoc_options: []
|
|
64
59
|
require_paths:
|
|
65
60
|
- lib
|
|
@@ -74,8 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
74
69
|
- !ruby/object:Gem::Version
|
|
75
70
|
version: '0'
|
|
76
71
|
requirements: []
|
|
77
|
-
rubygems_version: 3.
|
|
78
|
-
signing_key:
|
|
72
|
+
rubygems_version: 3.7.2
|
|
79
73
|
specification_version: 4
|
|
80
|
-
summary:
|
|
74
|
+
summary: Speed up ActiveRecord reads by skipping model instantiation.
|
|
81
75
|
test_files: []
|