UUID Associations ActiveRecord
Table of Contents
Installation
Add this line to your application's Gemfile:
gem 'uuid_associations-active_record'
And then execute:
$ bundle
Or install it yourself as:
$ gem install uuid_associations-active_record
Rationale
This gem is the result of some research I did to find out if using UUIDs as DB primary keys is a good thing (Rails added support for this!). On the process I ran into this article (among many others). If you read the article you'll see how UUIDs have benefits over using sequential Int values as primary keys, but also how you should be aware of what does that imply in terms of performance or even amount of storage used.
One of the author's conclusions is that you should use Int or Bigint columns as your primary keys, but also add a column with a UUID that you can use to reference your records when exposing them to the outside world. If you finish reading the article you'll find out that not even this is the perfect solution, but at least it's a huge improvement to exposing your sequential primary keys. This gem helps you implement this approach.
Usage
Adding the gem to your Gemfile is all you need for the gem to start working if you are using Rails. If you are not, you you can always install the gem and require it manually (probably right after you require active_record). Take a look at specs if you want to use the gem with plain ActiveRecord as that is how they are setup.
What this gem does is add some useful methods to your ActiveRecord models by calling the good old association methods like
has_many
, belongs_to
, has_and_belongs_to_many
. As you may already know, calling these methods creates multiple
methods on the calling model for you. I'll explain the ones we are interested in in the next section.
This gem also adds a helpful mechanism to handle nested attributes. This will also be explained in the next section.
That's it! That is what the gem does. This will allow you to pass UUIDs to your update or create operations instead of the actual IDs. Be aware that this will of course require an additional (but simple) DB query. That is the cost of hiding the actual IDs (I'd say it's not too costly).
Also be aware that the initial version of this gem is not very configurable,
so the only thing it checks before creating the methods is that the right side association
on the caller model has a column named uuid
, doesn't take the column type into account.
I have tested with string columns in SQlite3 and UUID columns in Postgres.
Association Methods
Lets explore the next example:
# app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :posts
# generates
#
# def post_ids=(ids)
# # Adds or deletes the association with posts based on the array of IDs received
# end
#
# def post_ids
# # returns an array with all the post IDs
# end
has_many :comments
# generates
#
# def comment_ids=(ids)
# # Adds or deletes the association with comments based on the array of IDs received
# end
#
# def comment_ids
# # returns an array with all the comment IDs
# end
end
# app/models/post.rb
class Post < ActiveRecord::Base
has_and_belongs_to_many :users
# generates
#
# def user_ids=(ids)
# # Adds or deletes the association with users based on the array of IDs received
# end
#
# def user_ids
# # returns an array with all the user IDs
# end
end
# app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :user
# generates
#
# def user_id=(id)
# associates the comment with the user
# end
# def user_id
# returns the ID of the associated user
# end
end
This gem will add two new methods for each call to one of the association methods.
Generated Methods
Calling any of these methods will also add the following methods to your model:
# app/models/user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :posts
# generates
#
# def post_uuids=(uuids)
# self.post_ids = Post.where(uuid: uuids).pluck(:id)
# end
#
# def post_uuids
# posts.pluck(:uuid)
# end
has_many :comments
# generates
#
# def comment_uuids=(uuids)
# self.comment_ids = Comment.where(uuid: uuids).pluck(:id)
# end
#
# def comment_uuids
# comments.pluck(:uuid)
# end
end
# app/models/post.rb
class Post < ActiveRecord::Base
has_and_belongs_to_many :users
# generates
#
# def user_uuids=(uuids)
# self.user_ids = User.where(uuid: uuids).pluck(:id)
# end
#
# def user_uuids
# users.pluck(:uuid)
# end
end
# app/models/comment.rb
class Comment < ActiveRecord::Base
belongs_to :user
# generates
#
# def user_uuid=(uuid)
# self.user_id = User.find_by!(uuid: uuid)
# end
# def user_id
# user.uuid
# end
end
Nested Attributes
Nested attributes don't generate additional methods, this gem just modifies one so you can update nested record using the record's UUID instead of the actual ID. Let's explore the next example:
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments, allow_destroy: true
# generates
#
# def comments_attributes=(attributes)
# # allows to create or update comments using this method on Post
# end
end
ActiveRecord allows for attributes
to be an array of hashes or a hash of hashes. Here are some examples of the payload
you can pass to comments_attributes
by using this gem:
# This is supported without this gem
[
{ id: 1, comment_body: 'updating comment with ID 1' },
{ id: 2, _destroy: true },
{ comment_body: 'this will create a new comment' }
]
# With the gem, you can use UUIDs instead
[
{ uuid: 'some-uuid', comment_body: 'updating comment with UUID: some-uuid' }.
{ uuid: 'other-uuid', _destroy: true },
{ comment_body: 'this will create a new comment' }
]
Here are some things to take into account:
- If the nested model (Comment in the example) does not have a column named
uuid
, the gem will take no action, will just preserve the original behavior. - If the hash has both the
:id
and:uuid
keys, the record will be fetched byid
, anduuid
will be passed as an attribute. - When the hash has a
:uuid
key and no record is found for that key, anActiveRecord::RecordNotFound
error will be raised. If you want the behavior to be that a new record is created when not found by UUID, you can set the optioncreate_missing_uuids: true
on theaccepts_nested_attributes_for
call.
Future Work
- Not commonly used by me, but testing and adding these methods to a
has_one
relationship. - Raise not found error if the array of UUIDs is bigger that the array of IDs fetched with the
where
statement (ActiveRecord's behavior).
Development
After checking out the repo, run bin/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To run the specs with all the supported versions of ActiveRecord you can use:
$ bundle exec appraisal install
$ bundle exec appraisal rspec
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/mcelicalderon/uuid_associations-active_record.
License
The gem is available as open source under the terms of the MIT License.