Rails: Querying through 2 join table associations - ruby-on-rails

I have a quite complicated relation between models and are now frustrated by a SQL Query to retrieve some objects.
given a Product model connected to a category model via a has_many :through association and a joint table categorization.
Also a User model connected to this category model via a has_many :through association and a joint table *category_friendship*.
I am now facing the problem to retrieve all products, which are within the categories of the array user.category_ids. However, I can't just not manage to write the WHERE statement properly.
I tried this:
u = User.first
uc = u.category_ids
Product.where("category_id IN (?)", uc)
However this won't work, as it doesn't have a category_id in the product table directly. But how can I change this to use the joint table categorizations?
I'm giving you the model details, maybe you find it helpful for answering my question:
Product.rb
class Product < ActiveRecord::Base
belongs_to :category
def self.from_users_or_categories_followed_by(user)
cf = user.category_ids
uf = user.friend_ids
where("user_id IN (?)", uf) # Products out of friend_ids (uf) works fine, but how to extend to categories (cf) with an OR clause?
end
Category.rb
class Category < ActiveRecord::Base
has_many :categorizations
has_many :products, through: :categorizations
has_many :category_friendships
has_many :users, through: :category_friendships
Categorization.rb
class Categorization < ActiveRecord::Base
belongs_to :category
belongs_to :product
Category_friendship.rb
class CategoryFriendship < ActiveRecord::Base
belongs_to :user
belongs_to :category
User.rb
class User < ActiveRecord::Base
has_many :category_friendships
has_many :categories, through: :category_friendships
def feed
Product.from_users_or_categories_followed_by(self) #this should aggregate the Products
end
If you need more details to answer, please feel free to ask!

Looking at the associations you have defined and simplifying things. Doing a bit refactoring in what we have to achieve.
Product.rb
class Product < ActiveRecord::Base
belongs_to :category
end
User.rb
class User < ActiveRecord::Base
has_many :categories, through: :category_friendships
scope :all_data , includes(:categories => [:products])
def get_categories
categories
end
def feed
all_products = Array.new
get_categories.collect {|category| category.get_products }.uniq
end
end
Category.rb
class Category < ActiveRecord::Base
has_many :users, through: :category_friendships
has_many :products
def get_products
products
end
end
NO NEED OF CREATING CATEGORY_FRIENDSHIP MODEL ONLY A JOIN TABLE IS NEEDED WITH NAME CATEGORIES_FRIENSHIPS WHICH WILL JUST HAVE USER_ID AND CATEGORY_ID
USAGE: UPDATED
Controller
class UserController < ApplicationController
def index
#all_user_data = User.all_data
end
end
view index.html.erb
<% for user in #all_user_data %>
<% for products in user.feed %>
<% for product in products %>
<%= product.name %>
end
end
end

I've upvoted Ankits answer but I realized there is a more elegant way of handeling this:
given:
u = User.first
uc = u.category_ids
then I can retrieve the products out of the categories by using:
products = Product.joins(:categories).where('category_id IN (?)', uc)

Related

Extracting data using rails query from a join table

