Searching by multiple possible attributes for one search field - ruby-on-rails

How can I have a search field that can search for multiple different attributess. For example, be able to search for an associated models attribute and the self attribute in one search field?
I want to allow the user to search by what they want to search by, as most websites have this feature...
I have:
Model:
def self.user_search(search)
joins(:shipments).where(shipments: { id: Shipment.where(shipping_label: ShippingLabel.where(tracking_number: "#{search.downcase}")) })
joins(:shipping_address).where(shipping_addresses: { id: ShippingAddress.where(shipping_address_final: ShippingAddressFinal.where(address1: "#{search.downcase}")) })
end
This doesn't work. each joins works on it's own, but together only the last one will ever work.
How can I combine these to work?
Overall, I will be implementing many more possible search attributes but starting with just these 2 will lead me in the correct way.
I have tried using ||
This only allows the first join to work.
Using:
def self.user_search(search)
tracking_number = joins(:shipments).where(shipments: { id: Shipment.where(shipping_label: ShippingLabel.where(tracking_number: "#{search.downcase}")) })
address1 = joins(:shipping_address).where(shipping_addresses: { id: ShippingAddress.where(shipping_address_final: ShippingAddressFinal.where(address1: "#{search.downcase}")) })
tracking_number.or(address1)
end
gives an error upon search:
ArgumentError (Relation passed to #or must be structurally compatible. Incompatible values: [:joins]):

Your or approach is a step in the right direction. The reason you get the error is because the structures are incompatible. Meaning that they should generate the same SQL FROM statement. Currently one query is joining shipments while the other is joining shipping_address.
# statement #1 (tracking_number)
FROM model INNER JOIN shipments ON ...
# statement #2 (address1)
FROM model INNER JOIN shipping_addresses ON ...
You should do the joins first. Both statements should have both joins to make them compatible.
scope = joins(:shipments).joins(:shipping_address)
tracking_number = scope.where(shipments: { id: Shipment.where(shipping_label: ShippingLabel.where(tracking_number: "#{search.downcase}")) })
address1 = scope.where(shipping_addresses: { id: ShippingAddress.where(shipping_address_final: ShippingAddressFinal.where(address1: "#{search.downcase}")) })
tracking_number.or(address1)
The only thing changed in the above statement is that both your queries now join both tables, making them compatible since they generate the same SQL FROM statement.
Resulting in:
# statement #1 (tracking_number)
FROM model
INNER JOIN shipments ON ...
INNER JOIN shipping_addresses ON ...
# statement #2 (address1)
FROM model
INNER JOIN shipments ON ...
INNER JOIN shipping_addresses ON ...

Related

Add computable column to multi-table select clause with eager_load in Ruby on Rails Activerecord

I have a query with a lot of joins and I'm eager_loading some of associations at the time. And I need to compute some value as attribute of one of models.
So, I'm trying this code:
ServiceObject
.joins([{service_days: :ou}, :address])
.eager_load(:address, :service_days)
.where(ous: {id: OU.where(sector_code: 5)})
.select('SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone')
Where SQL function call in select operates data from associated addresses and ous tables.
I'm getting next SQL (so my in_zone column getting calculated and returned as first column before other columns for all eager_loaded models):
SELECT SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone, "SERVICE_OBJECTS"."ID" AS t0_r0, "SERVICE_OBJECTS"."TYPE" AS t0_r1, <omitted for brevity> AS t2_r36 FROM "SERVICE_OBJECTS" INNER JOIN "SERVICE_DAYS" ON "SERVICE_DAYS"."SERVICE_OBJECT_ID" = "SERVICE_OBJECTS"."ID" INNER JOIN "OUS" ON "OUS"."ID" = "SERVICE_DAYS"."OU_ID" INNER JOIN "ADDRESSES" ON "ADDRESSES"."ID" = "SERVICE_OBJECTS"."ADDRESS_ID" WHERE "OUS"."ID" IN (SELECT "OUS"."ID" FROM "OUS" WHERE "OUS"."SECTOR_CODE" = :a1) [["sector_code", "5"]]
But it seems like that in_zone isn't accessible from either model used in query.
I need to have calculated in_zone as attribute of ServiceObject model object, how I can accomplish that?
Ruby on Rails 4.2.6, Ruby 2.3.0, oracle_enhanced adapter 1.6.7, Oracle 12.1
I have successfully replicated your issue and it turns out that this is a known issue in Rails. The problem is that when using eager_load, Rails maps the columns of all eager-loaded tables into table and column aliases in the form of t0_r0, t0_r1, etc... (you can see these in the SQL that you pasted in the question). And while doing that, it simply ignores the custom columns in the select, probably because it cannot determine which eager-loaded table it should attribute the custom column to. It is sad that this issue is open for more than 2 years now...
Nevertheless I think I found a workaround. It seems that if you don't eager load the tables but manually join them (with joins), you can as well include them (with includes) and the custom columns will be returned as there will be no column aliasing taking place. The point is that you must not use associations in the joins clauses but you have to specify the joins yourself. Also note that you must specify all columns from the main table in the select manually too (see the service_objects.* in the select).
Try the following approach:
ServiceObject
.joins('INNER JOIN "SERVICE_DAYS" ON "SERVICE_DAYS"."SERVICE_OBJECT_ID" = "SERVICE_OBJECTS"."ID"')
.joins('INNER JOIN "OUS" ON "OUS"."ID" = "SERVICE_DAYS"."OU_ID"')
.joins('INNER JOIN "ADDRESSES" ON "ADDRESSES"."ID" = "SERVICE_OBJECTS"."ADDRESS_ID"')
.includes(:service_days, :address)
.where(ous: {id: OU.where(sector_code: 5)})
.select('service_objects.*, SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone')
The computation in the select should still work as the related tables are joined together but there should be no column aliasing present.
Of course this approach means that you'll get three queries instead of just one but unless you return a huge amount of records, the following two queries run by the includes clause should be very fast as they simply load the relevant records using foreign keys.
That monkey patch helped #Envek:
module ActiveRecord
Base.send :attr_accessor, :_row_
module Associations
class JoinDependency
JoinBase && class JoinPart
def instantiate_with_row(row, *args)
instantiate_without_row(row, *args).tap { |i| i._row_ = row }
end; alias_method_chain :instantiate, :row
end
end
end
end
then it is possible to do:
ServiceObject
.joins([{service_days: :ou}, :address])
.eager_load(:address, :service_days)
.where(ous: {id: OU.where(sector_code: 5)})
.select('SDO_CONTAINS(ous.service_area_shape, SDO_GEOMETRY(2001, 8307, sdo_point_type(addresses.lat, addresses.lng, NULL), NULL, NULL) ) AS in_zone')
.first
._row_['in_zone']

What is wrong with my scope below

I am writing an active record scope in rails. I have the following sql:
SELECT user.roll_number, first_name, last_name FROM user
INNER JOIN classes ON user.roll_number = classes.roll_number
AND classes.id IN #{id} ORDER BY(roll_number)
I wrote the scope in the user class, basically the tables are in the db2 database, so i wrote the scope as shown below
scope :by_id, lambda { |id|
{ select("user.roll_number, first_name, last_name")
.joins("INNER JOIN classes ON (user.roll_number = classes.roll_number \
and classes.id = #{id})")
}
}
Is there something wrong with the scope? I am getting "unexpected keyword end error"
Like said in the comment, you need to remove those {} braces. I'm guessing the error you are seeing is because this:
{ select("...") }
Is interpreted as an instantiation of a Hash, with a key of select("...") and no value. Ruby isn't happy with you.
Your follow-up:
What if i pass multiple ids? i am getting empty list, even though there are records for id 1 and id 2. i am passing id in the array form as id = [1,2]
You made a SQL lambda, so it's all on you to make that SQL work. A list of [1,2] is going to look like:
INNER JOIN classes ON (user.roll_number = classes.roll_number
and classes.id = [1,2])
Will that work on your SQL DB? Wouldn't you want to use and classes.id IN (1,2)?

How Do I Use Joins for Three Tables in Ruby on Rails?

I am working on a Ruby on Rails application which already has logic for text searching using pg_search and two other fields on a model. The logic creates an 'array' of rows from the search result. I do not remember the actual name of this since technically this is not an array. It is an instance variable of selected rows. I want to add search criteria from two additional models. My database is PostgreSQL.
Here is a subset of all three model definitions:
MediaLibrary: name, media_creator_id, media_type_id (fields that are being used in current search; has many media_topics and has many media_targets)
MediaTopic: media_library_id, topic_id (want to search for topic_id; belongs to media_library; topic_id being searched is coming from a Topic model (id, name))
MediaTarget: media_library_id, target_id (want to search for target_id; belongs to media_library; target_id being searched is coming from a Target model (id, name))
I'm thinking that I should be able to do something like this if both topic and target are being searched along with the other three search criteria. I will also need to have topic_id and target_id in my results so I can display Topic.name and Target.name on my view.
#media_libraries = MediaLibrary.text_search(params[:query]).where("media_creator_id = ? AND media_type_id = ?", params[:media_creator_id].to_i, params[:media_type_id].to_i).joins(:media_topics.where("media_library_id = ? and topic_id = ?", id_from_media_library_row, params[:topic_id].to_i).joins(:media_targets.where("media_library_id = ? and target_id = ?", id_from_media_library_row, params[:target_id].to_i)
I have searched on postgresql.org and Stack Overflow but have not found anything joining three tables using Ruby on Rails that was answered by anyone.
You can pass a SQL join statement into #joins. I'm not sure what it'd be in your case but you can do something like:
#media_libraries = MediaLibrary.joins(%q(
JOIN media_targets
ON media_targets.media_library_id = media_libraries.id
JOIN media_topics
ON media_topics.media_library_id = media_libraries.id
)).text_search(params[:query])
.where(
media_libraries: {
media_creator_id: params[:media_creator_id],
media_type_id: params[:media_type_id]
},
media_topics: { id: params[:topic_id] },
)

