Skip to content

Added documentation for has_closure_tree_root #236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ for a description of different tree storage algorithms.

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

1. Add `gem 'closure_tree'` to your Gemfile
1. Add `gem 'closure_tree'` to your Gemfile

2. Run `bundle install`

Expand All @@ -74,7 +74,7 @@ Note that closure_tree only supports ActiveRecord 4.1 and later, and has test co
```

Make sure you check out the [large number options](#available-options) that `has_closure_tree` accepts.

**IMPORTANT: Make sure you add `has_closure_tree` _after_ `attr_accessible` and
`self.table_name =` lines in your model.**

Expand Down Expand Up @@ -114,9 +114,9 @@ NOTE: Run `rails g closure_tree:config` to create an initializer with extra

## Warning

As stated above, using multiple hierarchy gems (like `ancestry` or `nested set`) on the same model
As stated above, using multiple hierarchy gems (like `ancestry` or `nested set`) on the same model
will most likely result in pain, suffering, hair loss, tooth decay, heel-related ailments, and gingivitis.
Assume things will break.
Assume things will break.

## Usage

Expand Down Expand Up @@ -168,7 +168,7 @@ child1.ancestry_path

You can `find` as well as `find_or_create` by "ancestry paths".

If you provide an array of strings to these methods, they reference the `name` column in your
If you provide an array of strings to these methods, they reference the `name` column in your
model, which can be overridden with the `:name_column` option provided to `has_closure_tree`.

```ruby
Expand Down Expand Up @@ -255,6 +255,45 @@ server may not be happy trying to do this.

HT: [ancestry](https://github.com/stefankroes/ancestry#arrangement) and [elhoyos](https://github.com/mceachen/closure_tree/issues/11)

### Eager loading

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.

```ruby
comment.children.includes(:author)
```

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.

In some cases, a viable alternative is the following:

```ruby
comment.self_and_descendants.includes(:author)
```

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.

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:

```ruby
# app/models/post.rb
has_closure_tree_root :root_comment
```

This gives you a plain `has_one` association (`root_comment`) to the root `Comment` (i.e. that with null `parent_id`).

It also gives you a method called `root_comment_including_tree`, which you can invoke as follows:

```ruby
a_post.root_comment_including_tree(:author)
```

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.

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.

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.

### Graph visualization

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

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

**No.** `update_attribute` skips the validation hook that is required for maintaining the
**No.** `update_attribute` skips the validation hook that is required for maintaining the
hierarchy table.

### Does this gem support multiple parents?
Expand Down
18 changes: 8 additions & 10 deletions lib/closure_tree/has_closure_tree_root.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,16 @@ def has_closure_tree_root(assoc_name, options = {})
has_one assoc_name, -> { where(parent: nil) }, options

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

# Memoize
@closure_tree_roots ||= {}
@closure_tree_roots[assoc_name] ||= {}
unless reload
# Memoize
@closure_tree_roots ||= {}
@closure_tree_roots[assoc_name] ||= {}
if @closure_tree_roots[assoc_name].has_key?(assoc_map)
return @closure_tree_roots[assoc_name][assoc_map]
end
Expand All @@ -52,7 +50,7 @@ def has_closure_tree_root(assoc_name, options = {})

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

Expand Down
1 change: 1 addition & 0 deletions spec/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
create_table "contracts" do |t|
t.integer "user_id", :null => false
t.integer "contract_type_id"
t.string "title"
end

create_table "contract_types" do |t|
Expand Down
36 changes: 29 additions & 7 deletions spec/has_closure_tree_root_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,36 @@
user3.children << user5
user3.children << user6

user1.contracts.create!(contract_type: ct1)
user2.contracts.create!(contract_type: ct1)
user3.contracts.create!(contract_type: ct1)
user3.contracts.create!(contract_type: ct2)
user4.contracts.create!(contract_type: ct2)
user5.contracts.create!(contract_type: ct1)
user6.contracts.create!(contract_type: ct2)
user1.contracts.create!(title: "Contract 1", contract_type: ct1)
user2.contracts.create!(title: "Contract 2", contract_type: ct1)
user3.contracts.create!(title: "Contract 3", contract_type: ct1)
user3.contracts.create!(title: "Contract 4", contract_type: ct2)
user4.contracts.create!(title: "Contract 5", contract_type: ct2)
user5.contracts.create!(title: "Contract 6", contract_type: ct1)
user6.contracts.create!(title: "Contract 7", contract_type: ct2)
end

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

it "loads all nodes in a constant number of queries" do
expect do
root = group_reloaded.root_user_including_tree
expect(root.children[0].email).to eq "[email protected]"
expect(root.children[0].parent.children[1].email).to eq "[email protected]"
end.to_not exceed_query_limit(2)
end

it "loads all nodes plus single association in a constant number of queries" do
expect do
root = group_reloaded.root_user_including_tree(:contracts)
expect(root.children[0].email).to eq "[email protected]"
expect(root.children[0].parent.children[1].email).to eq "[email protected]"
expect(root.children[0].children[0].contracts[0].user.
parent.parent.children[1].children[1].contracts[0].title).to eq "Contract 7"
end.to_not exceed_query_limit(3)
end

it "loads all nodes and associations in a constant number of queries" do
expect do
root = group_reloaded.root_user_including_tree(contracts: :contract_type)
Expand Down Expand Up @@ -66,6 +84,10 @@
to eq "[email protected]"
end

it "works if true passed on first call" do
expect(group_reloaded.root_user_including_tree(true).email).to eq "[email protected]"
end

it "eager loads inverse association to group" do
expect do
root = group_reloaded.root_user_including_tree
Expand Down