nested_select 0.1.0 → 0.2.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/.idea/modules.xml +8 -0
- data/.idea/nested_select.iml +125 -0
- data/.idea/vcs.xml +6 -0
- data/ABOUT_NESTED_SELECT.md +87 -0
- data/CHANGELOG.md +5 -1
- data/Dockerfile +12 -0
- data/README.md +74 -38
- data/docker-compose.yml +13 -0
- data/lib/nested_select/preloader/association.rb +4 -2
- data/lib/nested_select/preloader/through_association.rb +1 -1
- data/lib/nested_select/preloader.rb +3 -3
- data/lib/nested_select/version.rb +1 -1
- metadata +134 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 295706af2c9d8f419f850ddcc81775e1ebb9708320fb6c06bd1d2fcc8f57405b
|
4
|
+
data.tar.gz: a9270172f9899183ca057294c5dbf358486ae7ca6cf97c2387ef3f009f4ca99c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 539b57d526b2d3d6a15f9bef1a8bfe104779e02c409aa64fd6765aaee0fc5b4c4a86d2a92fd44f559dbde686d5fb7713ee81e72df815aeada368fc28eeb9a480
|
7
|
+
data.tar.gz: e9bd63e18fd429b8e2443e3c20d34c5a37b9eb7dd9e408ded4bcee3d109c4617e15e006cec32f996aff299ac3bdbb7b0150b637a756e6d388980f8f07fcb7f58
|
data/.idea/modules.xml
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<project version="4">
|
3
|
+
<component name="ProjectModuleManager">
|
4
|
+
<modules>
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/nested_select.iml" filepath="$PROJECT_DIR$/.idea/nested_select.iml" />
|
6
|
+
</modules>
|
7
|
+
</component>
|
8
|
+
</project>
|
@@ -0,0 +1,125 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<module type="RUBY_MODULE" version="4">
|
3
|
+
<component name="ModuleRunConfigurationManager">
|
4
|
+
<shared />
|
5
|
+
</component>
|
6
|
+
<component name="NewModuleRootManager">
|
7
|
+
<content url="file://$MODULE_DIR$">
|
8
|
+
<sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
|
9
|
+
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
|
10
|
+
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
11
|
+
</content>
|
12
|
+
<orderEntry type="jdk" jdkName="RVM: ruby-3.3.4 [ns1]" jdkType="RUBY_SDK" />
|
13
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
14
|
+
<orderEntry type="library" scope="PROVIDED" name="actionpack (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
15
|
+
<orderEntry type="library" scope="PROVIDED" name="actionview (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
16
|
+
<orderEntry type="library" scope="PROVIDED" name="activemodel (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
17
|
+
<orderEntry type="library" scope="PROVIDED" name="activerecord (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
18
|
+
<orderEntry type="library" scope="PROVIDED" name="activesupport (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
19
|
+
<orderEntry type="library" scope="PROVIDED" name="amazing_print (v1.7.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
20
|
+
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
21
|
+
<orderEntry type="library" scope="PROVIDED" name="benchmark (v0.4.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
22
|
+
<orderEntry type="library" scope="PROVIDED" name="bigdecimal (v3.1.9, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
23
|
+
<orderEntry type="library" scope="PROVIDED" name="builder (v3.3.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
24
|
+
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.5.11, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
25
|
+
<orderEntry type="library" scope="PROVIDED" name="byebug (v11.1.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
26
|
+
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.4, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
27
|
+
<orderEntry type="library" scope="PROVIDED" name="connection_pool (v2.5.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
28
|
+
<orderEntry type="library" scope="PROVIDED" name="crass (v1.0.6, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
29
|
+
<orderEntry type="library" scope="PROVIDED" name="date (v3.4.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
30
|
+
<orderEntry type="library" scope="PROVIDED" name="drb (v2.2.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
31
|
+
<orderEntry type="library" scope="PROVIDED" name="erubi (v1.13.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
32
|
+
<orderEntry type="library" scope="PROVIDED" name="i18n (v1.14.6, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
33
|
+
<orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
34
|
+
<orderEntry type="library" scope="PROVIDED" name="irb (v1.14.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
35
|
+
<orderEntry type="library" scope="PROVIDED" name="json (v2.9.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
36
|
+
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
37
|
+
<orderEntry type="library" scope="PROVIDED" name="logger (v1.6.5, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
38
|
+
<orderEntry type="library" scope="PROVIDED" name="loofah (v2.24.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
39
|
+
<orderEntry type="library" scope="PROVIDED" name="minitest (v5.25.4, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
40
|
+
<orderEntry type="library" scope="PROVIDED" name="niceql (v0.6.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
41
|
+
<orderEntry type="library" scope="PROVIDED" name="nokogiri (v1.18.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
42
|
+
<orderEntry type="library" scope="PROVIDED" name="parallel (v1.26.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
43
|
+
<orderEntry type="library" scope="PROVIDED" name="parser (v3.3.6.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
44
|
+
<orderEntry type="library" scope="PROVIDED" name="psych (v5.2.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
45
|
+
<orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
46
|
+
<orderEntry type="library" scope="PROVIDED" name="rack (v3.1.8, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
47
|
+
<orderEntry type="library" scope="PROVIDED" name="rack-session (v2.1.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
48
|
+
<orderEntry type="library" scope="PROVIDED" name="rack-test (v2.2.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
49
|
+
<orderEntry type="library" scope="PROVIDED" name="rackup (v2.2.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
50
|
+
<orderEntry type="library" scope="PROVIDED" name="rails-dom-testing (v2.2.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
51
|
+
<orderEntry type="library" scope="PROVIDED" name="rails-html-sanitizer (v1.6.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
52
|
+
<orderEntry type="library" scope="PROVIDED" name="rails-i18n (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
53
|
+
<orderEntry type="library" scope="PROVIDED" name="rails_sql_prettifier (v7.0.4, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
54
|
+
<orderEntry type="library" scope="PROVIDED" name="railties (v8.0.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
55
|
+
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
56
|
+
<orderEntry type="library" scope="PROVIDED" name="rake (v13.2.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
57
|
+
<orderEntry type="library" scope="PROVIDED" name="rdoc (v6.11.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
58
|
+
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.10.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
59
|
+
<orderEntry type="library" scope="PROVIDED" name="reline (v0.6.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
60
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop (v1.70.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
61
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.37.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
62
|
+
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
63
|
+
<orderEntry type="library" scope="PROVIDED" name="securerandom (v0.4.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
64
|
+
<orderEntry type="library" scope="PROVIDED" name="sqlite3 (v2.5.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
65
|
+
<orderEntry type="library" scope="PROVIDED" name="stringio (v3.1.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
66
|
+
<orderEntry type="library" scope="PROVIDED" name="stubberry (v0.3.0, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
67
|
+
<orderEntry type="library" scope="PROVIDED" name="thor (v1.3.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
68
|
+
<orderEntry type="library" scope="PROVIDED" name="timeout (v0.4.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
69
|
+
<orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.6, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
70
|
+
<orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v3.1.3, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
71
|
+
<orderEntry type="library" scope="PROVIDED" name="unicode-emoji (v4.0.4, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
72
|
+
<orderEntry type="library" scope="PROVIDED" name="uri (v1.0.2, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
73
|
+
<orderEntry type="library" scope="PROVIDED" name="useragent (v0.16.11, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
74
|
+
<orderEntry type="library" scope="PROVIDED" name="zeitwerk (v2.7.1, RVM: ruby-3.3.4 [ns1]) [gem]" level="application" />
|
75
|
+
</component>
|
76
|
+
<component name="RakeTasksCache-v2">
|
77
|
+
<option name="myRootTask">
|
78
|
+
<RakeTaskImpl id="rake">
|
79
|
+
<subtasks>
|
80
|
+
<RakeTaskImpl description="Build nested_select-0.1.0.gem into the pkg directory" fullCommand="build" id="build" />
|
81
|
+
<RakeTaskImpl id="build">
|
82
|
+
<subtasks>
|
83
|
+
<RakeTaskImpl description="Generate SHA512 checksum of nested_select-0.1.0.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
|
84
|
+
</subtasks>
|
85
|
+
</RakeTaskImpl>
|
86
|
+
<RakeTaskImpl description="Remove any temporary products" fullCommand="clean" id="clean" />
|
87
|
+
<RakeTaskImpl description="Remove any generated files" fullCommand="clobber" id="clobber" />
|
88
|
+
<RakeTaskImpl description="Build and install nested_select-0.1.0.gem into system gems" fullCommand="install" id="install" />
|
89
|
+
<RakeTaskImpl id="install">
|
90
|
+
<subtasks>
|
91
|
+
<RakeTaskImpl description="Build and install nested_select-0.1.0.gem into system gems without network access" fullCommand="install:local" id="local" />
|
92
|
+
</subtasks>
|
93
|
+
</RakeTaskImpl>
|
94
|
+
<RakeTaskImpl description="Create tag v0.1.0 and build and push nested_select-0.1.0.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
|
95
|
+
<RakeTaskImpl description="Run RuboCop" fullCommand="rubocop" id="rubocop" />
|
96
|
+
<RakeTaskImpl id="rubocop">
|
97
|
+
<subtasks>
|
98
|
+
<RakeTaskImpl description="Autocorrect RuboCop offenses (only when it's safe)" fullCommand="rubocop:autocorrect" id="autocorrect" />
|
99
|
+
<RakeTaskImpl description="Autocorrect RuboCop offenses (safe and unsafe)" fullCommand="rubocop:autocorrect_all" id="autocorrect_all" />
|
100
|
+
<RakeTaskImpl description="" fullCommand="rubocop:auto_correct" id="auto_correct" />
|
101
|
+
</subtasks>
|
102
|
+
</RakeTaskImpl>
|
103
|
+
<RakeTaskImpl description="Run the test suite" fullCommand="test" id="test" />
|
104
|
+
<RakeTaskImpl id="test">
|
105
|
+
<subtasks>
|
106
|
+
<RakeTaskImpl description="Print out the test command" fullCommand="test:cmd" id="cmd" />
|
107
|
+
<RakeTaskImpl description="Show which test files fail when run in isolation" fullCommand="test:isolated" id="isolated" />
|
108
|
+
<RakeTaskImpl description="Run the test suite and report the slowest 25 tests" fullCommand="test:slow" id="slow" />
|
109
|
+
<RakeTaskImpl description="" fullCommand="test:deps" id="deps" />
|
110
|
+
</subtasks>
|
111
|
+
</RakeTaskImpl>
|
112
|
+
<RakeTaskImpl description="" fullCommand="default" id="default" />
|
113
|
+
<RakeTaskImpl description="" fullCommand="release" id="release" />
|
114
|
+
<RakeTaskImpl id="release">
|
115
|
+
<subtasks>
|
116
|
+
<RakeTaskImpl description="" fullCommand="release:guard_clean" id="guard_clean" />
|
117
|
+
<RakeTaskImpl description="" fullCommand="release:rubygem_push" id="rubygem_push" />
|
118
|
+
<RakeTaskImpl description="" fullCommand="release:source_control_push" id="source_control_push" />
|
119
|
+
</subtasks>
|
120
|
+
</RakeTaskImpl>
|
121
|
+
</subtasks>
|
122
|
+
</RakeTaskImpl>
|
123
|
+
</option>
|
124
|
+
</component>
|
125
|
+
</module>
|
data/.idea/vcs.xml
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# A little bit of nested_select history
|
2
|
+
Awhile ago I've investigated the potential performance boost from partial instantiation
|
3
|
+
of database records in rails applications: [Rails nitro-fast collection rendering with PostgreSQL](https://medium.com/@leshchuk/rails-nitro-fast-collection-rendering-with-postgresql-a5fb07cc215f)
|
4
|
+
|
5
|
+
To be short among the others I've tested the idea of Partial instantiation:
|
6
|
+
|
7
|
+
> Sometimes different actions needs different set of columns per ORM object. You can speedup instantiation
|
8
|
+
> by creating sets of attributes specific for particular request.
|
9
|
+
> It can be done through the scopes and scoped relations inside your model.
|
10
|
+
|
11
|
+
**Pluses**
|
12
|
+
> It may be faster. How fast? Highly depends on data structure and ratio of used columns. I started from instantinating 75% of object columns and go to 1 or 2 columns being instantiated.
|
13
|
+
> In terms of instantiation results are: 1.3–4.2 times faster on simple type columns ( text, string, int, bool etc.), and 1.2–10 times faster when you exclude json/jsonb/store instantiation.
|
14
|
+
> Also all this numbers received without any instantiation callbacks like after_find.
|
15
|
+
|
16
|
+
Also during my investigations I've kinda missed to mention the other aspects of the problem: RAM, DB IOps, network throughput.
|
17
|
+
Requesting less columns improves¹ all that things.
|
18
|
+
|
19
|
+
[1] I have much less idea on how other than PotsgreSQL DB-engines are working with a disk in terms of partial tuples/records reading.
|
20
|
+
Postgres itself will read a whole page from a disk to retrieve the record, but then lesser columns could switch retrieval to an Index-Only scan decreasing IOps significantly
|
21
|
+
|
22
|
+
But that's a pretty obvious. There are lot of articles covering this problem and idea of a partial selection:
|
23
|
+
|
24
|
+
ActiveRecord select :id column over 1000 records in a different way:
|
25
|
+
https://samsaffron.com/archive/2018/06/01/an-analysis-of-memory-bloat-in-active-record-5-2
|
26
|
+
|
27
|
+
Just another simple and newbie technics on boosting ActiveRecord ( including partial selection ):
|
28
|
+
https://medium.com/@snapsheetclaims/11-ways-to-boost-your-activerecord-query-performance-32b9986f093f
|
29
|
+
|
30
|
+
Just partial selection article:
|
31
|
+
https://pawelurbanek.com/activerecord-memory-usage
|
32
|
+
|
33
|
+
And others.
|
34
|
+
|
35
|
+
But the real problem is: **in rails you can't do any selection on preloading models** (until nested_select of course :)) ).
|
36
|
+
Ths means that all that tree of preloaded object goes with ```SELECT table_name.*``` query.
|
37
|
+
|
38
|
+
Technically speaking you may solve this problem by defining custom scopes and defining custom tailored relation with scopes.
|
39
|
+
But that's a lot of a boilerplate code, creating scopes and nested relations for all kinds of requests looks like unreal solution,
|
40
|
+
no one will do such madness.
|
41
|
+
|
42
|
+
## Nested Select patch
|
43
|
+
|
44
|
+
### How preloading happens in rails and when is the best time to interfere
|
45
|
+
|
46
|
+
**Preloading** is a part of activerecord which tends to change pretty often.
|
47
|
+
Practically all major releases interfere preloader code, the majority of current implementation was introduced in the rails 7.0 version.
|
48
|
+
But if you get the idea of current implementation, you can traverse the earlier state and get the idea how to make them work with nested select:
|
49
|
+
Regardless of the major rails version and implementation, you will end up patching the `build_scope` method of
|
50
|
+
`ActiveRecord::Associations::Preloader::Association`!
|
51
|
+
|
52
|
+
So you just need to define a way to deliver select_values to instance of `ActiveRecord::Associations::Preloader::Association`
|
53
|
+
|
54
|
+
### How preloading happens in rails >= 7.0
|
55
|
+
To be honest ( and opinionated :) ), current preloading implementation is a kinda mess, and we need to adapt to this mess without delivering some more.
|
56
|
+
|
57
|
+
Let's look at the scopes example from a specs:
|
58
|
+
```ruby
|
59
|
+
# user <-habtm-> bought_items
|
60
|
+
# user -> has_one -> user_profile -> has_many -> avatars
|
61
|
+
User.includes(:bought_items, user_profile: :avatars)
|
62
|
+
```
|
63
|
+
|
64
|
+
Preloading will create instances of the `Preloader` class for:
|
65
|
+
- each isolated preloading `Branch` which started from the root, in this case: `:bought_items` and `user_profile: :avatars`
|
66
|
+
- each trough relation inside preloading tree, including hidden ones like habtm relation does.
|
67
|
+
|
68
|
+
Each `Preloader` object building it's own preloader tree from a set of `Branch` objects.
|
69
|
+
In a given case it might roughly look like this:
|
70
|
+
```
|
71
|
+
Preloader(:bought_items) -> Branch(:root)
|
72
|
+
\__ Branch(:bought_items) -> Preloader::ThroughAssociation(:bought_items)
|
73
|
+
|
74
|
+
Preloader(user_profile: :avatars) -> Branch(:root)
|
75
|
+
\__ Branch(:user_profile) -> Preloader::Association(:user_profile)
|
76
|
+
\__Branch(:avatars) -> Preloader::Association(:avatars)
|
77
|
+
```
|
78
|
+
|
79
|
+
Each `Branch` will create a set of loaders objects of `Preloader::Association` or `Preloader::ThroughAssociation`
|
80
|
+
Then running all of them will preload nested records and establish a connections between records.
|
81
|
+
|
82
|
+
To be able to select limited attributes sets, we need to deliver them to `Association` level objects, and patch `build_scope` method with it.
|
83
|
+
|
84
|
+
**_Rem:_** Each `Preloader::ThroughAssociation` object creates it's own `Preloader` and starts additional 'isolated' preloading process.
|
85
|
+
|
86
|
+
The implementation of nested_seelct adds a `nested_select_values` attributes into instances of `Preloader`, `Branch`, `Association` hierarchy
|
87
|
+
and some methods to populate corresponding select_values over the tree, trying to be as less invasive as it could be.
|
data/CHANGELOG.md
CHANGED
data/Dockerfile
ADDED
data/README.md
CHANGED
@@ -1,54 +1,73 @@
|
|
1
|
-
|
1
|
+
# WIP disclaimer
|
2
|
+
The gem is under active development now. As of version 0.2.0 you are safe to try in your prod console
|
3
|
+
to uncover it's potential, and try in dev/test env.
|
2
4
|
|
3
|
-
|
5
|
+
Use in prod with caution only if you are properly covered by your CI.
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
Regardless of the major rails version and implementation, you will end up patching the `build_scope` method of
|
9
|
-
`ActiveRecord::Associations::Preloader::Association`!
|
7
|
+
# Nested select -- 7 times faster and 33 times less RAM on preloading relations with heavy columns!
|
8
|
+
nested_select allows to select attributes of relations during preloading process, leading to less RAM and CPU usage.
|
9
|
+
Here is a benchmark output for a [gist I've created](https://gist.github.com/alekseyl/5d08782808a29df6813f16965f70228a) to emulate real-life example: displaying a course with its structure.
|
10
10
|
|
11
|
-
|
11
|
+
Given:
|
12
|
+
- Models are Course, Topic, Lesson.
|
13
|
+
- Their relations has a following structure: course has_many topics, each topic has_many lessons.
|
14
|
+
- To display a single course you need its structure, minimum data needed: topic and lessons titles and ordering.
|
12
15
|
|
13
|
-
|
14
|
-
To be honest ( and opinionated :) ), current preloading implementation is a kinda mess, and we need to adapt to this mess without delivering some more.
|
16
|
+
**Single course**, a real prod set of data used by current UI (~ x33 times less RAM):
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
```
|
19
|
+
irb(main):216:0>compare_nested_select(ids, 1, silence_ar_logger_for_memory_profiling: false)
|
20
|
+
|
21
|
+
------- CPU comparison, for root_collection_size: 1 ----
|
22
|
+
user system total real
|
23
|
+
nested_select 0.096008 0.002876 0.098884 ( 0.466985)
|
24
|
+
simple includes 0.209188 0.058340 0.267528 ( 0.903893)
|
25
|
+
|
26
|
+
----------------- Memory comparison, for root_collection_size: 1 ---------
|
27
|
+
|
28
|
+
D, [2025-01-12T19:08:36.163282 #503] DEBUG -- : Topic Load (4.1ms) SELECT "topics"."id", "topics"."position", "topics"."title", "topics"."course_id" FROM "topics" WHERE "topics"."deleted_at" IS NULL AND "topics"."course_id" = $1 [["course_id", 1624]]
|
29
|
+
D, [2025-01-12T19:08:36.168803 #503] DEBUG -- : Lesson Load (3.9ms) SELECT "lessons"."id", "lessons"."title", "lessons"."topic_id", "lessons"."position", "lessons"."topic_id" FROM "lessons" WHERE "lessons"."deleted_at" IS NULL AND "lessons"."topic_id" = $1 [["topic_id", 7297]]
|
30
|
+
D, [2025-01-12T19:08:37.220379 #503] DEBUG -- : Topic Load (4.2ms) SELECT "topics"."id", "topics"."position", "topics"."title", "topics"."course_id" FROM "topics" WHERE "topics"."deleted_at" IS NULL AND "topics"."course_id" = $1 [["course_id", 1624]]
|
31
|
+
D, [2025-01-12T19:08:37.247484 #503] DEBUG -- : Lesson Load (25.7ms) SELECT "lessons".* FROM "lessons" WHERE "lessons"."deleted_at" IS NULL AND "lessons"."topic_id" = $1 [["topic_id", 7297]]
|
32
|
+
|
33
|
+
------ Nested Select memory consumption for root_collection_size: 1 ------
|
34
|
+
Total allocated: 80.84 kB (972 objects)
|
35
|
+
Total retained: 34.67 kB (288 objects)
|
36
|
+
|
37
|
+
------ Full preloading memory consumption for root_collection_size: 1 ----
|
38
|
+
Total allocated: 1.21 MB (1105 objects)
|
39
|
+
Total retained: 1.16 MB (432 objects)
|
40
|
+
RAM ratio improvements x33.54678126442086 on retain objects
|
41
|
+
RAM ratio improvements x15.002820281285949 on total_allocated objects
|
21
42
|
```
|
22
43
|
|
23
|
-
|
24
|
-
|
25
|
-
- each trough relation inside preloading tree, including hidden ones like habtm relation does.
|
44
|
+
**100 courses**, this is kinda a synthetic example (there is no UI for multiple courses display with their structure)
|
45
|
+
on the real prod data, but the bigger than needed collection (x7 faster):
|
26
46
|
|
27
|
-
Each `Preloader` object building it's own preloader tree from a set of `Branch` objects.
|
28
|
-
In a given case it might roughly look like this:
|
29
|
-
```
|
30
|
-
Preloader(:bought_items) -> Branch(:root)
|
31
|
-
\__ Branch(:bought_items) -> Preloader::ThroughAssociation(:bought_items)
|
32
|
-
|
33
|
-
Preloader(user_profile: :avatars) -> Branch(:root)
|
34
|
-
\__ Branch(:user_profile) -> Preloader::Association(:user_profile)
|
35
|
-
\__Branch(:avatars) -> Preloader::Association(:avatars)
|
36
47
|
```
|
48
|
+
irb(main):280:0> compare_nested_select(ids, 100)
|
37
49
|
|
38
|
-
|
39
|
-
|
50
|
+
------- CPU comparison, for root_collection_size: 100 ----
|
51
|
+
user system total real
|
52
|
+
nested_select 1.571095 0.021778 1.592873 ( 2.263369)
|
53
|
+
simple includes 5.374909 1.704284 7.079193 ( 15.488579)
|
54
|
+
|
55
|
+
----------------- Memory comparison, for root_collection_size: 100 ---------
|
56
|
+
------ Nested Select memory consumption for root_collection_size: 100 ------
|
40
57
|
|
41
|
-
|
58
|
+
Total allocated: 2.79 MB (30702 objects)
|
59
|
+
Total retained: 2.05 MB (16431 objects)
|
42
60
|
|
43
|
-
|
61
|
+
------ Full preloading memory consumption for root_collection_size: 100 ----
|
44
62
|
|
45
|
-
|
63
|
+
Total allocated: 33.05 MB (38332 objects)
|
64
|
+
Total retained: 32.00 MB (24057 objects)
|
65
|
+
RAM ratio improvements x15.57707431190517 on retain objects
|
66
|
+
RAM ratio improvements x11.836000856510193 on total_allocated objects
|
46
67
|
|
47
|
-
|
68
|
+
```
|
48
69
|
|
49
|
-
|
50
|
-
Please do not do it earlier due to security reasons.
|
51
|
-
Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
70
|
+
## Installation
|
52
71
|
|
53
72
|
Install the gem and add to the application's Gemfile by executing:
|
54
73
|
|
@@ -56,11 +75,28 @@ Install the gem and add to the application's Gemfile by executing:
|
|
56
75
|
|
57
76
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
58
77
|
|
59
|
-
$ gem install
|
78
|
+
$ gem install nested_select
|
60
79
|
|
61
80
|
## Usage
|
81
|
+
Assume you have a relation users <- profile, and you want to preview users in a paginated feed,
|
82
|
+
and you need only :photo attribute of a profile, with nested_select you can do it like this:
|
62
83
|
|
63
|
-
|
84
|
+
```ruby
|
85
|
+
# this will preload profile with exact attributes: :id, :user_id and :photo
|
86
|
+
User.includes(:profile).select(profile: :photo).limit(10)
|
87
|
+
```
|
88
|
+
|
89
|
+
## Safety
|
90
|
+
How safe is the partial model loading? Earlier version of rails and activerecord would return nil in the case,
|
91
|
+
when attribute wasn't selected from a DB, but rails 6 started to raise a ActiveModel::MissingAttributeError.
|
92
|
+
So the major problem is already solved -- your code will not operate based on falsy blank values, it will raise an exception.
|
93
|
+
Just cover your actions with proper tests and you are safe.
|
94
|
+
|
95
|
+
## Testing
|
96
|
+
|
97
|
+
```bash
|
98
|
+
docker compose run test
|
99
|
+
```
|
64
100
|
|
65
101
|
## Development
|
66
102
|
|
data/docker-compose.yml
ADDED
@@ -8,8 +8,10 @@ module NestedSelect
|
|
8
8
|
: super.select(*nested_select_values)
|
9
9
|
end
|
10
10
|
|
11
|
-
def apply_nested_select_values(
|
12
|
-
|
11
|
+
def apply_nested_select_values(partial_select_values)
|
12
|
+
foreign_key = reflection.foreign_key unless reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection)
|
13
|
+
|
14
|
+
@nested_select_values = [*partial_select_values, *foreign_key].uniq
|
13
15
|
end
|
14
16
|
end
|
15
17
|
end
|
@@ -11,7 +11,7 @@ module NestedSelect
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def apply_nested_select_values( partial_select_values )
|
14
|
-
return super unless reflection.parent_reflection.is_a?( ActiveRecord::Reflection::HasAndBelongsToManyReflection
|
14
|
+
return super unless reflection.parent_reflection.is_a?( ActiveRecord::Reflection::HasAndBelongsToManyReflection)
|
15
15
|
|
16
16
|
# when parent reflection is a HasAndBelongsToManyReflection,
|
17
17
|
# then we don't need foreign_key to be included, as it does in super
|
@@ -3,9 +3,9 @@ module NestedSelect
|
|
3
3
|
extend ActiveSupport::Autoload
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
-
autoload :Branch, "
|
7
|
-
autoload :ThroughAssociation, "
|
8
|
-
autoload :Association, "
|
6
|
+
autoload :Branch, "nested_select/preloader/branch"
|
7
|
+
autoload :ThroughAssociation, "nested_select/preloader/through_association"
|
8
|
+
autoload :Association, "nested_select/preloader/association"
|
9
9
|
|
10
10
|
included do
|
11
11
|
ActiveRecord::Associations::Preloader::Branch.prepend(Branch)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nested_select
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- alekseyl
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-01-
|
11
|
+
date: 2025-01-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -38,6 +38,132 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.11'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.11'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rails-i18n
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '4'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '4'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sqlite3
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: byebug
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: stubberry
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rails_sql_prettifier
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: amazing_print
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
41
167
|
description: ActiveRecord improved select on nested models, allows partial instantiation
|
42
168
|
on nested models, easy one step improvements on performance and memory
|
43
169
|
email:
|
@@ -49,12 +175,18 @@ files:
|
|
49
175
|
- ".idea/.gitignore"
|
50
176
|
- ".idea/inspectionProfiles/profiles_settings.xml"
|
51
177
|
- ".idea/misc.xml"
|
178
|
+
- ".idea/modules.xml"
|
179
|
+
- ".idea/nested_select.iml"
|
180
|
+
- ".idea/vcs.xml"
|
52
181
|
- ".rubocop.yml"
|
182
|
+
- ABOUT_NESTED_SELECT.md
|
53
183
|
- CHANGELOG.md
|
54
184
|
- CODE_OF_CONDUCT.md
|
185
|
+
- Dockerfile
|
55
186
|
- LICENSE.txt
|
56
187
|
- README.md
|
57
188
|
- Rakefile
|
189
|
+
- docker-compose.yml
|
58
190
|
- lib/nested_select.rb
|
59
191
|
- lib/nested_select/preloader.rb
|
60
192
|
- lib/nested_select/preloader/association.rb
|