Join two has many through associations in Rails 5 - ruby-on-rails

I have two has many through associations in my application which allows me to assign maps to a user through an admin dashboard.
Here are the tables I have in my application.
Mapgroups
class Mapgroup < ApplicationRecord
belongs_to :map, optional: true
belongs_to :group, optional: true
end
Usergroups
class Usergroup < ApplicationRecord
belongs_to :user, optional: true
belongs_to :group, optional: true
end
Groups
class Group < ApplicationRecord
has_many :usergroups, dependent: :destroy
has_many :users, through: :usergroups, dependent: :destroy
has_many :mapgroups, dependent: :destroy
has_many :maps, through: :mapgroups, dependent: :destroy
end
Users
class User < ApplicationRecord
has_many :groups, through: :usergroups, dependent: :destroy
end
I have been using this method to fetch the maps that the user has access to with this method:
def fetch_maps
self.groups.flat_map { |g| g.maps }
end
However, according to my logs and testing, this is a very expensive way to do this. How can I join tables on an association like this to just get the maps in this method instead?
I have tried this joins query
def fetch_maps
Map.joins(mapgroups: [:group, :user]).where(user: self)
end
But get this error
Can't join 'Mapgroup' to association named 'user'; perhaps you
misspelled it?
Any ideas on how I can join these tables to produce the maps the user has access to?
This is the usergroups table in my schema file:
create_table "usergroups", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t|
t.bigint "user_id"
t.bigint "group_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["group_id"], name: "index_usergroups_on_group_id"
t.index ["user_id"], name: "index_usergroups_on_user_id"
end
Maps model
class Map < ApplicationRecord
has_many :units, dependent: :destroy
belongs_to :subaccount, optional: true
belongs_to :term, optional: true
has_many :mapgroups, dependent: :destroy
has_many :groups, through: :mapgroups, dependent: :destroy
end

Your user.rb will be like this
class User < ApplicationRecord
has_many :usergroups
has_many :groups, through: :usergroups, dependent: :destroy
end
And reference the user with usergroups
and you don't need to dependent: :destroy use twice like
has_many :usergroups #=> remove this from this line , dependent: :destroy
has_many :users, through: :usergroups, dependent: :destroy
The has_many :through Association

Related

Rails: Favoriting System - models associations

I have the following model associations setup were a consumer can favorite a product or a variant. I just wanted to ask if my approach is correct?
class Favorite < ApplicationRecord
belongs_to :consumer
belongs_to :favorited, polymorphic: true
belongs_to :product, optional: true
belongs_to :variant, optional: true
end
class Consumer < ApplicationRecord
has_many :favorites
has_many :favorite_products, through: :favorites, source: :favorited, source_type: 'Product'
has_many :favorite_variants, through: :favorites, source: :favorited, source_type: 'Variant'
end
class Product < ApplicationRecord
has_many :favorites, dependent: :destroy
end
class Variant < ApplicationRecord
has_many :favorites, dependent: :destroy
end
class CreateFavorites < ActiveRecord::Migration[6.0]
def change
create_table :favorites do |t|
t.references :consumer, index: true
t.references :favorited, polymorphic: true, index: true
t.integer :product_id
t.integer :variant_id
t.timestamps
end
end
end
It is not clear why you have both polymorphic favorited and separate relation on product_id/variant_id. Usually one would go with one of these approaches.
Polymorphic associations can be unidirectional too - has_many :favorites, as: :favorited, dependent: :destroy
If you go with separate columns for each relation - indexes on product_id and variant_id may also be useful to prevent full table scan on product/variant deletion (on dependent: :destroy, also if you do not expect callbacks/nested relations there - delete_all is faster).

Rails: How to handle associations among namespaced models?

