Rails - loading related data on other side of many-to-many relationship - ruby-on-rails

I have Beta::Groups and Beta::Users.
Users can belong to more than one Group, but each Beta::User is also a regular User.
class Beta::User < ApplicationRecord
belongs_to :beta_group, class_name: "Beta::Group"
belongs_to :user
validates :user_id, uniqueness: { scope: :beta_group_id }
end
class Beta::Group < ApplicationRecord
has_many :beta_users, dependent: :destroy, class_name: "Beta::User", foreign_key: 'beta_group_id'
validates :name, presence: true, uniqueness: true
def beta_users_count
beta_users.count
end
end
Effectively, Beta::User is just a join table between Beta:Group and User
When I load a Beta::Group, how can I retrieve the data for all the user's in that group?
#beta_group = Beta::Group.find(beta_user_params[:beta_group_id])
#beta_users = #beta_group.beta_users.all
The last line only retrieves the beta_users data obviously (just the beta_group_id and user_id). I don't want to have to then iterate through all the user_id's to get the actual User's full data.
So how I can set this up so that I can do something like #beta_group.users.all to retrieve all the data for each user that is a Beta:User?
EDIT - What I have tried
I tried adding this to the Beta::Group model:
has_many :users, through: :beta_users, source: :user
But when I call beta_group.users the query that runs is as follows, returning an empty array:
SELECT "beta_users".* FROM "beta_users" INNER JOIN "beta_users" "beta_users_users" ON "beta_users"."id" = "beta_users_users"."user_id" WHERE "beta_users_users"."beta_group_id" = $1 [["beta_group_id", 1]]
Notice that it is not joining correctly. It should be trying to join on "beta_users"."user_id" to "users.id"

I believe you can add a has_many through association:
class Beta::Group < ApplicationRecord
has_many :beta_users, dependent: :destroy, class_name: "Beta::User", foreign_key: 'beta_group_id'
# here:
has_many :users, through: :beta_users, source: :user
validates :name, presence: true, uniqueness: true
def beta_users_count
beta_users.count
end
end
Then you should be able to call the association:
#beta_group = Beta::Group.find(beta_user_params[:beta_group_id])
#beta_users = #beta_group.users
EDIT - I believe you have an issue with scopes and the class_name because you have a User inside the Beta module.
Try adding the class_name: "::User" to both associations:
class Beta::User < ApplicationRecord
belongs_to :beta_group, class_name: "Beta::Group"
belongs_to :user, class_name: "::User" # without class_name it'll try a self association
end
class Beta::Group < ApplicationRecord
has_many :beta_users, dependent: :destroy, class_name: "Beta::User", foreign_key: 'beta_group_id'
has_many :users, class_name: "::User", through: :beta_users # without a class_name it'll try to join the Beta::User
end

The problem was due to the fact that User is a table in both the top-level namespace and inside the Beta namespace.
Therefore, it is necessary to inform rails which table to use to create the SQL for, by specifying the top-level class in the relation, like this:
class_name: '::User'
Final code:
class Beta::Group < ApplicationRecord
has_many :beta_users, dependent: :destroy, class_name: 'Beta::User', foreign_key: 'beta_group_id'
has_many :users, through: :beta_users, source: :user, class_name: '::User'
validates :name, presence: true, uniqueness: true
def beta_users_count
beta_users.count
end
end
which generates the correct SQL:
SELECT "users".* FROM "users" INNER JOIN "beta_users" ON "users"."id" = "beta_users"."user_id" WHERE "users"."viewer" = $1 AND "beta_users"."beta_group_id" = $2 [["viewer", false], ["beta_group_id", 1]]

Related

How do I combine the query from two different models

I have three models that look like this
Twit
class Twit < ApplicationRecord
belongs_to :user
has_many :retwits, foreign_key: :twit_id, class_name: "Retwit", dependent: :destroy
has_many :mentions, dependent: :destroy
validates :body, presence: true, length: { maximum: 280 }
end
Retwit
class Retwit < ApplicationRecord
belongs_to :twit, foreign_key: :twit_id, class_name: 'Twit'
belongs_to :retwiter, foreign_key: :retwiter_id, class_name: 'User'
validates :twit, uniqueness: { scope: :retwiter }
end
User
class User < ApplicationRecord
has_many :twits, dependent: :destroy
has_many :retwits, foreign_key: :retwiter_id, class_name: 'Retwit', dependent: :destroy
end
I want to query for Twits and Retwits where (twit.user = some_user or retwit.retwiter = some_user). I am using this code to do the query, however it becomes an array so I cant user ActiveRecords collection methods on it:
(Twit.where(user: people) + Retwit.where(retwiter: people))
I wanna be able to use ActiveRecords collection methods on it like so:
(Twit.where(user: people) + Retwit.where(retwiter: people)).offset(0).limit(5)
Which doesn't work since adding the two queries together return an array.
Is it possible to return a collection of Twits and Retwits in one single query? If so, how?

How to create a scope for current_user.following?