Rails basic search and PostgreSQL

I'm trying to create basic search in my web app. Here's code of search function.
def self.search(title, category_id, city_id)
if title || category_id || city_id
joins(:category).where('title LIKE (?) AND category.category_id IN (?) AND city.city_id IN (?)', "%#{title}%", "%#{category_id}%", "%#{city_id}%")
else
scoped
end
end
I have these associations in my model:
has_one :category
has_one :city
And I get this error
ActionView::Template::Error (PG::Error: ERROR: missing FROM-clause entry for ta
ble "category"
LINE 1: ..._id" = "events"."id" WHERE (title LIKE ('%%') AND category.c...
I'm using PostgreSQL. What I can do to remove this error?
The form of joins that you're using wants the association name, the SQL wants the table name. The table should be called categories.
A few other things:
I don't see you joining :city anywhere so your next error will be "Missing FROM-clause entry for table "city". The solution will be to .joins(:city) and use cities in the where. But keep reading anyway.
You don't need the parentheses around the value for LIKE, just title LIKE ? is fine.
You're using IN expressions for the category and city but you're giving them LIKE patterns and that won't work: the IDs will be numbers and you can use LIKE with numbers. If you're using IN then you'll usually want to supply a list of possible values, if you only want to match one value then just use = and a single value for the placeholder.
The categories table probably doesn't have a category_id column, similarly for the cities table and city_id column. Those two columns should be in your model's table.
Searching for a title when you don't have a title doesn't make much sense. Similarly for country and city.
That looks like a lot of problems but they can be fixed without too much effort:
def self.search(title, category_id, city_id)
rel = scoped
rel = rel.where('title like ?', "%#{title}%") if(title)
rel = rel.where('category_id = ?', category_id) if(category_id)
rel = rel.where('city_id = ?', city_id) if(city_id)
rel
end
and you don't even need joins or explicit table names at all.

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