ActiveRecord query w/ check for empty field - ruby-on-rails

I am generating the condition for an ActiveRecord where as follows:
query = {:status => status}
if (limit)
query[:limit] = #vals['limit'].to_i
end
if (offset && limit)
query[:offset] = (offset - 1) * limit
end
rows = Review.all(query)
This works just fine. I filter on 'status' of a review and I fill in limit and offset if passed in. Problem is now that I need to add a check for 'not null' on the reviews content field.
I.E.
AND review.content != '' && review.content != nil
I have read you can do something like
Review.were("review <> ''")
Which by itself works but I am not sure how to incorporate that into my above command. Or change the above command to work with a where statement rather than an 'all' statement.

I would write that code something like
query = Review.where("status = ?", status).where("review <> '' AND review IS NOT NULL")
if limit.present?
query = query.limit(limit)
if offset.present?
query = query.offset((offset - 1) * limit)
end
end
rows = query.all
rails query object does lazy evaluation, so you can build up the query, no sql is issued to the database until you begin to iterate over the rows
alternate to .where("review <> '' AND review IS NOT NULL")
.where("COALESCE(review, '') <> ''")

Related

Push all zero values to the end - Ruby on Rails (Postgresql)

I have a table vehicles that has_one vehicle_size. The VehicleSize model has a column in the table size, a String. Here are examples of a size value: 12ft, 19ft, EV. The goal is to sort vehicles based on the size from the vehicle_sizes table.
Here is my current solution:
def order_by_size(resources)
return resources unless context.params[:by_size] == 'asc' || context.params[:by_size] == 'desc'
if context.params[:by_size] == 'desc'
resources.joins(:vehicle_size).group('vehicle_sizes.size').order('vehicle_sizes.size DESC')
else
resources.joins(:vehicle_size).group('vehicle_sizes.size').order('vehicle_sizes.size ASC')
end
end
The solution above performs sorting. First, however, I need to push all zero values to the end regardless if the order is desc or asc (* zero means EV or any other string without numbers).
I tried to sort records with .sort { ... }, but it returns an array instead of active relation, with is necessary for me.
Solution where I get an array with sort:
def order_by_size(resources)
return resources unless context.params[:by_size] == 'asc' || context.params[:by_size] == 'desc'
if context.params[:by_size] == 'desc'
resources.joins(:vehicle_size).group('vehicle_sizes.size').sort do |x, y|
if x.vehicle_size.size.to_i.zero?
1
elsif y.vehicle_size.size.to_i.zero?
-1
else
y.vehicle_size.size.to_i <=> x.vehicle_size.size.to_i
end
end
else
resources.joins(:vehicle_size).group('vehicle_sizes.size').sort do |x, y|
if x.vehicle_size.size.to_i.zero?
1
elsif y.vehicle_size.size.to_i.zero?
-1
else
x.vehicle_size.size.to_i <=> y.vehicle_size.size.to_i
end
end
end
end
How can I modify my first or second solution to return an active relation where all String (zeros) will be pushed to the end regardless of sorting? Am I missing something here?
Many thanks for considering my request.
VehicleSize.order(Arel.sql("size = 'EV', size"))
or
VehicleSize.order(Arel.sql("size = 'EV', size desc"))
This way records with size = EV will be last, but others will be sorted as you need
Result will be relation
If you need specify table name, you can use vehicle_sizes.size instead of size
If you have few values without number (EV and ED here) you can do something like this to avoid hardcode
zero_array = %w[EV ED]
VehicleSize.order(
VehicleSize.sanitize_sql_for_order([Arel.sql("size IN (?), size DESC"), zero_array])
)
You can add a new field to the order
vehicle_sizes.size = 0, vehicle_sizes.size DESC
Or
vehicle_sizes.size <> 0, vehicle_sizes.size DESC

Dynamically create query - Rails 5