I am trying to figure out what's the best way to handle namespaced models. Here's the models that i have in my project:
class Member < ApplicationRecord
has_one :ledger, inverse_of: :member, class_name: "Member::Ledger", dependent: :destroy
has_many :ledger_entries, through: :ledger
end
class Member::Ledger < ApplicationRecord
belongs_to :member, inverse_of: :ledger
has_many :ledger_entries, foreign_key: "member_ledger_id", dependent: :destroy
end
class Member::LedgerEntry < ApplicationRecord
belongs_to :ledger, foreign_key: "member_ledger_id"
end
And here's how my migrations files look like:
create_table :members do |t|
t.timestamps
end
create_table :member_ledgers do |t|
t.references :member, foreign_key: true, null: false, index: { unique: true }
t.timestamps
end
create_table :member_ledger_entries do |t|
t.references :member_ledger, foreign_key: true, null: false
t.timestamps
end
So I have few questions here:
Are migration files correct? I mean should i have member_ledger_id in the member_ledger_entries table or just ledger_id?
Are associations defined in a correct way? Even though this works but i am not sure this is how we are supposed to proceed.
I am using ruby-2.5.1 and rails-5.2.0.
Any help would be appreciated. Thanks in advance !!
Perhaps your associations could look more like:
class Member < ApplicationRecord
has_one :member_ledger, inverse_of: :member, dependent: :destroy
has_many :member_ledger_entries, through: :member_ledger
end
class Member::Ledger < ApplicationRecord
belongs_to :member, inverse_of: :member_ledger
has_many :member_ledger_entries, dependent: :destroy
end
class Member::LedgerEntry < ApplicationRecord
belongs_to :member_ledger
end

Rails Many to Many with Extra Column

So I have these tables:
create_table :users do |t|
t.string :username
t.string :email
t.string :password_digest
t.timestamps
end
create_table :rooms do |t|
t.string :name
t.string :password
t.integer :size
t.integer :current_size
t.timestamps
end
create_table :rooms_users do |t|
t.belongs_to :user, index: true
t.belongs_to :room, index: true
t.boolean :is_admin
t.timestamps
end
I made it so, when I call Room.find(1).users I get a list of all the users in the room. However, I also want to be able to call something like Room.find(1).admins and get a list of users that are admins (where is_admin in rooms_users is true). How would I do that?
Thank you for your time!
You want to use has_many through: instead of has_and_belongs_to_many. Both define many to many associations but has_many through: uses a model for the join rows.
The lack of a model makes has_and_belongs_to_many very limited. You cannot query the join table directly or add additional columns since the rows are created indirectly.
class User < ApplicationRecord
has_many :user_rooms
has_many :rooms, through: :user_rooms
end
class Room < ApplicationRecord
has_many :user_rooms
has_many :users, through: :user_rooms
end
class UserRoom < ApplicationRecord
belongs_to :user
belongs_to :room
end
You can use your existing schema but you need to rename the table users_rooms to user_rooms with a migration - otherwise rails will deride the class name as Rooms::User.
class RenameUsersRooms < ActiveRecord::Migration[5.0]
def change
rename_table(:users_rooms, :user_rooms)
end
end
However, I also want to be able to call something like
Room.find(1).admins and get a list of users that are admins (where
is_admin in rooms_users is true). How would I do that?
You want to use a left inner join:
User.joins(:user_rooms)
.where(user_rooms: { room_id: 1, is_admin: true })
To roll that into the class you can setup an association with a scope applied:
class Room < ApplicationRecord
has_many :user_rooms
has_many :users, through: :user_rooms
has_many :user_room_admins, class_name: 'UserRoom', ->{ where(is_admin: true) }
has_many :user_room_admins, through: :user_rooms,
class_name: 'User',
source: :user
end
You can define a proc in the has_many relation to set SQL clauses, like ORDER or WHERE:
# room.rb
has_many :rooms_users, class_name: 'RoomsUser'
has_many :users, through: :rooms_users
has_many :admins,
proc { where(rooms_users: { is_admin: true }) },
through: :rooms_users,
class_name: 'User',
source: :users
# user.rb
has_many :administrated_rooms,
proc { where(rooms_users: { is_admin: true }) },
through: :rooms_users,
class_name: 'Room',
source: :rooms
You can simplify this with a simple scope defined in the RoomsUser model, something like:
# rooms_user.rb
scope :as_admins, -> { where(is_admin: true) }
And use it in the proc:
# user.rb
has_many :administrated_rooms,
proc { as_admins },
through: :rooms_users,
class_name: 'Room',
source: :rooms
source option explained:
With source: :users, we're telling Rails to use an association called :users on the RoomsUser model (as that's the model used for :rooms_users).
(from Understanding :source option of has_one/has_many through of Rails)