Here are my User and Relationships models
class User < ActiveRecord::Base
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
has_many :passive_relationships, class_name: "Relationship",
foreign_key: "followed_id",
dependent: :destroy
has_many :followers, through: passive_relationships, source: :follower
has_many :following, through: :active_relationships, source: :followed
class Relationship < ActiveRecord::Base
belongs_to :follower, class_name: "User", counter_cache: :followeds_count
belongs_to :followed, class_name: "User", counter_cache: :followers_count
validates :follower_id, presence: true
validates :followed_id, presence: true
validates :followed, uniqueness: { scope: [:follower, :followed] }
end
In Users Controller I can do:
#users = current_user.following
However I would like to turn this into a scope in my User model.
There are 2 things you may approach:
Find all users who are following someone
class User < ActiveRecord::Base
scope :following_to, -> (user_id) {
where(
"id IN ( SELECT followed_id
FROM relationships
WHERE follower_id = ?
)",
user_id
)
}
end
Find all users who are following anyone, that means they are a follower
class User < ActiveRecord::Base
scope :follower, -> {
where("id IN ( SELECT followed_id FROM relationships)")
}
end
Finally, you can use these scope as your expectation:
# Find all users who are following to User (id = 1)
User.following_to(1)
# Find all users who are following someone,
# aka they are a follower
User.follower
By using the Instance Method you can make a method For User Model
like this :
class User < ActiveRecord::Base
def following?
self.following.present?
end
end
By Using Scope you can call only the activerecord based query into the scope of model.
You should get also this way
scope :following?, lambda { |user|
{ user.following.present? }
And this should be call like in your controller
User.following?(current_user)

Rails has_many :through with the where clause

I've built easy Twitter application in Rails.
Now I would like to choose three random users that are not followed by the current user.
Here is my model:
class User < ActiveRecord::Base
has_many :tweets, dependent: :destroy
has_many :followerships, class_name: 'Followership', foreign_key: 'followed_id'
has_many :followedships, class_name: 'Followership', foreign_key: 'follower_id'
has_many :followers, through: :followerships, source: :follower
has_many :followed, through: :followedships, source: :followed
end
class Followership < ActiveRecord::Base
belongs_to :follower, class_name: "User"
belongs_to :followed, class_name: "User"
validates :follower_id, presence: true
validates :followed_id, presence: true
end
class Tweet < ActiveRecord::Base
belongs_to :user
end
I tried to use the following query:
User.where.not(followers: current_user).order("RANDOM()").limit(3)
But it obviously doesn't work as I get no such column: users.follower_id error.
Is it even possible to do without sql query?
Thank you!
Try this:
already_following = current_user.followed.map(&:id)
#users = User.where.not(id: already_following).order("RANDOM()").limit(3)
Basically what I did, was got the list of users already being followed. Then you check the User table for id's not matching users already being followed.

Rails: has_many through not returning correctly with namespaced models

I have 3 models. Rom::Favorite, Rom::Card, User. I am having an issue creating the User has_many rom_cards through rom_favorites
Here are my relevant parts of my models
Rom::Card
class Rom::Card < ActiveRecord::Base
has_many :rom_favorites, class_name: "Rom::Favorite", foreign_key: "rom_card_id", dependent: :destroy
self.table_name = "rom_cards"
end
User
class User < ActiveRecord::Base
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :role
has_many :rom_favorites, class_name: "Rom::Favorite", dependent: :destroy
has_many :rom_cards, class_name: "Rom::Card", through: :rom_favorites, class_name: "Rom::Favorite"
end
Rom::Favorite
class Rom::Favorite < ActiveRecord::Base
attr_accessible :rom_card_id, :user_id
belongs_to :user
belongs_to :rom_card, class_name: "Rom::Card"
validates :user, presence: true
validates :rom_card, presence: true
validates :rom_card_id, :uniqueness => {:scope => :user_id}
self.table_name = "rom_favorites"
end
All of the utility methods that come along with associations work except
a = User.find(1)
a.rom_cards
The call a.rom_cards returns an empty array and it seems to run this SQL query
SELECT "rom_favorites".* FROM "rom_favorites" INNER JOIN "rom_favorites" "rom_favorites_rom_cards_join" ON "rom_favorites"."id" = "rom_favorites_rom_cards_join"."rom_card_id" WHERE "rom_favorites_rom_cards_join"."user_id" = 1
I am not strong in SQL, but I think this seems correct.
I know a.rom_cards should return 2 cards because a.rom_favorites returns 2 favorites, and in those favorites are card_id's that exist.
The call that should allow rom_cards is the following
has_many :rom_cards, class_name: "Rom::Card", through: :rom_favorites, class_name: "Rom::Favorite"
I feel like the issue has something to do with the fact that it is trying to find the Users cards through favorites and it is looking for card_id (because I specified the class Rom::Card) instead of rom_card_id. But I could be wrong, not exactly sure.
You are duplicating the key class_name in the association hash. There is no need to write class_name: "Rom::Favorite" because by using through: :rom_favorites it will use the configuration options of has_many :rom_favorites.
Try with:
has_many :rom_cards, class_name: "Rom::Card", through: :rom_favorites

Self referenced has_many and deleting join records when any side of relation is destroyed

I have Board model. Each Board can be Source for other Board. This relationship is saved in Feed table.
class Board < ActiveRecord::Base
belongs_to :user
has_many :links, dependent: :destroy
has_many :feeds, dependent: :destroy
has_many :sources, through: :feeds
attr_accessible :description, :name, :user_id
validates :name, presence: true
end
class Feed < ActiveRecord::Base
belongs_to :board
belongs_to :source, class_name: "Board"
attr_accessible :board_id, :source_id
end
When I destroy Board record, then it destroys corresponding feeds. But how to do this the same but for source ?
Why don't you use :dependent => :destroy too?
Read Deleting from associations: has_many, has_one and belongs_to associations support the :dependent option
If that doesn't work you might also be able to use a ActiveRecord Callbacks:
after_destroy do |record|
other = BoardsSources.find_by_board_id_and_source_id(record.board_id, record.source_id)
other.destroy if other
end

Resources