File Coverage

blib/lib/DBIx/OO/Tree.pm
Criterion Covered Total %
statement 12 322 3.7
branch 0 104 0.0
condition 0 18 0.0
subroutine 4 18 22.2
pod 14 14 100.0
total 30 476 6.3


line stmt bran cond sub pod time code
1             package DBIx::OO::Tree;
2              
3 2     2   19989 use strict;
  2         5  
  2         125  
4 2     2   12 use vars qw(@EXPORT);
  2         4  
  2         90  
5 2     2   11 use version; our $VERSION = qv('0.0.1');
  2         4  
  2         18  
6              
7 2     2   178 use Carp;
  2         4  
  2         9791  
8              
9             require Exporter;
10             *import = \&Exporter::import;
11             @EXPORT = qw(
12             tree_append
13             tree_insert_before
14             tree_insert_after
15             tree_get_subtree
16             tree_compute_levels
17             tree_reparent
18             tree_move_after
19             tree_move_before
20             tree_delete
21             tree_get_path
22             tree_get_next_sibling
23             tree_get_prev_sibling
24             tree_get_next
25             tree_get_prev
26             );
27              
28             =head1 NAME
29              
30             DBIx::OO::Tree -- manipulate hierarchical data using the "nested sets" model
31              
32             =head1 SYNOPSYS
33              
34             CREATE TABLE Categories (
35             id INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY,
36             label VARCHAR(255),
37              
38             -- these columns are required by DBIx::OO::Tree
39             parent INTEGER UNSIGNED,
40             lft INTEGER UNSIGNED NOT NULL,
41             rgt INTEGER UNSIGNED NOT NULL,
42             mvg TINYINT DEFAULT 0,
43              
44             INDEX(lft),
45             INDEX(rgt),
46             INDEX(mvg),
47             INDEX(parent)
48             );
49              
50             * * *
51              
52             package Category;
53             use base 'DBIx::OO';
54             use DBIx::OO::Tree;
55              
56             __PACKAGE__->table('Categories');
57             __PACKAGE__->columns(P => [ 'id' ],
58             E => [ 'label', 'parent' ]);
59              
60             # note it's not necessary to declare lft, rgt, mvg or parent. We
61             # declare parent simply because it might be useful, but
62             # DBIx::OO:Tree works with low-level SQL therefore it doesn't
63             # require that the DBIx::OO object has these fields.
64              
65             # the code below creates the structure presented in [1]
66              
67             my $electronics = Category->tree_append({ label => 'electronics' });
68             my $tvs = $electronics->tree_append({ label => 'televisions' });
69             my $tube = $tvs->tree_append({ label => 'tube' });
70             my $plasma = $tvs->tree_append({ label => 'plasma' });
71             my $lcd = $plasma->tree_insert_before({ label => 'lcd' });
72             my $portable = $tvs->tree_insert_after({ label => 'portable electronics' });
73             my $mp3 = $portable->tree_append({ label => 'mp3 players' });
74             my $flash = $mp3->tree_append({ label => 'flash' });
75             my $cds = $portable->tree_append({ label => 'cd players' });
76             my $radios = Category->tree_append($portable->id,
77             { label => '2 way radios' });
78              
79             # fetch and display a subtree
80              
81             my $data = $electronics->tree_get_subtree({
82             fields => [qw( label lft rgt parent )]
83             });
84             my $levels = Category->tree_compute_levels($data);
85              
86             foreach my $i (@$data) {
87             print ' ' x $levels->{$i->{id}}, $i->{label}, "\n";
88             }
89              
90             ## or, create DBIx::OO objects from returned data:
91              
92             my $array = Category->init_from_data($data);
93             print join("\n", (map { ' ' x $levels->{$_->id} . $_->label } @$array));
94              
95             # display path info
96              
97             my $data = $flash->tree_get_path;
98             print join("\n", (map { $_->{label} } @$data));
99              
100             # move nodes around
101              
102             $mp3->tree_reparent($lcd->id);
103             $tvs->tree_reparent($portable->id);
104             $cds->tree_reparent(undef);
105              
106             $plasma->tree_move_before($tube->id);
107             $portable->tree_move_before($electronics->id);
108              
109             # delete nodes
110              
111             $lcd->tree_delete;
112              
113             =head1 OVERVIEW
114              
115             This module is a complement to DBIx::OO to facilitate storing trees in
116             database using the "nested sets model", presented in [1]. Its main
117             ambition is to be extremely fast at retrieving data (sacrificing for
118             this the performance of UPDATE-s, INSERT-s or DELETE-s). Currently
119             this module B you to have these columns in the table:
120              
121             - id: primary key (integer)
122             - parent: integer, references the parent node (NULL for root nodes)
123             - lft, rgt: store the node position
124             - mvg: used only when moving nodes
125              
126             "parent" and "mvg" are not esentially required by the nested sets
127             model as presented in [1], but they are necessary for this module to
128             work. In particular, "mvg" is only required by functions that move
129             nodes, such as tree_reparent(). If you don't want to move nodes
130             around you can omit "mvg".
131              
132             Retrieval functions should be very fast (one SQL executed). To
133             further promote speed, they don't return DBIx::OO blessed objects, but
134             an array of hashes instead. It's easy to create DBIx::OO objects from
135             these, if required, by calling DBIx::OO->init_from_data() (see
136             DBIx::OO for more information).
137              
138             Insert/delete/move functions, however, need to ensure the tree
139             integrity. Here's what happens currently:
140              
141             - tree_append, tree_insert_before, tree_insert_after -- these execute
142             one SELECT and two UPDATE-s (that potentially could affect a lot of
143             rows).
144              
145             - tree_delete: execute one SELECT, one DELETE and two UPDATE-s.
146              
147             - tree_reparent -- executes 2 SELECT-s and 7 UPDATE-s. I know, this
148             sounds horrible--if you have better ideas I'd love to hear them.
149              
150             B this module could well work with Class::DBI, although it is
151             untested. You just need to provide the get_dbh() method to your
152             packages, comply to this module's table requirements (i.e. provide the
153             right columns) and it should work just fine. Any success/failure
154             stories are welcome.
155              
156             =head1 DATABASE INTEGRITY
157              
158             Since the functions that update the database need to run multiple
159             queries in order to maintain integrity, they should normally do this
160             inside a transaction. However, it looks like MySQL does not support
161             nested transactions, therefore if I call transaction_start /
162             transaction_commit inside these functions they will mess with an
163             eventual transaction that might have been started by the calling code.
164              
165             In short: you should make sure the updates happen in a transaction,
166             but we can't enforce this in our module.
167              
168             =head1 API
169              
170             =head2 tree_append($parent_id, \%values)
171              
172             Appends a new node in the subtree of the specified parent. If
173             $parent_id is undef, it will add a root node. When you want to add a
174             root node you can as well omit specifying the $parent_id (our code
175             will realize that the first argument is a reference).
176              
177             $values is a hash as required by DBIx::OO::create().
178              
179             Examples:
180              
181             $node = Category->tree_append({ label => 'electronics' });
182             $node = Category->tree_append(undef, { label => 'electronics' });
183              
184             $lcd = Category->tree_append($tvs->id, { label => 'lcd' });
185             $lcd->tree_append({ label => 'monitors' });
186              
187             As you can see, you can call it both as a package method or as an
188             object method. When you call it as a package method, it will look at
189             the type of the first argument. If it's a reference, it will guess
190             that you want to add a root node. Otherwise it will add the new node
191             under the specified parent.
192              
193             Beware of mistakes! Do NOT call it like this:
194              
195             $tvs = Category->search({ label => 'televisions' })->[0];
196             Category->tree_append($tvs, { label => 'lcd' });
197              
198             If you specify a parent, it MUST be its ID, not an object!
199              
200             =cut
201              
202             sub tree_append {
203 0     0 1   my $self = shift;
204 0           my ($parent, $val);
205 0 0         if (ref $self) {
206 0           $parent = $self->id;
207             } else {
208 0           $parent = shift;
209 0 0         if (ref $parent eq 'HASH') {
    0          
210             # assuming $val and no parent
211 0           $val = $parent;
212 0           $parent = undef;
213             } elsif (ref $parent) {
214 0           $parent = $parent->id;
215             }
216             }
217 0   0       $val ||= shift;
218              
219 0           my $orig = 0;
220 0           my $dbh = $self->get_dbh;
221 0           my $table = $self->table;
222              
223 0 0         if (defined $parent) {
224 0           my $a = $dbh->selectrow_arrayref("SELECT rgt FROM `$table` WHERE id = $parent");
225 0           $orig = $a->[0] - 1;
226 0           $dbh->do("UPDATE `$table` SET rgt = rgt + 2 WHERE rgt > $orig");
227 0           $dbh->do("UPDATE `$table` SET lft = lft + 2 WHERE lft > $orig");
228             } else {
229 0           my $a = $dbh->selectrow_arrayref("SELECT MAX(rgt) FROM `$table` WHERE parent IS NULL");
230 0 0 0       $orig = $a ? ($a->[0] || 0) : 0;
231             }
232              
233 0           delete $val->{lft};
234 0           delete $val->{rgt};
235 0           delete $val->{mvg};
236 0           delete $val->{parent};
237              
238 0           my %args = ( lft => $orig + 1,
239             rgt => $orig + 2,
240             parent => $parent );
241 0 0         @args{keys %$val} = values %$val
242             if $val;
243 0           return $self->create(\%args);
244             }
245              
246             =head2 tree_insert_before, tree_insert_after ($anchor, \%values)
247              
248             Similar in function to tree_append, but these functions allow you to
249             insert a node before or after a specified node ($anchor).
250              
251             Examples:
252              
253             $lcd->tree_insert_after({ label => 'plasma' });
254             $lcd->tree_insert_before({ label => 'tube' });
255              
256             # Or, as a package method:
257              
258             Category->tree_insert_after($lcd->id, { label => 'plasma' });
259             Category->tree_insert_before($lcd->id, { label => 'tube' });
260              
261             Note that specifying the parent is not required, because it's clearly
262             that the new node should have the same parent as the anchor node.
263              
264             =cut
265              
266             sub tree_insert_before {
267 0     0 1   my $self = shift;
268 0           my ($pos, $val);
269 0 0         if (ref $self) {
270 0           $pos = $self->id;
271             } else {
272 0           $pos = shift;
273             }
274 0           $val = shift;
275              
276 0 0         Carp::croak('$pos MUST be a scalar (the ID of the referred node)')
277             if ref $pos;
278              
279 0           my $dbh = $self->get_dbh;
280 0           my $table = $self->table;
281              
282 0           my $a = $dbh->selectrow_arrayref("SELECT lft, parent FROM `$table` WHERE id = $pos");
283 0           my ($orig, $parent) = @$a;
284              
285 0           $dbh->do("UPDATE `$table` SET rgt = rgt + 2 WHERE rgt >= $orig");
286 0           $dbh->do("UPDATE `$table` SET lft = lft + 2 WHERE lft >= $orig");
287              
288 0           delete $val->{lft};
289 0           delete $val->{rgt};
290 0           delete $val->{mvg};
291 0           delete $val->{parent};
292              
293 0           my %args = ( lft => $orig,
294             rgt => $orig + 1,
295             parent => $parent );
296 0 0         @args{keys %$val} = values %$val
297             if $val;
298 0           return $self->create(\%args);
299             }
300              
301             sub tree_insert_after {
302 0     0 1   my $self = shift;
303 0           my ($pos, $val);
304 0 0         if (ref $self) {
305 0           $pos = $self->id;
306             } else {
307 0           $pos = shift;
308             }
309 0           $val = shift;
310              
311 0 0         Carp::croak('$pos MUST be a scalar (the ID of the referred node)')
312             if ref $pos;
313              
314 0           my $dbh = $self->get_dbh;
315 0           my $table = $self->table;
316              
317 0           my $a = $dbh->selectrow_arrayref("SELECT rgt, parent FROM `$table` WHERE id = $pos");
318 0           my ($orig, $parent) = @$a;
319              
320 0           $dbh->do("UPDATE `$table` SET rgt = rgt + 2 WHERE rgt > $orig");
321 0           $dbh->do("UPDATE `$table` SET lft = lft + 2 WHERE lft > $orig");
322              
323 0           delete $val->{lft};
324 0           delete $val->{rgt};
325 0           delete $val->{mvg};
326 0           delete $val->{parent};
327              
328 0           my %args = ( lft => $orig + 1,
329             rgt => $orig + 2,
330             parent => $parent );
331 0 0         @args{keys %$val} = values %$val
332             if $val;
333 0           return $self->create(\%args);
334             }
335              
336             =head2 tree_reparent($source_id, $dest_id)
337              
338             This function will remove the $source node from its current parent
339             and append it to the $dest node. As with the other functions, you can
340             call it both as a package method or as an object method. When you
341             call it as an object method, it's not necessary to specify $source.
342              
343             You can specify I for $dest_id, in which case $source will
344             become a root node (as if it would be appended with
345             tree_append(undef)).
346              
347             No nodes are DELETE-ed nor INSERT-ed by this function. It simply
348             moves I nodes, which means that any node ID-s that you
349             happen to have should remain valid and point to the same nodes.
350             However, the tree structure is changed, so if you maintain the tree in
351             memory you have to update it after calling this funciton. Same
352             applies to tree_move_before() and tree_move_after().
353              
354             Examples:
355              
356             # the following are equivalent
357              
358             Category->tree_reparent($lcd->id, $plasma->id);
359             $lcd->tree_reparent($plasma->id);
360              
361             This function does a lot of work in order to maintain the tree
362             integrity, therefore it might be slow.
363              
364             NOTE: it doesn't do any safety checks to make sure moving the node is
365             allowed. For instance, you can't move a node to one of its child
366             nodes.
367              
368             =cut
369              
370             # sub _check_can_move {
371             # my ($src_lft, $dest_lft, $dest_rgt) = @_;
372             # }
373              
374             sub tree_reparent {
375 0     0 1   my $self = shift;
376 0           my ($source, $dest);
377 0 0         if (ref $self) {
378 0           $source = $self->id;
379             } else {
380 0           $source = shift;
381             }
382 0           $dest = shift;
383              
384 0 0 0       Carp::croak('arguments MUST be scalars (source and destination parent node IDs)')
385             if ref $dest or ref $source;
386              
387 0           my $dbh = $self->get_dbh;
388 0           my $table = $self->table;
389              
390             # get source info
391 0           my $a = $dbh->selectrow_arrayref("SELECT lft, rgt FROM `$table` WHERE id = $source");
392 0           my ($orig_left, $orig_right) = @$a;
393 0           my $width = $orig_right - $orig_left + 1;
394              
395             # hint to ignore subtree items in further computation
396 0           $dbh->do("UPDATE `$table` SET mvg = 1 WHERE lft BETWEEN $orig_left AND $orig_right");
397              
398             # "collapse" tree by reducing rgt and lft for nodes after the removed one
399 0           $dbh->do("UPDATE `$table` SET rgt = rgt - $width WHERE rgt > $orig_right");
400 0           $dbh->do("UPDATE `$table` SET lft = lft - $width WHERE lft > $orig_right");
401              
402 0           my $diff;
403              
404 0 0         if (defined $dest) {
405             # get destination info (it's important to do it here as it can be modified by the UPDATE-s above)
406 0           $a = $dbh->selectrow_arrayref("SELECT rgt FROM `$table` WHERE id = $dest");
407 0           my ($dest_right) = @$a;
408 0           $diff = $dest_right - $orig_left;
409              
410 0           $dbh->do("UPDATE `$table` SET rgt = rgt + $width WHERE NOT mvg AND rgt >= $dest_right");
411 0           $dbh->do("UPDATE `$table` SET lft = lft + $width WHERE NOT mvg AND lft >= $dest_right");
412             } else {
413             # appending a root node
414 0           my $a = $dbh->selectrow_arrayref("SELECT MAX(rgt) FROM `$table` WHERE parent IS NULL");
415 0           my ($dest_right) = @$a;
416 0           $diff = $dest_right - $orig_left + 1;
417 0           $dest = 'NULL';
418             }
419              
420             # finally, update subtree items and remove the ignore hint
421 0           $dbh->do("UPDATE `$table` SET lft = lft + $diff, rgt = rgt + $diff, mvg = 0 WHERE mvg");
422 0           $dbh->do("UPDATE `$table` SET parent = $dest WHERE id = $source");
423             }
424              
425             =head2 tree_move_before, tree_move_after ($source_id, $anchor_id)
426              
427             These functions are similar to a reparent operation, but they allow
428             one to specify I to put the $source node, in the subtree of
429             $anchor's parent. See tree_reparent().
430              
431             Examples:
432              
433             $portable->tree_move_before($electronics->id);
434             Category->tree_move_after($lcd->id, $flash->id);
435              
436             =cut
437              
438             sub tree_move_before {
439 0     0 1   my ($self) = shift;
440 0           my ($source, $anchor);
441 0 0         if (ref $self) {
442 0           $source = $self->id;
443             } else {
444 0           $source = shift;
445             }
446 0           $anchor = shift;
447              
448 0 0 0       Carp::croak('arguments MUST be scalars (source and destination parent node IDs)')
449             if ref $anchor or ref $source;
450              
451 0           my $dbh = $self->get_dbh;
452 0           my $table = $self->table;
453              
454             # get source info
455 0           my $a = $dbh->selectrow_arrayref("SELECT lft, rgt FROM `$table` WHERE id = $source");
456 0           my ($orig_left, $orig_right) = @$a;
457 0           my $width = $orig_right - $orig_left + 1;
458              
459             # hint to ignore subtree items in further computation
460 0           $dbh->do("UPDATE `$table` SET mvg = 1 WHERE lft BETWEEN $orig_left AND $orig_right");
461              
462             # "collapse" tree by reducing rgt and lft for nodes after the removed one
463 0           $dbh->do("UPDATE `$table` SET rgt = rgt - $width WHERE rgt > $orig_right");
464 0           $dbh->do("UPDATE `$table` SET lft = lft - $width WHERE lft > $orig_right");
465              
466             # get destination info (it's important to do it here as it can be modified by the UPDATE-s above)
467 0           $a = $dbh->selectrow_arrayref("SELECT lft, parent FROM `$table` WHERE id = $anchor");
468 0           my ($dest_left, $dest_parent) = @$a;
469 0 0         if (!defined $dest_parent) {
470 0           $dest_parent = 'NULL';
471             }
472 0           my $diff = $dest_left - $orig_left;
473              
474 0           $dbh->do("UPDATE `$table` SET rgt = rgt + $width WHERE NOT mvg AND rgt >= $dest_left");
475 0           $dbh->do("UPDATE `$table` SET lft = lft + $width WHERE NOT mvg AND lft >= $dest_left");
476              
477             # finally, update subtree items and remove the ignore hint
478 0           $dbh->do("UPDATE `$table` SET lft = lft + $diff, rgt = rgt + $diff, mvg = 0 WHERE mvg");
479 0           $dbh->do("UPDATE `$table` SET parent = $dest_parent WHERE id = $source");
480             }
481              
482             sub tree_move_after {
483 0     0 1   my ($self) = shift;
484 0           my ($source, $anchor);
485 0 0         if (ref $self) {
486 0           $source = $self->id;
487             } else {
488 0           $source = shift;
489             }
490 0           $anchor = shift;
491              
492 0 0 0       Carp::croak('arguments MUST be scalars (source and destination parent node IDs)')
493             if ref $anchor or ref $source;
494              
495 0           my $dbh = $self->get_dbh;
496 0           my $table = $self->table;
497              
498             # get source info
499 0           my $a = $dbh->selectrow_arrayref("SELECT lft, rgt FROM `$table` WHERE id = $source");
500 0           my ($orig_left, $orig_right) = @$a;
501 0           my $width = $orig_right - $orig_left + 1;
502              
503             # hint to ignore subtree items in further computation
504 0           $dbh->do("UPDATE `$table` SET mvg = 1 WHERE lft BETWEEN $orig_left AND $orig_right");
505              
506             # "collapse" tree by reducing rgt and lft for nodes after the removed one
507 0           $dbh->do("UPDATE `$table` SET rgt = rgt - $width WHERE rgt > $orig_right");
508 0           $dbh->do("UPDATE `$table` SET lft = lft - $width WHERE lft > $orig_right");
509              
510             # get destination info (it's important to do it here as it can be modified by the UPDATE-s above)
511 0           $a = $dbh->selectrow_arrayref("SELECT rgt, parent FROM `$table` WHERE id = $anchor");
512 0           my ($dest_right, $dest_parent) = @$a;
513 0 0         if (!defined $dest_parent) {
514 0           $dest_parent = 'NULL';
515             }
516 0           my $diff = $dest_right + 1 - $orig_left;
517              
518 0           $dbh->do("UPDATE `$table` SET rgt = rgt + $width WHERE NOT mvg AND rgt > $dest_right");
519 0           $dbh->do("UPDATE `$table` SET lft = lft + $width WHERE NOT mvg AND lft > $dest_right");
520              
521             # finally, update subtree items and remove the ignore hint
522 0           $dbh->do("UPDATE `$table` SET lft = lft + $diff, rgt = rgt + $diff, mvg = 0 WHERE mvg");
523 0           $dbh->do("UPDATE `$table` SET parent = $dest_parent WHERE id = $source");
524             }
525              
526             =head2 tree_delete($node_id)
527              
528             Removes a node (and its full subtree) from the database.
529              
530             Equivalent examples:
531              
532             Category->tree_delete($lcd->id);
533             $lcd->tree_delete;
534              
535             =cut
536              
537             sub tree_delete {
538 0     0 1   my ($self) = shift;
539 0           my $id;
540 0 0         if (ref $self) {
541 0           $id = $self->id;
542             } else {
543 0           $id = shift;
544             }
545              
546 0           my $dbh = $self->get_dbh;
547 0           my $table = $self->table;
548              
549 0           my $a = $dbh->selectrow_arrayref("SELECT lft, rgt FROM `$table` WHERE id = $id");
550 0           my ($left, $right) = @$a;
551 0           my $width = $right - $left + 1;
552              
553 0           $dbh->do("DELETE FROM `$table` WHERE lft BETWEEN $left AND $right");
554 0           $dbh->do("UPDATE `$table` SET rgt = rgt - $width WHERE rgt > $right");
555 0           $dbh->do("UPDATE `$table` SET lft = lft - $width WHERE lft > $right");
556             }
557              
558             =head2 tree_get_subtree(\%args)
559              
560             Retrieves the full subtree of a specified node. $args is a hashref
561             that can contain:
562              
563             - parent : the ID of the node whose subtree we want to get
564             - where : an WHERE clause in SQL::Abstract format
565             - limit : allows you to limit the results (using SQL LIMIT)
566             - offset : SQL OFFSET
567             - fields : (arrayref) allows you to specify a list of fields you're
568             interested in
569              
570             This can be called as a package method, or as an object method.
571              
572             Examples first:
573              
574             $all_nodes = Category->tree_get_subtree;
575              
576             $nodes = Category->tree_get_subtree({ parent => $portable->id });
577             ## OR
578             $nodes = $portable->tree_get_subtree;
579              
580             # Filtering:
581             $nodes = Category->tree_get_subtree({ where => { label => { -like => '%a%' }}});
582              
583             # Specify fields:
584             $nodes = Category->tree_get_subtree({ fields => [ 'label' ] });
585              
586             This function returns an array of hashes that contain the fields you
587             required. If you specify no fields, 'id' and 'parent' will be
588             SELECT-ed by default. Even if you do specify an array of field names,
589             'id' and 'parent' would still be included in the SELECT (so you don't
590             want to specify them).
591              
592             Using this array you can easily create DBIx::OO objects (or in our
593             sample, Category objects):
594              
595             $arrayref = Category->init_from_data($nodes);
596              
597             OK, let's get to a more real-world example. Suppose we have a forum
598             and we need to list all messages in a thread ($thread_id). Here's
599             what we're going to do:
600              
601             $data = ForumMessage->tree_get_subtree({
602             parent => $thread_id,
603             fields => [qw( subject body author date )],
604             });
605              
606             # the above runs one SQL query
607              
608             $objects = ForumMessage->init_from_data($data);
609              
610             # the above simply initializes ForumMessage objects from the
611             # returned data, B calling the database (since we have
612             # the primary key automatically selected by tree_get_subtree, and
613             # also have cared to select the fields we're going to use).
614              
615             # compute the level of each message, to indent them easily
616              
617             $levels = ForumMessage->tree_compute_levels($data);
618              
619             # and now display them
620              
621             foreach my $msg (@$objects) {
622             my $class = 'level' . $levels{$msg->id};
623             print "
", $msg->subject, "

",
624             $msg->body, "

By: ", $msg->author, "";
625             }
626              
627             # and indentation is now a matter of CSS. ;-) (define level0,
628             # level1, level2, etc.)
629              
630             All this can be done with a single SQL query. Of course, note that we
631             didn't even need to initialize the $objects array--that's mainly
632             useful when you want to update the database.
633              
634             =cut
635              
636             sub tree_get_subtree {
637 0     0 1   my ($self, $args) = @_;
638 0           my ($parent, $where, $order);
639 0 0         if (defined $args->{parent}) {
    0          
640 0           $parent = $args->{parent}
641             } elsif (ref $self) {
642 0           $parent = $self->id;
643             }
644 0           $where = $args->{where};
645 0   0       $order = $args->{order} || 'TREE_NODE.lft';
646 0 0         if (defined $parent) {
647 0   0       $where ||= {};
648 0           $where->{'TREE_PARENT.id'} = $parent;
649             }
650 0           my @keys = qw(id parent lft rgt);
651 0 0         push @keys, @{$args->{fields}}
  0            
652             if ($args->{fields});
653 0           my @fields = map { "TREE_NODE.`$_`" } @keys;
  0            
654 0           my $sa = $self->get_sql_abstract;
655 0           my @bind;
656 0 0         if ($where) {
657 0           ($where, @bind) = $sa->where($where);
658             } else {
659 0           $where = '';
660             }
661 0           my $table = $self->table;
662 0           my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS TREE_NODE INNER JOIN `$table` AS TREE_PARENT " .
663             'ON TREE_NODE.lft BETWEEN TREE_PARENT.lft AND TREE_PARENT.rgt' .
664             $where .
665             ' GROUP BY TREE_NODE.lft' .
666             $sa->order_and_limit($order, $args->{limit}, $args->{offset});
667 0           my $sth = $self->_run_sql($select, \@bind);
668 0           my @ret = ();
669 0           while (my $row = $sth->fetchrow_arrayref) {
670 0           my %h;
671 0           @h{@keys} = @$row;
672 0           push @ret, \%h;
673             }
674 0 0         return wantarray ? @ret : \@ret;
675             }
676              
677             =head2 tree_get_path(\%args)
678              
679             Retrieves the path of a given node. $args is an hashref that can
680             contain:
681              
682             - id : the ID of the node whose path you're interested in
683             - fields : array of field names to be SELECT-ed (same like
684             tree_get_subtree)
685              
686             This returns data in the same format as tree_get_subtree().
687              
688             =cut
689              
690             sub tree_get_path {
691 0     0 1   my ($self, $args) = @_;
692 0           my $id;
693 0 0         if (defined $args->{id}) {
    0          
694 0           $id = $args->{id};
695             } elsif (ref $self) {
696 0           $id = $self->id;
697             }
698 0           my @keys = qw(id parent lft rgt);
699 0 0         push @keys, @{$args->{fields}}
  0            
700             if ($args->{fields});
701 0           my @fields = map { "TREE_PARENT.`$_`" } @keys;
  0            
702 0           my $table = $self->table;
703 0           my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS TREE_NODE INNER JOIN `$table` AS TREE_PARENT " .
704             'ON TREE_NODE.lft BETWEEN TREE_PARENT.lft AND TREE_PARENT.rgt' .
705             " WHERE TREE_NODE.id = $id ORDER BY TREE_PARENT.lft";
706 0           my $sth = $self->_run_sql($select);
707 0           my @ret = ();
708 0           while (my $row = $sth->fetchrow_arrayref) {
709 0           my %h;
710 0           @h{@keys} = @$row;
711 0           push @ret, \%h;
712             }
713 0 0         return wantarray ? @ret : \@ret;
714             }
715              
716             =head2 tree_get_next_sibling, tree_get_prev_sibling
717              
718             XXX: this info may be inaccurate
719              
720             Return the next/previous item in the tree view. C<$args> has the same
721             significance as in L. $args->{id} defines the
722             reference node; if missing, it's assumed to be $self.
723              
724             =cut
725              
726             sub tree_get_next_sibling {
727 0     0 1   my ($self, $args) = @_;
728 0           my $id;
729 0 0         if (defined $args->{id}) {
    0          
730 0           $id = $args->{id};
731             } elsif (ref $self) {
732 0           $id = $self->id;
733             }
734 0           my @keys = qw(id parent lft rgt);
735 0 0         push @keys, @{$args->{fields}}
  0            
736             if ($args->{fields});
737 0           my @fields = map { "T1.`$_`" } @keys;
  0            
738 0           my $table = $self->table;
739 0           my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS T1 INNER JOIN `$table` AS T2 " .
740             'ON T1.lft = T2.rgt + 1' .
741             " WHERE T2.id = $id LIMIT 1";
742 0           my $sth = $self->_run_sql($select);
743 0           my @ret = ();
744 0           my $row = $sth->fetchrow_arrayref;
745 0 0         if ($row) {
746 0           my %h;
747 0           @h{@keys} = @$row;
748 0           return \%h;
749             }
750 0           return undef;
751             }
752              
753             sub tree_get_prev_sibling {
754 0     0 1   my ($self, $args) = @_;
755 0           my $id;
756 0 0         if (defined $args->{id}) {
    0          
757 0           $id = $args->{id};
758             } elsif (ref $self) {
759 0           $id = $self->id;
760             }
761 0           my @keys = qw(id parent lft rgt);
762 0 0         push @keys, @{$args->{fields}}
  0            
763             if ($args->{fields});
764 0           my @fields = map { "T1.`$_`" } @keys;
  0            
765 0           my $table = $self->table;
766 0           my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS T1 INNER JOIN `$table` AS T2 " .
767             'ON T1.rgt = T2.lft - 1' .
768             " WHERE T2.id = $id LIMIT 1";
769 0           my $sth = $self->_run_sql($select);
770 0           my @ret = ();
771 0           my $row = $sth->fetchrow_arrayref;
772 0 0         if ($row) {
773 0           my %h;
774 0           @h{@keys} = @$row;
775 0           return \%h;
776             }
777 0           return undef;
778             }
779              
780             =head2 tree_get_next, tree_get_prev
781              
782             XXX: this info may be inaccurate
783              
784             Similar to L / L but
785             allow $args->{where} to contain a WHERE clause (in SQL::Abstract
786             format) and returns the next/prev item that matches the criteria.
787              
788             =cut
789              
790             sub tree_get_next {
791 0     0 1   my ($self, $args) = @_;
792 0           my $id;
793 0 0         if (defined $args->{id}) {
    0          
794 0           $id = $args->{id};
795             } elsif (ref $self) {
796 0           $id = $self->id;
797             }
798 0           my $where = $args->{where};
799 0           my @bind;
800 0           my $sa = $self->get_sql_abstract;
801 0 0         if ($where) {
802 0           ($where, @bind) = $sa->where($where);
803             }
804 0           my @keys = qw(id parent lft rgt);
805 0 0         push @keys, @{$args->{fields}}
  0            
806             if ($args->{fields});
807 0           my @fields = map { "T1.`$_`" } @keys;
  0            
808 0           my $table = $self->table;
809 0           my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS T1 INNER JOIN `$table` AS T2 " .
810             "ON T1.lft > T2.lft AND T2.id = $id $where ORDER BY T1.lft LIMIT 1";
811 0           my $sth = $self->_run_sql($select, \@bind);
812 0           my @ret = ();
813 0           my $row = $sth->fetchrow_arrayref;
814 0 0         if ($row) {
815 0           my %h;
816 0           @h{@keys} = @$row;
817 0           return \%h;
818             }
819 0           return undef;
820             }
821              
822             sub tree_get_prev {
823 0     0 1   my ($self, $args) = @_;
824 0           my $id;
825 0 0         if (defined $args->{id}) {
    0          
826 0           $id = $args->{id};
827             } elsif (ref $self) {
828 0           $id = $self->id;
829             }
830 0           my $where = $args->{where};
831 0           my @bind;
832 0           my $sa = $self->get_sql_abstract;
833 0 0         if ($where) {
834 0           ($where, @bind) = $sa->where($where);
835             }
836 0           my @keys = qw(id parent lft rgt);
837 0 0         push @keys, @{$args->{fields}}
  0            
838             if ($args->{fields});
839 0           my @fields = map { "T1.`$_`" } @keys;
  0            
840 0           my $table = $self->table;
841 0           my $select = 'SELECT ' . join(', ', @fields) . " FROM `$table` AS T1 INNER JOIN `$table` AS T2 " .
842             "ON T1.lft < T2.lft AND T2.id = $id $where ORDER BY T1.lft DESC LIMIT 1";
843 0           my $sth = $self->_run_sql($select, \@bind);
844 0           my @ret = ();
845 0           my $row = $sth->fetchrow_arrayref;
846 0 0         if ($row) {
847 0           my %h;
848 0           @h{@keys} = @$row;
849 0           return \%h;
850             }
851 0           return undef;
852             }
853              
854             =head2 tree_compute_levels($data)
855              
856             This is an utility function that computes the level of each node in
857             $data (where $data is an array reference as returned by
858             tree_get_subtree or tree_get_path).
859              
860             This is generic, and it's simply for convenience--in particular cases
861             you might find it faster to compute the levels yourself.
862              
863             It returns an hashref that maps node ID to its level.
864              
865             In [1] we can see there is a method to compute the subtree depth
866             directly in SQL, I will paste the relevant code here:
867              
868             SELECT node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
869             FROM nested_category AS node,
870             nested_category AS parent,
871             nested_category AS sub_parent,
872             (
873             SELECT node.name, (COUNT(parent.name) - 1) AS depth
874             FROM nested_category AS node,
875             nested_category AS parent
876             WHERE node.lft BETWEEN parent.lft AND parent.rgt
877             AND node.name = 'PORTABLE ELECTRONICS'
878             GROUP BY node.name
879             ORDER BY node.lft
880             )AS sub_tree
881             WHERE node.lft BETWEEN parent.lft AND parent.rgt
882             AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
883             AND sub_parent.name = sub_tree.name
884             GROUP BY node.name
885             ORDER BY node.lft;
886              
887             I find it horrible.
888              
889             =cut
890              
891             sub tree_compute_levels {
892 0     0 1   my ($self, $data) = @_;
893 0           my %levels = ();
894 0           my @par;
895 0           my $l = 0;
896 0           foreach my $h (@$data) {
897 0           while (@par > 0) {
898 0           my $prev = $par[$#par];
899 0 0         if ($h->{lft} < $prev->{rgt}) {
900             # contained
901 0           ++$l;
902 0           last;
903             } else {
904 0           pop @par;
905 0 0         if (@par) {
906 0           --$l;
907             }
908             }
909             }
910 0           push @par, $h;
911 0           $levels{$h->{id}} = $l;
912             }
913 0           return \%levels;
914             }
915              
916             1;
917              
918             =head1 TODO
919              
920             - Allow custom names for the required fields (lft, rgt, mvg, id,
921             parent).
922              
923             - Allow custom types for the primary key (currently they MUST be
924             integers).
925              
926             =head1 REFERENCES
927              
928             [1] MySQL AB: Managing Hierarchical Data in MySQL, by Mike Hillyer
929             http://dev.mysql.com/tech-resources/articles/hierarchical-data.html
930              
931             =head1 SEE ALSO
932              
933             L
934              
935             =head1 AUTHOR
936              
937             Mihai Bazon,
938             http://www.dynarch.com/
939             http://www.bazon.net/mishoo/
940              
941             =head1 COPYRIGHT
942              
943             Copyright (c) Mihai Bazon 2006. All rights reserved.
944              
945             This module is free software; you can redistribute it and/or modify it
946             under the same terms as Perl itself.
947              
948             =head1 DISCLAIMER OF WARRANTY
949              
950             BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
951             FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT
952             WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER
953             PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND,
954             EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
955             IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
956             PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
957             SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME
958             THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.
959              
960             IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
961             WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
962             REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE
963             TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR
964             CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
965             SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
966             RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
967             FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
968             SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
969             DAMAGES.
970              
971             =cut