If I manually write a query, it will be like
User.where("name LIKE(?) OR desc LIKE(?)",'abc','abc')
.where("name LIKE(?) OR desc LIKE(?)",'123','123')
However, I need to dynamically generate that query.
I am getting data like
def generate_query(the_query)
query,keywords = the_query
# Here
# query = "name LIKE(?) OR desc LIKE(?)"
# keywords = [['abc','abc'],['123','123']]
keywords.each do |keyword|
users = User.where(query,*keyword) <-- not sure how to dynamically add more 'where' conditions.
end
end
I am using Rails 5. Hope it is clear. Any help appreciated :)
Something like this:
q = User.where(a)
.where(b)
.where(c)
is equivalent to:
q = User
q = q.where(a)
q = q.where(b)
q = q.where(c)
So you could write:
users = User
keywords.each do |keyword|
users = users.where(query, *keyword)
end
But any time you see that sort of feedback pattern (i.e. apply an operation to the operation's result or f(f( ... f(x)))) you should start thinking about Enumerable#inject (AKA Enumerable#reduce):
users = keywords.inject(User) { |users, k| users.where(query, *k) }
That said, your query has two placeholders but keywords is just a flat array so you won't have enough values in:
users.where(query, *k)
to replace the placeholders. I think you'd be better off using a named placeholder here:
query = 'name like :k or desc like :k'
keywords = %w[abc 123]
users = keywords.inject(User) { |users, k| users.where(query, k: k) }
You'd probably also want to include some pattern matching for your LIKE so:
query = "name like '%' || :k || '%' or desc like '%' || :k || '%'"
users = keywords.inject(User) { |users, k| users.where(query, k: k)
where || is the standard SQL string concatenation operator (which AFAIK not all databases understand) and % in a LIKE pattern matches any sequence of characters. Or you could add the pattern matching in Ruby and avoid having to worry about the different ways that databases handle string concatenation:
query = 'name like :k or desc like :k'
users = keywords.inject(User) { |users, k| users.where(query, k: "%#{k}%")
Furthermore, this:
User.where("name LIKE(?) OR desc LIKE(?)",'abc','abc')
.where("name LIKE(?) OR desc LIKE(?)",'123','123')
produces a WHERE clause like:
where (name like 'abc' or desc like 'abc')
and (name like '123' or desc like '123')
so you're matching all the keywords, not any of them. This may or may not be your intent.

.sum data from query

I'm trying to .sum a field from the following query:
def self.busqueda_general(params)
query = select('venta.Id,venta.TOTAL')
.distinct
.joins('left outer join detallevet ON venta.Documento=detallevet.Docto and venta.RutaId=detallevet.RutaId')
.where("(venta.RutaId = :rutaId or :rutaId = '') AND (detallevet.Articulo = :articulo or :articulo = '') AND (venta.CodCliente = :codcliente or :codcliente = '') AND (venta.IdEmpresa = :idempresa)",{rutaId: params[:search], articulo: params[:search3], codcliente: params[:search2], idempresa: params[:search6]})
query = query.where('venta.Fecha >= ? AND venta.Fecha <= ?', (params[:search4].to_date.beginning_of_day).strftime('%Y-%m-%d %T'), (params[:search5].to_date.end_of_day).strftime('%Y-%m-%d %T')) if params[:search4].present? and params[:search5].present?
query
end
In the method of the controller I call the query and sum it as follows:
#monto_total = Vent.busqueda_general(params).sum(:TOTAL)
but the problem is that the query is showing me records that are not repeated thanks to .distinct but with the .sum is adding up all the records including the repeated ones, ignoring the .distinct
Try this
#monto_total = Vent.busqueda_general(params).sum(:TOTAL).to_f
you can to use to_s (to convert in string) to_f -> float, to_i -> integer
You have a group by clause, so you get one sum(Total) for every combination of ID/Total.
.group('venta.Id,venta.TOTAL')
To me looks like you don't need this group by clause.
UPDATE::
query = where(Id: select('venta.Id')
.joins('left outer join detallevet ON venta.Documento=detallevet.Docto and venta.RutaId=detallevet.RutaId')
.where("(venta.RutaId = :rutaId or :rutaId = '') AND (detallevet.Articulo = :articulo or :articulo = '') AND (venta.CodCliente = :codcliente or :codcliente = '') AND (venta.IdEmpresa = :idempresa)",{rutaId: params[:search], articulo: params[:search3], codcliente: params[:search2], idempresa: params[:search6]}))
Change your main query to this keeping everything else same. Ideal way would be to move join on detallevet to where clause.

Activerecord where array with less than condition

I have an array of conditions i'm passing to where(), with the conditions being added one at a time such as
conditions[:key] = values[:key]
...
search = ModelName.where(conditions)
which works fine for all those that i want to compare with '=', however I want to add a '<=' condition to the array instead of '=' such as
conditions[:key <=] = values[:key]
which of course won't work. Is there a way to make this work so it i can combine '=' clauses with '<=' clauses in the same condition array?
One way of doing it:
You could use <= in a where clause like this:
User.where('`users`.`age` <= ?', 20)
This will generate the following SQL:
SELECT `users`.* FROM `users` WHERE (`users`.`age` <= 20)
Update_1:
For multiple conditions, you could do this:
User.where('`users`.`age` <= ?', 20).where('`users`.`name` = ?', 'Rakib')
Update_2:
Here is another way for multiple conditions in where clause:
User.where('(id >= ?) AND (name= ?)', 1, 'Rakib')
You can add any amount of AND OR conditions like this in your ActiveRecord where clause. I just showed with 2 to keep it simple.
See Ruby on Rails Official Documentation for Array Conditions for more information.
Update_3:
Another slight variation of how to use Array Conditions in where clause:
conditions_array = ["(id >= ?) AND (name = ?)", 1, "Rakib"]
User.where(conditions_array)
I think, this one will fit your exact requirement.
You could use arel.
conditions = {x: [:eq, 1], y: [:gt, 2]}
model_names = ModelName.where(nil)
conditions.each do |field, options|
condition = ModelName.arel_table[field].send(*options)
model_names = model_names.where(condition)
end
model_names.to_sql --> 'SELECT * FROM model_names WHERE x = 1 and y > 2'

Ranking results with complex conditions using Rails and Squeel

I'm probably doing something silly here - and I'm open to other ways of doing - but I'm trying to order my results set based on a computed field:
Client.select{['clients.*',
(cast((surname == matching_surname).as int) * 10 +
cast((given_names == matching_given_names).as int) +
cast((date_of_birth == matching_date_of_birth).as int).as(ranking)]}.
where{(surname =~ matching_surname) |
(given_names =~ matching_given_names) |
(date_of_birth == matching_date_of_birth)}.
order{`ranking`.desc}
My problem is that date_of_birth could be nil. This causes the cast((...).as int) call to return three different values - 1 if the expression evaluated to true; 0 if the expression evaluated to false; and nil if the underlying column value was nil.
The nil values from the expressions cause the whole ranking to evaluate to NIL - which means that even if I have a record that matches exactly on surname and given_names, if the date_of_birth column is nil, the ranking for the record is nil.
I have tried to use a complex expression in the cast that checks if not nil or the matching_value, but it fails with a Squeel exception using | and ruby evaluates it when using || and or.
I've also tried to use a predicates in the order for aliased columns:
order{[`ranking` != nil, `ranking`.desc]}
but that throws an ActiveRecordexception complaining that the column ranking does not exist.
I'm at the end of my rope... any ideas?
After a bit of a dance, I was able to calculate the ranking using a series of outer joins to other scopes as follows:
def self.weighted_by_any (client)
scope =
select{[`clients.*`,
[
((cast((`not rank_A.id is null`).as int) * 100) if client[:social_insurance_number].present?),
((cast((`not rank_B.id is null`).as int) * 10) if client[:surname].present?),
((cast((`not rank_C.id is null`).as int) * 1) if client[:given_names].present?),
((cast((`not rank_D.id is null`).as int) * 1) if client[:date_of_birth].present?)
].compact.reduce(:+).as(`ranking`)
]}.by_any(client)
scope = scope.joins{"left join (" + Client.weigh_social_insurance_number(client).to_sql + ") AS rank_A ON rank_A.id = clients.id"} if client[:social_insurance_number].present?
scope = scope.joins{"left join (" + Client.weigh_surname(client).to_sql + ") AS rank_B on rank_B.id = clients.id"} if client[:surname].present?
scope = scope.joins{"left join (" + Client.weigh_given_names(client).to_sql + ") AS rank_C on rank_C.id = clients.id"} if client[:given_names].present?
scope = scope.joins{"left join (" + Client.weigh_date_of_birth(client).to_sql + ") AS rank_D on rank_D.id = clients.id"} if client[:date_of_birth].present?
scope.order{`ranking`.desc}
end
where Client.weigh_<attribute>(client) is another scope that looks like the following:
def self.weigh_social_insurance_number (client)
select{[:id]}.where{social_insurance_number == client[:social_insurance_number]}
end
This allowed me to break out the comparison of the value from the check for nil and so removed the third value in my boolean calculation (TRUE => 1, FALSE => 0).
Clean? Efficient? Elegant? Maybe not... but working. :)
EDIT base on new information
I've refactored this into something much more beautiful, thanks to Bigxiang's answer. Here's what I've come up with:
First, i replaced the weigh_<attribute>(client) scopes with sifters. I'd previously discovered that you can use sifters in the select{} portion of the scope - which we will be using in a minute.
sifter :weigh_social_insurance_number do |token|
# check if the token is present - we don't want to match on nil, but we want the column in the results
# cast the comparison of the token to the column to an integer -> nil = nil, true = 1, false = 0
# use coalesce to replace the nil value with `0` (for no match)
(token.present? ? coalesce(cast((social_insurance_number == token).as int), `0`) : `0`).as(weight_social_insurance_number)
end
sifter :weigh_surname do |token|
(token.present? ? coalesce(cast((surname == token).as int), `0`) :`0`).as(weight_surname)
end
sifter :weigh_given_names do |token|
(token.present? ? coalesce(cast((given_names == token).as int), `0`) : `0`).as(weight_given_names)
end
sifter :weigh_date_of_birth do |token|
(token.present? ? coalesce(cast((date_of_birth == token).as int), `0`) : `0`).as(weight_date_of_birth)
end
So, let's create a scope using the sifters to weigh all our criteria:
def self.weigh_criteria (client)
select{[`*`,
sift(weigh_social_insurance_number, client[:social_insurance_number]),
sift(weigh_surname, client[:surname]),
sift(weigh_given_names, client[:given_names]),
sift(weigh_date_of_birth, client[:date_of_birth])
]}
end
Now that we can determine if the criteria provided match the column value, we compute our ranking using another sifter:
sifter :ranking do
(weight_social_insurance_number * 100 + weight_surname * 10 + weight_date_of_birth * 5 + weight_given_names).as(ranking)
end
And adding it all together to make our scope that includes all the model attributes and our computed attributes:
def self.weighted_by_any (client)
# check if the date is valid
begin
client[:date_of_birth] = Date.parse(client[:date_of_birth])
rescue => e
client.delete(:date_of_birth)
end
select{[`*`, sift(ranking)]}.from("(#{weigh_criteria(client).by_any(client).to_sql}) clients").order{`ranking`.desc}
end
So, I can now search for a client and have the results ranked by how closely they match the provided criteria with:
irb(main): Client.weighted_by_any(client)
Client Load (8.9ms) SELECT *,
"clients"."weight_social_insurance_number" * 100 +
"clients"."weight_surname" * 10 +
"clients"."weight_date_of_birth" * 5 +
"clients"."weight_given_names" AS ranking
FROM (
SELECT *,
coalesce(cast("clients"."social_insurance_number" = '<sin>' AS int), 0) AS weight_social_insurance_number,
coalesce(cast("clients"."surname" = '<surname>' AS int), 0) AS weight_surname,
coalesce(cast("clients"."given_names" = '<given_names>' AS int), 0) AS weight_given_names, 0 AS weight_date_of_birth
FROM "clients"
WHERE ((("clients"."social_insurance_number" = '<sin>'
OR "clients"."surname" ILIKE '<surname>%')
OR "clients"."given_names" ILIKE '<given_names>%'))
) clients
ORDER BY ranking DESC
Cleaner, more elegant, and working better!

Resources