Note that there are some explanatory texts on larger screens.

plurals
  1. PORails idiom to avoid duplicates in has_many :through
    primarykey
    data
    text
    <p>I have a standard many-to-many relationship between users and roles in my Rails app:</p> <pre><code>class User &lt; ActiveRecord::Base has_many :user_roles has_many :roles, :through =&gt; :user_roles end </code></pre> <p>I want to make sure that a user can only be assigned any role once. Any attempt to insert a duplicate should ignore the request, not throw an error or cause validation failure. What I really want to represent is a "set", where inserting an element that already exists in the set has no effect. {1,2,3} U {1} = {1,2,3}, not {1,1,2,3}.</p> <p>I realize that I can do it like this:</p> <pre><code>user.roles &lt;&lt; role unless user.roles.include?(role) </code></pre> <p>or by creating a wrapper method (e.g. <code>add_to_roles(role)</code>), but I was hoping for some idiomatic way to make it automatic via the association, so that I can write:</p> <pre><code>user.roles &lt;&lt; role # automatically checks roles.include? </code></pre> <p>and it just does the work for me. This way, I don't have to remember to check for dups or to use the custom method. Is there something in the framework I'm missing? I first thought the :uniq option to has_many would do it, but it's basically just "select distinct."</p> <p>Is there a way to do this declaratively? If not, maybe by using an association extension?</p> <p>Here's an example of how the default behavior fails:</p> <pre> &gt;&gt; <strong>u = User.create</strong> User Create (0.6ms) INSERT INTO "users" ("name") VALUES(NULL) =&gt; #&lt;User id: 3, name: nil&gt; &gt;&gt; <strong>u.roles &lt;&lt; Role.first</strong> Role Load (0.5ms) SELECT * FROM "roles" LIMIT 1 UserRole Create (0.5ms) INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3) Role Load (0.4ms) SELECT "roles".* FROM "roles" INNER JOIN "user_roles" ON "roles".id = "user_roles".role_id WHERE (("user_roles".user_id = 3)) =&gt; [#&lt;Role id: 1, name: "1"&gt;] &gt;&gt; <strong>u.roles &lt;&lt; Role.first</strong> Role Load (0.4ms) SELECT * FROM "roles" LIMIT 1 UserRole Create (0.5ms) INSERT INTO "user_roles" ("role_id", "user_id") VALUES(1, 3) <strong><em>=&gt; [#&lt;Role id: 1, name: "1"&gt;, #&lt;Role id: 1, name: "1"&gt;]</em></strong></pre>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload