foobar
foobar

Reputation: 11384

How do I ensure unique id is indeed unique in Rails?

I want to replace an object's url from /object/123 to /object/f8a3b2, so instead of using the id column I'm generating an uid column manually when the object is created.

I can use the uuid libraries available to generate uid, but how do I make sure that the generated value is always unique?

I have a unique index set on the uid column, so if it's not unique, it should throw back an error - can I catch that on the model level, create another uid, and try again?

Upvotes: 1

Views: 3767

Answers (4)

jefflunt
jefflunt

Reputation: 33954

Regarding the uuid gem

If you're using the uuid gem you don't have to worry about uniqueness, as this has been taken care of for you. However, uuid will generate long, 128-bit unique IDs. So, if you want something that's shorter, you might not want to use it.

A simpler way, if your needs are simple

If you want, say, 8-digit unique codes which are also random, you can see an implementation of such a system in my cortex project under the new_unique_code method.

What this does is generate an 8-digit alpha-numeric code to represent a resource in the app. After generating a given code I try to do a .find_by_code, and if that returns nil I know that it's NOT already in use.

In my particular case, 8-digit alpha numeric codes give me a keyspace of some many millions of possible codes (I forget how many). Based on the use case of my app, the chances of a code collision (the percentage of used codes vs. available codes) are small enough to risk possibly needing to generate a new code more than once for a given resource.

The result of this is that needing to generate a new code more than once for a given resource is extremely rare, and as such, takes up almost no additional time.

What about multiple app servers/threads in the app?

One limitation of my solution is that if you have multiple app servers, but a single DB server, it's possible to have two independent app servers generate the same code before either of them creates the new resource record in the centralized DB. This creates a situation where it's possible to have code collisions and all but the first record saved to the DB will be marked invalid (due to a uniqueness constraint on the code column). This particular app only runs (and will probably only ever run) on a single app server instance. This is a known tradeoff that I'm aware of, so I can risk it.

But, if I wanted to solve this problem, I would limit each app server's range of random codes such that they could never accidentally overlap. For example, let's say I'm generating random codes that come out as 8-digit numeric codes between the values 00000000-99999999. If I had, say, three app servers running this app, I would simply limit server #1 to generating codes between 00000000-33333333, server #2 to 33333334-66666666, and server #3 to 66666667-99999999. This way they would be guaranteed never to overlap codes, and you could still generate unique, random keys all day long.

How exactly you would limit a given server to a given range is a bit off topic, but some of the available options are:

  1. Set a config setting within each app server manually. This is manual (and therefore brittle), but if you have a stable number of app servers that won't be changing, this is probably fine.
  2. Get the app servers to talk to one another and negotiate what their random number ranges should be.
  3. Give up, realize that this is a complex problem that has been solved by very smart people, that you don't need to recreate the wheel, and just use UUID.
  4. Consider using a centralized unique ID generator that runs as a network service, such as Twitter Snowflake

Upvotes: 1

Paul Richter
Paul Richter

Reputation: 11081

There's probably better ways to do this, but the first thing that popped in to my head was making use of the validates_uniqueness_of method on your active record model, as you were suggesting.

In your model:

class MyUniqueModel < ActiveRecord::Base
    validates_uniqueness_of :uuid_column
end

In your controller

@uniqueObject = MyUniqueModel.new(modelParams)
@uniqueObject.uuid_column = generate_uuid() # whatever method you use for generating your uuid
while !uniqueObject.save # if at first it didn't save...
    uniqueObject.uuid_column = generate_uuid() # try, try again
end # should break out of the loop if the save succeeded.

If you have other validation on your model, then the problem you'll encounter with my suggestion is determining whether the save failure was caused by your apparently non-unique uuid, or some other validation error. This means you'll have to filter the loop body by the failed column to figure out whether you should bother re-generating another UUID.

Upvotes: 4

Erez Rabih
Erez Rabih

Reputation: 15808

You should use

validates :uid, :uniqueness => true

I must add a warning to this advice since there is still a scenario where you will get an error from your DB for duplicate uid column value. You can find more about it in Michael Hartl's RoR tutorial

In order to catch and retry saving upon duplicate uid you could do something like:

model = Model.create(:uid => generate_uid)
if model.errors[:uid].any?
  model.create(:uid => generate_uid)
end

Upvotes: 0

Thomas Klemm
Thomas Klemm

Reputation: 10856

Using Rails Validations to validate the uniqueness:

class MyModel < ActiveRecord::Base
  validates :id, uniqueness: true
end

Upvotes: 0

Related Questions