Is there a way to have two join tables associate the same two classes in a rails application?

Long time listener, first time caller. I'm trying to create two associations between the same database tables, Chatrooms and Users. What I have so far is a has_many through relationship where a Chatroom has many Users through Messages. This part works fine. What I want to do is to create a second join table that connects Chatrooms to Users, through a join table called Chatroom_players. So what I'd like is for Chatroom.first.users to get me users through the messages join table and Chatroom.first.players to get me everyone from the chatroom_players join table. The reason I want this is so that I can maintain user presence even if a user hasn't written any messages in the chat, also so that a user can leave the room but maintain his or her messages in the chat.
Here is what I have so far that does not work:
chatroom.rb:
class Chatroom < ApplicationRecord
has_many :messages, dependent: :destroy
has_many :users, through: :messages
has_many :chatroom_players
has_many :users, through: :chatroom_players
end
message.rb:
class Message < ApplicationRecord
belongs_to :chatroom
belongs_to :user
validates :content, presence: true, length: {minimum: 2, maximum: 200}
end
chatroom_player.rb
class ChatroomPlayer < ApplicationRecord
belongs_to :chatroom
belongs_to :user
end
user.rb
class User < ApplicationRecord
has_many :messages, dependent: :destroy
has_many :chatrooms, through: :messages
has_many :chatroom_players
has_many :chatrooms, through: :chatroom_players
end
chatroom_players migration:
class AddChatroomPlayers < ActiveRecord::Migration[5.0]
def change
create_table :chatroom_players do |t|
t.references :user, index: true, foreign_key: true, null: false
t.references :chatroom, index: true, foreign_key: true, null: false
t.boolean :creator, default: false
t.timestamps null: false
end
end
end
You need to use different names for the associations:
class Chatroom < ApplicationRecord
has_many :messages, dependent: :destroy
has_many :users, through: :messages
has_many :chatroom_players
# this is a separate association to users through the
# chatroom_players table.
has_many :participants,
through: :chatroom_players,
source: :user, # what association on chatroom_players to use
class_name: 'User' # since it cannot be deduced automatically
end

Rails : Has_Many Through polymophic Could not find the association in model

I'm in front of a little problem, I try to have a polymophic has many through association :
post.rb
has_many :categories, as: :categorizable, through: :categorizations
category.rb
has_many :categorizables, through: :categorizations, dependent: :destroy
event.rb
has_many :categories, as: :categorizable, through: :categorizations
categorization.rb
belongs_to :categorizable, :polymorphic => true
belongs_to :category
My migration :
def change
create_table :categories do |t|
t.string :name
t.timestamps
end
create_table :categorizations do |t|
t.references :category
t.integer :categorizable_id, :polymorphic => true
t.string :categorizable_type, :polymorphic => true
t.datetime :created_at
end
add_index :categorizations, :category_id
end
the problem :
I got this error :
Could not find the association :categorizations in model Post
Or when I try in category
Could not find the association :categorizations in model Category
Does anyone know where is the problem?
You need to specify :categorizations association also, in Category, Post and Event. Also, your as option should go to categorizations association, since this is where you have polymorphism.
Post class:
class Post < ActiveRecord::Base
has_many :categorizations, as: :categorizable
has_many :categories, through: :categorizations
# ...
end
You should modify Event class in similar manner.
Category class:
class Category < ActiveRecord::Base
has_many :categorizations
has_many :categorizables, through: :categorizations, dependent: :destroy
# ...
end

Resources