I have users table, books table and books_users join table. In the users_controller.rb I am trying extract the users who have filtered_books. Please help me to resolve that problem.
user.rb
has_many :books_users, dependent: :destroy
has_and_belongs_to_many :books, join_table: :books_users
book.rb
has_and_belongs_to_many :users
books_user.rb
belongs_to :user
belongs_to :book
users_controller.rb
def filter_users
#filtered_books = Fiction.find(params[:ID]).books
#users = **I want only those users who have filtered_books**
end
has_and_belongs_to_many does not actually use a join model. What you are looking for is has_many through:
class User < ApplicationRecord
has_many :book_users
has_many :books, through: :book_users
end
class Book < ApplicationRecord
has_many :book_users
has_many :users, through: :book_users
end
class BookUser < ApplicationRecord
belongs_to :book
belongs_to :user
end
If you want to add categories to books you would do it by adding a Category model and another join table. Not by creating a Fiction model which will just create a crazy amount of code duplication if you want multiple categories.
class Book < ApplicationRecord
has_many :book_users
has_many :users, through: :book_users
has_many :book_categories
has_many :categories, through: :book_categories
end
class BookCategory < ApplicationRecord
belongs_to :book
belongs_to :category
end
class Category < ApplicationRecord
has_many :book_categories
has_many :books, through: :book_categories
end
If you want to query for users that follow a certain book you can do it by using an inner join with a condition on books:
User.joins(:books)
.where(books: { title: 'Lord Of The Rings' })
If you want to get books that have a certain category:
Book.joins(:categories)
.where(categories: { name: 'Fiction' })
Then for the grand finale - to query users with a relation to at least one book that's categorized with "Fiction" you would do:
User.joins(books: :categories)
.where(categories: { name: 'Fiction' })
# or if you have an id
User.joins(books: :categories)
.where(categories: { id: params[:category_id] })
You can also add an indirect association that lets you go straight from categories to users:
class Category < ApplicationRecord
# ...
has_many :users, though: :books
end
category = Category.includes(:users)
.find(params[:id])
users = category.users
See:
The has_many :through Association
Joining nested assocations.
Specifying Conditions on Joined Tables
From looking at the code i am assuming that Book model has fiction_id as well because of the has_many association shown in this line Fiction.find(params[:ID]).books. There could be two approaches achieve this. First one could be that you use #filtered_books variable and extract users from it like #filtered_books.collect {|b| b.users}.flatten to extract all the users. Second approach could be through associations using fiction_id which could be something like User.joins(:books).where(books: {id: #filtererd_books.pluck(:id)})

How to get relation records over relation table issue

When I developed my first sandbox application, I wanted to get some records for relational table.
User.rb:
class User < ActiveRecord::Base
#has many followed articles
has_many :follow_articles
And FollowArticle model:
class FollowArticle < ActiveRecord::Base
belongs_to :user
belongs_to :article
end
And Article model:
class Item < ActiveRecord::Base
has_many :follow_articles
end
I want to get all followed articles of a user so in my controller I have:
#articles = current_user.follow_articles
which gave me:
ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_FollowArticle:x3014X2
In my view I can iterate over these articles:
<% #articles.each do |article| %>
<%= article.article.name %>
<% end %>
which works perfectly.
Can I do this in this way to get an Articles array instead of a FollowArticles array, something like:
#items = current_user.follow_articles
to return articles instead of followArticles?
Use has_many :through.
From the Guide:
A has_many :through association is often used to set up a many-to-many connection with another model. This association indicates that the declaring model can be matched with zero or more instances of another model by proceeding through a third model. For example, consider a medical practice where patients make appointments to see physicians. The relevant association declarations could look like this:
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
So, fully connecting the dots...
#User.rb
class User < ActiveRecord::Base
has_many :follow_articles
has_many :followed_articles, through: :follow_articles
end

Filtring query in :through association

I have 3 models:
class User < ActiveRecord::Base
has_many :categories
has_many :links, through: :categories
end
class Category < ActiveRecord::Base
belongs_to :user
has_many :links
end
class Link < ActiveRecord::Base
belongs_to :category
end
For given user I want to find all his links which have favorite field equals true. I'm learning rails from 'rails guides' and I searched there for simple query to this, but I didn't find anything. Finally I resolved problem using select iterator:
#links = current_user.links.select{ |l| l.favorite }
But I'm not sure it is a good solution. How do that in rails way?
To add to #Pierre Michard's answer, you may also wish to look at ActiveRecord Association Extensions, which will basically replace the scope in the Link model:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :categories
has_many :links, through: :categories do
def favorites
where favorite: true
end
end
end
This will allow you to call:
#links = current_user.links.favorites
That works fine but the SQL query generated by that code will be something like
SELECT * FROM links where links.user_id = ?
Then the links you're interested in will be filtered by the select method.
If your user has many links and few of them are favorites, it could be more efficient to select the favorites this way:
#links = current_user.links.where(favorite: true)
This will generate this kind of query:
SELECT * FROM links where links.user_id = ? AND links.favorite = 't'
you can either create a scope in your links model to filter favorite links.
class Links < ActiveRecord::Base
scope :favorites, -> { where(favorite: true) }
end
corresponding query:
#links = current_user.links.favorites
This can be more efficient because that will create less ActiveModel objets.
Many to many relationship needs to be,
class User < ActiveRecord::Base
has_many :categories
has_many :links, through: :categories
end
class Category < ActiveRecord::Base
belongs_to :user
belongs_to :link
end
class Link < ActiveRecord::Base
has_many :categories
has_many :users, through: :categories
end
And then you can fetch link records whose favorite column is true as,
#links = current_user.categories.include(:links).where('links.favorite = ?', true)

