rails sort by specifc order - ruby-on-rails

I have model named Group which has users_count field in it.
I have to order groups based upon dynamic preference of users_count like [3,4,2]
means show groups with 3 users_count first, than with 4 and than with 2
Right now, I am using 3 separate queries and than merge records like
groups = Group.where(users_count: 3)
+ Group.where(users_count: 4)
+ Group.where(users_count: 2)
It works but It don't make sense to use 3 separate queries.
How can I achieve this in single query?

Since 3, 4, 2 is not a sequential order there should be a custom condition to order them properly you can do it by using the CASE WHEN expression.
order_sql = Arel.sql(
'CASE WHEN users_count = 3 THEN 0 ' \
'WHEN users_count = 4 THEN 1 ' \
'ELSE 3 END'
)
Group.where(users_count: [2,3,4]).order(order_sql)
Which will give 0 when users_count = 3, 1 when users_count = 4, and 3 for other cases. With default ascending order you'll get the result you want.

You can do
groups = Group.where(users_count: [3,4,2])
This will return the same groups as your 3 queries in a single query

Related

Rails order by range first

I have model named Group which has many users,
Group contains the fields for min_age and max_age describing the user's with minimum age and maximum age.
Each user has its settings where sets preferred age group like 18 to 25
When a user searches for groups than I have order groups with age b/w 18 to 25 first and than rest
I am doing it with 2 queries like
groups = Group.where("min_age >=? AND max_age <=?", setting.min_age, setting.max_age)
+ Group.where("min_age <? OR max_age >?", setting.min_age, setting.max_age)
It worked but thing is I have too many other filters and I want cut short number of queries.
Is it possible to do this in single query?
You can do that by ordering matching records before records that do not match:
Group.order("CASE WHEN min_age >= #{setting.min_age} AND max_age <= #{settings.max_age} then 1 else 2 end")
See: SQL CASE statement.

Rails: Possible to refactor this query?

I have 2 active record relation objects with the code as follow:
#obj1 = User.select('user.X, table2.Y, table2.Z, count (*)')
.merge(#some_variable)
.joins(:table1, :table2)
.group(1, 2, 3)
#obj2 = User.select('user.X, table2.Y, count (*)')
.merge(#some_variable)
.joins(:table1, :table2)
.group(1, 2)
Basically, the only difference between #obj1 and #obj2 is that #obj2 is not selecting table2.Z column data.
Here is a sample data that I would like both #obj to have:
#obj1
-------------------------------------
user.X table2.Y table2.Z count
-------------------------------------
1 1 A 1
1 1 B 1
2 1 A 1
2 1 B 1
2 1 C 1
#obj2
-------------------------
user.X table2.Y count
-------------------------
1 1 2
2 1 3
Currently the queries above are working fine, but I believe it is possible to further refactor the code? Like having #obj2 to get the records based on #obj1 data without having to do similar sql query? Appreciate if anyone got input on this. Many thanks in advance.
columns = %w(users.X, table2.Y table2.Z count(*))
#obj1 = User.merge(#some_variable)
.joins(:table1, :table2)
.group(1, 2, 3)
.select(*columns)
#obj2 = #obj1.select(*(columns - ["table2.Z"]))
A further step in refactoring would be to use Arel to replace the string conditions for portability.

can complex active record queries be made using AND OR conditions in rails

I have some filters that are put in sequence as per admin requirement to fetch target users
e.g (Filter_1 OR Filter_2) AND (Filter_3 OR Filter_4)
Application has many filters and these filters fetches users that are meeting some criteria. But these filters take use of 3-4 tables of inner join.
Filter_1 = Get users with avg perception score >= 0 generates query
1. select user_avg_table.* from
(select users.*, avg(perception_score) as avg from users
inner join notes
on notes.user_id = users.id group by user_id) as user_avg_table where avg >= 0
Filter_2 = User.where("Date(created_at) = DATE(NOW())")
2. SELECT * FROM `users` WHERE (Date(auth_token_created_at) = DATE(NOW()))
Filter_3 = User.joins(:notes).where(notes: {category: "Experiences"})
3. SELECT * FROM users INNER JOIN notes ON notes.user_id = users.id WHERE notes.category = ‘Experiences'
Filter_4 = User.joins(:transactions).where(transactions: {product_id: 2})
4. SELECT * FROM users INNER JOIN transactions ON transactions.user_id = users.id WHERE transactions.product_id = 3
Right now I am fetching users in 4 variables one for each filters and then performing ruby '|' and '&' methods over them.
eg.
users_1 = Filter_1.get_users
users_2 = Filter_2.get_users
users_3 = Filter_3.get_users
users_4 = Filter_4.get_users
target_users = (users_1 | users_2) & (users_3 | users_4)
it gives me an array of users.
Can I achieve this by using active record queries? which can give me array of active records rather than array of users. Can queries of all those be filters be combined? What is the best possible approach?
Rails 4 support for or commands is pretty minimal (source), but it sounds like Rails 5 will have better support for it. You can use this gem for a backport.
But that will generate a pretty complex query with lots of joins.
It might be faster to do 4 smaller queries (what you've already done). If you need the results of this to be in an active record collection, you can do this
ids = Filter_1.get_users.select(:id)
ids += Filter_2.get_users.select(:id)
ids += Filter_3.get_users.select(:id)
ids += Filter_4.get_users.select(:id)
User.where(id: ids)
As a side note, its generally better to create scopes on your model that name each subset. That way, rather than Filter_1.get_users, you can write Users.with_perception_score.

Limit PER user in rails query

So I have a standard users table structure, with a primary id key and what so not and the following persona table:
user_id | persona_id | time_inserted
2 1 x
2 2 x+1
2 3 x+2
1 1 x+3
5 8 x+6
5 9 x+1
What I'd like to do is retrieve the LAST inserted row and limit to ONE per user id. So, in that query, the result I want would be:
[2, 3] because the last inserted for 2 was persona_id 3 (x+2), [1, 1], and [5,8] because the last inserted for 5 was persona_id 8 (x+6)
This is my query:
to_return = Persona.select(to_get).where(to_condition)
This works, but retrieves them all. How can I restrict the query as asked? Thank you very much.
This should work:
to_return = Persona.select(to_get).where(to_condition).group('user_id').having('time_inserted = MAX(time_inserted)')
Update
You can't select a column if you don't put that in the group clause.
As you want to group by only user_id, one possible solution is, select the user_id s first with the maximum time_inserted like this:
users_ids_relation = Persona.select('user_id').group('user_id').having('time_inserted = MAX(time_inserted)')
Then, join it with the personas table based on the condition and then select the required columns:
users_ids_relation.joins('personas').where(to_condition).select(to_get)
It will give you the expected result.

mass increment field by 1

I have a query update deals set count = count + 1. In Rails, when I do this using ActiveRecord, I can think of
Deal.all.each { |deal| deal.update_attribute(:count => (deal.count + 1))}
and this take a lot more SQL queries instead of one query. Is there a better way to do this in Rails (not using the SQL query directly in the Rails app).
Deal.update_all("count = count + 1")
outputs
UPDATE "deals" SET count = count + 1
And with a conditional:
Deal.where(order_id: 2).update_all("count = count + 1")
outputs
UPDATE "deals" SET count = count + 1 WHERE "deals"."order_id" = 2
Using ActiveRelation update_all Updates all records with details given if they match a set of conditions supplied, limits and order can also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the database. It does not instantiate the involved models and it does not trigger Active Record callbacks.
http://apidock.com/rails/ActiveRecord/Base/update_all/class

Resources