tweak readme for markdown and rails version

This commit is contained in:
Keenan Brock 2017-05-05 12:50:47 -04:00
parent d5ef9ed13f
commit 2d3b4d2f30
1 changed files with 141 additions and 141 deletions

282
README.md
View File

@ -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