Rails has_many through intermediate model - ruby-on-rails

In rails, is there a way to fetch transitive models. We have following model structure.
A customer has many purchases and a purchase has many orders. There is no direct relation between customer and order model. They can be linked through purchase model. Now I want to fetch all orders belongs to a customer. Is there a way of achieving this through a single query. Our current models look something like.
Customer
- customer_id
Purchase
- purchase_id
- customer_id
Order
- order_id
- purchase_id
- status
My usecase is to given a customer object, list all orders of a customer which are in a specific state (e.g status = 'Complete').
Row SQL would look something like
SELECT purchase_id, order_id FROM Customer c INNER JOIN Purchase p ON p.customer_id = c.customer_id INNER JOIN Order o ON o.purchase_id = p.purchase_id WHERE o.status = 'Complete';

You can do with this:
Order.select('purchases.id AS purchase_id, orders.id AS order_id').joins(purchase: :customer).where('orders.status = ?', 'Complete')
I hope this help you.

Related

Ordering with condition by associated records count

I'm using rails 5 and I'm trying to bring all the Users order by the amount of Books read.
users :has_many books
I have tried
users = user.joins(:books).where('books.read = ?', 1).group('users.id').order("count(books.user_id)")
But this is removing all the users that haven't read books instead of show them last.
The column books.read can have several values. Users can have many or 0 books.
I want the users with books and no books orders by books.read = 1.
How can I achieved this?
Probably you need LEFT JOIN with required condition, but without WHERE clause
User.
joins('LEFT JOIN books on books.user_id = users.id AND books.read = 1').
group(:id).
order('COUNT(books.id) DESC')

Select records all of whose records exist in another join table

In the following book club example with associations:
class User
has_and_belongs_to_many :clubs
has_and_belongs_to_many :books
end
class Club
has_and_belongs_to_many :users
has_and_belongs_to_many :books
end
class Book
has_and_belongs_to_many :users
has_and_belongs_to_many :clubs
end
given a specific club record:
club = Club.find(params[:id])
how can I find all the users in the club who have all books in array of books?
club.users.where_has_all_books(books)
In PostgreSQL it can be done with a single query. (Maybe in MySQL too, I'm just not sure.)
So, some basic assumptions first. 3 tables: clubs, users and books, every table has id as a primary key. 3 join tables, books_clubs, books_users, clubs_users, each table contains pairs of ids (for books_clubs it will be [book_id, club_id]), and those pairs are unique within that table. Quite reasonable conditions IMO.
Building a query:
First, let's get ids of books from given club:
SELECT book_id
FROM books_clubs
WHERE club_id = 1
ORDER BY book_id
Then get users from given club, and group them by user.id:
SELECT CU.user_id
FROM clubs_users CU
JOIN users U ON U.id = CU.user_id
JOIN books_users BU ON BU.user_id = CU.user_id
WHERE CU.club_id = 1
GROUP BY CU.user_id
Join these two queries by adding having to 2nd query:
HAVING array_agg(BU.book_id ORDER BY BU.book_id) #> ARRAY(##1##)
where ##1## is the 1st query.
What's going on here: Function array_agg from the left part creates a sorted list (of array type) of book_ids. These are books of user. ARRAY(##1##) from the right part returns the sorted list of books of the club. And operator #> checks if 1st array contains all elements of the 2nd (ie if user has all books of the club).
Since 1st query needs to be performed only once, it can be moved to WITH clause.
Your complete query:
WITH club_book_ids AS (
SELECT book_id
FROM books_clubs
WHERE club_id = :club_id
ORDER BY book_id
)
SELECT CU.user_id
FROM clubs_users CU
JOIN users U ON U.id = CU.user_id
JOIN books_users BU ON BU.user_id = CU.user_id
WHERE CU.club_id = :club_id
GROUP BY CU.user_id
HAVING array_agg(BU.book_id ORDER BY BU.book_id) #> ARRAY(SELECT * FROM club_book_ids);
It can be verified in this sandbox: https://www.db-fiddle.com/f/cdPtRfT2uSGp4DSDywST92/5
Wrap it to find_by_sql and that's it.
Some notes:
ordering by book_id is not necessary; #> operator works with unordered arrays too. I just have a suspicion that comparison of ordered array is faster.
JOIN users U ON U.id = CU.user_id in 2nd query is only necessary for fetching user properties; in case of fetching user ids only it can be removed
It appears to work by grouping and counting.
club.users.joins(:books).where(books: { id: club.books.pluck(:id) }).group('users.id').having('count(*) = ?', club.books.count)
If anyone knows how to run the query without intermediate queries that would be great and I will accept the answer.
This looks like a situation where you'd make two queries, one to get all the ids you need, the other select perform a WHERE IN.

Rails 4 Selecting uniq objects related AR CollectionProxy

I'm dealing with a shopping cart, which has many products in it. The products can be sold by multiple different companies. What I'm trying to do is select the companies uniquely so that I can create one order per vendor.
the hack
companies = []
#cart.products.each { |p| companies << p.company }
companies.uniq
#create order for each company
I'm not sure if #pluck is something I should be using here, but I do know that there has got to be a more efficient way of collecting my companies "uniquely". #uniq doesn't seem to be working for me, and neither does #cart.products.pluck(:company)
I have no :company_id in my Bid model, it's a has_many/has_one relationship
pluck is used for retrieving array of values in provided column like:
#cart.products.pluck(:company_id) # => [1,2,3]
For collecting companies you can do companies = #cart.products.collect(&:company).uniq

Active Record or SQL query for leaderboard (postgres rank and sum)

I am looking or help creating an advanced query in Rails/Active Record (or SQL using Postgres) for a contest.
I have a contest table, a users table and an activities table. Contests have many users (participants) and users have many activities. Each activity has points, a 'trackable_type', and other attributes.
What I want selected:
- first_name from the users table
- last_name from the users table
- a sum of the points for each user as total_contest_points
- a count of activities that have the type 'Course' for each user as total_contest_courses
- the results returned ranked by total_contest_points and then total_contest_courses
I have taken a look at the postgres_ext gem and tried writing sql, but I can't seem to get the ranking to work. Here's what I have so far:
time_range = start_time..end_time
contestants = participants.joins(:activities).where('activities.created_at' => time_range).select("
users.id,
users.first_name,
users.last_name,
sum(activities.points) as total_contest_points,
sum(
case
when
activities.trackable_type='Course'
then
1
else
0
end
) as total_contest_courses,
rank() OVER (ORDER BY total_contest_points DESC) as contest_rank
").group('users.id').limit(limit)
Thanks for your help and suggestions.

How can I display the 1 to many and many to 1 relationship in RoR?

I have a table called "order" which has many "order_items" , each "order_items" is belongs_to "order" and "product". In the db, I have one record in order. the record is like this:
orders table: id = 1 name= customer
and the order_items table is like this:
id=1 product_id=233 order_id =1
id=2 product_id=454 order_id =1
I have the #orders obj, how can I use the orders and find out the order items and products information?
You should be able to use:
#orders.order_items
and
#orders.product
Check out the following page:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
..pay particular attention to the first introductory section.

Resources