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