In my experience, really great software is so consistent that when you're trying to do something you've never done before, you can guess at how it's done and at least half of the time,
it just works. For the most part, Ruby on Rails is like that. There are best practices baked into the framework for processes I never even realized you could have best practices for, like the unpleasant but necessary task of
writing upgrade scripts. Through Rails, I've learned CS concepts like
optimistic locking that were entirely new to me, and used them effectively after an hour's study and a dozen lines of code.
So it's disappointing and almost a little surprising when I encounter a feature of the Rails framework that doesn't just work automatically.
acts_as_list
is such a feature, requiring days of reading source files and experimenting with logfile to figure out the series of magical incantations needed to make the darn thing work.
The theory behind
acts_as_list
is pretty simple. You add an integer
position
column to a database table, and add
acts_as_list
to the model class it maps to. At that point, anytime you use the object in a collection, the position column gets set with the sort order of that object relative to everything else in the collection. List order is even scoped: a child table can be ordered within the context of its parent table by adding
:scope => :parent_model_name
to the model declaration.
In practice, however, there are some problems I ran into which I wasn't expecting. Some of them are well documented in
Agile Web Development with Rails, but some of them required a great deal of research to solve.
List items appear in order of record creation, not in order of :position
If an ImageSet has_many TitledImages, and TitledImage acts_as_list over the scope of an ImageSet, you'd expect ImageSet.titled_images to return a collection in the order that's set in the
position
column, right? This won't happen unless you modify the parent class definition (
ImageSet
, in this case) to specify an order column on your
has_many
declaration:
has_many :titled_images, :order => :position
Pagination loses list orderHaving fixed this problem, if you page through a list of items, you'll discover that the items once again appear in order of record creation, ignoring the value set in
position
. Fixing this requires you to manually specify the order for paged items using the
:order
option to
paginate
:
@image_pages, @titled_images = paginate(:titled_image,
{:per_page => 10,
:conditions => conditions,
:order => 'position' })
Adjusting list order doesn't update objects in memoryOkay, this one took me the most time to figure out.
acts_as_list
has nothing to do with the order of your collection. Using array operators to move elements around in the collection returned by
ImageSet.titled_image
does absolutely nothing to the
position
column.
Worse yet, using the
acts_as_list
position modifiers like
insert_at
will not affect the objects in memory. So if you've been working with a collection, then call an
acts_as_list
method that affects its position,
saving the elements that of collection will overwrite the position with old values. The position manipulation operators are designed to minimize SQL statements executed: among other side-effects, they circumvent optimistic locking. You must reload your collections after doing any list manipulation.
Moving list items from one parent object to another doesn't reorder their positions
Because
acts_as_list
pays little attention to order within a collection, removing an item from one parent and adding it to another requires explicit updates to the
position
attribute. You should probably use
remove_from_list
to zero out the position before you transfer items from one parent to another, but since this reshuffles position columns for all other items in the list, I'd be cautious about doing this within a loop. Since I was collating items from two different lists into a third, I just manually set the position:
0.upto(max_size-1) do |i|
# append the left element here
if i < left_size
new_set.titled_images << left_set.titled_images[i]
end
# append the right element
if i < right_size
new_set.titled_images << right_set.titled_images[i]
end
end
# this has no effect on acts as list unless I do it manually
1.upto(new_set.titled_images.size) do |i|
new_set.titled_images[i-1].position=i
end
In my opinion,
acts_as_list
is still worth using — in fact, I'm about to work with its reordering functionality a lot. But I won't be surprised if I find myself experimenting with logfiles again.