Rails optimal query to find all articles with has_many through

I'm struggling to find all articles by knowing only categories users have subscribed to. Each articles can have many categories they belong to, my models look like
Article:
class Article < ActiveRecord::Base
has_many :article_categories
has_many :categories, through: :article_categories
Category:
class Category < ActiveRecord::Base
has_many :article_categories
has_many :articles, through: :article_categories
ArticleCategory:
class ArticleCategory < ActiveRecord::Base
belongs_to :article
belongs_to :category
article_categories table is just a storage for Article Categories with two columns: article_id && category_id
So how do I make proper query, hopefully with AR, if I have array of categories id's:
#ids = #categories.map { |c| c.id }
If I understand your question correction, this should do:
#articles = Article.joins(:article_categories).where(article_categories: { category_id: #ids })
You may have another table to keep track of user_categories (categories subscribed to by a user - user_id, category_id)
class User < ActiveRecord::Base
has_many :category_users
has_many :categories, :through => :category_users
def articles
Article.joins(:article_categories).where( { :category_id => category_users.pluck(:category_id) } ).distinct
end
end

complex query... how to join many classes in rails?

I have the following associations:
class Venue < ActiveRecord::Base
has_many :sales
end
class Sale < ActiveRecord::Base
has_many :sale_lines
has_many :beverages, through: :sale_lines
end
class SaleLine < ActiveRecord::Base
belongs_to :sale
belongs_to :beverage
end
class Beverage < ActiveRecord::Base
has_many :sale_lines
has_many :sales, through: :sale_lines
has_many :recipes
has_many :products, through: :recipes
end
class Recipe < ActiveRecord::Base
belongs_to :beverage
belongs_to :product
end
class Product < ActiveRecord::Base
has_many :recipes
has_many :beverages, through: :recipes
end
I wan't to see the quantity of products sold by each venue, so basically I have to multiply the recipe.quantity by the sale_line.quantity of an specific product.
I would like to call #venue.calc_sales(product) to get the quantity sold of product.
Inside the class Venue I am trying to calculating it by:
class Venue < ActiveRecord::Base
has_many :sales
def calc_sales(product)
sales.joins(:sale_lines, :beverages, :recipes).where('recipes.product_id = ?', product.id).sum('sale_lines.quantity * recipe.quantity')
end
end
However, I can't access the recipes in that way.
Any idea on how to achieve it?
For the joins, you have to use a Hash to join a already-joined table. It's hard to explain, but here are some examples:
Venue.joins(:sales, :beverages) : This implies that the relations :sales and :beverages are declared on the Venue model.
Venue.joins(:sales => :beverages) : This implies that the relation :sales exists on the Venue model, and the relation :beverages exists on the Sale model.
Consider this:
Venue
has_one :sale
Venue.joins(:sales) : This would not work, you have to use the exact same name as the relation between the Venue model & Sale model.
Venue.joins(:sale) : This would work because you used the same name of the relation.
Attention: You have to use the pluralized name in the where clause:
Venue.joins(:sale).where(:sales => { :id => sale.id })
^^ ^^ # See the plural
In your case, you can do something like this:
sales.joins(:sale_lines => { :beverage => :recipes })
.where(:recipes => { :product_id => product.id })
.sum('sale_lines.quantity * recipes.quantity')

Resources