Skip to main content

Best way to create a belongs_to object from a has_many

General • Asked by Morgan
Sorry for the vague title but I cant think of a better explanation! 

Let's say I have a list of Users and an admin could click a button/link that would create a site for them from the existing user's data. What would be the best way to set this up?

This is what I currently have but I feel like I'm on the wrong track:

class Site < ApplicationRecord
  has_one :user
end

class User < ApplicationRecord
  belongs_to :site
end

I then have a link on the list of users:

<%= link_to "Create site", sites_path(user_id: user.id), method: :post %>


And then the Sites controller:

class SitesController < ActionController::Base
  def create
     # What now?
  end

  def site_params
    params.fetch(:site, {}).permit!
  end
end



Hey Morgan, 

Your associations are backwards for what you're wanting I think. Try:

class Site < ApplicationRecord
  belongs_to :user
end

class User < ApplicationRecord
  has_one :site
end

Now you can build the association like so:

user.build_site


Hi Jacob!

I started out with that association but the Site holds :subdomain, & :main_domain which is then used to load realted layouts, pages and other relations so I'm not sure how I would do this if a Site belongs_to a User?

Hmm, I'm not sure I follow what the problem is here..

Can you provide a specific use case that prohibits this setup from working for your needs? How are you querying for your :subdomain and :main_domain that would keep you from getting the desired result?

You could be completely correct for your use case, I'm just not tracking yet is all :)

I think what you suggested is correct and I have updated my models but I'm still stuck on the controllers.

So I have a list of users who each have a link that looks like this:

<%= link_to "Create site", sites_path(user_id: user.id), method: :post %>

But I'm not sure how to handle the params?

class SitesController < ActionController::Base
  def create
    @user = User.find(params[:user_id])
    @site = @user.build_site(subdomain: @user.subdomain)
    @site.save
  end


  def listing_params
    params.fetch(:listing, {}).permit!
  end
end


This is what my Sites schema looks like:

  create_table "sites", force: :cascade do |t|
    t.string "subdomain"
    t.string "main_domain"
    t.string "type"
    t.string "label"
    t.bigint "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_sites_on_user_id"
  end

As you can see, I have a subdomain which is created from the users first_name & last_name by friendly_id

Ok cool - so it looks like you just want the button to create the association and then later you'll provide a page to enter the additional info such as the subdomain and main_domain, correct?

If so, then just this would work:

class SitesController < ActionController::Base
  
  def create
    @user = User.find(params[:user_id])
    @site = @user.build_site.save
  end

end 

There's no need to mess with site_params in this case since you're not passing any of that information to the new record yet. So just pass the user_id, find that user, then use the build_site method to create the association and then call save to commit it all to the DB.

I forgot to specifically address the friendly_id part...

Can you post your method you use for friendly_id to create the subdomain? As long as it runs the `slugginator` after save when it will have the user_id then you should be good

Ahh perfect thanks, Jacob!

I was originally generating the :subdomains in the user model, but now that I have the Sites model I think I should move it there but this obviously won't work now as it doesn't know about the users :first_name & :last_name

class Site < ApplicationRecord
extend FriendlyId
friendly_id :slug_candidates, use: :slugged, slug_column: :subdomain
belongs_to :user

def slug_candidates
[
:first_name,
[:first_name, :last_name],
[:first_name, :last_name, :id],
]
end
end


No problem at all!

You can still set it thanks to the has_one:

class Site < ApplicationRecord
def slug_candidates
[
user.first_name,
[user.first_name, user.last_name],
[user.first_name, user.last_name, user.id],
]
end
end


Man, I love Ruby!

I just changed the above to below and it works perfectly!

  def slug_candidates
[
user.first_name,
[user.first_name, user.last_name,],
[user.first_name, user.last_name, :id]
]
end

Haha thanks, Mate, I think we must have posted at the same time.

Thanks for your help Jacob!

Hah, we sure did!

No problem at all, good luck!

Hi Jacob, I had all this working beautifully until I added a third model and I have been going around in circles since!

This is not valid, but I'm basically trying to achieve this:

A Site is a very small model which stores things like domains, subdomains etc can be used to create differnt type of sites.  i.e User site, Listing site, Company site.

