.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42) .\" .\" Standard preamble: .\" ======================================================================== .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' . ds C` . ds C' 'br\} .\" .\" Escape single quotes in literal strings from groff's Unicode transform. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" .\" If the F register is >0, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .\" .\" Avoid warning from groff about undefined register 'F'. .de IX .. .nr rF 0 .if \n(.g .if rF .nr rF 1 .if (\n(rF:(\n(.g==0)) \{\ . if \nF \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . if !\nF==2 \{\ . nr % 0 . nr F 2 . \} . \} .\} .rr rF .\" ======================================================================== .\" .IX Title "DBIx::OO::Tree 3pm" .TH DBIx::OO::Tree 3pm "2022-06-13" "perl v5.34.0" "User Contributed Perl Documentation" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh .SH "NAME" DBIx::OO::Tree \-\- manipulate hierarchical data using the "nested sets" model .SH "SYNOPSYS" .IX Header "SYNOPSYS" .Vb 3 \& CREATE TABLE Categories ( \& id INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY, \& label VARCHAR(255), \& \& \-\- these columns are required by DBIx::OO::Tree \& parent INTEGER UNSIGNED, \& lft INTEGER UNSIGNED NOT NULL, \& rgt INTEGER UNSIGNED NOT NULL, \& mvg TINYINT DEFAULT 0, \& \& INDEX(lft), \& INDEX(rgt), \& INDEX(mvg), \& INDEX(parent) \& ); \& \& * * * \& \& package Category; \& use base \*(AqDBIx::OO\*(Aq; \& use DBIx::OO::Tree; \& \& _\|_PACKAGE_\|_\->table(\*(AqCategories\*(Aq); \& _\|_PACKAGE_\|_\->columns(P => [ \*(Aqid\*(Aq ], \& E => [ \*(Aqlabel\*(Aq, \*(Aqparent\*(Aq ]); \& \& # note it\*(Aqs not necessary to declare lft, rgt, mvg or parent. We \& # declare parent simply because it might be useful, but \& # DBIx::OO:Tree works with low\-level SQL therefore it doesn\*(Aqt \& # require that the DBIx::OO object has these fields. \& \& # the code below creates the structure presented in [1] \& \& my $electronics = Category\->tree_append({ label => \*(Aqelectronics\*(Aq }); \& my $tvs = $electronics\->tree_append({ label => \*(Aqtelevisions\*(Aq }); \& my $tube = $tvs\->tree_append({ label => \*(Aqtube\*(Aq }); \& my $plasma = $tvs\->tree_append({ label => \*(Aqplasma\*(Aq }); \& my $lcd = $plasma\->tree_insert_before({ label => \*(Aqlcd\*(Aq }); \& my $portable = $tvs\->tree_insert_after({ label => \*(Aqportable electronics\*(Aq }); \& my $mp3 = $portable\->tree_append({ label => \*(Aqmp3 players\*(Aq }); \& my $flash = $mp3\->tree_append({ label => \*(Aqflash\*(Aq }); \& my $cds = $portable\->tree_append({ label => \*(Aqcd players\*(Aq }); \& my $radios = Category\->tree_append($portable\->id, \& { label => \*(Aq2 way radios\*(Aq }); \& \& # fetch and display a subtree \& \& my $data = $electronics\->tree_get_subtree({ \& fields => [qw( label lft rgt parent )] \& }); \& my $levels = Category\->tree_compute_levels($data); \& \& foreach my $i (@$data) { \& print \*(Aq \*(Aq x $levels\->{$i\->{id}}, $i\->{label}, "\en"; \& } \& \& ## or, create DBIx::OO objects from returned data: \& \& my $array = Category\->init_from_data($data); \& print join("\en", (map { \*(Aq \*(Aq x $levels\->{$_\->id} . $_\->label } @$array)); \& \& # display path info \& \& my $data = $flash\->tree_get_path; \& print join("\en", (map { $_\->{label} } @$data)); \& \& # move nodes around \& \& $mp3\->tree_reparent($lcd\->id); \& $tvs\->tree_reparent($portable\->id); \& $cds\->tree_reparent(undef); \& \& $plasma\->tree_move_before($tube\->id); \& $portable\->tree_move_before($electronics\->id); \& \& # delete nodes \& \& $lcd\->tree_delete; .Ve .SH "OVERVIEW" .IX Header "OVERVIEW" This module is a complement to DBIx::OO to facilitate storing trees in database using the \*(L"nested sets model\*(R", presented in [1]. Its main ambition is to be extremely fast at retrieving data (sacrificing for this the performance of UPDATE-s, INSERT-s or DELETE-s). Currently this module \fBrequires\fR you to have these columns in the table: .PP .Vb 4 \& \- id: primary key (integer) \& \- parent: integer, references the parent node (NULL for root nodes) \& \- lft, rgt: store the node position \& \- mvg: used only when moving nodes .Ve .PP \&\*(L"parent\*(R" and \*(L"mvg\*(R" are not esentially required by the nested sets model as presented in [1], but they are necessary for this module to work. In particular, \*(L"mvg\*(R" is only required by functions that move nodes, such as \fBtree_reparent()\fR. If you don't want to move nodes around you can omit \*(L"mvg\*(R". .PP Retrieval functions should be very fast (one \s-1SQL\s0 executed). To further promote speed, they don't return DBIx::OO blessed objects, but an array of hashes instead. It's easy to create DBIx::OO objects from these, if required, by calling DBIx::OO\->\fBinit_from_data()\fR (see DBIx::OO for more information). .PP Insert/delete/move functions, however, need to ensure the tree integrity. Here's what happens currently: .PP .Vb 3 \& \- tree_append, tree_insert_before, tree_insert_after \-\- these execute \& one SELECT and two UPDATE\-s (that potentially could affect a lot of \& rows). \& \& \- tree_delete: execute one SELECT, one DELETE and two UPDATE\-s. \& \& \- tree_reparent \-\- executes 2 SELECT\-s and 7 UPDATE\-s. I know, this \& sounds horrible\-\-if you have better ideas I\*(Aqd love to hear them. .Ve .PP \&\fBNote:\fR this module could well work with Class::DBI, although it is untested. You just need to provide the \fBget_dbh()\fR method to your packages, comply to this module's table requirements (i.e. provide the right columns) and it should work just fine. Any success/failure stories are welcome. .SH "DATABASE INTEGRITY" .IX Header "DATABASE INTEGRITY" Since the functions that update the database need to run multiple queries in order to maintain integrity, they should normally do this inside a transaction. However, it looks like MySQL does not support nested transactions, therefore if I call transaction_start / transaction_commit inside these functions they will mess with an eventual transaction that might have been started by the calling code. .PP In short: you should make sure the updates happen in a transaction, but we can't enforce this in our module. .SH "API" .IX Header "API" .SS "tree_append($parent_id, \e%values)" .IX Subsection "tree_append($parent_id, %values)" Appends a new node in the subtree of the specified parent. If \&\f(CW$parent_id\fR is undef, it will add a root node. When you want to add a root node you can as well omit specifying the \f(CW$parent_id\fR (our code will realize that the first argument is a reference). .PP \&\f(CW$values\fR is a hash as required by \fBDBIx::OO::create()\fR. .PP Examples: .PP .Vb 2 \& $node = Category\->tree_append({ label => \*(Aqelectronics\*(Aq }); \& $node = Category\->tree_append(undef, { label => \*(Aqelectronics\*(Aq }); \& \& $lcd = Category\->tree_append($tvs\->id, { label => \*(Aqlcd\*(Aq }); \& $lcd\->tree_append({ label => \*(Aqmonitors\*(Aq }); .Ve .PP As you can see, you can call it both as a package method or as an object method. When you call it as a package method, it will look at the type of the first argument. If it's a reference, it will guess that you want to add a root node. Otherwise it will add the new node under the specified parent. .PP Beware of mistakes! Do \s-1NOT\s0 call it like this: .PP .Vb 2 \& $tvs = Category\->search({ label => \*(Aqtelevisions\*(Aq })\->[0]; \& Category\->tree_append($tvs, { label => \*(Aqlcd\*(Aq }); .Ve .PP If you specify a parent, it \s-1MUST\s0 be its \s-1ID,\s0 not an object! .SS "tree_insert_before, tree_insert_after ($anchor, \e%values)" .IX Subsection "tree_insert_before, tree_insert_after ($anchor, %values)" Similar in function to tree_append, but these functions allow you to insert a node before or after a specified node ($anchor). .PP Examples: .PP .Vb 2 \& $lcd\->tree_insert_after({ label => \*(Aqplasma\*(Aq }); \& $lcd\->tree_insert_before({ label => \*(Aqtube\*(Aq }); \& \& # Or, as a package method: \& \& Category\->tree_insert_after($lcd\->id, { label => \*(Aqplasma\*(Aq }); \& Category\->tree_insert_before($lcd\->id, { label => \*(Aqtube\*(Aq }); .Ve .PP Note that specifying the parent is not required, because it's clearly that the new node should have the same parent as the anchor node. .ie n .SS "tree_reparent($source_id, $dest_id)" .el .SS "tree_reparent($source_id, \f(CW$dest_id\fP)" .IX Subsection "tree_reparent($source_id, $dest_id)" This function will remove the \f(CW$source\fR node from its current parent and append it to the \f(CW$dest\fR node. As with the other functions, you can call it both as a package method or as an object method. When you call it as an object method, it's not necessary to specify \f(CW$source\fR. .PP You can specify \fIundef\fR for \f(CW$dest_id\fR, in which case \f(CW$source\fR will become a root node (as if it would be appended with tree_append(undef)). .PP No nodes are DELETE-ed nor INSERT-ed by this function. It simply moves \fIexisting\fR nodes, which means that any node ID-s that you happen to have should remain valid and point to the same nodes. However, the tree structure is changed, so if you maintain the tree in memory you have to update it after calling this funciton. Same applies to \fBtree_move_before()\fR and \fBtree_move_after()\fR. .PP Examples: .PP .Vb 1 \& # the following are equivalent \& \& Category\->tree_reparent($lcd\->id, $plasma\->id); \& $lcd\->tree_reparent($plasma\->id); .Ve .PP This function does a lot of work in order to maintain the tree integrity, therefore it might be slow. .PP \&\s-1NOTE:\s0 it doesn't do any safety checks to make sure moving the node is allowed. For instance, you can't move a node to one of its child nodes. .ie n .SS "tree_move_before, tree_move_after ($source_id, $anchor_id)" .el .SS "tree_move_before, tree_move_after ($source_id, \f(CW$anchor_id\fP)" .IX Subsection "tree_move_before, tree_move_after ($source_id, $anchor_id)" These functions are similar to a reparent operation, but they allow one to specify \fIwhere\fR to put the \f(CW$source\fR node, in the subtree of \&\f(CW$anchor\fR's parent. See \fBtree_reparent()\fR. .PP Examples: .PP .Vb 2 \& $portable\->tree_move_before($electronics\->id); \& Category\->tree_move_after($lcd\->id, $flash\->id); .Ve .SS "tree_delete($node_id)" .IX Subsection "tree_delete($node_id)" Removes a node (and its full subtree) from the database. .PP Equivalent examples: .PP .Vb 2 \& Category\->tree_delete($lcd\->id); \& $lcd\->tree_delete; .Ve .SS "tree_get_subtree(\e%args)" .IX Subsection "tree_get_subtree(%args)" Retrieves the full subtree of a specified node. \f(CW$args\fR is a hashref that can contain: .PP .Vb 6 \& \- parent : the ID of the node whose subtree we want to get \& \- where : an WHERE clause in SQL::Abstract format \& \- limit : allows you to limit the results (using SQL LIMIT) \& \- offset : SQL OFFSET \& \- fields : (arrayref) allows you to specify a list of fields you\*(Aqre \& interested in .Ve .PP This can be called as a package method, or as an object method. .PP Examples first: .PP .Vb 1 \& $all_nodes = Category\->tree_get_subtree; \& \& $nodes = Category\->tree_get_subtree({ parent => $portable\->id }); \& ## OR \& $nodes = $portable\->tree_get_subtree; \& \& # Filtering: \& $nodes = Category\->tree_get_subtree({ where => { label => { \-like => \*(Aq%a%\*(Aq }}}); \& \& # Specify fields: \& $nodes = Category\->tree_get_subtree({ fields => [ \*(Aqlabel\*(Aq ] }); .Ve .PP This function returns an array of hashes that contain the fields you required. If you specify no fields, 'id' and 'parent' will be SELECT-ed by default. Even if you do specify an array of field names, \&'id' and 'parent' would still be included in the \s-1SELECT\s0 (so you don't want to specify them). .PP Using this array you can easily create DBIx::OO objects (or in our sample, Category objects): .PP .Vb 1 \& $arrayref = Category\->init_from_data($nodes); .Ve .PP \&\s-1OK,\s0 let's get to a more real-world example. Suppose we have a forum and we need to list all messages in a thread ($thread_id). Here's what we're going to do: .PP .Vb 4 \& $data = ForumMessage\->tree_get_subtree({ \& parent => $thread_id, \& fields => [qw( subject body author date )], \& }); \& \& # the above runs one SQL query \& \& $objects = ForumMessage\->init_from_data($data); \& \& # the above simply initializes ForumMessage objects from the \& # returned data, B calling the database (since we have \& # the primary key automatically selected by tree_get_subtree, and \& # also have cared to select the fields we\*(Aqre going to use). \& \& # compute the level of each message, to indent them easily \& \& $levels = ForumMessage\->tree_compute_levels($data); \& \& # and now display them \& \& foreach my $msg (@$objects) { \& my $class = \*(Aqlevel\*(Aq . $levels{$msg\->id}; \& print "
", $msg\->subject, "

", \& $msg\->body, "

By: ", $msg\->author, "
"; \& } \& \& # and indentation is now a matter of CSS. ;\-) (define level0, \& # level1, level2, etc.) .Ve .PP All this can be done with a single \s-1SQL\s0 query. Of course, note that we didn't even need to initialize the \f(CW$objects\fR array\*(--that's mainly useful when you want to update the database. .SS "tree_get_path(\e%args)" .IX Subsection "tree_get_path(%args)" Retrieves the path of a given node. \f(CW$args\fR is an hashref that can contain: .PP .Vb 3 \& \- id : the ID of the node whose path you\*(Aqre interested in \& \- fields : array of field names to be SELECT\-ed (same like \& tree_get_subtree) .Ve .PP This returns data in the same format as \fBtree_get_subtree()\fR. .SS "tree_get_next_sibling, tree_get_prev_sibling" .IX Subsection "tree_get_next_sibling, tree_get_prev_sibling" \&\s-1XXX:\s0 this info may be inaccurate .PP Return the next/previous item in the tree view. \f(CW$args\fR has the same significance as in \*(L"tree_get_path\*(R". \f(CW$args\fR\->{id} defines the reference node; if missing, it's assumed to be \f(CW$self\fR. .SS "tree_get_next, tree_get_prev" .IX Subsection "tree_get_next, tree_get_prev" \&\s-1XXX:\s0 this info may be inaccurate .PP Similar to \*(L"tree_get_next_sibling\*(R" / \*(L"tree_get_prev_sibling\*(R" but allow \f(CW$args\fR\->{where} to contain a \s-1WHERE\s0 clause (in SQL::Abstract format) and returns the next/prev item that matches the criteria. .SS "tree_compute_levels($data)" .IX Subsection "tree_compute_levels($data)" This is an utility function that computes the level of each node in \&\f(CW$data\fR (where \f(CW$data\fR is an array reference as returned by tree_get_subtree or tree_get_path). .PP This is generic, and it's simply for convenience\*(--in particular cases you might find it faster to compute the levels yourself. .PP It returns an hashref that maps node \s-1ID\s0 to its level. .PP In [1] we can see there is a method to compute the subtree depth directly in \s-1SQL, I\s0 will paste the relevant code here: .PP .Vb 10 \& SELECT node.name, (COUNT(parent.name) \- (sub_tree.depth + 1)) AS depth \& FROM nested_category AS node, \& nested_category AS parent, \& nested_category AS sub_parent, \& ( \& SELECT node.name, (COUNT(parent.name) \- 1) AS depth \& FROM nested_category AS node, \& nested_category AS parent \& WHERE node.lft BETWEEN parent.lft AND parent.rgt \& AND node.name = \*(AqPORTABLE ELECTRONICS\*(Aq \& GROUP BY node.name \& ORDER BY node.lft \& )AS sub_tree \& WHERE node.lft BETWEEN parent.lft AND parent.rgt \& AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt \& AND sub_parent.name = sub_tree.name \& GROUP BY node.name \& ORDER BY node.lft; .Ve .PP I find it horrible. .SH "TODO" .IX Header "TODO" .Vb 2 \& \- Allow custom names for the required fields (lft, rgt, mvg, id, \& parent). \& \& \- Allow custom types for the primary key (currently they MUST be \& integers). .Ve .SH "REFERENCES" .IX Header "REFERENCES" .Vb 2 \& [1] MySQL AB: Managing Hierarchical Data in MySQL, by Mike Hillyer \& http://dev.mysql.com/tech\-resources/articles/hierarchical\-data.html .Ve .SH "SEE ALSO" .IX Header "SEE ALSO" DBIx::OO .SH "AUTHOR" .IX Header "AUTHOR" Mihai Bazon, http://www.dynarch.com/ http://www.bazon.net/mishoo/ .SH "COPYRIGHT" .IX Header "COPYRIGHT" Copyright (c) Mihai Bazon 2006. All rights reserved. .PP This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. .SH "DISCLAIMER OF WARRANTY" .IX Header "DISCLAIMER OF WARRANTY" \&\s-1BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE \*(L"AS IS\*(R" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.\s0 .PP \&\s-1IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE\s0 (\s-1INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE\s0), \s-1EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.\s0