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 =head2 position_column
63 __PACKAGE__->position_column('position');
65 Sets and retrieves the name of the column that stores the
66 positional value of each record. Default to "position".
70 __PACKAGE__->mk_classdata( 'position_column' => 'position' );
74 my $rs = $employee->siblings();
75 my @siblings = $employee->siblings();
77 Returns either a result set or an array of all other objects
78 excluding the one you called it on.
84 my $position_column = $self->position_column;
85 my $rs = $self->search(
87 $position_column => { '!=' => $self->get_column($position_column) },
88 $self->_parent_clause(),
90 { order_by => $self->position_column },
92 return $rs->all() if (wantarray());
98 my $sibling = $employee->first_sibling();
100 Returns the first sibling object.
106 return ($self->search(
107 { $self->_parent_clause() },
108 { rows=>1, order_by => $self->position_column },
114 my $sibling = $employee->last_sibling();
116 Return the last sibling.
122 return ($self->search(
123 { $self->_parent_clause() },
124 { rows=>1, order_by => $self->position_column.' DESC' },
128 =head2 previous_sibling
130 my $sibling = $employee->previous_sibling();
132 Returns the sibling that resides one position higher. Undef
133 is returned if the current object is the first one.
137 sub previous_sibling {
139 my $position_column = $self->position_column;
140 return ($self->search(
142 $position_column => { '<' => $self->get_column($position_column) },
143 $self->_parent_clause(),
145 { rows=>1, order_by => $position_column.' DESC' },
151 my $sibling = $employee->next_sibling();
153 Returns the sibling that resides one position lower. Undef
154 is returned if the current object is the last one.
160 my $position_column = $self->position_column;
161 return ($self->search(
163 $position_column => { '>' => $self->get_column($position_column) },
164 $self->_parent_clause(),
166 { rows=>1, order_by => $position_column },
172 $employee->move_previous();
174 Swaps position with the sibling on position previous in the list.
175 1 is returned on success, and 0 is returned if the objects is already
182 my $previous = $self->previous_sibling();
183 return undef if (!$previous);
184 my $position_column = $self->position_column;
185 my $self_position = $self->get_column( $position_column );
186 $self->set_column( $position_column, $previous->get_column($position_column) );
187 $previous->set_column( $position_column, $self_position );
195 $employee->move_next();
197 Swaps position with the sibling in the next position. 1 is returned on
198 success, and 0 is returned if the object is already the last in the list.
204 my $next = $self->next_sibling();
205 return undef if (!$next);
206 my $position_column = $self->position_column;
207 my $self_position = $self->get_column( $position_column );
208 $self->set_column( $position_column, $next->get_column($position_column) );
209 $next->set_column( $position_column, $self_position );
217 $employee->move_first();
219 Moves the object to the first position. 1 is returned on
220 success, and 0 is returned if the object is already the first.
226 return $self->move_to( 1 );
231 $employee->move_last();
233 Moves the object to the very last position. 1 is returned on
234 success, and 0 is returned if the object is already the last one.
240 my $count = $self->search({$self->_parent_clause()})->count();
241 return $self->move_to( $count );
246 $employee->move_to( $position );
248 Moves the object to the specified position. 1 is returned on
249 success, and 0 is returned if the object is already at the
255 my( $self, $to_position ) = @_;
256 my $position_column = $self->position_column;
257 my $from_position = $self->get_column( $position_column );
258 return undef if ( $from_position==$to_position );
259 my $rs = $self->search({
261 $position_column => { ($from_position>$to_position?'<':'>') => $from_position },
262 $position_column => { ($from_position>$to_position?'>=':'<=') => $to_position },
264 $self->_parent_clause(),
266 my $op = ($from_position>$to_position) ? '+' : '-';
268 $position_column => \"$position_column $op 1",
270 $self->set_column( $position_column => $to_position );
277 Overrides the DBIC insert() method by providing a default
278 position number. The default will be the number of rows in
279 the table +1, thus positioning the new record at the last position.
285 my $position_column = $self->position_column;
286 $self->set_column( $position_column => $self->search( {$self->_parent_clause()} )->count()+1 )
287 if (!$self->get_column($position_column));
288 $self->next::method( @_ );
293 Overrides the DBIC delete() method by first moving the object
294 to the last position, then deleting it, thus ensuring the
295 integrity of the positions.
302 $self->next::method( @_ );
305 =head1 PRIVATE METHODS
307 These methods are used internally. You should never have the
310 =head2 _parent_clause
314 return ( parent_id => $self->parent_id );
317 This method is a placeholder for you, or another component, to
318 provide additional limits for all the various queries in this
319 module. This allows for more than one positionable list within
320 the same table since any move_* method will adhere to the clause
334 If a position is not specified for an insert than a position
335 will be chosen based on COUNT(*)+1. But, it first selects the
336 count then inserts the record. The space of time between select
337 and insert introduces a race condition. To fix this we need the
338 ability to lock tables in DBIC. I've added an entry in the TODO
343 Aran Deltac <bluefeet@cpan.org>
347 You may distribute this code under the same terms as Perl itself.