How to create auto incrementing primary key with prefix in RoR application.

How to create auto incrementing primary key with prefix in RoR application.

Recently, while experimenting with Stripe, I noticed that objects have IDs or identifiers with prefixes. Below, I've listed a few examples. You can find more in this gist: https://gist.github.com/fnky/76f533366f75cf75802c8052b577e2a5

PrefixDescriptionNotes
cus_Customer IDIdentifier for a Customer object.
pk_live_Live public keyPublic key in a live environment.
pk_test_Test public keyPublic key in a test environment.

I found this extremely useful. You see the ID, and right away, you can identify the object it belongs to. Here, I found a quote from Stripe's co-founder:

They're randomly generated by our Ruby application code. We use the ch_-style prefixes because we find it really useful to be able to immediately recognize the type of an ID when looking at logs or stacktraces.

https://qr.ae/pKjsuo

In this article, I will show you how to implement this in a RoR application. We will get ids like: usr_1 urs_2 usr_3. Let's dive into the implementation.

Implementation

First, let's create a migration:

rails g migration create_users email

This will generate our migration file:

class CreateUsers < ActiveRecord::Migration[7.1]
  def change
    create_table :users do |t|
      t.string :email

      t.timestamps
    end
  end
end

By default all tables will be crated with primary_key :id. We want to change this, so we add id: false option

create_table :users, id: false do |t|
  t.string :email

  t.timestamps
end

Now we need to attach primary key to the table:

create_table :users, id: false do |t|
  t.string :id, null: false, primary_key: true, default: -> { "'usr_'||nextval('usr_seq_id'::regclass)::TEXT" }
  t.string :email

  t.timestamps
end

Let's look closer on the implementation:

"'usr_'||nextval('usr_seq_id'::regclass)::TEXT"

If we don't attach an ID when creating a user, then this code will be called from the default. Here, we call a PostgreSQL sequence that will automatically generate our ID. Before that, we added the prefix usr_. However, we haven't created a sequence yet. So let's do that. Before creating a table, we need to create a sequence. I couldn't find an Active Record function to accomplish this, so we'll use the connection.execute method instead.

This is how it will look like:

connection.execute('CREATE SEQUENCE usr_seq_id')
create_table :users, id: false do |t|
    t.string :id, null: false, primary_key: true, default: -> { "'usr_'||nextval('usr_seq_id'::regclass)::TEXT" }
end

connection.execute is not reversible, so we need to change the method to up and down. Also, let's update the sequence with an owner, so the sequence will be deleted automatically when the table is dropped.

class CreateUsers < ActiveRecord::Migration[7.1]
  def up
    connection.execute('CREATE SEQUENCE usr_seq_id')
    create_table :users do |t|
      ...
    end
    connection.execute('ALTER SEQUENCE usr_seq_id OWNED BY users.id')
  end
  def down
     drop_table :users
   end
end

Now, everything is working. I hope you found this article helpful.

Resources