1 # vim: ts=8:sw=4:sts=4:et
2 package DBIx::Class::Positioned;
5 use base qw( DBIx::Class );
9 DBIx::Class::Positioned - Modify the position of objects in an ordered list.
13 Create a table for your positionable data.
15 CREATE TABLE employees (
16 employee_id INTEGER PRIMARY KEY AUTOINCREMENT,
18 position INTEGER NOT NULL
21 In your Schema or DB class add Positioned to the top
22 of the component list.
24 __PACKAGE__->load_components(qw( Positioned ... ));
26 Specify the column that stores the position number for
30 __PACKAGE__->position_column('position');
32 Thats it, now you can change the position of your objects.
37 my $employee = My::Employee->create({ name=>'Matt S. Trout' });
39 my $rs = $employee->siblings();
40 my @siblings = $employee->siblings();
43 $sibling = $employee->first_sibling();
44 $sibling = $employee->last_sibling();
45 $sibling = $employee->previous_sibling();
46 $sibling = $employee->next_sibling();
48 $employee->move_previous();
49 $employee->move_next();
50 $employee->move_first();
51 $employee->move_last();
52 $employee->move_to( $position );
56 This module provides a simple interface for modifying the position
57 of DBIx::Class objects.
61 All of the move_* methods automatically update the rows involved in
62 the query. This is not configurable and is due to the fact that if you
63 move a record it always causes other records in the list to be updated.
67 =head2 position_column
69 __PACKAGE__->position_column('position');
71 Sets and retrieves the name of the column that stores the
72 positional value of each record. Default to "position".
76 __PACKAGE__->mk_classdata( 'position_column' => 'position' );
78 =head2 collection_column
80 __PACKAGE__->collection_column('thing_id');
82 This method specified a column to limit all queries in
83 this module by. This effectively allows you to have multiple
84 positioned lists within the same table.
88 __PACKAGE__->mk_classdata( 'collection_column' );
92 my $rs = $employee->siblings();
93 my @siblings = $employee->siblings();
95 Returns either a result set or an array of all other objects
96 excluding the one you called it on.
102 my $position_column = $self->position_column;
103 my $rs = $self->result_source->resultset->search(
105 $position_column => { '!=' => $self->get_column($position_column) },
106 $self->_collection_clause(),
108 { order_by => $self->position_column },
110 return $rs->all() if (wantarray());
116 my $sibling = $employee->first_sibling();
118 Returns the first sibling object.
124 return ($self->result_source->resultset->search(
126 $self->position_column => 1,
127 $self->_collection_clause(),
134 my $sibling = $employee->last_sibling();
136 Return the last sibling.
142 my $count = $self->result_source->resultset->search({$self->_collection_clause()})->count();
143 return ($self->result_source->resultset->search(
145 $self->position_column => $count,
146 $self->_collection_clause(),
151 =head2 previous_sibling
153 my $sibling = $employee->previous_sibling();
155 Returns the sibling that resides one position higher. Undef
156 is returned if the current object is the first one.
160 sub previous_sibling {
162 my $position_column = $self->position_column;
163 my $position = $self->get_column( $position_column );
164 return 0 if ($position==1);
165 return ($self->result_source->resultset->search(
167 $position_column => $position - 1,
168 $self->_collection_clause(),
175 my $sibling = $employee->next_sibling();
177 Returns the sibling that resides one position lower. Undef
178 is returned if the current object is the last one.
184 my $position_column = $self->position_column;
185 my $position = $self->get_column( $position_column );
186 my $count = $self->result_source->resultset->search({$self->_collection_clause()})->count();
187 return 0 if ($position==$count);
188 return ($self->result_source->resultset->search(
190 $position_column => $position + 1,
191 $self->_collection_clause(),
198 $employee->move_previous();
200 Swaps position with the sibling on position previous in the list.
201 1 is returned on success, and 0 is returned if the objects is already
208 my $position = $self->get_column( $self->position_column() );
209 return $self->move_to( $position - 1 );
214 $employee->move_next();
216 Swaps position with the sibling in the next position. 1 is returned on
217 success, and 0 is returned if the object is already the last in the list.
223 my $position = $self->get_column( $self->position_column() );
224 my $count = $self->result_source->resultset->search({$self->_collection_clause()})->count();
225 return 0 if ($position==$count);
226 return $self->move_to( $position + 1 );
231 $employee->move_first();
233 Moves the object to the first position. 1 is returned on
234 success, and 0 is returned if the object is already the first.
240 return $self->move_to( 1 );
245 $employee->move_last();
247 Moves the object to the very last position. 1 is returned on
248 success, and 0 is returned if the object is already the last one.
254 my $count = $self->result_source->resultset->search({$self->_collection_clause()})->count();
255 return $self->move_to( $count );
260 $employee->move_to( $position );
262 Moves the object to the specified position. 1 is returned on
263 success, and 0 is returned if the object is already at the
269 my( $self, $to_position ) = @_;
270 my $position_column = $self->position_column;
271 my $from_position = $self->get_column( $position_column );
272 return 0 if ( $to_position < 1 );
273 return 0 if ( $from_position==$to_position );
274 my $rs = $self->result_source->resultset->search({
276 $position_column => { ($from_position>$to_position?'<':'>') => $from_position },
277 $position_column => { ($from_position>$to_position?'>=':'<=') => $to_position },
279 $self->_collection_clause(),
281 my $op = ($from_position>$to_position) ? '+' : '-';
283 $position_column => \"$position_column $op 1",
285 $self->set_column( $position_column => $to_position );
292 Overrides the DBIC insert() method by providing a default
293 position number. The default will be the number of rows in
294 the table +1, thus positioning the new record at the last position.
300 my $position_column = $self->position_column;
301 $self->set_column( $position_column => $self->result_source->resultset->search( {$self->_collection_clause()} )->count()+1 )
302 if (!$self->get_column($position_column));
303 $self->next::method( @_ );
308 Overrides the DBIC delete() method by first moving the object
309 to the last position, then deleting it, thus ensuring the
310 integrity of the positions.
317 $self->next::method( @_ );
320 =head1 PRIVATE METHODS
322 These methods are used internally. You should never have the
325 =head2 _collection_clause
327 This method returns a name=>value pare for limiting a search
328 by the collection column. If the collection column is not
329 defined then this will return an empty list.
333 sub _collection_clause {
335 if ($self->collection_column()) {
336 return ( $self->collection_column() => $self->get_column($self->collection_column()) );
346 =head2 Race Condition on Insert
348 If a position is not specified for an insert than a position
349 will be chosen based on COUNT(*)+1. But, it first selects the
350 count then inserts the record. The space of time between select
351 and insert introduces a race condition. To fix this we need the
352 ability to lock tables in DBIC. I've added an entry in the TODO
355 =head2 Multiple Moves
357 Be careful when issueing move_* methods to multiple objects. If
358 you've pre-loaded the objects then when you move one of the objects
359 the position of the other object will not reflect their new value
360 until you reload them from the database.
362 The are times when you will want to move objects as groups, such
363 as changeing the parent of several objects at once - this directly
364 conflicts with this problem. One solution is for us to write a
365 ResultSet class that supports a parent() method, for example. Another
366 solution is to somehow automagically modify the objects that exist
367 in the current object's result set to have the new position value.
371 Aran Deltac <bluefeet@cpan.org>
375 You may distribute this code under the same terms as Perl itself.