File Coverage

blib/lib/DBIx/Class/Tree/AdjacencyList.pm
Criterion Covered Total %
statement 55 79 69.6
branch 9 28 32.1
condition 3 8 37.5
subroutine 10 15 66.6
pod 10 11 90.9
total 87 141 61.7


line stmt bran cond sub pod time code
1             package DBIx::Class::Tree::AdjacencyList;
2             # vim: ts=8:sw=4:sts=4:et
3              
4 2     2   142773 use strict;
  2         3  
  2         59  
5 2     2   9 use warnings;
  2         3  
  2         59  
6              
7 2     2   9 use base qw( DBIx::Class );
  2         2  
  2         163  
8 2     2   8 use Carp qw( croak );
  2         2  
  2         1463  
9              
10             =head1 NAME
11              
12             DBIx::Class::Tree::AdjacencyList - Manage a tree of data using the common adjacency list model.
13              
14             =head1 SYNOPSIS
15              
16             Create a table for your tree data.
17              
18             CREATE TABLE employees (
19             employee_id INTEGER PRIMARY KEY AUTOINCREMENT,
20             parent_id INTEGER NOT NULL DEFAULT 0,
21             name TEXT NOT NULL
22             );
23              
24             In your Schema or DB class add Tree::AdjacencyList to the top
25             of the component list.
26              
27             __PACKAGE__->load_components(qw( Tree::AdjacencyList ... ));
28              
29             Specify the column that contains the parent ID of each row.
30              
31             package My::Employee;
32             __PACKAGE__->parent_column('parent_id');
33              
34             Optionally, automatically maintane a consistent tree structure.
35              
36             __PACKAGE__->repair_tree( 1 );
37              
38             Thats it, now you can modify and analyze the tree.
39              
40             #!/usr/bin/perl
41             use My::Employee;
42              
43             my $employee = My::Employee->create({ name=>'Matt S. Trout' });
44              
45             my $rs = $employee->children();
46             my @siblings = $employee->children();
47              
48             my $parent = $employee->parent();
49             $employee->parent( 7 );
50              
51             =head1 DESCRIPTION
52              
53             This module provides methods for working with adjacency lists. The
54             adjacency list model is a very common way of representing a tree structure.
55             In this model each row in a table has a prent ID column that references the
56             primary key of another row in the same table. Because of this the primary
57             key must only be one column and is usually some sort of integer. The row
58             with a parent ID of 0 is the root node and is usually the parent of all
59             other rows. Although, there is no limitation in this module that would
60             stop you from having multiple root nodes.
61              
62             =head1 METHODS
63              
64             =head2 parent_column
65              
66             __PACKAGE__->parent_column('parent_id');
67              
68             Declares the name of the column that contains the self-referential
69             ID which defines the parent row. This will create a has_many (children)
70             and belongs_to (parent) relationship.
71              
72             This method also sets up an additional has_many relationship called
73             parents which is useful when you want to treat an adjacency list
74             as a DAG.
75              
76             =cut
77              
78             __PACKAGE__->mk_classdata( '_parent_column' => 'parent_id' );
79              
80             sub parent_column {
81 2     2 1 209 my $class = shift;
82 2 50       7 if (@_) {
83 2         2 my $parent_col = shift;
84 2         33 my $primary_col = ($class->primary_columns())[0];
85 2         876 $class->belongs_to( '_parent' => $class => { "foreign.$primary_col" => "self.$parent_col" } );
86 2         1707 $class->has_many( 'children' => $class => { "foreign.$parent_col" => "self.$primary_col" } );
87 2         541 $class->has_many( 'parents' => $class => { "foreign.$primary_col" => "self.$parent_col" }, { cascade_delete => 0, cascade_copy => 0 } );
88 2         517 $class->_parent_column( $parent_col );
89 2         84 return 1;
90             }
91 0         0 return $class->_parent_column();
92             }
93              
94             =head2 repair_tree
95              
96             __PACKAGE__->repair_tree( 1 );
97              
98             When set a true value this flag causes all changes to a node's parent to
99             trigger an integrity check on the tree. If, when changing a node's parent
100             to one of it's descendents then all its children will first be moved to have
101             the same current parent, and then the node's parent is changed.
102              
103             So, for example, if the tree is like this:
104              
105             A
106             B
107             C
108             D
109             E
110             F
111              
112             And you execute:
113              
114             $b->parent( $d );
115              
116             Since D is a descendant of B then all of D's siblings get their parent
117             changed to A. Then B's parent is set to D.
118              
119             A
120             C
121             D
122             B
123             E
124             F
125              
126             =cut
127              
128             __PACKAGE__->mk_classdata( 'repair_tree' => 0 );
129              
130             =head2 parent
131              
132             my $parent = $employee->parent();
133             $employee->parent( $parent_obj );
134             $employee->parent( $parent_id );
135              
136             Retrieves the object's parent object, or changes the object's
137             parent to the specified parent or parent ID. If you would like
138             to make the object the root node, just set the parent to 0.
139              
140             If you are setting the parent then 0 will be returned if the
141             specified parent is already the object's parent and 1 on
142             success.
143              
144             =cut
145              
146             sub parent {
147 94     94 1 241702 my $self = shift;
148 94 100       224 if (@_) {
149 83         68 my $new_parent = shift;
150 83         2199 my $parent_col = $self->_parent_column();
151 83 100       1567 if (ref($new_parent)) {
152 3   33     31 $new_parent = $new_parent->id() || croak('Parent object does not have an ID');;
153             }
154 83 50 100     394 return 0 if ($new_parent == ($self->get_column($parent_col)||0));
155              
156 83 50       2822 if ($self->repair_tree()) {
157 0         0 my $found = $self->has_descendant( $new_parent );
158 0 0       0 if ($found) {
159 0         0 my $children = $self->children();
160              
161 0         0 while (my $child = $children->next()) {
162 0         0 $child->parent( $self->$parent_col() );
163             }
164             }
165             }
166              
167 83         3262 $self->set_column( $parent_col => $new_parent );
168 83         4759 $self->update();
169 83         82433 return 1;
170             }
171 11         186 return $self->_parent();
172             }
173             =head2 ancestors
174              
175             @list = $employee->ancestors();
176              
177             Returns a list of ancestors starting with a record's
178             parent and moving toward the tree root.
179              
180             =cut
181              
182             sub ancestors {
183 2     2 0 9733 my $self = shift;
184 2         5 my @ancestors = ();
185 2         4 my $rec = $self;
186 2         6 while ($rec = $rec->parent) {
187 16         40824 push(@ancestors, $rec);
188             }
189 2         550 return @ancestors;
190             }
191              
192              
193             =head2 has_descendant
194              
195             if ($employee->has_descendant( $id )) { ... }
196              
197             Returns true if the object has a descendant with the
198             specified ID.
199              
200             =cut
201              
202             sub has_descendant {
203 0     0 1 0 my ($self, $find_id) = @_;
204              
205 0         0 my $children = $self->children();
206 0         0 while (my $child = $children->next()) {
207 0 0       0 if ($child->id() eq $find_id) {
208 0         0 return 1;
209             }
210 0 0       0 return 1 if ($child->has_descendant( $find_id ));
211             }
212              
213 0         0 return 0;
214             }
215              
216             =head2 parents
217              
218             my $parents = $node->parents();
219             my @parents = $node->parents();
220              
221             This has_many relationship is not that useful as it will
222             never return more than one parent due to the one-to-many
223             structure of adjacency lists. The reason this relationship
224             is defined is so that this tree type may be treated as if
225             it was a DAG.
226              
227             =head2 children
228              
229             my $children_rs = $employee->children();
230             my @children = $employee->children();
231              
232             Returns a list or record set, depending on context, of all
233             the objects one level below the current one. This method
234             is created when parent_column() is called, which sets up a
235             has_many relationship called children.
236              
237             =head2 attach_child
238              
239             $parent->attach_child( $child );
240             $parent->attach_child( $child, $child, ... );
241              
242             Sets the child, or children, to the new parent. Returns 1
243             on success and returns 0 if the parent object already has
244             the child.
245              
246             =cut
247              
248             sub attach_child {
249 2     2 1 32541 my $self = shift;
250 2         26 my $return = 1;
251 2         5 foreach my $child (@_) {
252 2         8 $child->parent( $self );
253             }
254 2         6 return $return;
255             }
256              
257             =head2 siblings
258              
259             my $rs = $node->siblings();
260             my @siblings = $node->siblings();
261              
262             Returns either a result set or an array of all other objects
263             with the same parent as the calling object.
264              
265             =cut
266              
267             sub siblings {
268 4     4 1 33985 my( $self ) = @_;
269 4         106 my $parent_col = $self->_parent_column;
270 4         178 my $primary_col = ($self->primary_columns())[0];
271 4         167 my $rs = $self->result_source->resultset->search(
272             {
273             $parent_col => $self->get_column($parent_col),
274             $primary_col => { '!=' => $self->get_column($primary_col) },
275             },
276             );
277 4 50       1360 return $rs->all() if (wantarray());
278 4         15 return $rs;
279             }
280              
281             =head2 attach_sibling
282              
283             $obj->attach_sibling( $sibling );
284             $obj->attach_sibling( $sibling, $sibling, ... );
285              
286             Sets the passed in object(s) to have the same parent
287             as the calling object. Returns 1 on success and
288             0 if the sibling already has the same parent.
289              
290             =cut
291              
292             sub attach_sibling {
293 2     2 1 21313 my $self = shift;
294 2         4 my $return = 1;
295 2         5 foreach my $node (@_) {
296 2 50       9 $return = 0 if (!$node->parent( $self->parent() ));
297             }
298 2         6 return $return;
299             }
300              
301             =head2 is_leaf
302              
303             if ($obj->is_leaf()) { ... }
304              
305             Returns 1 if the object has no children, and 0 otherwise.
306              
307             =cut
308              
309             sub is_leaf {
310 0     0 1   my( $self ) = @_;
311              
312 0           my $has_child = $self->children_rs->count();
313              
314 0 0         return $has_child ? 0 : 1;
315             }
316              
317             =head2 is_root
318              
319             if ($obj->is_root()) { ... }
320              
321             Returns 1 if the object has no parent, and 0 otherwise.
322              
323             =cut
324              
325             sub is_root {
326 0     0 1   my( $self ) = @_;
327 0 0         return ( $self->get_column( $self->_parent_column ) ? 0 : 1 );
328             }
329              
330             =head2 is_branch
331              
332             if ($obj->is_branch()) { ... }
333              
334             Returns 1 if the object has a parent and has children.
335             Returns 0 otherwise.
336              
337             =cut
338              
339             sub is_branch {
340 0     0 1   my( $self ) = @_;
341 0 0 0       return ( ($self->is_leaf() or $self->is_root()) ? 0 : 1 );
342             }
343              
344             =head2 set_primary_key
345              
346             This method is an override of DBIx::Class' method for setting the
347             class' primary key column(s). This method passes control right on
348             to the normal method after first validating that only one column is
349             being selected as a primary key. If more than one column is then
350             an error will be thrown.
351              
352             =cut
353              
354             sub set_primary_key {
355 0     0 1   my $self = shift;
356 0 0         if (@_>1) {
357 0           croak('You may only specify a single column as the primary key for adjacency tree classes');
358             }
359 0           return $self->next::method( @_ );
360             }
361              
362             1;
363             __END__