I've got an array of columns that I want to loop through and optionally chain an or query onto an ActiveRecord query chain. I can get it to work, but the resulting query appends the or onto the query chain, therefore making the columns in my inital query optional. Here's my class:
class Claim
class MatchingAttributeFinder
ATTRIBUTE_GROUPS_TO_MATCH = [
["teacher_reference_number"],
["email_address"],
["national_insurance_number"],
["bank_account_number", "bank_sort_code", "building_society_roll_number"],
].freeze
def initialize(source_claim, claims_to_compare = Claim.submitted)
#source_claim = source_claim
#claims_to_compare = claims_to_compare
end
def matching_claims
claims = #claims_to_compare.where.not(id: #source_claim.id)
ATTRIBUTE_GROUPS_TO_MATCH.each do |attributes|
vals = values_for_attributes(attributes)
next if vals.blank?
concatenated_columns = "CONCAT(#{attributes.join(",")})"
claims = claims.or(
Claim.where("LOWER(#{concatenated_columns}) = LOWER(?)", vals.join)
)
end
claims
end
private
def values_for_attributes(attributes)
attributes.map { |attribute|
#source_claim.read_attribute(attribute)
}.reject(&:blank?)
end
end
end
The generated SQL looks like this:
SELECT "claims".* FROM "claims" WHERE (((("claims"."submitted_at" IS NOT NULL AND "claims"."id" != 'a7b25b99-4477-42b1-96ab-8262582c5541' OR (LOWER(CONCAT(teacher_reference_number)) = LOWER('0902344'))) OR (LOWER(CONCAT(email_address)) = LOWER('genghis.khan#mongol-empire.com'))) OR (LOWER(CONCAT(national_insurance_number)) = LOWER('QQ891011C'))) OR (LOWER(CONCAT(bank_account_number,bank_sort_code,building_society_roll_number)) = LOWER('34682151972654123456789/ABCD')))
But what I actually want is more like this:
SELECT "claims".* FROM "claims" WHERE "claims"."submitted_at" IS NOT NULL AND "claims"."id" != 'd6a53b4d-c569-49e6-a2ea-ac44b69b0451' AND (LOWER(concat(teacher_reference_number)) = LOWER('0902344') OR LOWER(concat(email_address)) = LOWER('genghis.khan#mongol-empire.com') OR LOWER(concat(national_insurance_number)) = LOWER('QQ891011C') OR LOWER(concat(bank_account_number,bank_sort_code,building_society_roll_number)) = LOWER('34682151972654123456789/ABCD'))
Is there any way to set up something like an empty scope that I can chain my OR queries to?
Try chaning all the "or" together first and then chain the original query
def matching_claims
claims = #claims_to_compare.where.not(id: #source_claim.id)
ors = nil
ATTRIBUTE_GROUPS_TO_MATCH.each do |attributes|
vals = values_for_attributes(attributes)
next if vals.blank?
concatenated_columns = "CONCAT(#{attributes.join(",")})"
aux = Claim.where("LOWER(#{concatenated_columns}) = LOWER(?)", vals.join)
if ors.nil?
ors = aux
else
ors = ors.or(aux)
end
end
claims.merge(ors)
end
Related
In a rails 4.1 application I need to add an object to an "AssociationRelation"
def index
employee = Employee.where(id_person: params[:id_person]).take
receipts_t = employee.receipts.where(:consent => true) #gives 3 results
receipts_n = employee.receipts.where(:consent => nil).limit(1) #gives 1 result
#I would need to add the null consent query result to the true consent results
#something similar to this and the result is still an association relation
#receipts = receipts_t + receipts_n
end
Is there a simple way to do this?
A way of solving this:
def index
employee_receipts = Employee.find_by(id_person: params[:id_person]).receipts
receipts_t = employee_receipts.where(consent: true)
receipts_n = employee_receipts.where(consent: nil).limit(1)
#receipts = Receipt.where(id: receipts_t.ids + receipts_n.ids)
end
Unfortunately .or() can't be used here because it's only available from Rails v5.0.0.1
you could do this way
receipts_t_ids = employee.receipts.where(:consent => true).pluck(:id)
receipts_n_ids = employee.receipts.where(:consent => nil).limit(1).pluck(:id)
#receipts = Receipt.where(id: receipts_t_ids + receipts_n_ids)
To avoid extra queries and keeping arrays in memory, you can use or
Like this:
def index
employee_receipts = Employee.find_by(id_person: params[:id_person]).receipts
#receipts =
employee_receipts.where(consent: true).or(
employee_receipts.where(consent: nil).limit(1)
)
end
I am trying to run following query through Rails query interface but unable to translate my logic. The query is
Select f.* from feeds f
Left join feed_items fi on fi.id = f.feedable_id
where
f.feedable_type in ('Homework', 'Datesheet')
and
(
(fi.assignable_type = 'Level' and assignable_id IN (1)) or
(fi.assignable_type = 'Student' and assignable_id IN (1)) or
(fi.assignable_type = 'Section' and assignable_id IN (1))
)
Scenario:
I receive following params hash in my action containing filters which will be added dynamically in my query
{"page"=>"1", "limit"=>"2", "type_filter"=>["Homework", "Datesheet"], "assignable_filter"=>{"Student"=>"[2]", "Section"=>"[1]", "Level"=>"[1]"}}
So far, what I have done is joining the tables and added where clause for type filter but not sure how to dynamically add assignable_filters. Here is my rails code, options are params in following code
def get_feeds(options)
base = Feed.includes(:feed_item)
base = add_type_filters base, options
base = add_assignable_filters base, options
format_response base, options
end
def add_type_filters(base, options)
type_filter = options[:type_filter]
if !type_filter.nil? and type_filter.length > 0
base = base.where('feedable_type IN (?)', options[:type_filter])
end
base
end
def add_assignable_filters(base, options)
assignable_filter = options[:assignable_filter]
if !assignable_filter.nil?
assignable_filter.each do |key, value|
# code for adding filters combined with or conditions
end
# wrap the or conditions and join them with an and in main where clause
end
base
end
P.S I am using rails 5
There was no straight forward way of building the query dynamically. I had to construct the where string to solve the problem. My current solution is
def get_feeds(options)
params_hash = {}
type_filters = add_type_filters options, params_hash
assignable_filters = add_assignable_filters options, params_hash
where = type_filters
where = where ? "#{where} and (#{assignable_filters})" : assignable_filters
base = Feed.eager_load(:feed_item).where(where, params_hash)
format_response base, options
end
def add_type_filters(options, params_hash)
type_filter = options[:type_filter]
type_filter_sql = nil
if !type_filter.nil? and type_filter.length > 0
type_filter_sql = 'feeds.feedable_type in (:type_filter)'
params_hash[:type_filter] = type_filter
end
type_filter_sql
end
def add_assignable_filters(options, params_hash)
assignable_filter_sql = []
assignable_filter = options[:assignable_filter]
if !assignable_filter.nil?
assignable_filter.each do |key, value|
assignable_filter_sql.push("(feed_items.assignable_type = '#{key}' and feed_items.assignable_id IN (:#{key}))")
params_hash[key.to_sym] = JSON.parse(value)
end
end
assignable_filter_sql.join(' or ')
end
I have a dashboard that allows for filtering of the results by different parameters. I build methods to filter the results by the given criteria. One area where I'm having trouble is if the previous line should null out the active record relation. Should I just put a bunch of if present? stat
def find_website_stats(options = {})
if options[:date_between].present?
start_date = options[:date_between].split(/-/).first.to_date
end_date = options[:date_between].split(/-/).last.to_date + 1
elsif options[:start_date].present?
start_date = options[:start_date].to_date
end_date = options[:end_date].to_date + 1 if options[:end_date].present?
end
contractor_name = options[:search_contractor] if options[:search_contractor].present?
distributor_name = options[:search_distributor] if options[:search_distributor].present?
distributor_ids = options[:with_distributor] if options[:with_distributor].present?
contractor_ids = options[:with_contractor] if options[:with_contractor].present?
with_country = options[:with_country] if options[:with_country].present?
with_state = options[:with_state] if options[:with_state].present?
search_city = options[:search_city] if options[:search_city].present?
web_stats = self.website_stats
if web_stats.present?
web_stats = web_stats.where(contractor_id: [*contractor_ids]) if contractor_ids.present?
if distributor_ids.present?
web_stat_ids = DistributorWebsiteStat.where(distributor_id: [*distributor_ids]).pluck(:website_stat_id)
web_stats = web_stats.where(id: [*web_stat_ids])
end
web_stats = web_stats.where(date_recorded: start_date..end_date) if start_date.present? && end_date.present?
web_stats = web_stats.with_country(with_country) if with_country.present?
web_stats = web_stats.with_state(with_state) if with_state.present?
web_stats = web_stats.search_city(search_city) if search_city.present?
#untested
if contractor_name.present?
searched_contractor_ids = Brand.search_contractor(contractor_name).pluck(:id)
web_stats = web_stats.where(contractor_id: [*searched_contractor_ids])
end
if distributor_name.present?
searched_distributor_ids = Brand.search_distributor(distributor_name).pluck(:id)
web_stat_ids = DistributorWebsiteStat.where(distributor_id: [*searched_distributor_ids])
web_stats = web_stats.where(id: [*web_stat_ids])
end
#end untested
end
web_stats
end
Where I'm specifically having a problem right now is the line that says if web_stat_ids.present?
So at first I grab all the website stats this object is associated with and then look to see if there are any for the given distributor.
If there is none for the given distributor web_stat_ids obviously returns nil
Then when I go to the line web_stats.where(id: [*web_stat_ids]) that's obviously going to return the same thing that I had before, rather than an empty active record relation, which is what I need it to be?
If I make this an empty array the next few statements with "where" won't work because it's an array and not an active record relation.
I know I can wrap this stuff in a bunch of if present? && statements...but I was wondering if there is a better solution to my problem?
In case anyone else is looking for this, found the answer from this SO post: How to return an empty ActiveRecord relation?
Model.none rails 4+
Assume I have an arbitrary number of Group records and I wanna query User record which has_many :groups, the catch is that users are queries by two bound fields from the groups table.
At the SQL level, I should end up with something like this:
SELECT * FROM users where (categories.id = 1 OR users.status = 0) OR(categories.id = 2 OR users.status = 1) ... -- to infinity
This is an example of what I came up with:
# Doesn't look like a good solution. Just for illustration.
or_query = groups.map do |g|
"(categories.id = #{g.category.id} AND users.status = #{g.user_status.id} )"
end.join('OR')
User.joins(:categories).where(or_query) # Works
What I think I should be doing is something along the lines of this:
# Better?
or_query = groups.map do |g|
"(categories.id = ? AND users.status = ? )".bind(g.category.id, g.user_status.id) #Fake method BTW
end.join('OR')
User.joins(:categories).where(or_query) # Works
How can I achieve this?
There has to be a better way, right?
I'm using Rails 4.2. So the shiny #or operator isn't supported for me.
I would collect the condition parameters separately into an array and pass that array (splatted, i.e. as an arguments list) to the where condition:
or_query_params = []
or_query = groups.map do |g|
or_query_params += [g.category_id, g.user_status.id]
"(categories.id = ? AND users.status = ?)"
end.join(' OR ')
User.joins(:categories).where(or_query, *or_query_params)
Alternatively, you might use ActiveRecord sanitization:
or_query = groups.map do |g|
"(categories.id = #{ActiveRecord::Base.sanitize(g.category_id)} AND users.status = #{ActiveRecord::Base.sanitize(g.user_status.id)})"
end.join(' OR ')
User.joins(:categories).where(or_query)
select
b.security_type,
b.symbol,
b.security_description,
b.trade_date_qty as 'axys_qty',
c.trade_date_qty as 'fidelity_qty',
c.trade_date_qty - b.trade_date_qty as 'qty_diff',
b.cost_basis as 'axys_cost',
c.cost_basis as 'fidelity_cost',
c.cost_basis - b.cost_basis as 'cost_diff'
from
account a
inner join advent_position b on a.fixed_account_number = b.account_number
inner join fidelity_position c on a.fixed_account_number = c.account_number and b.symbol = c.symbol
where
b.account_number = '636296651'
Basically, I have the ff. domains: Account, AdventPosition, FidelityPosition. I haven't set the relationship yet. I'm just wondering if there's a way to replicate the logic above using Criteria or HQL. Forgive me, I'm still new to Grails.
Thank you for any leads on this.
It'd be something close to this:
String hql = '''
select
b.securityType,
b.symbol,
b.securityDescription,
b.tradeDateQty,
c.tradeDateQty,
c.tradeDateQty - b.tradeDateQty,
b.costBasis,
c.costBasis,
c.costBasis - b.costBasis
from
Account a, AdventPosition b, FidelityPosition c
where
a.fixedAccountNumber = b.accountNumber
and a.fixedAccountNumber = c.accountNumber
and b.symbol = c.symbol
and b.accountNumber = :accountNumber
'''
def accountNumber = '636296651'
def results = Account.executeQuery(hql, [accountNumber: accountNumber])
The results will be an ArrayList of Object[], so you can iterate it with something like
for (row in results) {
def securityType = row[0]
def symbol = row[1]
def securityDescription = row[2]
def axys_qty = row[3]
def fidelity_qty = row[4]
def qty_diff = row[5]
def axys_cost = row[6]
def fidelity_cost = row[7]
def cost_diff = row[8]
}
I replaced the hard-coded account number with a named parameter; you can use regular ? like in SQL if you prefer and run 'def results = Account.executeQuery(hql, [accountNumber])', and of course if you intented it to be hard-coded then restore that and don't pass in the 2nd parameter, just run 'def results = Account.executeQuery(hql)'
just sharing the solution that I came up (while waiting for an answer :P) but note that the previous answer is way much better and faster:
def acc = Account.findByFixedAccountNumber('636296651')
List advPos = AdventPosition.findAllByAccountNumber('636296651')
List fidPos = advPos.collect {
FidelityPosition.findAllByAccountNumberAndSymbol('636296651', it.symbol)
}
def item = [:]
def res = []
def limit = advPos.size() - 1
for(i in 0..limit){
item.security_type = advPos[i].securityType
item.symbol = advPos[i].symbol
item.security_description = advPos[i].securityDescription
item.axys_qty = advPos[i].tradeDateQty
item.fidelity_qty = fidPos[i].tradeDateQty
item.qty_diff = item.fidelity_qty - item.axys_qty
item.axys_cost = advPos[i].costBasis
item.fidelity_cost = fidPos[i].costBasis
item.cost_diff = item.fidelity_cost - item.axys_cost
res.add(item)
}