Rails join and HABTM relationship - ruby-on-rails

In my application I have model Car which:
has_and_belongs_to_many :locations
Now I'm buidling searching and I want search Car which has given locations.
In my view I have:
.row
= horizontal_simple_form_for :cars, {url: cars_path, method: :get} do |f|
= f.input :handover_location, label: I18n.t('.handover'), collection: Location.all.map{|hl| [hl.location_address, hl.id]}
= f.input :return_location, label: I18n.t('.return') ,collection: Location.all.map{|rl| [rl.location_address, rl.id]}
= f.submit class: 'btn btn-success'
and in my controller I filter results based on params:
#cars = Car.joins(:locations).where("locations.id= ? AND locations.id= ?", params[:cars][:handover_location], params[:cars][:return_location])
But this code does not work properly. Maybe I shouldn't use "locations.id" twice?

I'm going to assume your join table is called cars_locations. If you wanted to do this just in sql, you could join this table to itself
... cars_locations cl1 join cars_locations cl2 on e1.car_id = e2.car_id ...
... which would make a pseudo-table for the duration of the query with this structure:
cl1.id | cl1.car_id | cl1.location_id | cl2.id | cl2.car_id | cl2.location_id
then query this for the required location_id - this will give you entries that have the same car at both locations - let's say the ids of the pickup and return locations are 123 and 456:
select distinct(cl1.car_id) from cars_locations cl1 join cars_locations cl2 on cl1.car_id = cl2.car_id where (c11.location_id = 123 and cl2.location_id = 456) or (cl1.location_id = 123 and cl2.location_id = 456);
Now we know the sql, you can wrap it into a method of the Car class
#in the Car class
def self.cars_at_both_locations(location1, location2)
self.find_by_sql("select * from cars where id in (select distinct(cl1.car_id) from cars_locations cl1 join cars_locations cl2 on cl1.car_id = cl2.car_id where (c11.location_id = #{location1.id} and cl2.location_id = #{location2.id}) or (cl1.location_id = #{location2.id} and cl2.location_id = #{location1.id}))")
end
This isn't the most efficient method, as joins on big tables start to get very slow.
A quicker method would be
def self.cars_at_both_locations(location1, location2)
self.find(location1.car_ids & location2.car_ids)
end
in this case we use & which is the "set intersection" operator (not to be confused with &&): ie it will return only values that are in both the arrays on either side of it.

You definitely shouldn't be using the locations.id twice in the where clause, as that is physically impossible. The resulting query from that will essentially try and find the location where it's id is both the handover location, AND the return location. So in essence, what you're asking for is something like
where 1 == 1 AND 1 == 2
Which needless to say, will always return nothing.
In theory if you just change the AND for an OR you'll get what you're after. This way, you'll be asking the database for any location that has an ID or either start_location OR handover_location
UPDATE
Re-read the question. It's a little tricker than I'd thought initially, so you'll probably need to do some processing on the results. As I've said, using the AND query the way you are is asking the database for something impossible, but using the OR as I originally said, will result in cars that have EITHER or the locations, not both. This could be done in raw SQL, but using Rails this is both awkward, and frowned upon, so here's another solution.
Query the data using the OR selector I originally proposed as this will reduce the data set considerably. Then manually go through it, and reject anything that doesn't have both locations:
locations = [params[:cars][:handover_location], params[:cars][:return_location]]
#cars = Car.joins(:locations).where("locations.id IN [?]")
#cars = #cars.reject { |c| !(locations - c.location_ids).empty? }
So what this does, is query all cars that have either of the requested locations. Then it loops through those cars, and rejects any whose list of location id's does not contain both of the supplied IDS. Now the remaining cars are available at both locations :)

Related

How to get a unique set of parent models after querying on child

