24

I have a model Token, which has a field token_number that I need to auto increment (starting from 1001), if and only if the user does not provide it. The problem is that, since the user has the option to provide this field, I can't exactly query the database and ask for the largest token_number. I found one answer on this forum, but I'm quite certain there has to be a better way to do it than to execute an SQL statement? Auto increment a non-primary key field in Ruby on Rails

Community
  • 1
  • 1
Wahaj Ali
  • 4,093
  • 3
  • 23
  • 35

2 Answers2

25

Interesting question for me. Unfortunately, rails doesn't provide a way to auto-increment columns, so we must resort to SQL with little automation. I tried this in Rails 3.0.7 using PostgreSQL as my database and it works and hope this will be useful:

Creating sequence for token_number PGSql Documentation

class CreateTokens < ActiveRecord::Migration

  def self.up
    create_table :tokens do |t|
      t.string :name
      t.integer :token_number

      t.timestamps
    end

    execute "CREATE SEQUENCE tokens_token_number_seq START 1001"
  end

  def self.down
    drop_table :tokens

    execute "DROP SEQUENCE tokens_token_number_seq"
  end
end

Now, since there is a possibility of token_number being set by the user manually, we'll need to generate the token_number only if it is not being set. Read about Callbacks here. With that we have,

class Token < ActiveRecord::Base
  # Generate the sequence no if not already provided.
  before_validation(:on => :create) do
    self.application_no = next_seq unless attribute_present?("application_no")
  end

  private
    def next_seq(column = 'application_no')
      # This returns a PGresult object [http://rubydoc.info/github/ged/ruby-pg/master/PGresult]
      result = Token.connection.execute("SELECT nextval('tokens_token_number_seq')")

      result[0]['nextval']
    end 
end

A sample run. Please note that for the first token I am not setting token_number and it generates the token_number sequence and for the second I am assigning.

token = Token.new
# => #<Token id: nil, name: nil, token_number: nil, created_at: nil, updated_at: nil> 

token.save
  SQL (0.8ms)  BEGIN
  SQL (1.7ms)  SELECT nextval('tokens_token_number_seq')
  SQL (6.6ms)   SELECT tablename
 FROM pg_tables
 WHERE schemaname = ANY (current_schemas(false))

  SQL (33.7ms)  INSERT INTO "tokens" ("name", "token_number", "created_at", "updated_at") VALUES (NULL, 1001, '2012-03-02 12:04:00.848863', '2012-03-02 12:04:00.848863') RETURNING "id"
  SQL (15.9ms)  COMMIT
# => true 

token = Token.new
# => #<Token id: nil, name: nil, token_number: nil, created_at: nil, updated_at: nil> 

token.token_number = 3000
# => 3000 

token.save
  SQL (0.8ms)  BEGIN
  SQL (1.5ms)  INSERT INTO "tokens" ("name", "token_number", "created_at", "updated_at") VALUES (NULL, 3000, '2012-03-02 12:04:22.924834', '2012-03-02 12:04:22.924834') RETURNING "id"
  SQL (19.2ms)  COMMIT
# => true 
Sebastián Palma
  • 32,692
  • 6
  • 40
  • 59
Syed Aslam
  • 8,707
  • 5
  • 40
  • 54
  • Is it possible to somehow use default value in combination with the `nextval` to avoid using callbacks? – freemanoid Sep 17 '14 at 10:26
  • If you're using PostgreSQL, you can use the [`serial` data type](https://www.postgresql.org/docs/current/datatype-numeric.html#DATATYPE-SERIAL) and avoid executing the `SECUENCE` creation step. To do this in the migration, update the `t.integer :token_number` for `t.column :token_number, :serial`. – Zeaneth Aug 24 '23 at 22:00
7

another alternative is set nextval as the default value in your postgresql

this make us not need to write callbacks

class CreateTokens < ActiveRecord::Migration

  def self.up
    create_table :tokens do |t|
      t.string :name
      t.integer :token_number

      t.timestamps
    end

    execute "CREATE SEQUENCE tokens_token_number_seq START 1001"
    execute "ALTER TABLE tokens ALTER COLUMN token_number SET DEFAULT NEXTVAL('tokens_token_number_seq')"
  end

  def self.down
    drop_table :tokens

    execute "DROP SEQUENCE tokens_token_number_seq"
  end
end
buncis
  • 2,148
  • 1
  • 23
  • 25
  • This method is working fine, but as I create a new record, it doesn't return the auto_increment field directly: `#` even if it's inserted right into the DB. Is there a way to get this field without calling "reload" on the new object? – Vi. May 28 '21 at 09:35
  • @Joe yeah if u create it via console the return value from `create!` is nil, to make it also return the auto generate value is via callback or patch the [active record persistence](https://github.com/rails/rails/blob/984c3ef2775781d47efa9f541ce570daa2434a80/activerecord/lib/active_record/persistence.rb) but I think you not need to do that since in the web it works / auto reload by itself somehow – buncis Dec 26 '21 at 09:41
  • also check this its recommended to do the reload in test or console https://stackoverflow.com/questions/5309430/after-create-callback-not-working-in-test-but-works-in-console – buncis Dec 26 '21 at 09:52