A gem to help you retrofit UUIDs to your existing Rails application.
The scenario
You have an existing Rails site that uses auto-incrementing ids, that you want to add, say, offline syncronization. The problem with auto-incrementing ids is that you will hit clashes if you create entries offline.
One solution is to use UUIDs, which has a very, very low probability of clashing. Now the problem is how do you add that to Rails? One way is to replace all the auto-incrementing ids with uuids, using something like activeuuid. This can be problematic if you have a running site, as converting all the ids and relationships would be a pain.
Enter: has_uuid
To use has_uuid you mirror all of the primary key id, and foreign key ids with another uuid column, and it makes sure you can search the whole object graph using uuids! If that didn't make sense check this out.
Installation
NOTE The name of the gem is rails_has_uuid because has_uuid was already taken.
Via Gemfile:
gem 'rails_has_uuid', require: 'has_uuid'
On the commandline
get install rails_has_uuid
Example
###Migration
class SetupDatabase < ActiveRecord::Migration
def up
create_table :record_labels do |t|
t.string :name
t.uuid :uuid
end
create_table :albums do |t|
t.uuid :uuid
t.string :name
t.integer :record_label_id
t.uuid :record_label_uuid
t.integer :artist_id
t.uuid :artist_uuid
end
end
end
has_uuid adds a uuid type to mirations. On SQLite and MySQL it's a binary(16), on PostgreSQL it uses their native uuid type. Notice how we have both a record_label_id and record_label_uuid column...
###Model
class RecordLabel < ActiveRecord::Base
has_uuid
has_many :albums
end
class Album < ActiveRecord::Base
has_uuid
belongs_to :record_label
end
By calling the has_uuid class method, your model is primed.
Finders
record_label = RecordLabel.create!(:name => 'Fat Wreck Chords')
# id: 1, the autogenerated uuid is: cf1ba930-6946-4bd5-9265-d9043e5dbb93
record_label = RecordLabel.create!(:name => 'Misfit Records')
# id: 2, the autogenerated uuid is: a957f2d6-371e-4275-9aec-b54a380688e0
RecordLabel.find(1, 2)
RecordLabel.find('cf1ba930-6946-4bd5-9265-d9043e5dbb93', a957f2d6-371e-4275-9aec-b54a380688e0)
RecordLabel.find(UUIDTools::UUID.parse('cf1ba930-6946-4bd5-9265-d9043e5dbb93'), UUIDTools::UUID.parse('a957f2d6-371e-4275-9aec-b54a380688e0'))
...will return an array of objects that match those ids
Relationships
record_label = RecordLabel.create!(:name => 'Fat Wreck Chords')
artist_1 = Artist.create!(:name => 'NOFX')
artist.record_label = record_label
artist.record_label_uuid
Will return the uuid of the associated record label.
The reverse is also true
record_label = RecordLabel.create!(:name => 'Fat Wreck Chords')
artist_1 = Artist.create!(:name => 'NOFX')
artist.record_label_uuid = record_label.uuid
artist.record_label
Will return the record label object
Finally, it'll find the uuid when you associate via id i
record_label = RecordLabel.create!(:name => 'Fat Wreck Chords')
artist_1 = Artist.create!(:name => 'NOFX')
artist.record_label_id = record_label.id
artist.record_label_uuid
Will be the uuid of the record label
Generally, a UUID will be a UUIDTools::UUID, but you can set uuids via a string, so these are equivalent:
uuid = UUIDTools::UUID.random_create
=> #<UUID:0x3fd4186240e0 UUID:7c9748da-f9fe-467e-bdb3-34ce2dc67605>
record_label = RecordLabel.create!(:name => 'Fat Wreck Chords', :uuid => uuid)
artist_1 = Artist.create!(:name => 'NOFX')
artist.record_label_uuid = uuid.to
# is the same as
artist.record_label_uuid = '7c9748da-f9fe-467e-bdb3-34ce2dc67605'
Collections
record_label = RecordLabel.create!(:name => 'Fat Wreck Chords')
artist_1 = Artist.create!(:name => 'NOFX')
artist_2 = Artist.create!(:name => 'Strung Out')
artist_3 = Artist.create!(:name => 'Screeching Weasel')
record_label.artists = [ artist_1, artist_2, artist_3 ]
record_label.save!
record_label.artists_uuids
Returns an array of UUIDTools::UUID objects that correspond to artist_1, artist_2, artist_3
it also works the other way:
record_label = RecordLabel.create!(:name => 'Fat Wreck Chords')
artist_1 = Artist.create!(:name => 'NOFX')
artist_2 = Artist.create!(:name => 'Strung Out')
artist_3 = Artist.create!(:name => 'Screeching Weasel')
record_label.artist_uuids = [ artist_1.uuid, artist_2.uuid, artist_3.uuid ]
record_label.save!
record_label.artists
Returns artist_1, artist_2, artist_3
Finally, if you set a relationship id, it will automatically fetch the uuid for you
record_label = RecordLabel.create!(:name => 'Fat Wreck Chords')
artist_1 = Artist.create!(:name => 'NOFX')
artist_2 = Artist.create!(:name => 'Strung Out')
artist_3 = Artist.create!(:name => 'Screeching Weasel')
record_label.artist_ids = [ artist_1.uuid, artist_2.uuid, artist_3.uuid ]
record_label.save!
record_label.artists_uuids
Will also return an array of UUIDTools::UUID objects that correspond to artist_1, artist_2, artist_3
All of these will return the same record
record_label = RecordLabel.create!(:name => 'Fat Wreck Chords')
# id: 1, the autogenerated uuid is: cf1ba930-6946-4bd5-9265-d9043e5dbb93
RecordLabel.find(1)
RecordLabel.find('cf1ba930-6946-4bd5-9265-d9043e5dbb93')
RecordLabel.find(UUIDTools::UUID.parse('cf1ba930-6946-4bd5-9265-d9043e5dbb93'))
What doesn't work
Unfortunately, because of the way ARel works, you can only search via a UUIDTools::UUID
uuid = UUIDTools::UUID.random_create
=> #<UUID:0x3fd4186323c0 UUID:7cd2feb5-6929-4288-9ea4-c4e68927f289>
Artist.where('uuid = ?', uuid) # This works
Artist.where('uuid = ?', '7cd2feb5-6929-4288-9ea4-c4e68927f289') # This won't (except on PostgreSQL)
As a result, this also won't work
Artist.find_by_uuid('7cd2feb5-6929-4288-9ea4-c4e68927f289')
TODO
- Some more testing - I'm sure it will fail if you have a relationship between a has_uuidmodel and a regular one
- Release as a gem - it's not tested well enough yet.
- Probably other stuff I haven't thought of yet
Contributing to has_uuid
- Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
- Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
- Fork the project.
- Start a feature/bugfix branch.
- Commit and push until you are happy with your contribution.
- Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
- Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
Setting up for development environment
has_uuid uses the appraisal gem for testing against multiple versions of Rails.
To get started run:
appraisal install
Then to run tests against rails 3.2:
appraisal rails-3-2 rspec
Against rails 4.0
appraisal rails-4-0 rspec
Against rails 4.1
appraisal rails-4-1 rspec
Copyright
Copyright (c) 2012 MadPilot Productions. See LICENSE.txt for further details.