A Listing could have its own Site (but doesn't have to) and must belong to at least one User but possibly more.

A User can have one Site and can have many Listings.


So something like:

User has_one Site
User
has_many Listings

Listing
has_one Site
Listing
has_many Users

A Site has_many Users

Is had a look at has many through and polymorphic associations but kept getting stuck with all the two way relationships so I suspect I'm going about this the wrong way.


This may not be *correct* but I believe it does what you want. There could very well be a more railsy way or a cleaner / slicker way... but here goes:

class User < ApplicationRecord
  has_one :site
  has_one :listing
  has_many :association_groups
  has_many :listings, through: :association_groups
end

#columns: user_id
class Listing < ApplicationRecord
  belongs_to :user
  has_one :site
  has_many :association_groups
  has_many :users, through: :association_groups
end

#columns: user_id, listing_id
class Site < ApplicationRecord
  has_one :user
  has_one :listing
  has_many :association_groups
  has_many :users, through: :association_groups
end

#columns: listing_id, user_id, site_id
class AssociationGroup < ApplicationRecord
  belongs_to :listing, optional: true
  belongs_to :user, optional: true
  belongs_to :site, optional: true
end



user1 = User.create
user1_site = user1.build_site.save
user1_listing = user1.build_listing.save
user1_association_group = user1.association_groups.build(listing_id: user1.listing.id, site_id: user1.site.id).save


user2 = User.create
user2_site = user2.build_site.save
user2_listing = user2.build_listing.save
user2_association_group = user2.association_groups.build(listing_id: user2.listing.id, site_id: user2.site.id).save

# assign user1 to the listing user2 created
user2.listing.association_groups.build(user_id: user1.id).save
user2.listing.users

# assign user2 to the site user1 created
user1.site.association_groups.build(user_id: user2.id).save
user1.site.users

I had to create a new table - AssociationGroup - to handle the additional associations. You may want to come up with a more descriptive name... I'm bad at naming :)

If you're using Rails 5 - you'll have to use optional: true on the AssociationGroup table since it's now required by default

Probably the trickiest thing to remember is that when you're creating a users site or listing, you can use user.build_listing or user.build_site - but if you're assigning a user to another site (that's not initially theirs), then you have to create the association through association_groups.

Be interesting to see if anyone has any other ideas!


Thanks, Jacob, this makes a lot of sense, but how would you handle the freindlyId subdomains on the Site table with this setup?

Before I was just doing  @listing.assign_attributes(listing_body) which would pass the required attributes to build the subdomain.

It looks like I need to somehow pass the listing to user1_site = user1.build_site.save 

Well, considering a site now can be created for a user or a listing, I believe you're going to have to set the slug manually or potentially put a "type" like field on the site table so your slug generator can check which type of site it is then set the slug based on that.

So for instance:

def slug_candidates
  if self.user_site?
    [
      user.first_name,
      [user.first_name, user.last_name,],
      [user.first_name, user.last_name, :id]
    ]
  else # listing site
    [
      listing.title
    ]
  end
end

This way you can kind of control how the slug gets created based on whatever criteria you want

Yes, I think that would have been the next issue I would have ran into, but I think my problem now is that I am trying to build up the associations in the same order as your example with a User first but I'm creating the Listing first. 

I'll need to play with it some more!

Do you have a patreon account or similar setup? I really appreciate all your help!

Can a listing exist without a user? If so then you'll want to add the optional: true to the has_one :user on the listing model then you should be able to create it without any problems.

#columns: user_id
class Listing < ApplicationRecord
  belongs_to :user, optional: true
  has_one :site
  has_many :association_groups
  has_many :users, through: :association_groups
end

listing = Listing.create
listing_site = listing.build_site.save

And thanks for the offer, but no need! Answering questions helps me learn more and I enjoy the challenge! :)

Login or Create An Account to join the conversation.

Subscribe to the newsletter

Join 22,346+ developers who get early access to new screencasts, articles, guides, updates, and more.

    By clicking this button, you agree to the GoRails Terms of Service and Privacy Policy.

    More of a social being? We're also on Twitter and YouTube.