diff --git a/README.md b/README.md index aa110ab4..e82e556b 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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.** @@ -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 @@ -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 @@ -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/). @@ -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? diff --git a/lib/closure_tree/has_closure_tree_root.rb b/lib/closure_tree/has_closure_tree_root.rb index 8e03bc16..56d5568f 100644 --- a/lib/closure_tree/has_closure_tree_root.rb +++ b/lib/closure_tree/has_closure_tree_root.rb @@ -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 @@ -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]] diff --git a/spec/db/schema.rb b/spec/db/schema.rb index cb53906b..18d3f4fd 100644 --- a/spec/db/schema.rb +++ b/spec/db/schema.rb @@ -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| diff --git a/spec/has_closure_tree_root_spec.rb b/spec/has_closure_tree_root_spec.rb index 858b54e6..04fcccf0 100644 --- a/spec/has_closure_tree_root_spec.rb +++ b/spec/has_closure_tree_root_spec.rb @@ -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 "2@example.com" + expect(root.children[0].parent.children[1].email).to eq "3@example.com" + 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 "2@example.com" + expect(root.children[0].parent.children[1].email).to eq "3@example.com" + 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) @@ -66,6 +84,10 @@ to eq "1@example.com" end + it "works if true passed on first call" do + expect(group_reloaded.root_user_including_tree(true).email).to eq "1@example.com" + end + it "eager loads inverse association to group" do expect do root = group_reloaded.root_user_including_tree