Skip to content

Commit 0e48fa3

Browse files
committed
Merge branch 'has_closure_tree_root_docs' of git://github.com/sassafrastech/closure_tree into sassafrastech-has_closure_tree_root_docs
2 parents 69f0151 + 043b0bc commit 0e48fa3

File tree

4 files changed

+83
-23
lines changed

4 files changed

+83
-23
lines changed

README.md

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ for a description of different tree storage algorithms.
5858

5959
Note that closure_tree only supports ActiveRecord 4.1 and later, and has test coverage for MySQL, PostgreSQL, and SQLite.
6060

61-
1. Add `gem 'closure_tree'` to your Gemfile
61+
1. Add `gem 'closure_tree'` to your Gemfile
6262

6363
2. Run `bundle install`
6464

@@ -75,7 +75,7 @@ Note that closure_tree only supports ActiveRecord 4.1 and later, and has test co
7575
```
7676

7777
Make sure you check out the [large number options](#available-options) that `has_closure_tree` accepts.
78-
78+
7979
**IMPORTANT: Make sure you add `has_closure_tree` _after_ `attr_accessible` and
8080
`self.table_name =` lines in your model.**
8181

@@ -115,9 +115,9 @@ NOTE: Run `rails g closure_tree:config` to create an initializer with extra
115115
116116
## Warning
117117
118-
As stated above, using multiple hierarchy gems (like `ancestry` or `nested set`) on the same model
118+
As stated above, using multiple hierarchy gems (like `ancestry` or `nested set`) on the same model
119119
will most likely result in pain, suffering, hair loss, tooth decay, heel-related ailments, and gingivitis.
120-
Assume things will break.
120+
Assume things will break.
121121
122122
## Usage
123123
@@ -169,7 +169,7 @@ child1.ancestry_path
169169
170170
You can `find` as well as `find_or_create` by "ancestry paths".
171171
172-
If you provide an array of strings to these methods, they reference the `name` column in your
172+
If you provide an array of strings to these methods, they reference the `name` column in your
173173
model, which can be overridden with the `:name_column` option provided to `has_closure_tree`.
174174
175175
```ruby
@@ -256,6 +256,45 @@ server may not be happy trying to do this.
256256
257257
HT: [ancestry](https://github.com/stefankroes/ancestry#arrangement) and [elhoyos](https://github.com/mceachen/closure_tree/issues/11)
258258
259+
### Eager loading
260+
261+
Since most of closure_tree's methods (e.g. `children`) return regular `ActiveRecord` scopes, you can use the `includes` method for eager loading, e.g.
262+
263+
```ruby
264+
comment.children.includes(:author)
265+
```
266+
267+
However, note that the above approach only eager loads the requested associations for the immediate children of `comment`. If you want to walk through the entire tree, you may still end up making many queries and loading duplicate copies of objects.
268+
269+
In some cases, a viable alternative is the following:
270+
271+
```ruby
272+
comment.self_and_descendants.includes(:author)
273+
```
274+
275+
This would load authors for `comment` and all its descendants in a constant number of queries. However, the return value is an array of `Comment`s, and the tree structure is thus lost, which makes it difficult to walk the tree using elegant recursive algorithms.
276+
277+
A third option is to use `has_closure_tree_root` on the model that is composed by the closure_tree model (e.g. a `Post` may be composed by a tree of `Comment`s). So in `post.rb`, you would do:
278+
279+
```ruby
280+
# app/models/post.rb
281+
has_closure_tree_root :root_comment
282+
```
283+
284+
This gives you a plain `has_one` association (`root_comment`) to the root `Comment` (i.e. that with null `parent_id`).
285+
286+
It also gives you a method called `root_comment_including_tree`, which you can invoke as follows:
287+
288+
```ruby
289+
a_post.root_comment_including_tree(:author)
290+
```
291+
292+
The result of this call will be the root `Comment` with all descendants _and_ associations loaded in a constant number of queries. Inverse associations are also setup on all nodes, so as you walk the tree, calling `children` or `parent` on any node will _not_ trigger any further queries and no duplicate copies of objects are loaded into memory.
293+
294+
The class and foreign key of `root_comment` are assumed to be `Comment` and `post_id`, respectively. These can be overridden in the usual way.
295+
296+
The same caveat stated above with `hash_tree` also applies here: this method will load the entire tree into memory. If the tree is very large, this may be a bad idea, in which case using the eager loading methods above may be preferred.
297+
259298
### Graph visualization
260299

261300
```to_dot_digraph``` is suitable for passing into [Graphviz](http://www.graphviz.org/).
@@ -473,7 +512,7 @@ Yup! [Ilya Bodrov](https://github.com/bodrovis) wrote [Nested Comments with Rail
473512

474513
### Can I update parentage with `update_attribute`?
475514

476-
**No.** `update_attribute` skips the validation hook that is required for maintaining the
515+
**No.** `update_attribute` skips the validation hook that is required for maintaining the
477516
hierarchy table.
478517

479518
### Can I assign a parent to multiple children with ```#update_all```?

lib/closure_tree/has_closure_tree_root.rb

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,16 @@ def has_closure_tree_root(assoc_name, options = {})
1515
has_one assoc_name, -> { where(parent: nil) }, options
1616

1717
# Fetches the association, eager loading all children and given associations
18-
define_method("#{assoc_name}_including_tree") do |assoc_map_or_reload = nil, assoc_map = nil|
18+
define_method("#{assoc_name}_including_tree") do |*args|
1919
reload = false
20-
if assoc_map_or_reload.is_a?(::Hash)
21-
assoc_map = assoc_map_or_reload
22-
else
23-
reload = assoc_map_or_reload
24-
end
20+
reload = args.shift if args && (args.first == true || args.first == false)
21+
assoc_map = args
22+
assoc_map = [nil] if assoc_map.blank?
2523

24+
# Memoize
25+
@closure_tree_roots ||= {}
26+
@closure_tree_roots[assoc_name] ||= {}
2627
unless reload
27-
# Memoize
28-
@closure_tree_roots ||= {}
29-
@closure_tree_roots[assoc_name] ||= {}
3028
if @closure_tree_roots[assoc_name].has_key?(assoc_map)
3129
return @closure_tree_roots[assoc_name][assoc_map]
3230
end
@@ -52,7 +50,7 @@ def has_closure_tree_root(assoc_name, options = {})
5250

5351
# Fetch all descendants in constant number of queries.
5452
# This is the last query-triggering statement in the method.
55-
temp_root.self_and_descendants.includes(assoc_map).each do |node|
53+
temp_root.self_and_descendants.includes(*assoc_map).each do |node|
5654
id_hash[node.id] = node
5755
parent_node = id_hash[node[parent_col_id]]
5856

spec/db/schema.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
create_table "contracts" do |t|
7272
t.integer "user_id", :null => false
7373
t.integer "contract_type_id"
74+
t.string "title"
7475
end
7576

7677
create_table "contract_types" do |t|

spec/has_closure_tree_root_spec.rb

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,36 @@
2626
user3.children << user5
2727
user3.children << user6
2828

29-
user1.contracts.create!(contract_type: ct1)
30-
user2.contracts.create!(contract_type: ct1)
31-
user3.contracts.create!(contract_type: ct1)
32-
user3.contracts.create!(contract_type: ct2)
33-
user4.contracts.create!(contract_type: ct2)
34-
user5.contracts.create!(contract_type: ct1)
35-
user6.contracts.create!(contract_type: ct2)
29+
user1.contracts.create!(title: "Contract 1", contract_type: ct1)
30+
user2.contracts.create!(title: "Contract 2", contract_type: ct1)
31+
user3.contracts.create!(title: "Contract 3", contract_type: ct1)
32+
user3.contracts.create!(title: "Contract 4", contract_type: ct2)
33+
user4.contracts.create!(title: "Contract 5", contract_type: ct2)
34+
user5.contracts.create!(title: "Contract 6", contract_type: ct1)
35+
user6.contracts.create!(title: "Contract 7", contract_type: ct2)
3636
end
3737

3838
context "with basic config" do
3939
let!(:group) { Group.create!(name: "TheGroup") }
4040

41+
it "loads all nodes in a constant number of queries" do
42+
expect do
43+
root = group_reloaded.root_user_including_tree
44+
expect(root.children[0].email).to eq "2@example.com"
45+
expect(root.children[0].parent.children[1].email).to eq "3@example.com"
46+
end.to_not exceed_query_limit(2)
47+
end
48+
49+
it "loads all nodes plus single association in a constant number of queries" do
50+
expect do
51+
root = group_reloaded.root_user_including_tree(:contracts)
52+
expect(root.children[0].email).to eq "2@example.com"
53+
expect(root.children[0].parent.children[1].email).to eq "3@example.com"
54+
expect(root.children[0].children[0].contracts[0].user.
55+
parent.parent.children[1].children[1].contracts[0].title).to eq "Contract 7"
56+
end.to_not exceed_query_limit(3)
57+
end
58+
4159
it "loads all nodes and associations in a constant number of queries" do
4260
expect do
4361
root = group_reloaded.root_user_including_tree(contracts: :contract_type)
@@ -66,6 +84,10 @@
6684
to eq "1@example.com"
6785
end
6886

87+
it "works if true passed on first call" do
88+
expect(group_reloaded.root_user_including_tree(true).email).to eq "1@example.com"
89+
end
90+
6991
it "eager loads inverse association to group" do
7092
expect do
7193
root = group_reloaded.root_user_including_tree

0 commit comments

Comments
 (0)