Skip to content

Repository files navigation

Sequenced

.github/workflows/ci.ymlCode ClimateGem Version

Sequenced is a simple gem that generates scoped sequential IDs for ActiveRecord models. This gem provides an acts_as_sequenced macro that automatically assigns a unique, sequential ID to each record. The sequential ID is not a replacement for the database primary key, but rather adds another way to retrieve the object without exposing the primary key.

Purpose

It's generally a bad practice to expose your primary keys to the world in your URLs. However, it is often appropriate to number objects in sequence (in the context of a parent object).

For example, given a Question model that has many Answers, it makes sense to number answers sequentially for each individual question. You can achieve this with Sequenced in one line of code:

classQuestion < ActiveRecord::Basehas_many:answersendclassAnswer < ActiveRecord::Basebelongs_to:questionacts_as_sequencedscope: :question_idend

Installation

Add the gem to your Gemfile:

gem 'sequenced'

Install the gem with bundler:

bundle install

Usage

To add a sequential ID to a model, first add an integer column called sequential_id to the model (or you many name the column anything you like and override the default). For example:

rails generate migration add_sequential_id_to_answers sequential_id:integer
rake db:migrate

Then, call the acts_as_sequenced macro in your model class:

classAnswer < ActiveRecord::Basebelongs_to:questionacts_as_sequencedscope: :question_idend

The scope option can be any attribute, but will typically be the foreign key of an associated parent object. You can even scope by multiple columns for polymorphic relationships:

classAnswer < ActiveRecord::Basebelongs_to:questionable,:polymorphic=>trueacts_as_sequencedscope: [:questionable_id,:questionable_type]end

Multiple sequences can be defined by using the macro multiple times:

classAnswer < ActiveRecord::Basebelongs_to:accountbelongs_to:questionacts_as_sequencedcolumn: :question_answer_number,scope: :question_idacts_as_sequencedcolumn: :account_answer_number,scope: :account_idend

Schema and data integrity

This gem is only concurrent-safe for PostgreSQL databases. For other database systems, unexpected behavior may occur if you attempt to create records concurrently.

You can mitigate this somewhat by applying a unique index to your sequential ID column (or a multicolumn unique index on sequential ID and scope columns, if you are using scopes). This will ensure that you can never have duplicate sequential IDs within a scope, causing concurrent updates to instead raise a uniqueness error at the database-level.

It is also a good idea to apply a not-null constraint to your sequential ID column as well if you never intend to skip it.

Here is an example migration for a model that has a sequential_id scoped to a burrow_id:

# app/db/migrations/20151120190645_create_badgers.rbclassCreateBadgers < ActiveRecord::Migrationdefchangecreate_table:badgersdo |t|
t.integer:sequential_id,null: falset.integer:burrow_idendadd_index:badgers,[:sequential_id,:burrow_id],unique: trueendend

If you are adding a sequenced column to an existing table, you need to account for that in your migration.

Here is an example migration that adds and sets the sequential_id column based on the current database records:

# app/db/migrations/20151120190645_add_sequental_id_to_badgers.rbclassAddSequentalIdToBadgers < ActiveRecord::Migrationadd_column:badgers,:sequential_id,:integerexecute<<~SQL UPDATE badgers SET sequential_id = old_badgers.next_sequential_id FROM ( SELECT id, ROW_NUMBER() OVER( PARTITION BY burrow_id ORDER BY id ) AS next_sequential_id FROM badgers ) old_badgers WHERE badgers.id = old_badgers.id SQLchange_column:badgers,:sequential_id,:integer,null: falseadd_index:badgers,[:sequential_id,:burrow_id],unique: trueend

Configuration

Overriding the default sequential ID column

By default, Sequenced uses the sequential_id column and assumes it already exists. If you wish to store the sequential ID in different integer column, simply specify the column name with the column option:

acts_as_sequencedscope: :question_id,column: :my_sequential_id

Starting the sequence at a specific number

By default, Sequenced begins sequences with 1. To start at a different integer, simply set the start_at option:

acts_as_sequencedstart_at: 1000

You may also pass a lambda to the start_at option:

acts_as_sequencedstart_at: lambda{ |r| r.computed_start_value}

Indexing the sequential ID column

For optimal performance, it's a good idea to index the sequential ID column on sequenced models.

Skipping sequential ID generation

If you'd like to skip generating a sequential ID under certain conditions, you may pass a lambda to the skip option:

acts_as_sequencedskip: lambda{ |r| r.score == 0}

Example

Suppose you have a question model that has many answers. This example demonstrates how to use Sequenced to enable access to the nested answer resource via its sequential ID.

# app/models/question.rbclassQuestion < ActiveRecord::Basehas_many:answersend# app/models/answer.rbclassAnswer < ActiveRecord::Basebelongs_to:questionacts_as_sequencedscope: :question_id# Automatically use the sequential ID in URLsdefto_paramself.sequential_id.to_sendend# config/routes.rbresources:questionsdoresources:answersend# app/controllers/answers_controller.rbclassAnswersController < ApplicationControllerdefshow@question=Question.find(params[:question_id])@answer=@question.answers.find_by(sequential_id: params[:id])endend

Now, answers are accessible via their sequential IDs:

http://example.com/questions/5/answers/1 # Good

instead of by their primary keys:

http://example.com/questions/5/answer/32454 # Bad

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

About

Generate scoped sequential IDs for ActiveRecord models

Topics

Resources

Stars

407 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages