tweak readme for markdown and rails version
This commit is contained in:
parent
d5ef9ed13f
commit
2d3b4d2f30
282
README.md
282
README.md
|
|
@ -2,72 +2,69 @@
|
|||
|
||||
# Ancestry
|
||||
|
||||
Ancestry is a gem/plugin that allows the records of a Ruby on Rails
|
||||
Ancestry is a gem that allows the records of a Ruby on Rails
|
||||
ActiveRecord model to be organised as a tree structure (or hierarchy). It uses
|
||||
a single, intuitively formatted database column, using a variation on the
|
||||
materialised path pattern. It exposes all the standard tree structure
|
||||
a single database column, using the materialised path pattern. It exposes all the standard tree structure
|
||||
relations (ancestors, parent, root, children, siblings, descendants) and all
|
||||
of them can be fetched in a single SQL query. Additional features are STI
|
||||
support, scopes, depth caching, depth constraints, easy migration from older
|
||||
plugins/gems, integrity checking, integrity restoration, arrangement of
|
||||
gems, integrity checking, integrity restoration, arrangement of
|
||||
(sub)tree into hashes and different strategies for dealing with orphaned
|
||||
records.
|
||||
|
||||
# Installation
|
||||
|
||||
To apply Ancestry to any ActiveRecord model, follow these simple steps:
|
||||
To apply Ancestry to any `ActiveRecord` model, follow these simple steps:
|
||||
|
||||
## Install
|
||||
### Rails 2
|
||||
* See 1-3-stable branch
|
||||
|
||||
### Rails 3 and 4
|
||||
* Add to Gemfile:
|
||||
# Gemfile
|
||||
* Add to Gemfile:
|
||||
```ruby
|
||||
# Gemfile
|
||||
|
||||
gem 'ancestry'
|
||||
gem 'ancestry'
|
||||
```
|
||||
|
||||
* Install required gems:
|
||||
$ bundle install
|
||||
* Install required gems:
|
||||
```bash
|
||||
$ bundle install
|
||||
```
|
||||
|
||||
|
||||
## Add ancestry column to your table
|
||||
* Create migration:
|
||||
$ rails g migration add_ancestry_to_[table] ancestry:string
|
||||
* Create migration:
|
||||
```bash
|
||||
$ rails g migration add_ancestry_to_[table] ancestry:string
|
||||
```
|
||||
|
||||
* Add index to migration:
|
||||
# db/migrate/[date]_add_ancestry_to_[table].rb
|
||||
```ruby
|
||||
# db/migrate/[date]_add_ancestry_to_[table].rb
|
||||
|
||||
class AddAncestryTo[Table] < ActiveRecord::Migration
|
||||
# Rails 4 Syntax
|
||||
def change
|
||||
add_column [table], :ancestry, :string
|
||||
add_index [table], :ancestry
|
||||
end
|
||||
|
||||
# Rails 3 Syntax
|
||||
def up
|
||||
add_column [table], :ancestry, :string
|
||||
add_index [table], :ancestry
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column [table], :ancestry
|
||||
remove_index [table], :ancestry
|
||||
end
|
||||
class AddAncestryTo[Table] < ActiveRecord::Migration
|
||||
def change
|
||||
add_column [table], :ancestry, :string
|
||||
add_index [table], :ancestry
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
* Migrate your database:
|
||||
$ rake db:migrate
|
||||
```bash
|
||||
$ rake db:migrate
|
||||
```
|
||||
|
||||
|
||||
## Add ancestry to your model
|
||||
* Add to [app/models/](model).rb:
|
||||
# app/models/[model.rb]
|
||||
* Add to [app/models/](model).rb:
|
||||
|
||||
class [Model] < ActiveRecord::Base
|
||||
has_ancestry
|
||||
end
|
||||
```ruby
|
||||
# app/models/[model.rb]
|
||||
|
||||
class [Model] < ActiveRecord::Base
|
||||
has_ancestry
|
||||
end
|
||||
```
|
||||
|
||||
Your model is now a tree!
|
||||
|
||||
|
|
@ -75,10 +72,7 @@ Your model is now a tree!
|
|||
|
||||
In version 1.2.0 the **acts_as_tree** method was **renamed to has_ancestry**
|
||||
in order to allow usage of both the acts_as_tree gem and the ancestry gem in a
|
||||
single application. To not break backwards compatibility, the has_ancestry
|
||||
method is aliased with acts_as_tree if ActiveRecord::Base does not respond to
|
||||
acts_as_tree. acts_as_tree will continue to be supported in the future as I
|
||||
personally prefer it.
|
||||
single application. method `acts_as_tree` will continue to be supported in the future.
|
||||
|
||||
# Organising records into a tree
|
||||
|
||||
|
|
@ -89,11 +83,15 @@ parent_id can be set using parent= and parent_id= on a record or by including
|
|||
them in the hash passed to new, create, create!, update_attributes and
|
||||
update_attributes!. For example:
|
||||
|
||||
TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
|
||||
```ruby
|
||||
TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
|
||||
```
|
||||
|
||||
You can also create children through the children relation on a node:
|
||||
|
||||
node.children.create :name => 'Stinky'
|
||||
```ruby
|
||||
node.children.create :name => 'Stinky'
|
||||
```
|
||||
|
||||
# Navigating your tree
|
||||
|
||||
|
|
@ -126,7 +124,7 @@ record:
|
|||
* If the record is a root, other root records are considered siblings
|
||||
|
||||
|
||||
# Options for has_ancestry
|
||||
# Options for `has_ancestry`
|
||||
|
||||
The has_ancestry methods supports the following options:
|
||||
|
||||
|
|
@ -154,9 +152,11 @@ means additional ordering, conditions, limits, etc. can be applied and that
|
|||
the result can be either retrieved, counted or checked for existence. For
|
||||
example:
|
||||
|
||||
node.children.exists?(:name => 'Mary')
|
||||
node.subtree.all(:order => :name, :limit => 10).each do; ...; end
|
||||
node.descendants.count
|
||||
```ruby
|
||||
node.children.where(:name => 'Mary').exists?
|
||||
node.subtree.order(:name).limit(10).each do; ...; end
|
||||
node.descendants.count
|
||||
```
|
||||
|
||||
For convenience, a couple of named scopes are included at the class level:
|
||||
|
||||
|
|
@ -223,70 +223,87 @@ on type for that.
|
|||
Ancestry can arrange an entire subtree into nested hashes for easy navigation
|
||||
after retrieval from the database. TreeNode.arrange could for example return:
|
||||
|
||||
{ #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
|
||||
=> { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
|
||||
=> { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
|
||||
=> {}
|
||||
}
|
||||
}
|
||||
```ruby
|
||||
{ #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
|
||||
=> { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
|
||||
=> { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
|
||||
=> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The arrange method also works on a scoped class, for example:
|
||||
|
||||
TreeNode.find_by_name('Crunchy').subtree.arrange
|
||||
```ruby
|
||||
TreeNode.find_by_name('Crunchy').subtree.arrange
|
||||
```
|
||||
|
||||
The arrange method takes ActiveRecord find options. If you want your hashes to
|
||||
The arrange method takes `ActiveRecord` find options. If you want your hashes to
|
||||
be ordered, you should pass the order to the arrange method instead of to the
|
||||
scope. This also works for Ruby 1.8 since an OrderedHash is returned. For
|
||||
example:
|
||||
scope. example:
|
||||
|
||||
TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
|
||||
```ruby
|
||||
TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
|
||||
```
|
||||
|
||||
To get the arranged nodes as a nested array of hashes for serialization:
|
||||
|
||||
TreeNode.arrange_serializable
|
||||
TreeNode.arrange_serializable
|
||||
|
||||
[
|
||||
{
|
||||
"ancestry" => nil, "id" => 1, "children" => [
|
||||
{ "ancestry" => "1", "id" => 2, "children" => [] }
|
||||
]
|
||||
}
|
||||
```ruby
|
||||
[
|
||||
{
|
||||
"ancestry" => nil, "id" => 1, "children" => [
|
||||
{ "ancestry" => "1", "id" => 2, "children" => [] }
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
You can also supply your own serialization logic using blocks:
|
||||
|
||||
For example, using Active Model Serializers:
|
||||
For example, using `ActiveModel` Serializers:
|
||||
|
||||
TreeNode.arrange_serializable do |parent, children|
|
||||
MySerializer.new(parent, children: children)
|
||||
end
|
||||
```ruby
|
||||
TreeNode.arrange_serializable do |parent, children|
|
||||
MySerializer.new(parent, children: children)
|
||||
end
|
||||
```
|
||||
|
||||
Or plain hashes:
|
||||
|
||||
TreeNode.arrange_serializable do |parent, children|
|
||||
{
|
||||
my_id: parent.id
|
||||
my_children: children
|
||||
}
|
||||
end
|
||||
```ruby
|
||||
TreeNode.arrange_serializable do |parent, children|
|
||||
{
|
||||
my_id: parent.id
|
||||
my_children: children
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
The result of arrange_serializable can easily be serialized to json with
|
||||
'to_json', or some other format:
|
||||
`to_json`, or some other format:
|
||||
|
||||
TreeNode.arrange_serializable.to_json
|
||||
```
|
||||
TreeNode.arrange_serializable.to_json
|
||||
```
|
||||
|
||||
You can also pass the order to the arrange_serializable method just as you can
|
||||
pass it to the arrange method:
|
||||
|
||||
TreeNode.arrange_serializable(:order => :name)
|
||||
```
|
||||
TreeNode.arrange_serializable(:order => :name)
|
||||
```
|
||||
|
||||
# Sorting
|
||||
|
||||
If you just want to sort an array of nodes as if you were traversing them in
|
||||
preorder, you can use the sort_by_ancestry class method:
|
||||
|
||||
TreeNode.sort_by_ancestry(array_of_nodes)
|
||||
```
|
||||
TreeNode.sort_by_ancestry(array_of_nodes)
|
||||
```
|
||||
|
||||
Note that since materialised path trees don't support ordering within a rank,
|
||||
the order of siblings depends on their order in the original array.
|
||||
|
|
@ -307,17 +324,14 @@ provide a more detailed explanation:
|
|||
* Migrate your database: **rake db:migrate**
|
||||
|
||||
|
||||
2. Remove old tree plugin or gem and add in Ancestry
|
||||
* Remove plugin: rm -Rf vendor/plugins/[old plugin]
|
||||
* Remove gem config line from environment.rb: config.gem [old gem]
|
||||
* Add Ancestry to environment.rb: config.gem :ancestry
|
||||
2. Remove old tree gem and add in Ancestry to `Gemfile`
|
||||
* See 'Installation' for more info on installing and configuring gems
|
||||
|
||||
|
||||
3. Change your model
|
||||
* Remove any macros required by old plugin/gem from
|
||||
[app/models/](model).rb
|
||||
* Add to [app/models/](model).rb: **has_ancestry**
|
||||
`[app/models/](model).rb`
|
||||
* Add to `[app/models/](model).rb`: `has_ancestry`
|
||||
|
||||
|
||||
4. Generate ancestry columns
|
||||
|
|
@ -332,13 +346,9 @@ provide a more detailed explanation:
|
|||
|
||||
|
||||
6. Drop parent_id column:
|
||||
* Create migration: **rails g migration
|
||||
[remove_parent_id_from_](table)**
|
||||
* Add to migration: **remove_column [table], :parent_id** (UP) /
|
||||
**add_column [table], :parent_id, :integer** (DOWN)
|
||||
* Migrate your database: **rake db:migrate**
|
||||
|
||||
|
||||
* Create migration: `rails g migration [remove_parent_id_from_](table)`
|
||||
* Add to migration: `remove_column [table], :parent_id`
|
||||
* Migrate your database: `rake db:migrate`
|
||||
|
||||
# Integrity checking and restoration
|
||||
|
||||
|
|
@ -355,73 +365,63 @@ To restore integrity use: [Model].restore_ancestry_integrity!.
|
|||
|
||||
For example, from IRB:
|
||||
|
||||
>> stinky = TreeNode.create :name => 'Stinky'
|
||||
$ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
|
||||
>> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
|
||||
$ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
|
||||
>> stinky.update_attribute :parent, squeeky
|
||||
$ true
|
||||
>> TreeNode.all
|
||||
$ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
|
||||
>> TreeNode.check_ancestry_integrity!
|
||||
!! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
|
||||
>> TreeNode.restore_ancestry_integrity!
|
||||
$ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
|
||||
```
|
||||
>> stinky = TreeNode.create :name => 'Stinky'
|
||||
$ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
|
||||
>> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
|
||||
$ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
|
||||
>> stinky.update_attribute :parent, squeeky
|
||||
$ true
|
||||
>> TreeNode.all
|
||||
$ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
|
||||
>> TreeNode.check_ancestry_integrity!
|
||||
!! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
|
||||
>> TreeNode.restore_ancestry_integrity!
|
||||
$ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
|
||||
```
|
||||
|
||||
Additionally, if you think something is wrong with your depth cache:
|
||||
|
||||
>> TreeNode.rebuild_depth_cache!
|
||||
```
|
||||
>> TreeNode.rebuild_depth_cache!
|
||||
```
|
||||
|
||||
# Tests
|
||||
|
||||
The Ancestry gem comes with a unit test suite consisting of about 1900
|
||||
assertions in about 50 tests. It takes about 10 seconds to run on sqlite. It
|
||||
is run against three databases (sqlite3, mysql and postgresql) and four
|
||||
versions of Activerecord (3.0, 3.1, 3.2 and 4.0) using Appraisals. To run it
|
||||
yourself:
|
||||
* Check out the repository from GitHub
|
||||
* Copy test/database.example.yml to test/database.yml
|
||||
* Run `bundle`
|
||||
* Run `appraisal install`
|
||||
* Run `appraisal rake test`
|
||||
|
||||
|
||||
You can also run against a specific database and specific version of
|
||||
Activerecord:
|
||||
* Run the above commands, except for the last one
|
||||
* Run `appraisal sqlite3-ar-32 rake test` (to test against sqlite3 and
|
||||
Activerecord 3.2)
|
||||
# Running Tests
|
||||
|
||||
```bash
|
||||
git clone git@github.com:stefankroes/ancestry.git
|
||||
cd ancestry
|
||||
cp test/database.example.yml test/database.yml
|
||||
bundle
|
||||
appraisal install
|
||||
# all tests
|
||||
appraisal rake test
|
||||
# single test version (sqlite and rails 5.0)
|
||||
appraisal sqlite3-ar-50 rake test
|
||||
```
|
||||
|
||||
# Internals
|
||||
|
||||
As can be seen in the previous section, Ancestry stores a path from the root
|
||||
to the parent for every node. This is a variation on the materialised path
|
||||
database pattern. It allows Ancestry to fetch any relation (siblings,
|
||||
Ancestry stores a path from the root to the parent for every node.
|
||||
This is a variation on the materialised path database pattern.
|
||||
It allows Ancestry to fetch any relation (siblings,
|
||||
descendants, etc.) in a single SQL query without the complicated algorithms
|
||||
and incomprehensibility associated with left and right values. Additionally,
|
||||
any inserts, deletes and updates only affect nodes within the affected node's
|
||||
own subtree.
|
||||
|
||||
In the example above, the ancestry column is created as a string. This puts a
|
||||
limitation on the depth of the tree of about 40 or 50 levels, which I think
|
||||
may be enough for most users. To increase the maximum depth of the tree,
|
||||
increase the size of the string that is being used or change it to a text to
|
||||
In the example above, the `ancestry` column is created as a `string`. This puts a
|
||||
limitation on the depth of the tree of about 40 or 50 levels. To increase the
|
||||
maximum depth of the tree, increase the size of the `string` or use `text` to
|
||||
remove the limitation entirely. Changing it to a text will however decrease
|
||||
performance because an index cannot be put on the column in that case.
|
||||
|
||||
The materialised path pattern requires Ancestry to use a 'like' condition in
|
||||
order to fetch descendants. This should not be particularly slow however since
|
||||
the the condition never starts with a wildcard which allows the DBMS to use
|
||||
the column index. If you have any data on performance with a large number of
|
||||
records, please drop me line.
|
||||
order to fetch descendants. The wild character (`%`) is on the left of the
|
||||
query, so indexes should be used.
|
||||
|
||||
# Contributing and license
|
||||
|
||||
I will try to keep Ancestry up to date with changing versions of Rails and
|
||||
Ruby and also with any bug reports I might receive. I will implement new
|
||||
features on request as I see fit and have time.
|
||||
|
||||
Question? Bug report? Faulty/incomplete documentation? Feature request? Please
|
||||
post an issue on 'http://github.com/stefankroes/ancestry/issues'. Make sure
|
||||
you have read the documentation and you have included tests and documentation
|
||||
|
|
|
|||
Loading…
Reference in New Issue