1 package DBIx::Class::Ordered;
4 use base qw( DBIx::Class );
8 DBIx::Class::Ordered - Modify the position of objects in an ordered list.
12 Create a table for your ordered data.
15 item_id INTEGER PRIMARY KEY AUTOINCREMENT,
17 position INTEGER NOT NULL
20 Optionally, add one or more columns to specify groupings, allowing you
21 to maintain independent ordered lists within one table:
24 item_id INTEGER PRIMARY KEY AUTOINCREMENT,
26 position INTEGER NOT NULL,
27 group_id INTEGER NOT NULL
33 item_id INTEGER PRIMARY KEY AUTOINCREMENT,
35 position INTEGER NOT NULL,
36 group_id INTEGER NOT NULL,
37 other_group_id INTEGER NOT NULL
40 In your Schema or DB class add "Ordered" to the top
41 of the component list.
43 __PACKAGE__->load_components(qw( Ordered ... ));
45 Specify the column that stores the position number for
49 __PACKAGE__->position_column('position');
51 If you are using one grouping column, specify it as follows:
53 __PACKAGE__->grouping_column('group_id');
55 Or if you have multiple grouping columns:
57 __PACKAGE__->grouping_column(['group_id', 'other_group_id']);
59 That's it, now you can change the position of your objects.
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 });
68 my $rs = $item->siblings();
69 my @siblings = $item->siblings();
72 $sibling = $item->first_sibling();
73 $sibling = $item->last_sibling();
74 $sibling = $item->previous_sibling();
75 $sibling = $item->next_sibling();
77 $item->move_previous();
81 $item->move_to( $position );
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 );
89 This module provides a simple interface for modifying the ordered
90 position of DBIx::Class objects.
94 All of the move_* methods automatically update the rows involved in
95 the query. This is not configurable and is due to the fact that if you
96 move a record it always causes other records in the list to be updated.
100 =head2 position_column
102 __PACKAGE__->position_column('position');
104 Sets and retrieves the name of the column that stores the
105 positional value of each record. Defaults to "position".
109 __PACKAGE__->mk_classaccessor( 'position_column' => 'position' );
111 =head2 grouping_column
113 __PACKAGE__->grouping_column('group_id');
115 This method specifies a column to limit all queries in
116 this module by. This effectively allows you to have multiple
117 ordered lists within the same table.
121 __PACKAGE__->mk_group_accessors( inherited => 'grouping_column' );
123 =head2 null_position_value
125 __PACKAGE__->null_position_value(undef);
127 This method specifies a value of L</position_column> which B<would
128 never be assigned to a row> during normal operation. When
129 a row is moved, its position is set to this value temporarily, so
130 that any unique constraints can not be violated. This value defaults
131 to 0, which should work for all cases except when your positions do
136 __PACKAGE__->mk_classaccessor( 'null_position_value' => 0 );
140 my $rs = $item->siblings();
141 my @siblings = $item->siblings();
143 Returns an B<ordered> resultset of all other objects in the same
144 group excluding the one you called it on.
146 The ordering is a backwards-compatibility artifact - if you need
147 a resultset with no ordering applied use C<_siblings>
152 return $self->_siblings->search ({}, { order_by => $self->position_column } );
155 =head2 previous_siblings
157 my $prev_rs = $item->previous_siblings();
158 my @prev_siblings = $item->previous_siblings();
160 Returns a resultset of all objects in the same group
161 positioned before the object on which this method was called.
164 sub previous_siblings {
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 } })
176 my $next_rs = $item->next_siblings();
177 my @next_siblings = $item->next_siblings();
179 Returns a resultset of all objects in the same group
180 positioned after the object on which this method was called.
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 } })
193 =head2 previous_sibling
195 my $sibling = $item->previous_sibling();
197 Returns the sibling that resides one position back. Returns 0
198 if the current object is the first one.
202 sub previous_sibling {
204 my $position_column = $self->position_column;
206 my $psib = $self->previous_siblings->search(
208 { rows => 1, order_by => { '-desc' => $position_column } },
211 return defined $psib ? $psib : 0;
216 my $sibling = $item->first_sibling();
218 Returns the first sibling object, or 0 if the first sibling
225 my $position_column = $self->position_column;
227 my $fsib = $self->previous_siblings->search(
229 { rows => 1, order_by => { '-asc' => $position_column } },
232 return defined $fsib ? $fsib : 0;
237 my $sibling = $item->next_sibling();
239 Returns the sibling that resides one position forward. Returns 0
240 if the current object is the last one.
246 my $position_column = $self->position_column;
247 my $nsib = $self->next_siblings->search(
249 { rows => 1, order_by => { '-asc' => $position_column } },
252 return defined $nsib ? $nsib : 0;
257 my $sibling = $item->last_sibling();
259 Returns the last sibling, or 0 if the last sibling is this
266 my $position_column = $self->position_column;
267 my $lsib = $self->next_siblings->search(
269 { rows => 1, order_by => { '-desc' => $position_column } },
272 return defined $lsib ? $lsib : 0;
275 # an optimized method to get the last sibling position value without inflating a result object
276 sub _last_sibling_posval {
278 my $position_column = $self->position_column;
280 my $cursor = $self->next_siblings->search(
282 { rows => 1, order_by => { '-desc' => $position_column }, select => $position_column },
285 my ($pos) = $cursor->next;
291 $item->move_previous();
293 Swaps position with the sibling in the position previous in
294 the list. Returns 1 on success, and 0 if the object is
295 already the first one.
301 return $self->move_to ($self->_position - 1);
308 Swaps position with the sibling in the next position in the
309 list. Returns 1 on success, and 0 if the object is already
310 the last in the list.
316 return 0 unless defined $self->_last_sibling_posval; # quick way to check for no more siblings
317 return $self->move_to ($self->_position + 1);
324 Moves the object to the first position in the list. Returns 1
325 on success, and 0 if the object is already the first.
330 return shift->move_to( 1 );
337 Moves the object to the last position in the list. Returns 1
338 on success, and 0 if the object is already the last one.
344 my $last_posval = $self->_last_sibling_posval;
346 return 0 unless defined $last_posval;
348 return $self->move_to( $self->_position_from_value ($last_posval) );
353 $item->move_to( $position );
355 Moves the object to the specified position. Returns 1 on
356 success, and 0 if the object is already at the specified
362 my( $self, $to_position ) = @_;
363 return 0 if ( $to_position < 1 );
365 my $position_column = $self->position_column;
367 my $rsrc = $self->result_source;
370 if ($is_txn = $rsrc->schema->storage->transaction_depth) {
371 # Reload position state from storage
372 # The thinking here is that if we are in a transaction, it is
373 # *more likely* the object went out of sync due to resultset
374 # level shenanigans. Instead of always reloading (slow) - go
375 # ahead and hand-hold only in the case of higher layers
376 # requesting the safety of a txn
381 ->search($self->_storage_ident_condition, { rows => 1, columns => $position_column })
384 )[0] || $self->throw_exception(
385 sprintf "Unable to locate object '%s' in storage - object went ouf of sync...?",
389 delete $self->{_dirty_columns}{$position_column};
391 elsif ($self->is_column_changed ($position_column) ) {
392 # something changed our position, we need to know where we
393 # used to be - use the stashed value
394 $self->store_column($position_column, delete $self->{_column_data_in_storage}{$position_column});
395 delete $self->{_dirty_columns}{$position_column};
398 my $from_position = $self->_position;
400 if ( $from_position == $to_position ) { # FIXME this will not work for non-numeric order
404 my $guard = $is_txn ? undef : $rsrc->schema->txn_scope_guard;
406 my ($direction, @between);
407 if ( $from_position < $to_position ) {
409 @between = map { $self->_position_value ($_) } ( $from_position + 1, $to_position );
413 @between = map { $self->_position_value ($_) } ( $to_position, $from_position - 1 );
416 my $new_pos_val = $self->_position_value ($to_position); # record this before the shift
418 # we need to null-position the moved row if the position column is part of a constraint
419 if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $rsrc->unique_constraints }} ) ) ) {
420 $self->_ordered_internal_update({ $position_column => $self->null_position_value });
423 $self->_shift_siblings ($direction, @between);
424 $self->_ordered_internal_update({ $position_column => $new_pos_val });
426 $guard->commit if $guard;
432 $item->move_to_group( $group, $position );
434 Moves the object to the specified position of the specified
435 group, or to the end of the group if $position is undef.
436 1 is returned on success, and 0 is returned if the object is
437 already at the specified position of the specified group.
439 $group may be specified as a single scalar if only one
440 grouping column is in use, or as a hashref of column => value pairs
441 if multiple grouping columns are in use.
446 my( $self, $to_group, $to_position ) = @_;
448 # if we're given a single value, turn it into a hashref
449 unless (ref $to_group eq 'HASH') {
450 my @gcols = $self->_grouping_columns;
452 $self->throw_exception ('Single group supplied for a multi-column group identifier') if @gcols > 1;
453 $to_group = {$gcols[0] => $to_group};
456 my $position_column = $self->position_column;
458 return 0 if ( defined($to_position) and $to_position < 1 );
460 # check if someone changed the _grouping_columns - this will
461 # prevent _is_in_group working, so we need to restore the
462 # original stashed values
463 for ($self->_grouping_columns) {
464 if ($self->is_column_changed ($_)) {
465 $self->store_column($_, delete $self->{_column_data_in_storage}{$_});
466 delete $self->{_dirty_columns}{$_};
470 if ($self->_is_in_group ($to_group) ) {
472 if (defined $to_position) {
473 $ret = $self->move_to ($to_position);
479 my $guard = $self->result_source->schema->txn_scope_guard;
481 # Move to end of current group to adjust siblings
484 $self->set_inflated_columns({ %$to_group, $position_column => undef });
485 my $new_group_last_posval = $self->_last_sibling_posval;
486 my $new_group_last_position = $self->_position_from_value (
487 $new_group_last_posval
490 if ( not defined($to_position) or $to_position > $new_group_last_position) {
492 $position_column => $new_group_last_position
493 ? $self->_next_position_value ( $new_group_last_posval )
494 : $self->_initial_position_value
498 my $bumped_pos_val = $self->_position_value ($to_position);
499 my @between = map { $self->_position_value ($_) } ($to_position, $new_group_last_position);
500 $self->_shift_siblings (1, @between); #shift right
501 $self->set_column( $position_column => $bumped_pos_val );
504 $self->_ordered_internal_update;
513 Overrides the DBIC insert() method by providing a default
514 position number. The default will be the number of rows in
515 the table +1, thus positioning the new record at the last position.
521 my $position_column = $self->position_column;
523 unless ($self->get_column($position_column)) {
524 my $lsib_posval = $self->_last_sibling_posval;
526 $position_column => (defined $lsib_posval
527 ? $self->_next_position_value ( $lsib_posval )
528 : $self->_initial_position_value
533 return $self->next::method( @_ );
538 Overrides the DBIC update() method by checking for a change
539 to the position and/or group columns. Movement within a
540 group or to another group is handled by repositioning
541 the appropriate siblings. Position defaults to the end
542 of a new group if it has been changed to undef.
549 # this is set by _ordered_internal_update()
550 return $self->next::method(@_) if $self->result_source->schema->{_ORDERED_INTERNAL_UPDATE};
553 $self->set_inflated_columns($upd) if $upd;
555 my $position_column = $self->position_column;
556 my @group_columns = $self->_grouping_columns;
558 # see if the order is already changed
559 my $changed_ordering_cols = { map { $_ => $self->get_column($_) } grep { $self->is_column_changed($_) } ($position_column, @group_columns) };
561 # nothing changed - short circuit
562 if (! keys %$changed_ordering_cols) {
563 return $self->next::method( undef, @_ );
565 elsif (grep { exists $changed_ordering_cols->{$_} } @group_columns ) {
566 $self->move_to_group(
567 # since the columns are already re-set the _grouping_clause is correct
568 # move_to_group() knows how to get the original storage values
569 { $self->_grouping_clause },
571 # The FIXME bit contradicts the documentation: POD states that
572 # when changing groups without supplying explicit positions in
573 # move_to_group(), we push the item to the end of the group.
574 # However when I was rewriting this, the position from the old
575 # group was clearly passed to the new one
576 # Probably needs to go away (by ribasushi)
577 (exists $changed_ordering_cols->{$position_column}
578 ? $changed_ordering_cols->{$position_column} # means there was a position change supplied with the update too
579 : $self->_position # FIXME! (replace with undef)
584 $self->move_to($changed_ordering_cols->{$position_column});
592 Overrides the DBIC delete() method by first moving the object
593 to the last position, then deleting it, thus ensuring the
594 integrity of the positions.
601 my $guard = $self->result_source->schema->txn_scope_guard;
605 $self->next::method( @_ );
612 # add the current position/group to the things we track old values for
613 sub _track_storage_value {
614 my ($self, $col) = @_;
616 $self->next::method($col)
618 grep { $_ eq $col } ($self->position_column, $self->_grouping_columns)
622 =head1 METHODS FOR EXTENDING ORDERED
624 You would want to override the methods below if you use sparse
625 (non-linear) or non-numeric position values. This can be useful
626 if you are working with preexisting non-normalised position data,
627 or if you need to work with materialized path columns.
629 =head2 _position_from_value
631 my $num_pos = $item->_position_from_value ( $pos_value )
633 Returns the B<absolute numeric position> of an object with a B<position
634 value> set to C<$pos_value>. By default simply returns C<$pos_value>.
637 sub _position_from_value {
638 my ($self, $val) = @_;
640 return 0 unless defined $val;
642 # #the right way to do this
643 # return $self -> _group_rs
644 # -> search({ $self->position_column => { '<=', $val } })
650 =head2 _position_value
652 my $pos_value = $item->_position_value ( $pos )
654 Returns the B<value> of L</position_column> of the object at numeric
655 position C<$pos>. By default simply returns C<$pos>.
658 sub _position_value {
659 my ($self, $pos) = @_;
661 # #the right way to do this (not optimized)
662 # my $position_column = $self->position_column;
663 # return $self -> _group_rs
664 # -> search({}, { order_by => $position_column })
665 # -> slice ( $pos - 1)
667 # -> get_column ($position_column);
672 =head2 _initial_position_value
674 __PACKAGE__->_initial_position_value(0);
676 This method specifies a B<value> of L</position_column> which is assigned
677 to the first inserted element of a group, if no value was supplied at
678 insertion time. All subsequent values are derived from this one by
679 L</_next_position_value> below. Defaults to 1.
683 __PACKAGE__->mk_classaccessor( '_initial_position_value' => 1 );
685 =head2 _next_position_value
687 my $new_value = $item->_next_position_value ( $position_value )
689 Returns a position B<value> that would be considered C<next> with
690 regards to C<$position_value>. Can be pretty much anything, given
691 that C<< $position_value < $new_value >> where C<< < >> is the
692 SQL comparison operator (usually works fine on strings). The
693 default method expects C<$position_value> to be numeric, and
694 returns C<$position_value + 1>
697 sub _next_position_value {
701 =head2 _shift_siblings
703 $item->_shift_siblings ($direction, @between)
705 Shifts all siblings with B<positions values> in the range @between
706 (inclusive) by one position as specified by $direction (left if < 0,
707 right if > 0). By default simply increments/decrements each
708 L</position_column> value by 1, doing so in a way as to not violate
709 any existing constraints.
711 Note that if you override this method and have unique constraints
712 including the L</position_column> the shift is not a trivial task.
713 Refer to the implementation source of the default method for more
717 sub _shift_siblings {
718 my ($self, $direction, @between) = @_;
719 return 0 unless $direction;
721 my $position_column = $self->position_column;
724 if ($direction < 0) {
733 my $shift_rs = $self->_group_rs-> search ({ $position_column => { -between => \@between } });
735 # some databases (sqlite, pg, perhaps others) are dumb and can not do a
736 # blanket increment/decrement without violating a unique constraint.
737 # So what we do here is check if the position column is part of a unique
738 # constraint, and do a one-by-one update if this is the case.
739 my $rsrc = $self->result_source;
741 # set in case there are more cascades combined with $rs->update => $rs_update_all overrides
742 local $rsrc->schema->{_ORDERED_INTERNAL_UPDATE} = 1;
743 my @pcols = $rsrc->primary_columns;
745 grep { $_ eq $position_column } ( map { @$_ } (values %{{ $rsrc->unique_constraints }} ) )
747 my $clean_rs = $rsrc->resultset;
749 for ( $shift_rs->search (
750 {}, { order_by => { "-$ord", $position_column }, select => [$position_column, @pcols] }
753 $clean_rs->find(@$_)->update ({ $position_column => $pos + ( ($op eq '+') ? 1 : -1 ) });
757 $shift_rs->update ({ $position_column => \ "$position_column $op 1" } );
762 # This method returns a resultset containing all members of the row
763 # group (including the row itself).
766 return $self->result_source->resultset->search({$self->_grouping_clause()});
769 # Returns an unordered resultset of all objects in the same group
770 # excluding the object you called this method on.
773 my $position_column = $self->position_column;
775 return defined ($pos = $self->get_column($position_column))
776 ? $self->_group_rs->search(
777 { $position_column => { '!=' => $pos } },
783 # Returns the B<absolute numeric position> of the current object, with the
784 # first object being at position 1, its sibling at position 2 and so on.
787 return $self->_position_from_value ($self->get_column ($self->position_column) );
790 # This method returns one or more name=>value pairs for limiting a search
791 # by the grouping column(s). If the grouping column is not defined then
792 # this will return an empty list.
793 sub _grouping_clause {
795 return map { $_ => $self->get_column($_) } $self->_grouping_columns();
798 # Returns a list of the column names used for grouping, regardless of whether
799 # they were specified as an arrayref or a single string, and returns ()
800 # if there is no grouping.
801 sub _grouping_columns {
803 my $col = $self->grouping_column();
804 if (ref $col eq 'ARRAY') {
813 # Returns true if the object is in the group represented by hashref $other
815 my ($self, $other) = @_;
816 my $current = {$self->_grouping_clause};
818 no warnings qw/uninitialized/;
821 join ("\x00", sort keys %$current)
823 join ("\x00", sort keys %$other)
825 for my $key (keys %$current) {
826 return 0 if $current->{$key} ne $other->{$key};
831 # This is a short-circuited method, that is used internally by this
832 # module to update positioning values in isolation (i.e. without
833 # triggering any of the positioning integrity code).
835 # Some day you might get confronted by datasets that have ambiguous
836 # positioning data (e.g. duplicate position values within the same group,
837 # in a table without unique constraints). When manually fixing such data
838 # keep in mind that you can not invoke L<DBIx::Class::Row/update> like
839 # you normally would, as it will get confused by the wrong data before
840 # having a chance to update the ill-defined row. If you really know what
841 # you are doing use this method which bypasses any hooks introduced by
843 sub _ordered_internal_update {
845 local $self->result_source->schema->{_ORDERED_INTERNAL_UPDATE} = 1;
846 return $self->update (@_);
855 =head2 Resultset Methods
857 Note that all Insert/Create/Delete overrides are happening on
858 L<DBIx::Class::Row> methods only. If you use the
859 L<DBIx::Class::ResultSet> versions of
860 L<update|DBIx::Class::ResultSet/update> or
861 L<delete|DBIx::Class::ResultSet/delete>, all logic present in this
862 module will be bypassed entirely (possibly resulting in a broken
863 order-tree). Instead always use the
864 L<update_all|DBIx::Class::ResultSet/update_all> and
865 L<delete_all|DBIx::Class::ResultSet/delete_all> methods, which will
866 invoke the corresponding L<row|DBIx::Class::Row> method on every
867 member of the given resultset.
869 =head2 Race Condition on Insert
871 If a position is not specified for an insert, a position
872 will be chosen based either on L</_initial_position_value> or
873 L</_next_position_value>, depending if there are already some
874 items in the current group. The space of time between the
875 necessary selects and insert introduces a race condition.
876 Having unique constraints on your position/group columns,
877 and using transactions (see L<DBIx::Class::Storage/txn_do>)
878 will prevent such race conditions going undetected.
880 =head2 Multiple Moves
882 If you have multiple same-group result objects already loaded from storage,
883 you need to be careful when executing C<move_*> operations on them:
884 without a L</position_column> reload the L</_position_value> of the
885 "siblings" will be out of sync with the underlying storage.
887 Starting from version C<0.082800> DBIC will implicitly perform such
888 reloads when the C<move_*> happens as a part of a transaction
889 (a good example of such situation is C<< $ordered_resultset->delete_all >>).
891 If it is not possible for you to wrap the entire call-chain in a transaction,
892 you will need to call L<DBIx::Class::Row/discard_changes> to get an object
893 up-to-date before proceeding, otherwise undefined behavior will result.
895 =head2 Default Values
897 Using a database defined default_value on one of your group columns
898 could result in the position not being assigned correctly.
900 =head1 FURTHER QUESTIONS?
902 Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
904 =head1 COPYRIGHT AND LICENSE
906 This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
907 by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
908 redistribute it and/or modify it under the same terms as the
909 L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.