Ask A Question

Notifications

You’re not receiving notifications from this thread.

Best way to create a belongs_to object from a has_many

Morgan asked in General
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


Reply
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

Reply
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?
Reply
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 :)
Reply
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
Reply
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.
Reply
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
Reply

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

Reply

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

Reply

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

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

Thanks for your help Jacob!
Reply
Hah, we sure did!

No problem at all, good luck!
Reply
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.

Reply
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!

Reply
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 
Reply
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
Reply
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!
Reply
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! :)
Reply

Yes, a listing can exist without a user in the model you've outlined, thanks to the optional: true clause within the belongs_to :user association. This clause specifically informs Rails that the user_id field in the listings table is not mandatory, allowing for listings to be created without an associated user.

Reply

In essence, this code structure enables the creation of listings that are independent of users, offering flexibility in your application's data modeling.

Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 82,464+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.