Reputation: 2741
Trying to construct a query such that I have multiple statement specifying joins, each with a where message chained onto them. When the query is run, I get all the joins, but only the where from my first call. Here's the method body that's doing the query:
observations_joins = Observation.joins(:obs_session => :project).where(:obs_sessions=>{:project_id=>self.project.id})
descriptor_hash = descriptor_where_hash if tag_descriptors && tag_descriptors.size > 0
puts "The descriptor_hash: #{descriptor_hash}"
observations = observations_joins.joins(:obs_descriptors).where("#{descriptor_hash['query_string']}", descriptor_hash['match_values']) if tag_descriptors && tag_descriptors.size > 0
arel = observations.arel
puts "The arel sql should be: #{arel.to_sql}"
observations
I have another method that gets called from inside the second joins statement, that iterates over the potential match values and generates the string and the values used; body here:
match_values = []
query_string = "obs_descriptors.tag_name = ?"
tag_descriptors.each_index do |index|
query_string = query_string + " #{tag_descriptors.fetch(index).qualifier_key} obs_descriptors.tag_name = ?" if index != 0
match_values << tag_descriptors.fetch(index).tag_name
end
{:match_values=>match_values, :query_string=>query_string}
So the sql getting generated looks like:
SELECT `observations`.* FROM `observations` INNER JOIN `obs_sessions` ON `obs_sessions`.`id` = `observations`.`obs_session_id` INNER JOIN `projects` ON `projects`.`id` = `obs_sessions`.`project_id` INNER JOIN `obs_descriptors` ON `obs_descriptors`.`observation_id` = `observations`.`id` WHERE (`obs_sessions`.`project_id` = 1)
and doesn't include the second set of where conditions. I also print the hash, just to make sure I'm not losing my mind and there are values in there, and there indeed are.
So, what am I missing to make this go as I'd expect it to?
Upvotes: 4
Views: 2848
Reputation: 2741
Answering my own question here. The most elegant, concise way I found to get this working was to drop down to arel directly. Also, there were some issues with the original code posted, but even still, I needed to use arel to get properly grouped conditions. For context, I've got an object that, based on it's related data, needs to dynamically construct a semi advanced query, so I wanted to do things like checking for the existence of certain related data, and if present, then tack on the additional joins and wheres. Here's the final versions of the relevant methods:
def find_observations
observations = Observation.select('distinct observations.*').includes(:obs_session).includes(:judgements).includes(:concepts).includes(:obs_descriptors)
observations = observations.joins(:obs_session => :project).where(:obs_sessions=>{:project_id=>self.project.id})
if tag_descriptors && tag_descriptors.size > 0
observations = observations.where(descriptor_predicate)
end
if session_descriptors && session_descriptors.size > 0
observations = observations.where(session_predicate)
end
if user_descriptors && user_descriptors.size > 0
observations = observations.where(user_predicate)
end
#puts "observations sql is: #{observations.to_sql}"
observations.all
end
The above method optionally calls the remaining methods, which return the arel used in the where calls when chaining the AR object while building up the eventual query. Notice the disctinct; I'd had a version of this using arel entirely, that appeared to be working, but was in fact returning duplicates. I found references to using group(some_attribute) to fake things, but that turned out to cause problems down the chain, so to speak. So I fell back to using ActiveRelation to specify the distinct, joins and includes, and arel for the rest.
The next one was the part that was originally giving me lots of trouble; there are a variable number of possibilities, and each one could be either an AND or OR condition, and needed to be grouped separately so as not to mess up the rest of the generated where clause.
def descriptor_predicate
od = Arel::Table.new :obs_descriptors
predicate = nil
self.tag_descriptors.each_index do |index|
descriptor = self.tag_descriptors.fetch(index)
qual_key = descriptor.qualifier_key
tag_name = descriptor.tag_name
if index == 0
predicate = od[:descriptor].eq(tag_name)
else
if qual_key == "OR"
predicate = predicate.or(od[:descriptor].eq(tag_name))
else
predicate = predicate.and(od[:descriptor].eq(tag_name))
end
end
end
predicate
end
And finally the other predicate methods for the potential joined entity values:
def session_predicate
o = Arel::Table.new :observations
predicate = nil
self.session_descriptors.each_index do |index|
obs = self.session_descriptors.fetch(index)
if index == 0
predicate = o[:obs_session_id].eq(obs.entity_id)
else
predicate = predicate.or(o[:obs_session_id].eq(obs.entity_id))
end
end
predicate
end
def user_predicate
o = Arel::Table.new :observations
predicate = nil
self.user_descriptors.each_index do |index|
obs = self.user_descriptors.fetch(index)
if index == 0
predicate = o[:contributor_id].eq(obs.entity_id)
else
predicate = predicate.or(o[:contributor_id].eq(obs.entity_id))
end
end
predicate
end
def descriptor_where_string(included_where_statements)
tag_descriptors.each_index do |index|
qual_key = tag_descriptors.fetch(index).qualifier_key
tag_name = tag_descriptors.fetch(index).tag_name
if index == 0
query_string = "obs_descriptors.descriptor = #{tag_name}"
else
if qual_key == "OR"
query_string = query_string + " #{qual_key} obs_descriptors.descriptor = #{tag_name} AND #{included_where_statements} "
else
query_string = query_string + " #{qual_key} obs_descriptors.descriptor = ?"
end
end
end
query_string
end
Ultimately, I found the best solution involved leveraging both ActiveRelation chaining for providing the distinct and includes, and using arel directly for the conditions on the related values. Hope this helps somebody at some point.
Upvotes: 5