1 # vim: ts=8:sw=4:sts=4:et
2 package DBIx::Class::Ordered;
5 use base qw( DBIx::Class );
9 DBIx::Class::Ordered - Modify the position of objects in an ordered list.
13 Create a table for your ordered data.
16 item_id INTEGER PRIMARY KEY AUTOINCREMENT,
18 position INTEGER NOT NULL
20 # Optional: group_id INTEGER NOT NULL
22 In your Schema or DB class add Ordered to the top
23 of the component list.
25 __PACKAGE__->load_components(qw( Ordered ... ));
27 Specify the column that stores the position number for
31 __PACKAGE__->position_column('position');
32 __PACKAGE__->grouping_column('group_id'); # optional
34 Thats it, now you can change the position of your objects.
39 my $item = My::Item->create({ name=>'Matt S. Trout' });
40 # If using grouping_column:
41 my $item = My::Item->create({ name=>'Matt S. Trout', group_id=>1 });
43 my $rs = $item->siblings();
44 my @siblings = $item->siblings();
47 $sibling = $item->first_sibling();
48 $sibling = $item->last_sibling();
49 $sibling = $item->previous_sibling();
50 $sibling = $item->next_sibling();
52 $item->move_previous();
56 $item->move_to( $position );
60 This module provides a simple interface for modifying the ordered
61 position of DBIx::Class objects.
65 All of the move_* methods automatically update the rows involved in
66 the query. This is not configurable and is due to the fact that if you
67 move a record it always causes other records in the list to be updated.
71 =head2 position_column
73 __PACKAGE__->position_column('position');
75 Sets and retrieves the name of the column that stores the
76 positional value of each record. Default to "position".
80 __PACKAGE__->mk_classdata( 'position_column' => 'position' );
82 =head2 grouping_column
84 __PACKAGE__->grouping_column('group_id');
86 This method specified a column to limit all queries in
87 this module by. This effectively allows you to have multiple
88 ordered lists within the same table.
92 __PACKAGE__->mk_classdata( 'grouping_column' );
96 my $rs = $item->siblings();
97 my @siblings = $item->siblings();
99 Returns either a result set or an array of all other objects
100 excluding the one you called it on.
106 my $position_column = $self->position_column;
107 my $rs = $self->result_source->resultset->search(
109 $position_column => { '!=' => $self->get_column($position_column) },
110 $self->_grouping_clause(),
112 { order_by => $self->position_column },
114 return $rs->all() if (wantarray());
120 my $sibling = $item->first_sibling();
122 Returns the first sibling object, or 0 if the first sibling
129 return 0 if ($self->get_column($self->position_column())==1);
131 return ($self->result_source->resultset->search(
133 $self->position_column => 1,
134 $self->_grouping_clause(),
141 my $sibling = $item->last_sibling();
143 Return the last sibling, or 0 if the last sibling is this
150 my $count = $self->result_source->resultset->search({$self->_grouping_clause()})->count();
151 return 0 if ($self->get_column($self->position_column())==$count);
152 return ($self->result_source->resultset->search(
154 $self->position_column => $count,
155 $self->_grouping_clause(),
160 =head2 previous_sibling
162 my $sibling = $item->previous_sibling();
164 Returns the sibling that resides one position back. Undef
165 is returned if the current object is the first one.
169 sub previous_sibling {
171 my $position_column = $self->position_column;
172 my $position = $self->get_column( $position_column );
173 return 0 if ($position==1);
174 return ($self->result_source->resultset->search(
176 $position_column => $position - 1,
177 $self->_grouping_clause(),
184 my $sibling = $item->next_sibling();
186 Returns the sibling that resides one position foward. Undef
187 is returned if the current object is the last one.
193 my $position_column = $self->position_column;
194 my $position = $self->get_column( $position_column );
195 my $count = $self->result_source->resultset->search({$self->_grouping_clause()})->count();
196 return 0 if ($position==$count);
197 return ($self->result_source->resultset->search(
199 $position_column => $position + 1,
200 $self->_grouping_clause(),
207 $item->move_previous();
209 Swaps position with the sibling on position previous in the list.
210 1 is returned on success, and 0 is returned if the objects is already
217 my $position = $self->get_column( $self->position_column() );
218 return $self->move_to( $position - 1 );
225 Swaps position with the sibling in the next position. 1 is returned on
226 success, and 0 is returned if the object is already the last in the list.
232 my $position = $self->get_column( $self->position_column() );
233 my $count = $self->result_source->resultset->search({$self->_grouping_clause()})->count();
234 return 0 if ($position==$count);
235 return $self->move_to( $position + 1 );
242 Moves the object to the first position. 1 is returned on
243 success, and 0 is returned if the object is already the first.
249 return $self->move_to( 1 );
256 Moves the object to the very last position. 1 is returned on
257 success, and 0 is returned if the object is already the last one.
263 my $count = $self->result_source->resultset->search({$self->_grouping_clause()})->count();
264 return $self->move_to( $count );
269 $item->move_to( $position );
271 Moves the object to the specified position. 1 is returned on
272 success, and 0 is returned if the object is already at the
278 my( $self, $to_position ) = @_;
279 my $position_column = $self->position_column;
280 my $from_position = $self->get_column( $position_column );
281 return 0 if ( $to_position < 1 );
282 return 0 if ( $from_position==$to_position );
284 ( $from_position < $to_position )
285 ? ( $from_position+1, $to_position )
286 : ( $to_position, $from_position-1 )
288 my $rs = $self->result_source->resultset->search({
289 $position_column => { -between => [ @between ] },
290 $self->_grouping_clause(),
292 my $op = ($from_position>$to_position) ? '+' : '-';
293 $rs->update({ $position_column => \"$position_column $op 1" }); #" Sorry, GEdit bug
294 $self->{_ORDERED_INTERNAL_UPDATE} = 1;
295 $self->update({ $position_column => $to_position });
303 $item->move_to_group( $group, $position );
305 Moves the object to the specified position of the specified
306 group, or to the end of the group if $position is undef.
307 1 is returned on success, and 0 is returned if the object is
308 already at the specified position of the specified group.
310 $group should be supplied as a hashref of column => value pairs,
311 e.g. if the grouping columns were 'user' and 'list',
312 { user => 'fred', list => 'work' }.
317 my( $self, $to_group, $to_position ) = @_;
319 # if we're given a string, turn it into a hashref
320 unless (ref $to_group eq 'HASH') {
321 $to_group = {($self->_grouping_columns)[0] => $to_group};
324 my $position_column = $self->position_column;
325 #my @grouping_columns = $self->_grouping_columns;
327 return 0 if ( ! defined($to_group) );
328 return 0 if ( defined($to_position) and $to_position < 1 );
329 return 0 if ( $self->_is_in_group($to_group)
330 and ((not defined($to_position))
331 or (defined($to_position) and $self->$position_column==$to_position)
335 # Move to end of current group and adjust siblings
338 $self->set_columns($to_group);
339 my $new_group_count = $self->result_source->resultset->search({$self->_grouping_clause()})->count();
340 if (!defined($to_position) or $to_position > $new_group_count) {
341 $self->{_ORDERED_INTERNAL_UPDATE} = 1;
342 $self->update({ $position_column => $new_group_count + 1 });
345 my @between = ($to_position, $new_group_count);
347 my $rs = $self->result_source->resultset->search({
348 $position_column => { -between => [ @between ] },
349 $self->_grouping_clause(),
351 $rs->update({ $position_column => \"$position_column + 1" }); #"
352 $self->{_ORDERED_INTERNAL_UPDATE} = 1;
353 $self->update({ $position_column => $to_position });
361 Overrides the DBIC insert() method by providing a default
362 position number. The default will be the number of rows in
363 the table +1, thus positioning the new record at the last position.
369 my $position_column = $self->position_column;
370 $self->set_column( $position_column => $self->result_source->resultset->search( {$self->_grouping_clause()} )->count()+1 )
371 if (!$self->get_column($position_column));
372 return $self->next::method( @_ );
377 Overrides the DBIC update() method by checking for a change
378 to the position and/or group columns. Movement within a
379 group or to another group is handled by repositioning
380 the appropriate siblings. Position defaults to the end
381 of a new group if it has been changed to undef.
388 if ($self->{_ORDERED_INTERNAL_UPDATE}) {
389 delete $self->{_ORDERED_INTERNAL_UPDATE};
390 return $self->next::method( @_ );
393 $self->set_columns($_[0]) if @_ > 0;
394 my %changes = $self->get_dirty_columns;
395 $self->discard_changes;
397 my $pos_col = $self->position_column;
399 # is there a chance in hell of this working?
400 # if any of our grouping columns have been changed
402 if (grep {$_} map {exists $changes{$_}} $self->_grouping_columns ) {
404 # create new_group by taking the current group and inserting changes
405 my $new_group = {$self->_grouping_clause};
406 foreach my $col (keys %$new_group) {
407 if (exists $changes{$col}) {
408 $new_group->{$col} = $changes{$col};
409 delete $changes{$col}; # don't want to pass this on to next::method
413 $self->move_to_group(
415 exists($changes{$pos_col}) ? delete($changes{$pos_col}) : $self->$pos_col
418 elsif (exists $changes{$pos_col}) {
419 $self->move_to(delete $changes{$pos_col});
421 return $self->next::method( \%changes );
426 Overrides the DBIC delete() method by first moving the object
427 to the last position, then deleting it, thus ensuring the
428 integrity of the positions.
435 return $self->next::method( @_ );
438 =head1 PRIVATE METHODS
440 These methods are used internally. You should never have the
443 =head2 _grouping_clause
445 This method returns one or more name=>value pairs for limiting a search
446 by the grouping column(s). If the grouping column is not
447 defined then this will return an empty list.
450 sub _grouping_clause {
452 return map { $_ => $self->get_column($_) } $self->_grouping_columns();
457 =head2 _get_grouping_columns
459 Returns a list of the column names used for grouping, regardless of whether
460 they were specified as an arrayref or a single string, and even returns ()
461 if we're not grouping.
464 sub _grouping_columns {
466 my $col = $self->grouping_column();
467 if (ref $col eq 'ARRAY') {
478 =head2 _is_in_group($other)
480 $item->_is_in_group( {user => 'fred', list => 'work'} )
482 Returns true if the object is in the group represented by hashref $other
485 my ($self, $other) = @_;
486 my $current = {$self->_grouping_clause};
487 return 0 unless (ref $other eq 'HASH') and (keys %$current == keys %$other);
488 for my $key (keys %$current) {
489 return 0 unless exists $other->{$key};
490 return 0 if $current->{$key} ne $other->{$key};
503 =head2 Unique Constraints
505 Unique indexes and constraints on the position column are not
506 supported at this time. It would be make sense to support them,
507 but there are some unexpected database issues that make this
508 hard to do. The main problem from the author's view is that
509 SQLite (the DB engine that we use for testing) does not support
512 =head2 Race Condition on Insert
514 If a position is not specified for an insert than a position
515 will be chosen based on COUNT(*)+1. But, it first selects the
516 count then inserts the record. The space of time between select
517 and insert introduces a race condition. To fix this we need the
518 ability to lock tables in DBIC. I've added an entry in the TODO
521 =head2 Multiple Moves
523 Be careful when issueing move_* methods to multiple objects. If
524 you've pre-loaded the objects then when you move one of the objects
525 the position of the other object will not reflect their new value
526 until you reload them from the database.
528 There are times when you will want to move objects as groups, such
529 as changeing the parent of several objects at once - this directly
530 conflicts with this problem. One solution is for us to write a
531 ResultSet class that supports a parent() method, for example. Another
532 solution is to somehow automagically modify the objects that exist
533 in the current object's result set to have the new position value.
537 Aran Deltac <bluefeet@cpan.org>
541 You may distribute this code under the same terms as Perl itself.