An acts_as_sortable/acts_as_list replacement built for Rails 4+
An acts_as_sortable/acts_as_list replacement built for Rails 4+
As maintainer of both Acts As List and the Ranked Model gems, I've become intimately aquainted with the strengths and weaknesses of each. I ended up writing a small scale Rails Concern for positioning database rows for a recent project and it worked really well so I've decided to release it as a gem: Positioning
Positioning works similarly to Acts As List in that it maintains a sequential list of integer values as positions. It differs in that it encourages a unique constraints on the position column and supports multiple lists per database table. It borrows Ranked Model's concept of relative positioning. I encourage you to check it out and give it a whirl on your project!
ranked-model passes specs with Rails 4.2, 5.0, 5.1, 5.2, 6.0 and 6.1 for MySQL, Postgres, and SQLite on Ruby 2.4 through 3.0 (with exceptions, please check the CI setup for supported combinations), and jruby-9.1.17.0 where Rails supports the platform.
To install ranked-model, just add it to your Gemfile:
gem 'ranked-model'
# Or pin ranked-model to git
# gem 'ranked-model',
# git: 'git@github.com:mixonic/ranked-model.git'Then use bundle install to update your Gemfile.lock.
Use of ranked-model is straight ahead. Get some ducks:
class Duck 0
Duck.rank(:row_order).third.row_order_rank # => 2The ranks method takes several arguments:
class Duck 0
truck.row_order
=> 0In this example, the row_order for both car and truck will be set to 0 because they have
different class names (Car and Truck, respectively).
If you would like for both car and truck to be ranked together based on the base Vehicle
class instead, use the class_name option:
class Vehicle 0
truck.row_order
=> 4194304If you use ranked_model with existing data, the following migration (for Rails
6) can be a starting point. Make sure to declare include RankedModel and
ranks :row_order in your Duck before running the migration.
rails g migration AddRowOrderToDucks row_order:integerThen, adjust the migration:
# e.g. file db/migrate/20200325095038_add_row_order_to_ducks.rb
class AddRowOrderToDucks < ActiveRecord::Migration[6.0]
def change
add_column :ducks, :row_order, :integer
# Newest Duck shall rank "highest"" (be last).
Duck.update_all('row_order = EXTRACT(EPOCH FROM created_at)')
# Alternatively, implement any other sorting default
# Duck.order(created_at: :desc).each do |duck|
# duck.update!(row_order: duck.created_at.to_i + duck.age / 2)
# end
end
endThis library is written using ARel from the ground-up. This leaves the code much cleaner than many implementations. ranked-model is also optimized to write to the database as little as possible: ranks are stored as a number between -2147483648 and 2147483647 (the INT range in MySQL). When an item is given a new position, it assigns itself a rank number between two neighbors. This allows several movements of items before no digits are available between two neighbors. When this occurs, ranked-model will try to shift other records out of the way. If items can't be easily shifted anymore, it will rebalance the distribution of rank numbers across all members of the ranked group.
Record updates to rebalance ranks do not trigger ActiveRecord callbacks. If you need to react to these updates
(to index them in a secondary data store, for example), you can subscribe to the ranked_model.ranks_updated
ActiveSupport notification.
Subscribed consumers receive an event for each rearrangement or rebalancing, the payload of which includes the
triggering instance and the scope and with_same options for the ranking, which can be used to retrieve the
affected records.
ActiveSupport::Notifications.subscribe("ranked_model.ranks_updated") do |_name, _start, _finish, _id, payload|
# payload[:instance] - the instance whose update triggered the rebalance
# payload[:scope] - the scope applied to the ranking
# payload[:with_same] - the with_same option applied to the ranking
endFork, clone, write a test, write some code, commit, push, send a pull request. Github FTW!
The code is published under the MIT License.
The specs can be run with sqlite, postgres, and mysql:
bundle
appraisal install
DB=postgresql bundle exec appraisal rakeIf no DB is specified (sqlite, mysql, or postgresql), the tests run against sqlite.
RankedModel is mostly the handiwork of Matthew Beale:
A hearty thanks to these contributors:
No open issues yet, or sync has not completed.