with_recursive_tree 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +161 -0
- data/Rakefile +3 -0
- data/lib/with_recursive_tree/version.rb +3 -0
- data/lib/with_recursive_tree.rb +83 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9c1ccdf38306a7ddff768b00b73f3d3ff58bf094f7524e55b63e0502f4d757dd
|
4
|
+
data.tar.gz: 5572ccbc0023f7d7226b41f24ffa8513084dc2e8780075f48904dac3aa510a96
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0a9003bb5a943e7a4d48abed8db293a292dd21dfae943ab1c4899673920fa1c943b5f949d4caf9351c763b3f62e0a14bbfca0ff3e9dfa8fe7daa8e9f4ea6462e
|
7
|
+
data.tar.gz: 378da103d4c78855cc5c2acef7557e19a27eb2c3f11a4c63d26fa1e29dadff551d433d84060b0f7ef5bd2b711808d97b5c071e80e8f1ff5b61801ddca7a8bd38
|
data/README.md
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
# with_recursive_tree
|
2
|
+
|
3
|
+
Tree structures for ActiveRecord using CTE (Common Table Expressions). Traverse the whole tree with just one query.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem "with_recursive_tree"
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
First, your model needs a reference to the its parent. Tipically, this is a `parent_id` column in your table. Once you have that reference, you can add `with_recursive_tree` to your model:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class Category < ApplicationRecord
|
25
|
+
with_recursive_tree
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
By doing this, with_recursive_tree will add 2 associations:
|
30
|
+
|
31
|
+
* `parent`: the parent of the node
|
32
|
+
* `children`: the children of this node
|
33
|
+
|
34
|
+
To build these associations, with_recursive_tree will use the `id` and the `parent_id` columns as the primary and foreing keys, respectively. If you want to specify different primary and foreign keys, you can do that by passing the `primary_key` and `foreign_key` options. For example, for a categories table whose primary key is `category_id` and the parent record id is `parent_category_id`, you would set it up as follows:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class Category < ApplicationRecord
|
38
|
+
with_recursive_tree foreign_key: :parent_category_id, primary_key: :category_id
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
Lastly, you can specify how to sort each node's `children` by passing the `order` option to `with_recursive_tree`. If no `order` option is set, it will default to `id`. This option is useful especially when you need to traverse the tree in a specific order. For example:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class Category < ApplicationRecord
|
46
|
+
with_recursive_tree order: :name
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
### Class methods
|
51
|
+
|
52
|
+
| Method | Description |
|
53
|
+
|--------|-------------|
|
54
|
+
| `::roots` | Returns all roots (nodes without parent). |
|
55
|
+
|
56
|
+
### Instance methods
|
57
|
+
|
58
|
+
| Method | Description |
|
59
|
+
|--------|-------------|
|
60
|
+
| `#ancestors` | Returns all ancestors of the node. |
|
61
|
+
| `#descendants` | Returns all descendants of the node (subtree). |
|
62
|
+
| `#leaf?` | Returns whether the node is a leaf (has no children). |
|
63
|
+
| `#depth` | Returns the depth of the current node. |
|
64
|
+
| `#root` | Returns the root node of the current node's tree. |
|
65
|
+
| `#root?` | Returns whether the node is a root (has no parent). |
|
66
|
+
| `#self_and_ancestors` | Returns the node and all its ancestors. |
|
67
|
+
| `#self_and_descendants` | Returns the node and all its descendants (subtree). |
|
68
|
+
| `#self_and_siblings` | Returns the current node and all its siblings. |
|
69
|
+
| `#siblings` | Returns the current node's siblings. |
|
70
|
+
|
71
|
+
### Tree traversing
|
72
|
+
|
73
|
+
You can traverse the tree using `#descendants` or `#self_and_descendants` in combination with the `#bfs` (breadth-first search) and `#dfs` (depth-first search, pre-order) scopes.
|
74
|
+
|
75
|
+
For example, given the following tree:
|
76
|
+
|
77
|
+
![sample tree](/assets/tree.png)
|
78
|
+
|
79
|
+
and the following class:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
class Node < ApplicationRecord
|
83
|
+
with_recursive_tree order: :name
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
You can do:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
root = Node.roots.first
|
91
|
+
|
92
|
+
puts root.self_and_descendants.bfs.map { |node| "#{"-" * node.depth}#{node.name}" }
|
93
|
+
```
|
94
|
+
|
95
|
+
and you will get:
|
96
|
+
|
97
|
+
```
|
98
|
+
A
|
99
|
+
-B
|
100
|
+
-L
|
101
|
+
--C
|
102
|
+
--H
|
103
|
+
--M
|
104
|
+
--N
|
105
|
+
---D
|
106
|
+
---I
|
107
|
+
---O
|
108
|
+
---P
|
109
|
+
---Q
|
110
|
+
---R
|
111
|
+
----E
|
112
|
+
----F
|
113
|
+
----G
|
114
|
+
----J
|
115
|
+
-----K
|
116
|
+
```
|
117
|
+
|
118
|
+
Similarly, you can do the same with `#dfs`:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
puts root.self_and_descendants.dfs.map { |node| "#{"-" * node.depth}#{node.name}" }
|
122
|
+
```
|
123
|
+
|
124
|
+
and you will get:
|
125
|
+
|
126
|
+
```
|
127
|
+
A
|
128
|
+
-B
|
129
|
+
--C
|
130
|
+
---D
|
131
|
+
----E
|
132
|
+
----F
|
133
|
+
----G
|
134
|
+
--H
|
135
|
+
---I
|
136
|
+
----J
|
137
|
+
-----K
|
138
|
+
-L
|
139
|
+
--M
|
140
|
+
--N
|
141
|
+
---O
|
142
|
+
---P
|
143
|
+
---Q
|
144
|
+
---R
|
145
|
+
```
|
146
|
+
|
147
|
+
## Benchmarks
|
148
|
+
|
149
|
+
You can run some [benchmarks](/benchmarks/benchmark.rb) to compare with_recursive_tree agains [acts_as_tree](https://github.com/amerine/acts_as_tree), [ancestry](https://github.com/stefankroes/ancestry/) and [closure_tree](https://github.com/ClosureTree/closure_tree).
|
150
|
+
|
151
|
+
Spoiler: benchmarks are always basic cases so you mustn't trust them as if they were the word of god, but they are useful tools for development/testing and setting a baseline performance requirement..
|
152
|
+
|
153
|
+
In any case, you must weight the trade-offs between what you need to accomplish and performance.
|
154
|
+
|
155
|
+
## Contributing
|
156
|
+
|
157
|
+
Fork the repo, add your feature, create a PR.
|
158
|
+
|
159
|
+
## License
|
160
|
+
|
161
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
require "with_recursive_tree/version"
|
4
|
+
|
5
|
+
module WithRecursiveTree
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
scope :bfs, -> { order :depth }
|
10
|
+
scope :dfs, -> { self }
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
def with_recursive_tree(primary_key: :id, foreign_key: :parent_id, order: nil)
|
15
|
+
belongs_to :parent, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :children, optional: true
|
16
|
+
|
17
|
+
has_many :children, -> { order order }, class_name: name, primary_key: primary_key, foreign_key: foreign_key, inverse_of: :parent
|
18
|
+
|
19
|
+
define_singleton_method(:with_recursive_tree_primary_key) { primary_key }
|
20
|
+
define_singleton_method(:with_recursive_tree_foreign_key) { foreign_key }
|
21
|
+
define_singleton_method(:with_recursive_tree_order) { order || primary_key }
|
22
|
+
end
|
23
|
+
|
24
|
+
def roots
|
25
|
+
where with_recursive_tree_foreign_key => nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def ancestors
|
30
|
+
self_and_ancestors.excluding self
|
31
|
+
end
|
32
|
+
|
33
|
+
def descendants
|
34
|
+
self_and_descendants.excluding self
|
35
|
+
end
|
36
|
+
|
37
|
+
def leaf?
|
38
|
+
children.none?
|
39
|
+
end
|
40
|
+
|
41
|
+
def depth
|
42
|
+
attributes["depth"] || ancestors.count
|
43
|
+
end
|
44
|
+
alias_method :level, :depth
|
45
|
+
|
46
|
+
def root
|
47
|
+
self_and_ancestors.find_by self.class.with_recursive_tree_foreign_key => nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def root?
|
51
|
+
parent.blank?
|
52
|
+
end
|
53
|
+
|
54
|
+
def self_and_ancestors
|
55
|
+
self.class.with(search_tree: self.class.with_recursive(
|
56
|
+
search_tree: [
|
57
|
+
self.class.where(self.class.with_recursive_tree_primary_key => send(self.class.with_recursive_tree_primary_key)),
|
58
|
+
self.class.joins("JOIN search_tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} = search_tree.#{self.class.with_recursive_tree_foreign_key}")
|
59
|
+
]
|
60
|
+
).select("*").from("search_tree")).from("search_tree AS #{self.class.table_name}")
|
61
|
+
end
|
62
|
+
|
63
|
+
def self_and_descendants
|
64
|
+
self.class.with(search_tree: self.class.with_recursive(
|
65
|
+
search_tree: [
|
66
|
+
self.class.where(self.class.with_recursive_tree_primary_key => send(self.class.with_recursive_tree_primary_key)).select("*, '/' || #{self.class.with_recursive_tree_primary_key} || '/' AS path, 0 AS depth"),
|
67
|
+
Arel.sql(self.class.joins("JOIN search_tree ON #{self.class.table_name}.#{self.class.with_recursive_tree_foreign_key} = search_tree.#{self.class.with_recursive_tree_primary_key}").select("#{self.class.table_name}.*, search_tree.path || #{self.class.table_name}.#{self.class.with_recursive_tree_primary_key} || '/' AS path, depth + 1 AS depth").order(self.class.with_recursive_tree_order).to_sql)
|
68
|
+
]
|
69
|
+
).select("*").from("search_tree")).from("search_tree AS #{self.class.table_name}")
|
70
|
+
end
|
71
|
+
|
72
|
+
def self_and_siblings
|
73
|
+
root? ? self.class.roots : parent.children
|
74
|
+
end
|
75
|
+
|
76
|
+
def siblings
|
77
|
+
self_and_siblings.excluding self
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
ActiveSupport.on_load :active_record do
|
82
|
+
include WithRecursiveTree
|
83
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: with_recursive_tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Patricio Mac Adden
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '7.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '7.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activesupport
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '7.2'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '7.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: railties
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '7.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '7.2'
|
55
|
+
description: Tree structures for ActiveRecord
|
56
|
+
email:
|
57
|
+
- patriciomacadden@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- README.md
|
63
|
+
- Rakefile
|
64
|
+
- lib/with_recursive_tree.rb
|
65
|
+
- lib/with_recursive_tree/version.rb
|
66
|
+
homepage: https://github.com/sinaptia/with_recursive_tree
|
67
|
+
licenses: []
|
68
|
+
metadata:
|
69
|
+
homepage_uri: https://github.com/sinaptia/with_recursive_tree
|
70
|
+
source_code_uri: https://github.com/sinaptia/with_recursive_tree
|
71
|
+
post_install_message:
|
72
|
+
rdoc_options: []
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubygems_version: 3.5.11
|
87
|
+
signing_key:
|
88
|
+
specification_version: 4
|
89
|
+
summary: Tree structures for ActiveRecord
|
90
|
+
test_files: []
|