acts_as_list

repository·master·Indexed 24 days ago

https://github.com/brendon/acts_as_list

An ActiveRecord extension for sorting, reordering, and managing objects in a sequential list using an integer position column. It provides methods for moving items (move_to_top, move_to_bottom, insert_at), querying neighbors, and managing scoped lists. The library includes configuration options for indexing, new item placement, and tools like acts_as_list_no_update to disable automatic reordering during bulk updates.

Tokens
2.8K
Snippets
5
Records
14
Agent score
75%

What's inside acts_as_list

  1. Ensure list data integrity

    master

    To prevent issues like repeated or incorrect position values caused by concurrency, do not rely solely on the gem. Instead, enforce data integrity at the database level:

    1. Database Constraints: Add unique constraints to your table. For example, if an Item belongs to an Order, add a unique constraint on [:order_id, :position] to ensure each item has a distinct position within its list.
    2. Mutexes/Locks: Use row-level locks or mutexes to handle contention, though these should complement rather than replace database constraints.
  2. Configure acts_as_list in an ActiveRecord model

    master
    To enable list functionality, your model must have a position column (integer) defined on its database table. You can then call acts_as_list within the model class. You can also specify a scope to partition the list (e.g., by an association or a plain field) and include fixed parameters for queries.
  3. Add acts_as_list to an existing model

    master

    If adding acts_as_list to a table that already contains data, you must populate the position column via a migration to ensure existing items have valid positions.

    Basic migration:

    class AddPositionToTodoItem < ActiveRecord::Migration
      def change
        add_column :todo_items, :position, :integer
        TodoItem.order(:updated_at).each.with_index(1) do |todo_item, index|
          todo_item.update_column :position, index
        end
      end
    end

    Migration with scope: If using scope: :todo_list, you must iterate through the parent records:

    TodoList.all.each do |todo_list|
      todo_list.todo_items.order(:updated_at).each.with_index(1) do |todo_item, index|
        todo_item.update_column :position, index
      end
    end

    PostgreSQL optimized migration:

    execute <<~SQL.squish
       UPDATE todo_items
       SET position = mapping.new_position
       FROM (
         SELECT
           id,
           ROW_NUMBER() OVER (
             PARTITION BY todo_list_id
             ORDER BY updated_at
           ) AS new_position
         FROM todo_items
       ) AS mapping
       WHERE todo_items.id = mapping.id;
     SQL
  4. Configure acts_as_list options

    master

    The acts_as_list method accepts several configuration options to customize behavior:

    • column: The name of the integer column used for positioning (defaults to position).
    • top_of_list: The integer value representing the top of the list (defaults to 1). Use 0 for 0-based indexing.
    • add_new_at: Where new items are placed (:top, :bottom, or nil to keep position nil). Defaults to :bottom.
    • touch_on_update: Whether to update timestamps of associated records (defaults to true).
    • sequential_updates: Whether insert_at updates objects one by one to respect unique constraints. Defaults to true if the column has a unique index, otherwise false.
  5. Mitigate database deadlock errors

    master

    In high-concurrency environments, you may encounter database deadlock errors. You can mitigate these using three primary strategies:

    1. Use Concise APIs: Use single-step creation methods instead of multiple calls. This reduces the number of SQL statements and the duration of transactions.
    2. Rescue and Retry: Catch deadlock exceptions and retry the transaction. For Rails >= 5.1.0, rescue ActiveRecord::Deadlocked. For older versions, rescue ActiveRecord::StatementInvalid and check the #cause.
    3. Lock the Parent Record: Use pessimistic locking on the parent record (the list owner) to act as a mutex for the entire list. This serializes operations on that specific list, reducing contention at the cost of throughput.
    # 1) Concise API Example
    # Good: One transaction, fewer statements
    TodoItem.create(todo_list: todo_list, position: 1)
    
    # Bad: Multiple steps, more likely to deadlock
    item = TodoItem.create(todo_list: todo_list)
    item.insert_at(1)
    
    # 2) Rescue and Retry Example (Rails >= 5.1.0)
    attempts_left = 2
    while attempts_left > 0
      attempts_left -= 1
      begin
        TodoItem.transaction do
          TodoItem.create(todo_list: todo_list, position: 1)
        end
        attempts_left = 0
      rescue ActiveRecord::Deadlocked
        raise unless attempts_left > 0
      end
    end
    
    # 3) Lock Parent Record Example
    todo_list = TodoList.create(name: "The List")
    todo_list.with_lock do
      item = TodoItem.create(description: "Buy Groceries", todo_list: todo_list, position: 1)
    end
  6. Temporarily disable acts_as_list updates

    master
    To perform mass updates or imports without triggering position reordering and callbacks, wrap your code in an acts_as_list_no_update block. You can optionally pass an array of classes to disable updates only for specific models.
  7. Move items and reorder lists

    master

    The following instance methods allow you to change the position of an item and trigger reordering of the rest of the list. Note that in acts_as_list, "higher" means a lower position value (closer to the top) and "lower" means a higher position value (closer to the bottom).

    # Reordering methods
    list_item.insert_at(2)
    list_item.move_lower      # Does nothing if item is already lowest
    list_item.move_higher    # Does nothing if item is already highest
    list_item.move_to_bottom
    list_item.move_to_top
    list_item.remove_from_list
    
    # Position change without reordering others
    list_item.increment_position
    list_item.decrement_position
    list_item.set_list_position(3)
  8. Query item positions and neighbors

    master

    Use these methods to inspect an item's status or find adjacent items in the list:

    • current_position: Returns the integer value of the position column.
    • first?: Returns true if the item is at the top of the list.
    • last?: Returns true if the item is at the bottom of the list.
    • in_list?: Returns true if the item has a position assigned.
    • higher_item: Returns the next item above the current one.
    • lower_item: Returns the next item below the current one.
    • higher_items(limit): Returns an array of the next n higher items.
    • lower_items(limit): Returns an array of the next n lower items.
  9. Configure `acts_as_list` in ActiveRecord models

    master

    Use the acts_as_list macro in your ActiveRecord model to enable sorting and reordering capabilities. The model must have an integer column to store the position.

    Available configuration options:

    • column: The name of the column used for the position integer (default: :position).
    • scope: Restricts the list to a specific subset of records. Providing a symbol (e.g., :todo_list) will automatically append _id to it to use as a foreign key restriction. You can also provide a full SQL string for complex scoping.
    • top_of_list: The integer value representing the top of the list (default: 1). Use 0 for zero-based array-like indexing.
    • add_new_at: Determines where new items are placed. Options are :top, :bottom (default), or nil (new items won't be added to the list automatically).
    • sequential_updates: Determines if insert_at should update positions one by one. This is useful for respecting unique and not null constraints on the position column. Defaults to true if the column has a unique index, otherwise false.
    • touch_on_update: If false, prevents updating the model's updated_at timestamps when only the position is changed (default: true).
    class TodoItem < ActiveRecord::Base
      belongs_to :todo_list
      acts_as_list scope: :todo_list
    end
  10. Move items within a list

    master

    Once acts_as_list is configured, your model instances gain several methods to manipulate their position within the list:

    Direct Movement:

    • move_to_top: Moves the item to the top_of_list position.
    • move_to_bottom: Moves the item to the end of the list.
    • move_lower: Swaps the item with the next lower item (if one exists).
    • move_higher: Swaps the item with the next higher item (if one exists).

    Manual Insertion:

    • insert_at(position): Inserts the item at a specific position (defaults to top_of_list).
    • insert_at!(position): Same as insert_at, but raises an exception if the save fails.

    Relative Adjustment:

    • increment_position: Increases the position by 1 without reordering other items.
    • decrement_position: Decreases the position by 1 without reordering other items.

    Scope-based Movement:

    • move_within_scope(scope_id): Changes the item's scope (e.g., moving it to a different list) and saves it.
    todo_list.first.move_to_bottom
    todo_list.last.move_higher