1

I've been researching friendship models using roles, custom associations, etc. But I haven't been able to connect my project to the concepts in a clear way.

I want a "User" to be able to create an event I'm calling a "Gather". A User can also attend a Gather created by other Users. By attending a Gather, the "User" can also be a "Gatherer".

The list of Gatherers will technically be considered friends of the "creator". This is how far I've gotten:

Models: User Gather Gatherer (?)

User

class User < ApplicationRecord
    has_many :gathers_as_creator,
        foreign_key: :creator_id,
        class_name: :Gather
    
    has_many :gathers_as_gatherer, 
        foreign_key: :gatherer_id,
        class_name: :Gather
    

end

Gather

class Gather < ApplicationRecord

    belongs_to :creator, class_name: :User 
    belongs_to :gatherer, class_name: :User

end

My question is, do I need to a join table, such as Gatherer, to allow multiple attendees and then later pull a friend list for the user/creator ?

Gatherer

belongs_to :gather_attendee, class_name: "User" 
belongs_to :attended_gather, class_name: "Gather"

Here's what I think that schema would look like:

create_table "gatherers", force: :cascade do |t|
    t.bigint "attended_gather_id"
    t.bigint "gather_attendee_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["attended_gather_id"], name: "index_gatherers_on_attended_gather_id"
    t.index ["gather_attendee_id"], name: "index_gatherers_on_gather_attendee_id"
  end

Help, my head is spinning trying to understand the connections and how to proceed.

Previous planning:

Schema:

create_table "activities", force: :cascade do |t|
    t.string "a_type"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "gatherers", force: :cascade do |t|
    t.bigint "attended_gather_id"
    t.bigint "gather_attendee_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["attended_gather_id"], name: "index_gatherers_on_attended_gather_id"
    t.index ["gather_attendee_id"], name: "index_gatherers_on_gather_attendee_id"
  end

  create_table "gathers", force: :cascade do |t|
    t.integer "creator_id"
    t.integer "activity_id"
    t.text "gather_point"
    t.boolean "active"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "interest_gathers", force: :cascade do |t|
    t.string "gather_id"
    t.string "interest_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "interests", force: :cascade do |t|
    t.string "i_type"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  create_table "users", force: :cascade do |t|
    t.string "username"
    t.string "img"
    t.string "first_name"
    t.string "last_name"
    t.string "state"
    t.string "city"
    t.string "bio"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  add_foreign_key "gatherers", "gathers", column: "attended_gather_id"
  add_foreign_key "gatherers", "users", column: "gather_attendee_id"
end
class User < ActiveRecord::Base 
  has_many :gatherers, foreign_key: gather_attendee_id
  has_many :attended_gathers, through: :gatherers
  has_many :created_gathers, foreign_key: :creator_id, class_name: "Gather"
 end
class Gather < ActiveRecord::Base 
  has_many :gatherers, foreign_key: :attended_gather_id 
  has_many :attendees, through: :gatherers, source: :gather_attendee 
  belongs_to :creator, class_name: "User" 
end

class Gatherer < ActiveRecord::Base 
  belongs_to :gather_attendee, class_name: "User" 
  belongs_to :attended_gather, class_name: "Gather" 

end

2 Answers2

0

The naming here is not great. When naming your models choose nouns as models represent the actual things in your buisness logic - choosing verbs/adverbs makes the names of your assocations very confusing.

class User < ApplicationRecord
  has_many :gatherings_as_creator, 
    class_name: 'Gathering',
    foreign_key: :creator_id
  has_many :attendences
  has_many :gatherings, 
    through: :attendences
end

# think of this kind of like a ticket to an event
# rails g model Attendence user:references gathering:references
class Attendence < ApplicationRecord
  belongs_to :user
  belongs_to :gathering
end

# this is the proper noun form of gather
class Gathering < ApplicationRecord
  belongs_to :creator, 
    class_name: 'User'
  has_many :attendences
  has_many :attendees,
    though: :attendences,
    class_name: 'User'
end

My question is, do I need to a join table, such as Gatherer, to allow multiple attendees and then later pull a friend list for the user/creator ?

Yes. You always need a join table to create many to many assocations. Gatherer is a pretty confusing name for it though as that's a person who gathers things.

If you want to get users attending Gatherings created by a given user you can do it through:

User.joins(attendences: :groups)
    .where(groups: { creator_id: user.id })
max
  • 96,212
  • 14
  • 104
  • 165
-1

You're on the right track.

If I understand what you're looking for correctly, you want a Gather to have many Users and a User to have many Gathers (for the attending piece). So you need a join table like this (this is similar to your gatherers table, but is in a more conventional Rails style):

create_join_table :gathers, :users do |t|
  t.index [:gather_id, :user_id]
  t.index [:user_id, :gather_id]
end

And then you'd want your User model to be like this:

class User < ApplicationRecord
  has_many :gathers_as_creator, foreign_key: :creator_id, class_name: "Gather"
    
  has_and_belongs_to_many :gathers
end
class Gather < ApplicationRecord
  belongs_to :creator, class_name: "User"
  has_and_belongs_to_many :users
end

(You can change the name of that :users association if you really want, by specifying extra options -- I just like to keep to the Rails defaults as much as I can.)

That should be the bulk of what you need. If you want to pull all the friends of a creator for a specific gather, you would just do gather.users. If you want to pull all of the friends of a creator across all their gathers, that will be:

creator = User.find(1)
friends = User.joins(:gathers).where(gathers: { creator: creator }).all
Mike A.
  • 3,189
  • 22
  • 20
  • Don't use `create_join_table` and HABTM here - you're almost guarenteed to want to add additional columns to that join table at some point and it doesn't add the primary key column and names the tables wrong. HABTM is near useless in reality. – max Feb 11 '23 at 22:08
  • I disagree. I've used HABTM on several production projects and it's been great. It's true that if your join table represents something "real" and needs extra columns then you'll want to create a custom model for it and not use HABTM, but creating a custom model if you know you won't need one is overkill. I'd advise the question poster to decide what's right for them by reading the Rails guide: https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many – Mike A. Feb 11 '23 at 22:15
  • I would rather have a proper table with a PK and timestamps then to have to try to fix that later because of a stupid premature optimization. But to each his own I guess. – max Feb 11 '23 at 22:19
  • You can add timestamps easily to the HABTM join tables (`t.timestamps` inside the `create_join_table` block). There's nothing that really needs to be "fixed" later and this isn't an optimization. It's just about making the right decision for the needs of the application (edit: and, to be clear, I'm not claiming I know what is right for this person's needs). – Mike A. Feb 11 '23 at 22:29
  • Well except that all your timestamp columns for existing data will be empty which would make it impossible to sort by the `created_at` column for example. I would suggest that you instead switch to HABTM if having a model ever becomes an issue and you need that optimization. – max Feb 12 '23 at 09:18