Merge 'column_attr' into 'trunk'
[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
b250066f 365
366 # we need to null-position the moved row if the position column is part of a constraint
367 if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $self->result_source->unique_constraints }} ) ) ) {
368 $self->_ordered_internal_update({ $position_column => $self->null_position_value });
369 }
370
bd7ca9e8 371 $self->_shift_siblings ($direction, @between);
372 $self->_ordered_internal_update({ $position_column => $new_pos_val });
fa6b598f 373
bd7ca9e8 374 return 1;
375 }
376}
fa6b598f 377
79dc353a 378=head2 move_to_group
379
380 $item->move_to_group( $group, $position );
381
382Moves the object to the specified position of the specified
383group, or to the end of the group if $position is undef.
3841 is returned on success, and 0 is returned if the object is
385already at the specified position of the specified group.
386
1d941d67 387$group may be specified as a single scalar if only one
388grouping column is in use, or as a hashref of column => value pairs
389if multiple grouping columns are in use.
fa6b598f 390
79dc353a 391=cut
392
393sub move_to_group {
394 my( $self, $to_group, $to_position ) = @_;
fa6b598f 395
bd7ca9e8 396 $self->throw_exception ('move_to_group() expects a group specification')
397 unless defined $to_group;
398
fa6b598f 399 # if we're given a string, turn it into a hashref
400 unless (ref $to_group eq 'HASH') {
bd7ca9e8 401 my @gcols = $self->_grouping_columns;
402
403 $self->throw_exception ('Single group supplied for a multi-column group identifier') if @gcols > 1;
404 $to_group = {$gcols[0] => $to_group};
fa6b598f 405 }
406
79dc353a 407 my $position_column = $self->position_column;
79dc353a 408
79dc353a 409 return 0 if ( defined($to_position) and $to_position < 1 );
bd7ca9e8 410 if ($self->_is_in_group ($to_group) ) {
411 return 0 if not defined $to_position;
412 return $self->move_to ($to_position);
79dc353a 413 }
414
bd7ca9e8 415 # FIXME this needs to be wrapped in a transaction
416 {
417 # Move to end of current group to adjust siblings
418 $self->move_last;
419
420 $self->set_inflated_columns({ %$to_group, $position_column => undef });
421 my $new_group_count = $self->_group_rs->count;
422
423 if ( not defined($to_position) or $to_position > $new_group_count) {
424 $self->set_column(
425 $position_column => $new_group_count
426 ? $self->_next_position_value ( $self->last_sibling->get_column ($position_column) ) # FIXME - no need to inflate last_sibling
427 : $self->_initial_position_value
428 );
429 }
430 else {
431 my $bumped_pos_val = $self->_position_value ($to_position);
432 my @between = ($to_position, $new_group_count);
433 $self->_shift_siblings (1, @between); #shift right
434 $self->set_column( $position_column => $bumped_pos_val );
435 }
436
437 $self->_ordered_internal_update;
438
439 return 1;
440 }
79dc353a 441}
442
118e6b96 443=head2 insert
444
445Overrides the DBIC insert() method by providing a default
446position number. The default will be the number of rows in
447the table +1, thus positioning the new record at the last position.
448
449=cut
450
451sub insert {
452 my $self = shift;
453 my $position_column = $self->position_column;
bd7ca9e8 454
455 unless ($self->get_column($position_column)) {
456 my $lsib = $self->last_sibling; # FIXME - no need to inflate last_sibling
457 $self->set_column(
458 $position_column => ($lsib
459 ? $self->_next_position_value ( $lsib->get_column ($position_column) )
460 : $self->_initial_position_value
461 )
462 );
463 }
464
0a298c73 465 return $self->next::method( @_ );
118e6b96 466}
467
79dc353a 468=head2 update
469
470Overrides the DBIC update() method by checking for a change
471to the position and/or group columns. Movement within a
472group or to another group is handled by repositioning
473the appropriate siblings. Position defaults to the end
474of a new group if it has been changed to undef.
475
476=cut
477
478sub update {
479 my $self = shift;
480
bd7ca9e8 481 # this is set by _ordered_internal_update()
482 return $self->next::method(@_) if $self->{_ORDERED_INTERNAL_UPDATE};
79dc353a 483
bd7ca9e8 484 my $upd = shift;
485 $self->set_inflated_columns($upd) if $upd;
79dc353a 486 my %changes = $self->get_dirty_columns;
487 $self->discard_changes;
488
bd7ca9e8 489 my $position_column = $self->position_column;
fa6b598f 490
bd7ca9e8 491 # if nothing group/position related changed - short circuit
492 if (not grep { exists $changes{$_} } ($self->_grouping_columns, $position_column) ) {
493 return $self->next::method( \%changes, @_ );
494 }
fa6b598f 495
bd7ca9e8 496 # FIXME this needs to be wrapped in a transaction
497 {
498 # if any of our grouping columns have been changed
499 if (grep { exists $changes{$_} } ($self->_grouping_columns) ) {
500
501 # create new_group by taking the current group and inserting changes
502 my $new_group = {$self->_grouping_clause};
503 foreach my $col (keys %$new_group) {
504 if (exists $changes{$col}) {
505 $new_group->{$col} = delete $changes{$col}; # don't want to pass this on to next::method
506 }
fa6b598f 507 }
bd7ca9e8 508
509 $self->move_to_group(
510 $new_group,
511 (exists $changes{$position_column}
512 # The FIXME bit contradicts the documentation: when changing groups without supplying explicit
513 # positions in move_to_group(), we push the item to the end of the group.
514 # However when I was rewriting this, the position from the old group was clearly passed to the new one
515 # Probably needs to go away (by ribasushi)
516 ? delete $changes{$position_column} # means there was a position change supplied with the update too
517 : $self->_position # FIXME!
518 ),
519 );
520 }
521 elsif (exists $changes{$position_column}) {
522 $self->move_to(delete $changes{$position_column});
fa6b598f 523 }
524
bd7ca9e8 525 return $self->next::method( \%changes, @_ );
79dc353a 526 }
79dc353a 527}
528
118e6b96 529=head2 delete
530
531Overrides the DBIC delete() method by first moving the object
bd7ca9e8 532to the last position, then deleting it, thus ensuring the
118e6b96 533integrity of the positions.
534
535=cut
536
537sub delete {
538 my $self = shift;
bd7ca9e8 539 # FIXME this needs to be wrapped in a transaction
540 {
541 $self->move_last;
542 return $self->next::method( @_ );
543 }
544}
545
b250066f 546=head1 METHODS FOR EXTENDING ORDERED
bd7ca9e8 547
548You would want to override the methods below if you use sparse
549(non-linear) or non-numeric position values. This can be useful
550if you are working with preexisting non-normalised position data,
551or if you need to work with materialized path columns.
552
553=head2 _position
554
555 my $num_pos = $item->_position;
556
b250066f 557Returns the B<absolute numeric position> of the current object, with the
bd7ca9e8 558first object being at position 1, its sibling at position 2 and so on.
559By default simply returns the value of L</position_column>.
560
561=cut
562sub _position {
563 my $self = shift;
564
565# #the right way to do this
566# return $self->previous_siblings->count + 1;
567
568 return $self->get_column ($self->position_column);
569}
570
571=head2 _position_value
572
573 my $pos_value = $item->_position_value ( $pos )
574
b250066f 575Returns the B<value> of L</position_column> of the object at numeric
bd7ca9e8 576position C<$pos>. By default simply returns C<$pos>.
577
578=cut
579sub _position_value {
580 my ($self, $pos) = @_;
581
582# #the right way to do this (not optimized)
583# my $position_column = $self->position_column;
584# return $self -> _group_rs
585# -> search({}, { order_by => $position_column })
586# -> slice ( $pos - 1)
587# -> single
588# -> get_column ($position_column);
589
590 return $pos;
591}
592
593=head2 _initial_position_value
594
595 __PACKAGE__->_initial_position_value(0);
596
b250066f 597This method specifies a B<value> of L</position_column> which is assigned
bd7ca9e8 598to the first inserted element of a group, if no value was supplied at
599insertion time. All subsequent values are derived from this one by
600L</_next_position_value> below. Defaults to 1.
601
602=cut
603
604__PACKAGE__->mk_classdata( '_initial_position_value' => 1 );
605
606=head2 _next_position_value
607
608 my $new_value = $item->_next_position_value ( $position_value )
609
b250066f 610Returns a position B<value> that would be considered C<next> with
bd7ca9e8 611regards to C<$position_value>. Can be pretty much anything, given
612that C<< $position_value < $new_value >> where C<< < >> is the
613SQL comparison operator (usually works fine on strings). The
614default method expects C<$position_value> to be numeric, and
615returns C<$position_value + 1>
616
617=cut
618sub _next_position_value {
619 return $_[1] + 1;
620}
621
622=head2 _shift_siblings
623
624 $item->_shift_siblings ($direction, @between)
625
b250066f 626Shifts all siblings with B<positions values> in the range @between
627(inclusive) by one position as specified by $direction (left if < 0,
628 right if > 0). By default simply increments/decrements each
629L<position_column> value by 1, doing so in a way as to not violate
630any existing constraints.
631
632Note that if you override this method and have unique constraints
633including the L<position_column> the shift is not a trivial task.
634Refer to the implementation source of the default method for more
635information.
bd7ca9e8 636
637=cut
638sub _shift_siblings {
639 my ($self, $direction, @between) = @_;
640 return 0 unless $direction;
641
642 my $position_column = $self->position_column;
643
644 my ($op, $ord);
645 if ($direction < 0) {
646 $op = '-';
647 $ord = 'asc';
648 }
649 else {
650 $op = '+';
651 $ord = 'desc';
652 }
653
654 my $shift_rs = $self->_group_rs-> search ({ $position_column => { -between => \@between } });
655
656 # some databases (sqlite) are dumb and can not do a blanket
657 # increment/decrement. So what we do here is check if the
658 # position column is part of a unique constraint, and do a
659 # one-by-one update if this is the case
660
b250066f 661 if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $self->result_source->unique_constraints }} ) ) ) {
bd7ca9e8 662
663 my $rs = $shift_rs->search ({}, { order_by => { "-$ord", $position_column } } );
664 # FIXME - no need to inflate each row
665 while (my $r = $rs->next) {
666 $r->_ordered_internal_update ({ $position_column => \ "$position_column $op 1" } );
667 }
668 }
669 else {
670 $shift_rs->update ({ $position_column => \ "$position_column $op 1" } );
671 }
118e6b96 672}
673
7a76f44c 674=head1 PRIVATE METHODS
675
676These methods are used internally. You should never have the
677need to use them.
678
bd7ca9e8 679=head2 _group_rs
680
b250066f 681This method returns a resultset containing all members of the row
bd7ca9e8 682group (including the row itself).
683
684=cut
685sub _group_rs {
686 my $self = shift;
687 return $self->result_source->resultset->search({$self->_grouping_clause()});
688}
689
690=head2 _siblings
691
692Returns an unordered resultset of all objects in the same group
693excluding the object you called this method on.
694
695=cut
696sub _siblings {
697 my $self = shift;
698 my $position_column = $self->position_column;
699 return $self->_group_rs->search(
700 { $position_column => { '!=' => $self->get_column($position_column) } },
701 );
702}
703
a733c37f 704=head2 _grouping_clause
118e6b96 705
bd7ca9e8 706This method returns one or more name=>value pairs for limiting a search
707by the grouping column(s). If the grouping column is not
133dd22a 708defined then this will return an empty list.
118e6b96 709
7a76f44c 710=cut
a733c37f 711sub _grouping_clause {
169bb185 712 my( $self ) = @_;
fa6b598f 713 return map { $_ => $self->get_column($_) } $self->_grouping_columns();
714}
715
fa6b598f 716=head2 _get_grouping_columns
717
718Returns a list of the column names used for grouping, regardless of whether
1d941d67 719they were specified as an arrayref or a single string, and returns ()
720if there is no grouping.
fa6b598f 721
722=cut
723sub _grouping_columns {
724 my( $self ) = @_;
a733c37f 725 my $col = $self->grouping_column();
fa6b598f 726 if (ref $col eq 'ARRAY') {
727 return @$col;
728 } elsif ($col) {
729 return ( $col );
730 } else {
731 return ();
133dd22a 732 }
7a76f44c 733}
734
bd7ca9e8 735=head2 _is_in_group
fa6b598f 736
737 $item->_is_in_group( {user => 'fred', list => 'work'} )
738
739Returns true if the object is in the group represented by hashref $other
bd7ca9e8 740
fa6b598f 741=cut
742sub _is_in_group {
743 my ($self, $other) = @_;
744 my $current = {$self->_grouping_clause};
bd7ca9e8 745
746 no warnings qw/uninitialized/;
747
748 return 0 if (
749 join ("\x00", sort keys %$current)
750 ne
751 join ("\x00", sort keys %$other)
752 );
fa6b598f 753 for my $key (keys %$current) {
fa6b598f 754 return 0 if $current->{$key} ne $other->{$key};
755 }
756 return 1;
757}
758
b250066f 759=head2 _ordered_internal_update
760
761This is a short-circuited method, that is used internally by this
762module to update positioning values in isolation (i.e. without
763triggering any of the positioning integrity code).
764
f045efad 765Some day you might get confronted by datasets that have ambiguous
766positioning data (i.e. duplicate position values within the same group,
b250066f 767in a table without unique constraints). When manually fixing such data
768keep in mind that you can not invoke L<DBIx::Class::Row/update> like
f045efad 769you normally would, as it will get confused by the wrong data before
b250066f 770having a chance to update the ill-defined row. If you really know what
f045efad 771you are doing use this method which bypasses any hooks introduced by
b250066f 772this module.
773
774=cut
775
bd7ca9e8 776sub _ordered_internal_update {
777 my $self = shift;
778 local $self->{_ORDERED_INTERNAL_UPDATE} = 1;
779 return $self->update (@_);
780}
fa6b598f 781
7a76f44c 7821;
118e6b96 783
bd7ca9e8 784__END__
dc66dea1 785
bd7ca9e8 786=head1 CAVEATS
dc66dea1 787
133dd22a 788=head2 Race Condition on Insert
789
118e6b96 790If a position is not specified for an insert than a position
bd7ca9e8 791will be chosen based either on L</_initial_position_value> or
792L</_next_position_value>, depending if there are already some
793items in the current group. The space of time between the
794necessary selects and insert introduces a race condition.
795Having unique constraints on your position/group columns,
796and using transactions (see L<DBIx::Class::Storage/txn_do>)
797will prevent such race conditions going undetected.
118e6b96 798
133dd22a 799=head2 Multiple Moves
800
801Be careful when issueing move_* methods to multiple objects. If
802you've pre-loaded the objects then when you move one of the objects
803the position of the other object will not reflect their new value
bd7ca9e8 804until you reload them from the database - see
805L<DBIx::Class::Row/discard_changes>.
133dd22a 806
dc66dea1 807There are times when you will want to move objects as groups, such
133dd22a 808as changeing the parent of several objects at once - this directly
809conflicts with this problem. One solution is for us to write a
810ResultSet class that supports a parent() method, for example. Another
811solution is to somehow automagically modify the objects that exist
812in the current object's result set to have the new position value.
813
58755bba 814=head2 Default Values
815
816Using a database defined default_value on one of your group columns
817could result in the position not being assigned correctly.
818
118e6b96 819=head1 AUTHOR
820
821Aran Deltac <bluefeet@cpan.org>
822
823=head1 LICENSE
824
825You may distribute this code under the same terms as Perl itself.
826