1

I need to generate a unique number for my model, which is called Shipment. The number must be in the following format:

  • Two digits - current month
  • Two digits - current year
  • Six digits - just digits

For instance: 1114000001. 11 - current month. 14 - year. 0000001 - just six digits.

So. My idea is to use the counter for 6 digits. I want store it in database, then I can use the lock and ensure uniqueness. But then it is obvious that it is a potential bottleneck. Because each new shipment will wait in line to access to counter.

My question is: What better? Use counter or generate a random number, and query the database:

while Shipment.where(:id => supposedly_unique_number.).exists?

And one more question: Where do I store this Сounter? I need only one for whole application. Create standalone table for only one value?

UPDATE

I just realized that this check Shipment.where(:id => supposedly_unique_number.).exists? does not guarantee uniqueness. :)

So where better to store the counter? I will be glad to any of your suggestions.

UPDATE2

mu is too short, mentioned database sequence. And I can`t find understandable information how to use it. I would be grateful if someone explain to me how it works.

Rails 4, Ruby 2, PostgreSql.

mu is too short
  • 426,620
  • 70
  • 833
  • 800
user3219683
  • 37
  • 1
  • 6
  • Do you need this value to be the `id`? Do you need it to be in the database? – mu is too short Nov 15 '14 at 06:59
  • Yes. It must be uniqueness id. And it will stored in database. For every Shipment instance. – user3219683 Nov 15 '14 at 07:01
  • The sensible approach would use a database trigger or function combined with a database sequence but that would violate Rails dogma and make DHH cry (not that that would be a bad thing though). – mu is too short Nov 15 '14 at 07:03
  • Sounds too tricky for me. Could you give a link to read about it? – user3219683 Nov 15 '14 at 07:09
  • I don't know neither Rails or Postgre, so I'm a bit reluctant to offer a suggestion, but here goes. I assume every database has a capability of generating a numeric ID (call it ID) for your table that is guaranteed to be unique, and that number is incremented by one for each record insertion. If so, why not just obtain the value of that counter at the beginning of each month, say N, store that in a table indexed by month and year, and use ID-N for the six-digits in your custom ID? In fact, the counter value a day or two before the start of the month could be used as N. – Cary Swoveland Nov 15 '14 at 07:35
  • Thanks for your answer. As you noticed ID, generated just before the insert operation to avoid collisions. So I was able to access this value only after the record is saved. That is, I have to first save the record, then take the value to generate my shipment number, and then update the record. And it deprives me of the opportunity to use validations. – user3219683 Nov 15 '14 at 07:51
  • Cary Swoveland, Sorry if I did not understand your idea. – user3219683 Nov 15 '14 at 07:56
  • You want to use a `text` column for the `id` (due to possible leading zeros) and then use `to_char(current_date, 'MMYY') || to_char(nextval('some_sequence'), 'FM000000')` as the default value where `some_sequence` is a database sequence. Any pure Ruby solution will be an ugly pile of nasty and race conditions. I'll try to come back tomorrow and show you how this is done (but someone else is welcome to answer-ify this comment in the mean time). – mu is too short Nov 15 '14 at 08:10
  • mu is to short, Okay, thanks. Some_sequence can be any of the six digits are not necessarily in order, but necessarily unique within a month. – user3219683 Nov 15 '14 at 08:19
  • I don't want to belabor this, but I'll try to clarify. At an arbitrary time near the end of a month we add a record to the table and make a note of the id supplied by the db. Call that N. Each record added the following month will also have a unique id, say ID, so ID-N will be a unique non-negative number, presumably with at most six digits. Yes, you'd have to update the shipping id after the record is created, but you wouldn't have to worry about a conflict at that time, because ID-N is guaranteed to be unique. In effect, a unique shipping id is "reserved" when each record is created. – Cary Swoveland Nov 15 '14 at 09:15
  • Yes it is a possible solution. Maybe I'll use it, if other options are too complex. I still like to avoid updating records. – user3219683 Nov 15 '14 at 10:02
  • What was I thinking?? If you're prepared to update the record, just use the last six digits of the unique id supplied by the db. Of course, you could skip the update and compute the shipping id as needed, rather than storing it in the record, but I can see why you wouldn't want to do that. – Cary Swoveland Nov 15 '14 at 17:40

1 Answers1

2

There are a couple things you need to consider here:

  1. Your ids will have leading zeros so they're not numbers, they're strings that contain digit characters.
  2. You should let the database take care of the counter. Anything you try to do in Ruby will be a nasty mess of kludges and race conditions. Similarly, you should let the database take care of building the id.

Using a string for the id is a bit of work to keep ActiveRecord from thinking it owns you but not that bad. You need to create the table without the implicit id that create_table adds, add your own id, and set it as the primary key by hand:

create_table :shipments, :id => false do |t|
  # Don't bother with `t.string` when using PostgreSQL, it is pointless.
  t.text :id, :null => false
  ...
end
connection.execute('alter table shipments add primary key (id)')

Now we need to hook up a default value so that the database can generate our ids. Getting the date prefix is a simple of matter of using the to_char SQL function to format the current time (now()):

to_char(now(), 'MMYY')

To create the suffix, we can use a sequence. A database sequence is just database object that returns incrementing numbers in a safe way in the presence of multiple database connections. First, create the sequence:

connection.execute('create sequence shipments_id_seq owned by shipments.id')

To get the next value from a sequence we use the nextval SQL function:

nextval(regclass)
Advance sequence and return new value

and again use to_char to format it:

to_char(nextval('shipments_id_seq'), 'FM000000')

The linked document explains what the FM000000 format means. Combining those two parts of the id gives us a default value of:

to_char(now(), 'MMYY') || to_char(nextval('shipments_id_seq'), 'FM000000')

Note that || is string concatenation in SQL. Wrapping that in another connection.execute lets us attach the default to the id column:

connection.execute(%q{
  alter table shipments
  alter column id
  set default to_char(now(), 'MMYY') || to_char(nextval('shipments_id_seq'), 'FM000000')
})

If you want to reset the counters at the beginning of each month (or when you get close to the six digit limit), you can use the setval SQL function.

Finally, since you're using all kinds of things that ActiveRecord doesn't understand, you'll want to switch from schema.rb to structure.sql for managing your schema information. You can do this in your configs by setting:

config.active_record.schema_format = :sql

and you'll use rake db:structure:dump and db:structure:load instead of the schema tasks.


Putting all that together will give you a migration like this:

def up
  create_table :shipments, :id => false do |t|
    # Don't bother with `t.string` when using PostgreSQL, it is pointless.
    t.text :id, :null => false
    ...
  end
  connection.execute('alter table shipments add primary key (id)')
  connection.execute('create sequence shipments_id_seq owned by shipments.id')
  connection.execute(%q{
    alter table shipments
    alter column id
    set default to_char(now(), 'MMYY') || to_char(nextval('shipments_id_seq'), 'FM000000')
  })
end
mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • I want to notice one thing that put me in a deadlock. `shipment_id` field is not to populated after a save. Instance should be reloaded `shipment.reload`. http://stackoverflow.com/questions/7606994/how-to-add-sequences-to-a-migration-and-use-them-in-a-model – user3219683 Nov 17 '14 at 10:13
  • And one more thing. To the number of digits has never exceeded 6, I use the sequence options `MAXVALUE 999999 CYCLE` – user3219683 Nov 17 '14 at 10:40