Joining multiple table order - join

I think I have read somewhere that join in associative and commutative so order of table in join query is irrelevant, is this true?!!
If so then I have a situation here where I have 4 tables like this:
S(s_id, sname, status, city) supplier
J(j_id, jname, city) job/project
P(p_id, pname, color, weight, city) part
SPJ(s_id, p_id, j_id, qnty) supplier - parts - jobs
So my question is are the following joins the same?
S join J join SPJ
S join SPJ join J

Related

How to write sub query in active record?

I have two tables users and posts and they have association of has_many. I want to fetch details of both users and posts in a single query. I'm able to manage the sql query but I don't want to use the raw query in the code (using execute method) as i think it is kind of simple thing and can be written using active record.
Here is the sql query
SELECT a.id, a.name, a.timestamp, b.id, b.user_id, b.title
FROM users a
INNER JOIN (SELECT id, user_id, title, from, to FROM posts) b on b.user_id = a.id
where id IN ( 1, 2, 3);
I think includes does not help here because i'm dealing with large data.
Can any one help me ?
If you just want those specific columns and nothing else then this will work
User.joins(:post)
.where(id: [1,2,3])
.select("users.id, users.name, users.timestamp,
posts.id as post_id, posts.user_id as post_user_id,
posts.title as post_title")
This will return an ActiveRecord::Relation of User objects with virtual attributes for post_id, post_user_id (Not sure why you need this one since you already selected users.id), and post_title.
The query produced will be
SELECT users.id,
users.name,
users.timestamp,
posts.id as post_id,
posts.user_id as post_user_id,
posts.title as post_title
FROM users
INNER JOIN posts on posts.user_id = users.id
where users.id IN ( 1, 2, 3);
Please note you may have multiple User objects, one for each Post, just as the SQL query does.
You can execute your exact query using the string version of joins e.g.
User.joins("INNER JOIN (SELECT id, user_id, title, from, to FROM posts) b on b.user_id = users.id")
.where(id: [1,2,3])
.select("users.id, users.name, users.timestamp,
b.id as post_id, b.user_id as post_user_id,
b.title as post_title")
Additionally to avoid some of the overhead you can use arel instead e.g.
users_table = User.arel_table
posts_table = Post.arel_table
query = users_table.project(Arel.star)
.join(posts_table)
.on(posts_table[:user_id].eq(users_table[:id]))
.where(users_table[:id].in([1,2,3]))
ActiveRecord::Base.connection.exec_query(query.to_sql)
This will return an ActiveRecord::Result with 2 useful methods columns (the columns selected) and rows. You can convert this to a Hash(#to_hash) but note that any columns with duplicate names (id for instance) will overwrite one another.
You could fix this by specifying the colums you want selected in the project portion. e.g. your current query would be:
query = users_table.project(
users_table[:id],
users_table[:name],
users_table[:timestamp],
posts_table[:id].as('post_id'),
posts_table[:user_id].as('post_user_id'),
posts_table[:title].as('post_title')
).join(posts_table)
.on(posts_table[:user_id].eq(users_table[:id]))
.where(users_table[:id].in([1,2,3]))
ActiveRecord::Base.connection.exec_query(query.to_sql).to_hash
Since none of the names collide now it can be structured into a nice Hash where the keys are the column names and the values or the row value for that record.
users = User.joins(:posts).includes(:posts).where(id: [1, 2, 3])
Will give you all the users with theirs posts.
then you can do whatever you want with them, but to access posts data for first retrieved user
first_user_posts = users.first.posts # this will not make additional DB queries as you used includes and data is already added
We use joins to have INNER JOIN statement in the SQL
We use includes to load all posts in the memory
I have two tables users and posts and they have association of
has_many. I want to fetch details of both users and posts in a single
query.
can be done with includes like
users = User.includes(:posts).where({posts: {user_id: [1,2,3]}})
other is eager_load and preload you can use as per your requirements, for more https://blog.arkency.com/2013/12/rails4-preloading/

Can I find a bookshelf that has book A and book (B, C, or D) in one activerecord query?

Imagine these associations:
class Bookshelf
has_many :book_associations, dependent: :destroy
has_many :books, through: :book_associations
end
class Book
has_many :book_associations, dependent: :destroy
has_many :bookshelves, through: :book_associations
end
class BookAssociation
belongs_to :book
belongs_to :bookshelf
end
I need to find all bookshelves that contain a book with ID A and a book with ID B, C, or D
I can do this in a multi-step process using ruby like:
bookshelf_ids1 = Book.find(A).bookshelves.pluck(:id)
bookshelf_ids2 = Book.where(id: [B, C, D]).map(&:bookshelves).flatten.uniq.pluck(:id)
Bookshelf.where(id: bookshelf_ids1 & bookshelf_ids2)
But there must be a way to do this in one line, either through ActiveRecord or a raw SQL query.
What this question boils down to is that you're looking for an intersection of Bookshelf objects in set A (contains book with ID a) and Bookshelf objects in set B (contains book with ID in array b).
I don't recall seeing any easy way to express this intersection using a single ActiveRecord query. And as you probably have suspected, a multi query approach wouldn't scale well. Why run three queries when you can run one?
So here's my solution:
Finding the Bookshelve IDs
SELECT DISTINCT b1.bookshelf_id
FROM book_associations b1
INNER JOIN book_associations b2 ON b1.bookshelf_id = b2.bookshelf_id
WHERE b1.book_id = 1 AND b2.book_id IN (2,3,4);
This is a bit complicated so let me break it down. We are joining the book_associations table on itself, on its own bookshelf_id. This has the effect of making two tables available for filtering. We then filter query table b1 for the first criteria (ID = 1), and filter query table b2 for the other criteria (ID in (2,3,4)). With the INNER JOIN we are then ensuring that we are only getting the intersection of tables b1 and b2. We retrieve only the bookshelf_id because we're looking only to retrieve the bookshelves. Finally, the DISTINCT query is the SQL equivalent of .uniq and ensures the returned values are unique.
Retrieving the Bookshelves
From here, we then need to instantiate the Bookshelf objects. While we could do this:
bookshelf_ids = ActiveRecord::Base.connection.query(["SELECT DISTINCT b1.bookshelf_id
FROM book_associations b1
INNER JOIN book_associations b2 ON b1.bookshelf_id = b2.bookshelf_id
WHERE b1.book_id = ? AND b2.book_id IN (?);", 1, [2,3,4]])
bookshelves = Bookshelf.find(bookshelf_ids)
It's still two steps. Here's a single-step solution:
Bookshelf.find_by_sql(["SELECT * FROM bookshelves bs
WHERE bs.id IN (SELECT DISTINCT b1.bookshelf_id
FROM book_associations b1
INNER JOIN book_associations b2 ON b1.bookshelf_id = b2.bookshelf_id
WHERE b1.book_id = ? AND b2.book_id IN (?)
)", first_id,second_ids])
The Bookshelf.select_by_sql command instantiates records from the query results. The bookshelf_ids are retrieved in the subquery and used as a condition for the query on the bookshelves table.
Caveats
I haven't tested this code and can't confirm it will work, but the broad strokes should be correct.
The SQL should be valid for PostgresQL, but may require some tweaks depending on your specific DB implementation.
Edit
I've corrected the code above, I had joined the book_associations table on the wrong column (id) instead of the correct column bookshelf_id, and the subquery was returning the wrong column (again id when it should've been bookshelf_id).
I've created a proof of concept with test included.

Ruby on Rails - Has Many, Through: find multiple conditions

I realised its quite difficult to explain my problem with only words, so i'm going to use an example to describe what i am trying to do instead.
So for example:
#model Book
has_many: book_genres
has_many: genres, through: :book_genres
#model Genre
has_many: book_genres
has_many: books, through: :book_genres
So finding books that belong to one genre only would be relatively straightforward, such as:
#method in books model
def self.find_books(genre)
#g = Genre.where('name LIKE ?' , "#{genre}").take
#b = #g.books
#get all the books that are of that genre
end
So in rails console i can do Book.find_books("Fiction") and then i would get all the books that are of fiction genre.
But how can i find all the books that are both "Young Adult" and "Fiction" ? Or what if i would like to query for books that have 3 genres, such as "Young Adult", "Fiction" and "Romance" ?
I could do g = Genre.where(name: ["Young Adult", "Fiction", "Romance"]) but subsequent to that i cannot do g.books and get all the books that are related to this 3 genres.
I am actually quite bad with active record so im not even sure if theres a better way to query through Books directly instead of finding Genre then finding all books that are associated with it.
But what i cannot wrap my head around is how do i get all the books that have multiple (specific)genres?
UPDATE:
So the current answers provided Book.joins(:genres).where("genres.name" => ["Young Adult", "Fiction", "Romance"]) works, but the problem is it returns all books that has the genre of Young Adult OR Fiction OR Romance.
What query do i pass so that the books return has ALL 3 Genres and not only 1 or 2 out of the 3?
Matching any of the given genres
The following should work for both an Array and a String:
Book.joins(:genres).where("genres.name" => ["Young Adult", "Fiction", "Romance"])
Book.joins(:genres).where("genres.name" => "Young Adult")
In general, it's better to pass a Hash to where, rather than trying to write a SQL snippet yourself.
See the Rails Guides for more details:
http://guides.rubyonrails.org/active_record_querying.html#hash-conditions
http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-the-joined-tables
Matching all of the given genres with one query
A single query could be built and then passed to .find_by_query:
def self.in_genres(genres)
sql = genres.
map { |name| Book.joins(:genres).where("genres.name" => name) }.
map { |relation| "(#{relation.to_sql})" }.
join(" INTERSECT ")
find_by_sql(sql)
end
This means that calling Book.in_genres(["Young Adult", "Fiction", "Romance"]) will run a query that looks something like this:
(SELECT books.* FROM books INNER JOIN … WHERE genres.name = 'Young Adult')
INTERSECT
(SELECT books.* FROM books INNER JOIN … WHERE genres.name = 'Fiction')
INTERSECT
(SELECT books.* FROM books INNER JOIN … WHERE genres.name = 'Romance');
It has the upside of letting the database do the heavy lifting of combining the result sets.
The downside is that we're using raw SQL, so we can't chain this with other ActiveRecord methods, for example Books.order(:title).in_genres(["Young Adult", "Fiction"]) will ignore the ORDER BY clause we've tried to add.
We're also manipulating SQL queries as strings. It's possible we could avoid this using Arel, but the way Rails and Arel handle binding query values makes this pretty complicated.
Matching all of the given genres with multiple query
It's also possible to use multiple queries:
def self.in_genres(genres)
ids = genres.
map { |name| Book.joins(:genres).where("genres.name" => name) }.
map { |relation| relation.pluck(:id).to_set }.
inject(:intersection).to_a
where(id: ids)
end
This means that calling Book.in_genres(["Young Adult", "Fiction", "Romance"]) will run four queries that look something like this:
SELECT id FROM books INNER JOIN … WHERE genres.name = 'Young Adult';
SELECT id FROM books INNER JOIN … WHERE genres.name = 'Fiction';
SELECT id FROM books INNER JOIN … WHERE genres.name = 'Romance';
SELECT * FROM books WHERE id IN (1, 3, …);
The downside here is that for N genres, we're making N+1 queries. The upside is that this can be combined with other ActiveRecord methods; Books.order(:title).in_genres(["Young Adult", "Fiction"]) will do our genre filtering, and sort by title.
I didn't try this but I think it will work
Book.joins(:genres).where("genres.name IN (?)", ["Young Adult", "Fiction", "Romance"])
Here is how I would do it in SQL:
SELECT *
FROM books
WHERE id IN (
SELECT bg.book_id
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE g.name LIKE 'Young Adult'
INTERSECT
SELECT bg.book_id
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE g.name LIKE 'Fiction'
INTERSECT
...
)
The inner query will contain only books belonging to all the genres you ask about.
Here is how I'd do it in ActiveRecord:
# book.rb
def self.in_genres(genre_names)
subquery = genre_names.map{|n|
<<-EOQ
SELECT bg.book_id
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE g.name LIKE ?
EOQ
}.join("\nINTERSECT\n")
where(<<-EOQ, *genre_names)
id IN (
#{subquery}
)
EOQ
end
Note that I am using ? to avoid sql injection vulnerabilities, which is a problem in the code you proposed in your question.
Another approach would be to use multiple EXISTS conditions with correlated sub-queries:
SELECT *
FROM books
WHERE EXISTS (SELECT 1
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE g.name LIKE 'Young Adult'
AND bg.book_id = books.id)
AND EXISTS (SELECT 1
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE g.name LIKE 'Fiction'
AND bg.book_id = books.id)
AND ...
You'd construct this query in ActiveRecord similarly to the first approach. I'm not sure which would be faster, so you could try both if you like.
Here is yet another way to do the SQL---possibly fastest:
SELECT *
FROM books
WHERE id IN (
SELECT bg.book_id
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE (g.name LIKE 'Young Adult' OR g.name LIKE 'Fiction' OR ...)
GROUP BY bg.book_id
HAVING COUNT(DISTINCT bg.genre_id) >= 2 -- or 3, or whatever
)

How to join tables to get only the last record of has-many association?

I have two models: Company and Transaction (has-many relationship). Transaction model has balance attribute. I have a query to join the models:
scope :joined_transactions, (lambda do
select('transactions.balance as current_balance')
.joins('LEFT OUTER JOIN transactions ON transactions.company_id = companies.id')
end)
However, I want to include only the last transaction into this query. As a result Company.joined_transactions.first.current_balance == Company.first.transactions.last.balance should be true.
subquery .joins('left join transactions t on t.company_id = companies.id AND t.id = (SELECT MAX(id) FROM transactions WHERE transactions.company_id = t.company_id)')
I believe this problem was already solved many times. You can do it using subquery or double join.
Please add more info on your current case if this doesn't work for you

Left outer join query (i think)

I have two tables that look like this:
Products: id category name description active
Sales_sheets: id product_id link
product_id is a foreign key from the products id table
I wrote a prepared statement JOIN like this which works:
SELECT p.name, p.description, s.link FROM products AS p
INNER JOIN sales_sheets AS s ON p.id = s.product_id WHERE active=1 AND category=?
Basically a product can have a link to a PDF, but not every product will have a sales sheet. So if i try to bring up a product which doesn't have a sales sheet attached to it then it always returns no rows.
So i thought I'd have to use a LEFT OUTER JOIN in place of the INNER JOIN, but that returns no rows too, am I naming the tables in the wrong order? I've never had to use an OUTER join before?
SELECT p.name, p.description, s.link FROM products p
LEFT JOIN sales_sheets s ON p.id = s.product_id
WHERE active = 1 && category = ?

Resources