FInally rewrote Ordered properly - a number of FIXME's still remain (grep source)
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Ordered.pm
CommitLineData
a733c37f 1package DBIx::Class::Ordered;
118e6b96 2use strict;
3use warnings;
4use base qw( DBIx::Class );
5
6=head1 NAME
7
a733c37f 8DBIx::Class::Ordered - Modify the position of objects in an ordered list.
118e6b96 9
10=head1 SYNOPSIS
11
a733c37f 12Create a table for your ordered data.
118e6b96 13
a733c37f 14 CREATE TABLE items (
15 item_id INTEGER PRIMARY KEY AUTOINCREMENT,
118e6b96 16 name TEXT NOT NULL,
17 position INTEGER NOT NULL
18 );
1d941d67 19
e9188247 20Optionally, add one or more columns to specify groupings, allowing you
21to maintain independent ordered lists within one table:
22
23 CREATE TABLE items (
24 item_id INTEGER PRIMARY KEY AUTOINCREMENT,
25 name TEXT NOT NULL,
26 position INTEGER NOT NULL,
27 group_id INTEGER NOT NULL
28 );
29
30Or even
31
32 CREATE TABLE items (
33 item_id INTEGER PRIMARY KEY AUTOINCREMENT,
34 name TEXT NOT NULL,
35 position INTEGER NOT NULL,
36 group_id INTEGER NOT NULL,
37 other_group_id INTEGER NOT NULL
38 );
39
a8492531 40In your Schema or DB class add "Ordered" to the top
118e6b96 41of the component list.
42
a733c37f 43 __PACKAGE__->load_components(qw( Ordered ... ));
118e6b96 44
45Specify the column that stores the position number for
46each row.
47
a733c37f 48 package My::Item;
118e6b96 49 __PACKAGE__->position_column('position');
1d941d67 50
e9188247 51If you are using one grouping column, specify it as follows:
52
53 __PACKAGE__->grouping_column('group_id');
54
55Or if you have multiple grouping columns:
56
57 __PACKAGE__->grouping_column(['group_id', 'other_group_id']);
58
a8492531 59That's it, now you can change the position of your objects.
118e6b96 60
61 #!/use/bin/perl
a733c37f 62 use My::Item;
118e6b96 63
a733c37f 64 my $item = My::Item->create({ name=>'Matt S. Trout' });
65 # If using grouping_column:
66 my $item = My::Item->create({ name=>'Matt S. Trout', group_id=>1 });
118e6b96 67
a733c37f 68 my $rs = $item->siblings();
69 my @siblings = $item->siblings();
118e6b96 70
71 my $sibling;
a733c37f 72 $sibling = $item->first_sibling();
73 $sibling = $item->last_sibling();
74 $sibling = $item->previous_sibling();
75 $sibling = $item->next_sibling();
118e6b96 76
a733c37f 77 $item->move_previous();
78 $item->move_next();
79 $item->move_first();
80 $item->move_last();
81 $item->move_to( $position );
1d941d67 82 $item->move_to_group( 'groupname' );
83 $item->move_to_group( 'groupname', $position );
84 $item->move_to_group( {group_id=>'groupname', 'other_group_id=>'othergroupname'} );
85 $item->move_to_group( {group_id=>'groupname', 'other_group_id=>'othergroupname'}, $position );
118e6b96 86
87=head1 DESCRIPTION
88
a733c37f 89This module provides a simple interface for modifying the ordered
90position of DBIx::Class objects.
118e6b96 91
133dd22a 92=head1 AUTO UPDATE
93
94All of the move_* methods automatically update the rows involved in
95the query. This is not configurable and is due to the fact that if you
96move a record it always causes other records in the list to be updated.
97
118e6b96 98=head1 METHODS
99
100=head2 position_column
101
102 __PACKAGE__->position_column('position');
103
104Sets and retrieves the name of the column that stores the
a8492531 105positional value of each record. Defaults to "position".
118e6b96 106
107=cut
108
109__PACKAGE__->mk_classdata( 'position_column' => 'position' );
110
a733c37f 111=head2 grouping_column
133dd22a 112
a733c37f 113 __PACKAGE__->grouping_column('group_id');
133dd22a 114
a8492531 115This method specifies a column to limit all queries in
133dd22a 116this module by. This effectively allows you to have multiple
a733c37f 117ordered lists within the same table.
133dd22a 118
119=cut
120
a733c37f 121__PACKAGE__->mk_classdata( 'grouping_column' );
133dd22a 122
bd7ca9e8 123=head2 null_position_value
124
125 __PACKAGE__->null_position_value(undef);
126
127This method specifies a value of L</position_column> which B<would
128never be assigned to a row> during normal operation. When
129a row is moved, its position is set to this value temporarily, so
130that any unique constrainst can not be violated. This value defaults
131to 0, which should work for all cases except when your positions do
132indeed start from 0.
133
134=cut
135
136__PACKAGE__->mk_classdata( 'null_position_value' => 0 );
137
118e6b96 138=head2 siblings
139
a733c37f 140 my $rs = $item->siblings();
141 my @siblings = $item->siblings();
118e6b96 142
bd7ca9e8 143Returns an B<ordered> resultset of all other objects in the same
144group excluding the one you called it on.
118e6b96 145
bd7ca9e8 146The ordering is a backwards-compatibility artifact - if you need
147a resultset with no ordering applied use L</_siblings>
118e6b96 148
bd7ca9e8 149=cut
118e6b96 150sub siblings {
bd7ca9e8 151 my $self = shift;
152 return $self->_siblings->search ({}, { order_by => $self->position_column } );
118e6b96 153}
154
bd7ca9e8 155=head2 previous_siblings
118e6b96 156
bd7ca9e8 157 my $prev_rs = $item->previous_siblings();
158 my @prev_siblings = $item->previous_siblings();
118e6b96 159
bd7ca9e8 160Returns a resultset of all objects in the same group
161positioned before the object on which this method was called.
118e6b96 162
163=cut
bd7ca9e8 164sub previous_siblings {
165 my $self = shift;
166 my $position_column = $self->position_column;
167 my $position = $self->get_column ($position_column);
168 return ( defined $position
169 ? $self->_siblings->search ({ $position_column => { '<', $position } })
170 : $self->_siblings
171 );
118e6b96 172}
173
bd7ca9e8 174=head2 next_siblings
118e6b96 175
bd7ca9e8 176 my $next_rs = $item->next_siblings();
177 my @next_siblings = $item->next_siblings();
118e6b96 178
bd7ca9e8 179Returns a resultset of all objects in the same group
180positioned after the object on which this method was called.
118e6b96 181
182=cut
bd7ca9e8 183sub next_siblings {
184 my $self = shift;
185 my $position_column = $self->position_column;
186 my $position = $self->get_column ($position_column);
187 return ( defined $position
188 ? $self->_siblings->search ({ $position_column => { '>', $position } })
189 : $self->_siblings
190 );
118e6b96 191}
192
193=head2 previous_sibling
194
a733c37f 195 my $sibling = $item->previous_sibling();
118e6b96 196
bd7ca9e8 197Returns the sibling that resides one position back. Returns 0
a8492531 198if the current object is the first one.
118e6b96 199
200=cut
201
202sub previous_sibling {
bd7ca9e8 203 my $self = shift;
118e6b96 204 my $position_column = $self->position_column;
bd7ca9e8 205
206 my $psib = $self->previous_siblings->search(
207 {},
208 { rows => 1, order_by => { '-desc' => $position_column } },
209 )->single;
210
211 return defined $psib ? $psib : 0;
212}
213
214=head2 first_sibling
215
216 my $sibling = $item->first_sibling();
217
218Returns the first sibling object, or 0 if the first sibling
219is this sibling.
220
221=cut
222
223sub first_sibling {
224 my $self = shift;
225 my $position_column = $self->position_column;
226
227 my $fsib = $self->previous_siblings->search(
228 {},
229 { rows => 1, order_by => { '-asc' => $position_column } },
230 )->single;
231
232 return defined $fsib ? $fsib : 0;
118e6b96 233}
234
235=head2 next_sibling
236
a733c37f 237 my $sibling = $item->next_sibling();
118e6b96 238
bd7ca9e8 239Returns the sibling that resides one position forward. Returns 0
a8492531 240if the current object is the last one.
118e6b96 241
242=cut
243
244sub next_sibling {
bd7ca9e8 245 my $self = shift;
246 my $position_column = $self->position_column;
247 my $nsib = $self->next_siblings->search(
248 {},
249 { rows => 1, order_by => { '-asc' => $position_column } },
250 )->single;
251
252 return defined $nsib ? $nsib : 0;
253}
254
255=head2 last_sibling
256
257 my $sibling = $item->last_sibling();
258
259Returns the last sibling, or 0 if the last sibling is this
260sibling.
261
262=cut
263
264sub last_sibling {
265 my $self = shift;
118e6b96 266 my $position_column = $self->position_column;
bd7ca9e8 267 my $lsib = $self->next_siblings->search(
268 {},
269 { rows => 1, order_by => { '-desc' => $position_column } },
270 )->single;
271
272 return defined $lsib ? $lsib : 0;
118e6b96 273}
274
80010e2b 275=head2 move_previous
118e6b96 276
a733c37f 277 $item->move_previous();
118e6b96 278
a8492531 279Swaps position with the sibling in the position previous in
280the list. Returns 1 on success, and 0 if the object is
281already the first one.
118e6b96 282
283=cut
284
80010e2b 285sub move_previous {
bd7ca9e8 286 my $self = shift;
287 return $self->move_to ($self->_position - 1);
118e6b96 288}
289
80010e2b 290=head2 move_next
118e6b96 291
a733c37f 292 $item->move_next();
118e6b96 293
a8492531 294Swaps position with the sibling in the next position in the
295list. Returns 1 on success, and 0 if the object is already
296the last in the list.
118e6b96 297
298=cut
299
80010e2b 300sub move_next {
bd7ca9e8 301 my $self = shift;
302 return 0 unless $self->next_siblings->count;
303 return $self->move_to ($self->_position + 1);
118e6b96 304}
305
306=head2 move_first
307
a733c37f 308 $item->move_first();
118e6b96 309
a8492531 310Moves the object to the first position in the list. Returns 1
311on success, and 0 if the object is already the first.
118e6b96 312
313=cut
314
315sub move_first {
bd7ca9e8 316 return shift->move_to( 1 );
118e6b96 317}
318
319=head2 move_last
320
a733c37f 321 $item->move_last();
118e6b96 322
a8492531 323Moves the object to the last position in the list. Returns 1
324on success, and 0 if the object is already the last one.
118e6b96 325
326=cut
327
328sub move_last {
bd7ca9e8 329 my $self = shift;
330 return $self->move_to( $self->_group_rs->count );
118e6b96 331}
332
333=head2 move_to
334
a733c37f 335 $item->move_to( $position );
118e6b96 336
a8492531 337Moves the object to the specified position. Returns 1 on
338success, and 0 if the object is already at the specified
339position.
118e6b96 340
341=cut
342
343sub move_to {
344 my( $self, $to_position ) = @_;
133dd22a 345 return 0 if ( $to_position < 1 );
118e6b96 346
bd7ca9e8 347 my $from_position = $self->_position;
348 return 0 if ( $from_position == $to_position );
349
350 my $position_column = $self->position_column;
351
352 # FIXME this needs to be wrapped in a transaction
353 {
354 my ($direction, @between);
355 if ( $from_position < $to_position ) {
356 $direction = -1;
357 @between = map { $self->_position_value ($_) } ( $from_position + 1, $to_position );
358 }
359 else {
360 $direction = 1;
361 @between = map { $self->_position_value ($_) } ( $to_position, $from_position - 1 );
362 }
363
364 my $new_pos_val = $self->_position_value ($to_position); # record this before the shift
365 $self->_ordered_internal_update({ $position_column => $self->null_position_value }); # take the row out of the picture for a bit
366 $self->_shift_siblings ($direction, @between);
367 $self->_ordered_internal_update({ $position_column => $new_pos_val });
fa6b598f 368
bd7ca9e8 369 return 1;
370 }
371}
fa6b598f 372
79dc353a 373=head2 move_to_group
374
375 $item->move_to_group( $group, $position );
376
377Moves the object to the specified position of the specified
378group, or to the end of the group if $position is undef.
3791 is returned on success, and 0 is returned if the object is
380already at the specified position of the specified group.
381
1d941d67 382$group may be specified as a single scalar if only one
383grouping column is in use, or as a hashref of column => value pairs
384if multiple grouping columns are in use.
fa6b598f 385
79dc353a 386=cut
387
388sub move_to_group {
389 my( $self, $to_group, $to_position ) = @_;
fa6b598f 390
bd7ca9e8 391 $self->throw_exception ('move_to_group() expects a group specification')
392 unless defined $to_group;
393
fa6b598f 394 # if we're given a string, turn it into a hashref
395 unless (ref $to_group eq 'HASH') {
bd7ca9e8 396 my @gcols = $self->_grouping_columns;
397
398 $self->throw_exception ('Single group supplied for a multi-column group identifier') if @gcols > 1;
399 $to_group = {$gcols[0] => $to_group};
fa6b598f 400 }
401
79dc353a 402 my $position_column = $self->position_column;
79dc353a 403
79dc353a 404 return 0 if ( defined($to_position) and $to_position < 1 );
bd7ca9e8 405 if ($self->_is_in_group ($to_group) ) {
406 return 0 if not defined $to_position;
407 return $self->move_to ($to_position);
79dc353a 408 }
409
bd7ca9e8 410 # FIXME this needs to be wrapped in a transaction
411 {
412 # Move to end of current group to adjust siblings
413 $self->move_last;
414
415 $self->set_inflated_columns({ %$to_group, $position_column => undef });
416 my $new_group_count = $self->_group_rs->count;
417
418 if ( not defined($to_position) or $to_position > $new_group_count) {
419 $self->set_column(
420 $position_column => $new_group_count
421 ? $self->_next_position_value ( $self->last_sibling->get_column ($position_column) ) # FIXME - no need to inflate last_sibling
422 : $self->_initial_position_value
423 );
424 }
425 else {
426 my $bumped_pos_val = $self->_position_value ($to_position);
427 my @between = ($to_position, $new_group_count);
428 $self->_shift_siblings (1, @between); #shift right
429 $self->set_column( $position_column => $bumped_pos_val );
430 }
431
432 $self->_ordered_internal_update;
433
434 return 1;
435 }
79dc353a 436}
437
118e6b96 438=head2 insert
439
440Overrides the DBIC insert() method by providing a default
441position number. The default will be the number of rows in
442the table +1, thus positioning the new record at the last position.
443
444=cut
445
446sub insert {
447 my $self = shift;
448 my $position_column = $self->position_column;
bd7ca9e8 449
450 unless ($self->get_column($position_column)) {
451 my $lsib = $self->last_sibling; # FIXME - no need to inflate last_sibling
452 $self->set_column(
453 $position_column => ($lsib
454 ? $self->_next_position_value ( $lsib->get_column ($position_column) )
455 : $self->_initial_position_value
456 )
457 );
458 }
459
0a298c73 460 return $self->next::method( @_ );
118e6b96 461}
462
79dc353a 463=head2 update
464
465Overrides the DBIC update() method by checking for a change
466to the position and/or group columns. Movement within a
467group or to another group is handled by repositioning
468the appropriate siblings. Position defaults to the end
469of a new group if it has been changed to undef.
470
471=cut
472
473sub update {
474 my $self = shift;
475
bd7ca9e8 476 # this is set by _ordered_internal_update()
477 return $self->next::method(@_) if $self->{_ORDERED_INTERNAL_UPDATE};
79dc353a 478
bd7ca9e8 479 my $upd = shift;
480 $self->set_inflated_columns($upd) if $upd;
79dc353a 481 my %changes = $self->get_dirty_columns;
482 $self->discard_changes;
483
bd7ca9e8 484 my $position_column = $self->position_column;
fa6b598f 485
bd7ca9e8 486 # if nothing group/position related changed - short circuit
487 if (not grep { exists $changes{$_} } ($self->_grouping_columns, $position_column) ) {
488 return $self->next::method( \%changes, @_ );
489 }
fa6b598f 490
bd7ca9e8 491 # FIXME this needs to be wrapped in a transaction
492 {
493 # if any of our grouping columns have been changed
494 if (grep { exists $changes{$_} } ($self->_grouping_columns) ) {
495
496 # create new_group by taking the current group and inserting changes
497 my $new_group = {$self->_grouping_clause};
498 foreach my $col (keys %$new_group) {
499 if (exists $changes{$col}) {
500 $new_group->{$col} = delete $changes{$col}; # don't want to pass this on to next::method
501 }
fa6b598f 502 }
bd7ca9e8 503
504 $self->move_to_group(
505 $new_group,
506 (exists $changes{$position_column}
507 # The FIXME bit contradicts the documentation: when changing groups without supplying explicit
508 # positions in move_to_group(), we push the item to the end of the group.
509 # However when I was rewriting this, the position from the old group was clearly passed to the new one
510 # Probably needs to go away (by ribasushi)
511 ? delete $changes{$position_column} # means there was a position change supplied with the update too
512 : $self->_position # FIXME!
513 ),
514 );
515 }
516 elsif (exists $changes{$position_column}) {
517 $self->move_to(delete $changes{$position_column});
fa6b598f 518 }
519
bd7ca9e8 520 return $self->next::method( \%changes, @_ );
79dc353a 521 }
79dc353a 522}
523
118e6b96 524=head2 delete
525
526Overrides the DBIC delete() method by first moving the object
bd7ca9e8 527to the last position, then deleting it, thus ensuring the
118e6b96 528integrity of the positions.
529
530=cut
531
532sub delete {
533 my $self = shift;
bd7ca9e8 534 # FIXME this needs to be wrapped in a transaction
535 {
536 $self->move_last;
537 return $self->next::method( @_ );
538 }
539}
540
541=head1 Methods for extending Ordered
542
543You would want to override the methods below if you use sparse
544(non-linear) or non-numeric position values. This can be useful
545if you are working with preexisting non-normalised position data,
546or if you need to work with materialized path columns.
547
548=head2 _position
549
550 my $num_pos = $item->_position;
551
552Returns the absolute numeric position of the current object, with the
553first object being at position 1, its sibling at position 2 and so on.
554By default simply returns the value of L</position_column>.
555
556=cut
557sub _position {
558 my $self = shift;
559
560# #the right way to do this
561# return $self->previous_siblings->count + 1;
562
563 return $self->get_column ($self->position_column);
564}
565
566=head2 _position_value
567
568 my $pos_value = $item->_position_value ( $pos )
569
570Returns the value of L</position_column> of the object at numeric
571position C<$pos>. By default simply returns C<$pos>.
572
573=cut
574sub _position_value {
575 my ($self, $pos) = @_;
576
577# #the right way to do this (not optimized)
578# my $position_column = $self->position_column;
579# return $self -> _group_rs
580# -> search({}, { order_by => $position_column })
581# -> slice ( $pos - 1)
582# -> single
583# -> get_column ($position_column);
584
585 return $pos;
586}
587
588=head2 _initial_position_value
589
590 __PACKAGE__->_initial_position_value(0);
591
592This method specifies a value of L</position_column> which is assigned
593to the first inserted element of a group, if no value was supplied at
594insertion time. All subsequent values are derived from this one by
595L</_next_position_value> below. Defaults to 1.
596
597=cut
598
599__PACKAGE__->mk_classdata( '_initial_position_value' => 1 );
600
601=head2 _next_position_value
602
603 my $new_value = $item->_next_position_value ( $position_value )
604
605Returns a position value that would be considered C<next> with
606regards to C<$position_value>. Can be pretty much anything, given
607that C<< $position_value < $new_value >> where C<< < >> is the
608SQL comparison operator (usually works fine on strings). The
609default method expects C<$position_value> to be numeric, and
610returns C<$position_value + 1>
611
612=cut
613sub _next_position_value {
614 return $_[1] + 1;
615}
616
617=head2 _shift_siblings
618
619 $item->_shift_siblings ($direction, @between)
620
621Shifts all siblings with position in the range @between (inclusive)
622by one position as specified by $direction (left if < 0, right if > 0).
623By default simply increments/decrements each L<position_column> value
624by 1.
625
626=cut
627sub _shift_siblings {
628 my ($self, $direction, @between) = @_;
629 return 0 unless $direction;
630
631 my $position_column = $self->position_column;
632
633 my ($op, $ord);
634 if ($direction < 0) {
635 $op = '-';
636 $ord = 'asc';
637 }
638 else {
639 $op = '+';
640 $ord = 'desc';
641 }
642
643 my $shift_rs = $self->_group_rs-> search ({ $position_column => { -between => \@between } });
644
645 # some databases (sqlite) are dumb and can not do a blanket
646 # increment/decrement. So what we do here is check if the
647 # position column is part of a unique constraint, and do a
648 # one-by-one update if this is the case
649
650 my %uc = $self->result_source->unique_constraints;
651 if (grep { $_ eq $position_column } ( map { @$_ } (values %uc) ) ) {
652
653 my $rs = $shift_rs->search ({}, { order_by => { "-$ord", $position_column } } );
654 # FIXME - no need to inflate each row
655 while (my $r = $rs->next) {
656 $r->_ordered_internal_update ({ $position_column => \ "$position_column $op 1" } );
657 }
658 }
659 else {
660 $shift_rs->update ({ $position_column => \ "$position_column $op 1" } );
661 }
118e6b96 662}
663
7a76f44c 664=head1 PRIVATE METHODS
665
666These methods are used internally. You should never have the
667need to use them.
668
bd7ca9e8 669=head2 _group_rs
670
671This method returns a resultset containing all memebers of the row
672group (including the row itself).
673
674=cut
675sub _group_rs {
676 my $self = shift;
677 return $self->result_source->resultset->search({$self->_grouping_clause()});
678}
679
680=head2 _siblings
681
682Returns an unordered resultset of all objects in the same group
683excluding the object you called this method on.
684
685=cut
686sub _siblings {
687 my $self = shift;
688 my $position_column = $self->position_column;
689 return $self->_group_rs->search(
690 { $position_column => { '!=' => $self->get_column($position_column) } },
691 );
692}
693
a733c37f 694=head2 _grouping_clause
118e6b96 695
bd7ca9e8 696This method returns one or more name=>value pairs for limiting a search
697by the grouping column(s). If the grouping column is not
133dd22a 698defined then this will return an empty list.
118e6b96 699
7a76f44c 700=cut
a733c37f 701sub _grouping_clause {
169bb185 702 my( $self ) = @_;
fa6b598f 703 return map { $_ => $self->get_column($_) } $self->_grouping_columns();
704}
705
fa6b598f 706=head2 _get_grouping_columns
707
708Returns a list of the column names used for grouping, regardless of whether
1d941d67 709they were specified as an arrayref or a single string, and returns ()
710if there is no grouping.
fa6b598f 711
712=cut
713sub _grouping_columns {
714 my( $self ) = @_;
a733c37f 715 my $col = $self->grouping_column();
fa6b598f 716 if (ref $col eq 'ARRAY') {
717 return @$col;
718 } elsif ($col) {
719 return ( $col );
720 } else {
721 return ();
133dd22a 722 }
7a76f44c 723}
724
bd7ca9e8 725=head2 _is_in_group
fa6b598f 726
727 $item->_is_in_group( {user => 'fred', list => 'work'} )
728
729Returns true if the object is in the group represented by hashref $other
bd7ca9e8 730
fa6b598f 731=cut
732sub _is_in_group {
733 my ($self, $other) = @_;
734 my $current = {$self->_grouping_clause};
bd7ca9e8 735
736 no warnings qw/uninitialized/;
737
738 return 0 if (
739 join ("\x00", sort keys %$current)
740 ne
741 join ("\x00", sort keys %$other)
742 );
fa6b598f 743 for my $key (keys %$current) {
fa6b598f 744 return 0 if $current->{$key} ne $other->{$key};
745 }
746 return 1;
747}
748
bd7ca9e8 749sub _ordered_internal_update {
750 my $self = shift;
751 local $self->{_ORDERED_INTERNAL_UPDATE} = 1;
752 return $self->update (@_);
753}
fa6b598f 754
7a76f44c 7551;
118e6b96 756
bd7ca9e8 757__END__
dc66dea1 758
bd7ca9e8 759=head1 CAVEATS
dc66dea1 760
133dd22a 761=head2 Race Condition on Insert
762
118e6b96 763If a position is not specified for an insert than a position
bd7ca9e8 764will be chosen based either on L</_initial_position_value> or
765L</_next_position_value>, depending if there are already some
766items in the current group. The space of time between the
767necessary selects and insert introduces a race condition.
768Having unique constraints on your position/group columns,
769and using transactions (see L<DBIx::Class::Storage/txn_do>)
770will prevent such race conditions going undetected.
118e6b96 771
133dd22a 772=head2 Multiple Moves
773
774Be careful when issueing move_* methods to multiple objects. If
775you've pre-loaded the objects then when you move one of the objects
776the position of the other object will not reflect their new value
bd7ca9e8 777until you reload them from the database - see
778L<DBIx::Class::Row/discard_changes>.
133dd22a 779
dc66dea1 780There are times when you will want to move objects as groups, such
133dd22a 781as changeing the parent of several objects at once - this directly
782conflicts with this problem. One solution is for us to write a
783ResultSet class that supports a parent() method, for example. Another
784solution is to somehow automagically modify the objects that exist
785in the current object's result set to have the new position value.
786
118e6b96 787=head1 AUTHOR
788
789Aran Deltac <bluefeet@cpan.org>
790
791=head1 LICENSE
792
793You may distribute this code under the same terms as Perl itself.
794