Order has_many Items is the relationship.
So let's say I have something like the following 2 orders with items in the database:
Order1 {email: alpha#example.com, items_attributes:
[{name: "apple"},
{name: "peach"}]
}
Order2 {email: beta#example.com, items_attributes:
[{name: "apple"},
{name: "apple"}]
}
I'm running queries for Order based on child attributes. So let's say I want the emails of all the orders where they have an Item that's an apple. If I set up the query as so:
orders = Order.joins(:items).where(items: {name:"apple"})
Then the result, because it's pulling at the Item level, will be such that:
orders.count = 3
orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com", "beta#example.com"]
But my desired outcome is actually to know what unique orders there are (I don't care that beta#example.com has 2 apples, only that they have at least 1), so something like:
orders.count = 2
orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
How do I do this?
If I do orders.select(:id).distinct, this will fix the problem such that orders.count == 2, BUT this distorts the result (no longer creates AR objects), so that I can't iterate over it. So the below is fine
deduped_orders = orders.select(:id).distinct
deduped_orders.count = 2
deduped_orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
But then the below does NOT work:
deduped_orders.each do |o|
puts o.email # ActiveModel::MissingAttributeError: missing attribute: email
end
Like I basically want the output of orders, but in a unique way.
I find using subqueries instead of joins a bit cleaner for this sort of thing:
Order.where(id: Item.select(:order_id).where(name: 'apple'))
that ends up with this (more or less) SQL:
select *
from orders
where id in (
select order_id
from items
where name = 'apple'
)
and the in (...) will clear up duplicates for you. Using a subquery also clearly expresses what you want to do–you want the orders that have an item named 'apple'–and the query says exactly that.
use .uniq instead of .distinct
deduped_orders = orders.select(:id).uniq
deduped_orders.count = 2
deduped_orders.pluck(:email) = ["alpha#exmaple.com", "beta#example.com"]
If you want to keep all the attributes of orders use group
deduped_orders = orders.group(:id).distinct
deduped_orders.each do |o|
puts o.email
end
#=> output: "alpha#exmaple.com", "beta#example.com"
I think you just need to remove select(:id)
orders = Order.joins(:items).where(items: {name:"apple"}).distinct
orders.pluck(:email)
# => ["alpha#exmaple.com", "beta#example.com"]
orders = deduped_orders
deduped_orders.each do |o|
puts o.email # loop twice
end

Rails joins doesn't return values from joined table

It must be something pretty simple i'm doing wrong. i'm trying to join 3 tables together, where group contains a location_id which references a row in location and a info_id which does the same for info.
Here's my code:
#groups = Group.joins(
'INNER JOIN location on location.id = "group".id',
'INNER JOIN info on info.id = "group".id'
)
This seems to work without errors, but all i'm getting back is the column from my group table. What am I doing wrong here?
P.S. my associations are location and info belong_to group. and group has_one of location and info
Just add a select tag to select the desired columns.
Group.joins(:location, :info).select("location.*, info.*")
You may add some where class if need some special conditions too.
Group.joins(:location, :info).select("location.*, info.*").where("locations.id = conditions.id")
When you say that you have location_id and info_id in groups table, this means that you intend to have belongs_to association in Group.
Pre-requisite:
Group model should have:
belongs_to :location
belongs_to :info
Location and Info should have :
has_one :group
For your question, this is expected. In ActiveRecord, Relation will return the object of the invoker, in this case Group.
For your use case, I think you will need this approach to get the right join working:
Query:
#groups = Group.joins(:location, :info)
# "SELECT `groups`.* FROM `groups` INNER JOIN `locations` ON `locations`.`id` = `groups`.`location_id` INNER JOIN `infos` ON `infos`.`id` = `groups`.`info_id`"
After this, you can iterate on each group to get info and location as something like: #groups.map { |group| [group.location, group.info] }
This will give correct location and info.
Optimization: [group.location, group.info] will make queries again to get location and info. You can optimize this by changing the original query to include location and info data:
#groups = Group.joins(:location, :info).includes(:location, :info)
In Rails / AR we have includes and joins.
You are using joins. As the SQL shows, it only selects Group. You use joins when you need Group results but also want to query through Location and Info.
If you would use all information, which I suspect is your case, like to display details in a table, you should also use includes.
# #groups = Groups.includes(:locations, :info) results an left outer join
#groups = Groups.joins(:locations, :info).includes(:locations, :info) # results inner join
Now when you do some thing like this it will not make additional db calls. But if you use joins it use multiple queries (N+1).
- #groups.each do |group|
tr
td = group.id
td = group.location.lat
td = group.info.description
Use the bullet gem to find if you have such N+1 queries to optimize your project.
If you Google Rails includes vs joins you will find more information on the topic.
If you used this:
Group.joins(:location, :info).select("location.*, info.*").where("locations.id =
conditions.id")
and nothing changed
It is because the column names of the two tables are the same.

Rails - How can I avoid using the database while performing a detect method?

When performing detect on a int array, it works:
#number = [1,2,3,4,5,6,7].detect{|n| n == 4}
Variable #number becomes 4.
But when I do something like this:
#categories = Category.all
#current_category = #categories.detect{|cat| cat.id == params[:category]}
The program outputs
Category Load (0.2ms) SELECT "categories".* FROM "categories"
Which means it's using the database to find it.
However, the element I'm trying to find is already in the collection #categories, I just want to find it to assign it to a variable.
Of course another solution would be to implement a linear search algorithm, but I just want to keep the code as clean as possible.
How can I avoid using the database for this search?
EDIT: I just realized that this could be lazy fetching. Because before detect, I never use #categories, so it does the query when I do detect. Could this be true?
Rails is actually performing a SELECT COUNT(*) query when you call #categories.all, essentially performing a lazy-fetch.
Your #categories object still needs to query the database for the data.
See the documentation here: http://apidock.com/rails/ActiveRecord/Scoping/Named/ClassMethods/all
posts = Post.all
posts.size # Fires "select count(*) from posts" and returns the count
posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
fruits = Fruit.all
fruits = fruits.where(color: 'red') if options[:red_only]
fruits = fruits.limit(10) if limited?
In your case, you should use active record and SQL requesting.
#current_category = #categories.find_by(id: params[:category])
Using array methods on Active Record relations tend to fetch all the data then apply the algorithm in-memory, while SQL filtering is faster.
In you case I love to define the operator [] on my model:
#in category.rb
def self.[](x)
self.find_by(id: x)
end
# anywhere after:
if c = Category[params[:id]]
puts "Category found and it's #{c.name} !"
else
puts "Not found :("
end

Finding records which belong to multiple models with HABTM

My Track model has_and_belongs_to_many :moods, :genres, and :tempos (each of which likewise has_and_belongs_to_many :tracks).
I'm trying to build a search "filter" where users can specify any number of genres, moods, and tempos which will return tracks that match any conditions from each degree of filtering.
An example query might be
params[:genres] => "Rock, Pop, Punk"
params[:moods] => "Happy, Loud"
params[:tempos] => "Fast, Medium"
If I build an array of tracks matching all those genres, how can I select from that array those tracks which belong to any and all of the mood params, then select from that second array, all tracks which also match any and all of the tempo params?
I'm building the initial array with
#tracks = []
Genre.find_all_by_name(genres).each do |g|
#tracks = #tracks | g.tracks
end
where genres = params[:genres].split(",")
Thanks.
I'd recommend using your database to actually perform this query as that would be a lot more efficient.
You can try to join all these tables in SQL first and then using conditional queries i.e. where clauses to first try it out.
Once you succeed you can write it in the Active Record based way. I think its fairly important that you write it in SQL first so that you can properly understand whats going on.
This ended up working
#tracks = []
Genre.find_all_by_name(genres).each do |g|
g.tracks.each do |t|
temptempos = []
tempartists = []
tempmoods = []
t.tempos.each do |m|
temptempos.push(m.name)
end
tempartists.push(t.artist)
t.moods.each do |m|
tempmoods.push(m.name)
end
if !(temptempos & tempos).empty? && !(tempartists & artists).empty? && !(tempmoods & moods).empty?
#tracks.push(t)
end
end
end
#tracks = #tracks.uniq

Rails ActiveRecord Join

I'm using rails and am trying to figure out how to use ActiveRecord within the method to combine the following into one query:
def children_active(segment)
parent_id = Category.select('id').where('segment' => segment)
Category.where('parent_id'=>parent_id, 'active' => true)
end
Basically, I'm trying to get sub categories of a category that is designated by a unique column called segment. Right now, I'm getting the id of the category in the first query, and then using that value for the parent_id in the second query. I've been trying to figure out how to use AR to do a join so that it can be accomplished in just one query.
You can use self join with a alias table name:
Category.joins("LEFT OUTER JOIN categories AS segment_categories on segment_categories.id = categories.parent_id").where("segment_categories.segment = ?", segment).where("categories.active = ?", true)
This may looks not so cool, but it can implement the query in one line, and there will be much less performance loss than your solution when data collection is big, because "INCLUDE IN" is much more slower than "JOIN".

Resources