complex query... how to join many classes in rails? - ruby-on-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')

Related

Creating a has_one through a has_many relationship

I currently have a ProductSale model that has_many sales.
Also a sale belongs to an invoice.
My goal is to access an invoice through a ProductSale's association to sales. (product_sale.invoice)
Current ProductSale model below:
class ProductSale < ApplicationRecord
has_many :sales
has_one :invoice, through: :sales
end
However my current error is saying that this can't be done because the :through association is a collection, which i understand. Is there a way that this can be possible?
class Sale < ApplicationRecord
belongs_to :invoice
end
class Invoice < ApplicationRecord
has_many :sales, inverse_of: :invoice, dependent: :destroy
end
All the sales on the ProductSale object have the same invoice. You know that you can just use the invoice of the first sale, but associations won't know that all the sales have the same invoice so you can use any, for instance the first one.
To have a invoices method to get each of the invoices you could do this:
class ProductSale < ApplicationRecord
has_many :sales
has_many :invoices, through: :sales
end
However, if you want to use your business logic that all the invoices are assumed to be the same, you will have to write a method to implement that business logic.
class ProductSale < ApplicationRecord
has_many :sales
def invoice
sales.first&.invoice
end
end
If all the sales on the invoice will be sales in the ProductSale object, then maybe you should refactor as below.
class ProductSale < ApplicationRecord
has_one :invoice
delegate :sales, :to => :invoice, :prefix => false, :allow_nil => true
end
Then you can both call the invoice method to get the invoice and also call the sales method to get all the sales on the invoice for the ProductSale object.
How about:
class ProductSale < ApplicationRecord
has_many :sales
has_one :sale
has_one :invoice, through: :sale
end

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)})

Should this has_many :through relationship be polymorphic or not?

I have a many-to-many relationship setup for Teachers and Classrooms via has_many :through:
class Teacher < ActiveRecord::Base
has_many :classrooms, :through => :classroom_memberships
end
class Classroom < ActiveRecord::Base
has_many :students
has_many :teachers, :through => :classroom_memberships
end
class ClassroomMemberships < ActiveRecord::Base
belongs_to :teacher
belongs_to :classroom
end
Currently, Students can only belong to one Classroom:
class Student < ActiveRecord::Base
belongs_to :classroom
end
Now I have the need to track historical classroom memberships for students, creating a second many-to-many relationship for classrooms. So, while a student can only belong to one classroom at a time, I need to know that last year, student A belonged to classroom B.
I'm thinking I have two viable options:
1.) Make the classroom_memberships association polymorphic so I'd have a classroomable_id and classroomable_type that would point to either a teacher OR a student.
2.) Simplify things and add another foreign key to ClassroomMemberships called student_id, in which case, for a given row, either student_id OR teacher_id would have a value.
Which is the better option?
I would probably go the route of:
class Course < ActiveRecord::Base
# like "MATH 100"
has_many :sections
has_many :teachers, :through => :sections
end
class Term < ActiveRecord::Base
# like "Fall 2015"
has_many :sections
end
class Teacher < ActiveRecord::Base
has_many :sections
has_many :courses, :through => :sections
end
class Section < ActiveRecord::Base
# a course, in a term, taught by a teacher, with registered students
belongs_to :term
belongs_to :course
belongs_to :teacher
has_many :registrations
has_many :students, :through => :registrations
end
class Registration < ActiveRecord::Base
# a student in a specific section
belongs_to :section
belongs_to :student
end
class Student < ActiveRecord::Base
# a student's registrations are their course history
has_many :registrations
has_many :sections, through :registrations
end
As a start, since this is a fairly basic modeling of an educational system.
It sounds like maybe you want a ClassroomMembershipHistory model.
Something like
class ClassroomMembershipHistory < ActiveRecord::Base
belongs_to :student
belongs_to :classroom
end
with a year attribute, stored however is easiest to query for your use case.

Rails habtm joins

I have this relationship between categories, products & brands:
class Brand < ActiveRecord::Base
has_many :products
end
class Category < ActiveRecord::Base
has_and_belongs_to_many :products
end
class Product < ActiveRecord::Base
has_and_belongs_to_many :categories
belongs_to :brand
end
How can I select all categories by specified brand with this relations?
I try this but get an error
b = Brand.find(1)
Category.joins(:products).where(:products => b.products)
You did the right thing with the join, just add a more complex where definition:
Category.joins(:products).where(:products => {:brand_id => 1})
Controversially HABTM's are rarely, if ever, a good design and IMO just about the only thing Rails got wrong.
Introduce an xref table to join products and categories and use has_many :through on both sides of the relationship so you end up with
class Brand < ActiveRecord::Base
has_many :products
has_many categories :through => products # This is now allowed in Rails 3.x and above
end
class Category < ActiveRecord::Base
belongs_to :product_category
has_many :products :through => product_category
end
class Product < ActiveRecord::Base
belongs_to :brand
belongs_to :product_category
has_many :categories :through => product_category
end
class ProductCategory < ActiveRecord::Base
has_many :products
has_many :categories
end
This gives you the best flexibility with the least amount of code re-factoring for you plus a much more intuitive path to get whatever data you need on either side of the relationship and will enable you to achieve the following
b = Brand.find(1)
b.categories.all
Update
The above is totally untested code and I have just corrected a glaringly stupid mistake I made. If you have any issues implementing this then come back

Rails: Querying through 2 join table associations

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)

Resources