The Spaghetti Refactory Established 2015

Unique objects from a three way join

I have a model that is essentially a three-way join of three models. Let’s call this model Widget. It has a user_id, account_id, and place_id. I want to make sure each of those widget objects is unique in some way - in other words, I don’t want any widget object with the same user_id, account_id, and place_id.

I should probably validate for uniqueness, but only within the confines of what is in the other columns. Here’s where uniqueness scope comes in handy:

validates_uniqueness_of :user_id, scope: [:account_id, :place_id]

Now, if we try to create a Widget with the same user_id, account_id, AND place_id as one already in the system, ActiveRecord will yell at us. But if any of those values are different from what’s already in the db, we’re good to go.

If this was going to be user-facing, I’d probably also want to add a custom error message to this, otherwise it would say something like ‘user_id has been taken’, which isn’t ideal. However, these Widget objects will be created in the background, not explicitly by users, so we shouldn’t ever see these error messages.

Now let’s say we want to control for what might already be in the db. Suppose that we didn’t find out this was an issue until a bunch of people already created duplicate Widgets. Sure, we could create a rake task to comb the db and delete the duplicates, and that would probably be a good idea at some point if we truly don’t need them. But what if for some reason we wanted duplicates to save to the db, but on certain pages only wanted to display unique Widget objects?

Running Widget.all.uniq doesn’t do anything for us, because each widget object has a unique id/primary key. So, assuming we don’t need the primary key on the page we’re displaying these widgets (and since this is essentially just a three-way join with no other attributes, I think that’s a fair assumption), we can pull all other attributes besides the primary id for all the widget objects using ActiveRecord’s select:

Widget.select(:user_id, :account_id, :place_id)

This will give us all of the widget objects in the system, but with nil values for any attributes we didn’t specify - namely, the id field. Now, since we don’t have the unique id to deal with, we can easily filter duplicates using uniq:

Widget.select(:user_id, :account_id, :place_id).uniq

Just to make it cleaner, I would probably make that a scope on Widget so I could give it a name and have it make more sense for other coders on the project.

class Widget < ActiveRecord::Base
scope :unique, -> { select(:user_id, :account_id, :place_id).uniq }
# other stuff in Widget class
end

Widget.unique

Bingo bango bongo.