Added a TODO for deploy().
[dbsrgits/DBIx-Class.git] / lib / DBIx / Class / Positional.pm
CommitLineData
118e6b96 1# vim: ts=8:sw=4:sts=4:et
1e3ff014 2package DBIx::Class::Positional;
118e6b96 3use strict;
4use warnings;
5use base qw( DBIx::Class );
6
7=head1 NAME
8
1e3ff014 9DBIx::Class::Positional - Modify the position of objects in an ordered list.
118e6b96 10
11=head1 SYNOPSIS
12
13Create a table for your positionable data.
14
15 CREATE TABLE employees (
16 employee_id INTEGER PRIMARY KEY AUTOINCREMENT,
17 name TEXT NOT NULL,
18 position INTEGER NOT NULL
19 );
169bb185 20 # Optional: group_id INTEGER NOT NULL
118e6b96 21
1e3ff014 22In your Schema or DB class add Positional to the top
118e6b96 23of the component list.
24
1e3ff014 25 __PACKAGE__->load_components(qw( Positional ... ));
118e6b96 26
27Specify the column that stores the position number for
28each row.
29
30 package My::Employee;
31 __PACKAGE__->position_column('position');
169bb185 32 __PACKAGE__->collection_column('group_id'); # optional
118e6b96 33
34Thats it, now you can change the position of your objects.
35
36 #!/use/bin/perl
37 use My::Employee;
38
39 my $employee = My::Employee->create({ name=>'Matt S. Trout' });
169bb185 40 # If using collection_column:
41 my $employee = My::Employee->create({ name=>'Matt S. Trout', group_id=>1 });
118e6b96 42
43 my $rs = $employee->siblings();
44 my @siblings = $employee->siblings();
45
46 my $sibling;
47 $sibling = $employee->first_sibling();
48 $sibling = $employee->last_sibling();
49 $sibling = $employee->previous_sibling();
50 $sibling = $employee->next_sibling();
51
52 $employee->move_previous();
53 $employee->move_next();
54 $employee->move_first();
55 $employee->move_last();
56 $employee->move_to( $position );
57
58=head1 DESCRIPTION
59
60This module provides a simple interface for modifying the position
61of DBIx::Class objects.
62
133dd22a 63=head1 AUTO UPDATE
64
65All of the move_* methods automatically update the rows involved in
66the query. This is not configurable and is due to the fact that if you
67move a record it always causes other records in the list to be updated.
68
118e6b96 69=head1 METHODS
70
71=head2 position_column
72
73 __PACKAGE__->position_column('position');
74
75Sets and retrieves the name of the column that stores the
76positional value of each record. Default to "position".
77
78=cut
79
80__PACKAGE__->mk_classdata( 'position_column' => 'position' );
81
133dd22a 82=head2 collection_column
83
84 __PACKAGE__->collection_column('thing_id');
85
86This method specified a column to limit all queries in
87this module by. This effectively allows you to have multiple
1e3ff014 88positional lists within the same table.
133dd22a 89
90=cut
91
92__PACKAGE__->mk_classdata( 'collection_column' );
93
118e6b96 94=head2 siblings
95
96 my $rs = $employee->siblings();
97 my @siblings = $employee->siblings();
98
99Returns either a result set or an array of all other objects
100excluding the one you called it on.
101
102=cut
103
104sub siblings {
105 my( $self ) = @_;
106 my $position_column = $self->position_column;
a9cdbec2 107 my $rs = $self->result_source->resultset->search(
7a76f44c 108 {
109 $position_column => { '!=' => $self->get_column($position_column) },
133dd22a 110 $self->_collection_clause(),
7a76f44c 111 },
118e6b96 112 { order_by => $self->position_column },
113 );
7a76f44c 114 return $rs->all() if (wantarray());
115 return $rs;
118e6b96 116}
117
118=head2 first_sibling
119
120 my $sibling = $employee->first_sibling();
121
5faa95af 122Returns the first sibling object, or 0 if the first sibling
123is this sibliing.
118e6b96 124
125=cut
126
127sub first_sibling {
128 my( $self ) = @_;
5faa95af 129 return 0 if ($self->get_column($self->position_column())==1);
a9cdbec2 130 return ($self->result_source->resultset->search(
131 {
132 $self->position_column => 1,
133 $self->_collection_clause(),
134 },
118e6b96 135 )->all())[0];
136}
137
138=head2 last_sibling
139
140 my $sibling = $employee->last_sibling();
141
5faa95af 142Return the last sibling, or 0 if the last sibling is this
143sibling.
118e6b96 144
145=cut
146
147sub last_sibling {
148 my( $self ) = @_;
a9cdbec2 149 my $count = $self->result_source->resultset->search({$self->_collection_clause()})->count();
5faa95af 150 return 0 if ($self->get_column($self->position_column())==$count);
a9cdbec2 151 return ($self->result_source->resultset->search(
152 {
153 $self->position_column => $count,
154 $self->_collection_clause(),
155 },
118e6b96 156 )->all())[0];
157}
158
159=head2 previous_sibling
160
161 my $sibling = $employee->previous_sibling();
162
163Returns the sibling that resides one position higher. Undef
164is returned if the current object is the first one.
165
166=cut
167
168sub previous_sibling {
169 my( $self ) = @_;
170 my $position_column = $self->position_column;
707cbb2d 171 my $position = $self->get_column( $position_column );
172 return 0 if ($position==1);
3ffca97b 173 return ($self->result_source->resultset->search(
7a76f44c 174 {
707cbb2d 175 $position_column => $position - 1,
133dd22a 176 $self->_collection_clause(),
707cbb2d 177 }
118e6b96 178 )->all())[0];
179}
180
181=head2 next_sibling
182
183 my $sibling = $employee->next_sibling();
184
185Returns the sibling that resides one position lower. Undef
186is returned if the current object is the last one.
187
188=cut
189
190sub next_sibling {
191 my( $self ) = @_;
192 my $position_column = $self->position_column;
707cbb2d 193 my $position = $self->get_column( $position_column );
194 my $count = $self->result_source->resultset->search({$self->_collection_clause()})->count();
195 return 0 if ($position==$count);
133dd22a 196 return ($self->result_source->resultset->search(
7a76f44c 197 {
707cbb2d 198 $position_column => $position + 1,
133dd22a 199 $self->_collection_clause(),
7a76f44c 200 },
118e6b96 201 )->all())[0];
202}
203
80010e2b 204=head2 move_previous
118e6b96 205
80010e2b 206 $employee->move_previous();
118e6b96 207
80010e2b 208Swaps position with the sibling on position previous in the list.
2091 is returned on success, and 0 is returned if the objects is already
210the first one.
118e6b96 211
212=cut
213
80010e2b 214sub move_previous {
118e6b96 215 my( $self ) = @_;
133dd22a 216 my $position = $self->get_column( $self->position_column() );
217 return $self->move_to( $position - 1 );
118e6b96 218}
219
80010e2b 220=head2 move_next
118e6b96 221
80010e2b 222 $employee->move_next();
118e6b96 223
80010e2b 224Swaps position with the sibling in the next position. 1 is returned on
225success, and 0 is returned if the object is already the last in the list.
118e6b96 226
227=cut
228
80010e2b 229sub move_next {
118e6b96 230 my( $self ) = @_;
133dd22a 231 my $position = $self->get_column( $self->position_column() );
232 my $count = $self->result_source->resultset->search({$self->_collection_clause()})->count();
233 return 0 if ($position==$count);
234 return $self->move_to( $position + 1 );
118e6b96 235}
236
237=head2 move_first
238
239 $employee->move_first();
240
241Moves the object to the first position. 1 is returned on
242success, and 0 is returned if the object is already the first.
243
244=cut
245
246sub move_first {
247 my( $self ) = @_;
248 return $self->move_to( 1 );
249}
250
251=head2 move_last
252
253 $employee->move_last();
254
255Moves the object to the very last position. 1 is returned on
256success, and 0 is returned if the object is already the last one.
257
258=cut
259
260sub move_last {
261 my( $self ) = @_;
133dd22a 262 my $count = $self->result_source->resultset->search({$self->_collection_clause()})->count();
118e6b96 263 return $self->move_to( $count );
264}
265
266=head2 move_to
267
268 $employee->move_to( $position );
269
270Moves the object to the specified position. 1 is returned on
271success, and 0 is returned if the object is already at the
272specified position.
273
274=cut
275
276sub move_to {
277 my( $self, $to_position ) = @_;
278 my $position_column = $self->position_column;
279 my $from_position = $self->get_column( $position_column );
133dd22a 280 return 0 if ( $to_position < 1 );
281 return 0 if ( $from_position==$to_position );
282 my $rs = $self->result_source->resultset->search({
118e6b96 283 -and => [
284 $position_column => { ($from_position>$to_position?'<':'>') => $from_position },
285 $position_column => { ($from_position>$to_position?'>=':'<=') => $to_position },
7a76f44c 286 ],
133dd22a 287 $self->_collection_clause(),
118e6b96 288 });
289 my $op = ($from_position>$to_position) ? '+' : '-';
290 $rs->update({
291 $position_column => \"$position_column $op 1",
292 });
293 $self->set_column( $position_column => $to_position );
294 $self->update();
295 return 1;
296}
297
298=head2 insert
299
300Overrides the DBIC insert() method by providing a default
301position number. The default will be the number of rows in
302the table +1, thus positioning the new record at the last position.
303
304=cut
305
306sub insert {
307 my $self = shift;
308 my $position_column = $self->position_column;
133dd22a 309 $self->set_column( $position_column => $self->result_source->resultset->search( {$self->_collection_clause()} )->count()+1 )
118e6b96 310 if (!$self->get_column($position_column));
0a298c73 311 return $self->next::method( @_ );
118e6b96 312}
313
314=head2 delete
315
316Overrides the DBIC delete() method by first moving the object
317to the last position, then deleting it, thus ensuring the
318integrity of the positions.
319
320=cut
321
322sub delete {
323 my $self = shift;
324 $self->move_last;
0a298c73 325 return $self->next::method( @_ );
118e6b96 326}
327
7a76f44c 328=head1 PRIVATE METHODS
329
330These methods are used internally. You should never have the
331need to use them.
332
133dd22a 333=head2 _collection_clause
118e6b96 334
133dd22a 335This method returns a name=>value pare for limiting a search
336by the collection column. If the collection column is not
337defined then this will return an empty list.
118e6b96 338
7a76f44c 339=cut
340
133dd22a 341sub _collection_clause {
169bb185 342 my( $self ) = @_;
133dd22a 343 if ($self->collection_column()) {
344 return ( $self->collection_column() => $self->get_column($self->collection_column()) );
345 }
7a76f44c 346 return ();
347}
348
3491;
350__END__
118e6b96 351
352=head1 BUGS
353
133dd22a 354=head2 Race Condition on Insert
355
118e6b96 356If a position is not specified for an insert than a position
357will be chosen based on COUNT(*)+1. But, it first selects the
358count then inserts the record. The space of time between select
359and insert introduces a race condition. To fix this we need the
360ability to lock tables in DBIC. I've added an entry in the TODO
361about this.
362
133dd22a 363=head2 Multiple Moves
364
365Be careful when issueing move_* methods to multiple objects. If
366you've pre-loaded the objects then when you move one of the objects
367the position of the other object will not reflect their new value
368until you reload them from the database.
369
370The are times when you will want to move objects as groups, such
371as changeing the parent of several objects at once - this directly
372conflicts with this problem. One solution is for us to write a
373ResultSet class that supports a parent() method, for example. Another
374solution is to somehow automagically modify the objects that exist
375in the current object's result set to have the new position value.
376
118e6b96 377=head1 AUTHOR
378
379Aran Deltac <bluefeet@cpan.org>
380
381=head1 LICENSE
382
383You may distribute this code under the same terms as Perl itself.
384