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