Ordering in a select clause, to support many columns - ruby-on-rails

I want to select users from the users table based on:
user.created
user.sales_count
So I want to fetch all users, sometimes ordered by created date, and sometimes based on sales_count. And I want to be able to switch between ASC or DESC order.
All queries need to have this WHERE clause:
WHERE region = 123
How can I build my active record query to support these order by conditions?

def get_users(options={})
options[:order_col] ||= "created"
options[:order_type] ||= ""
User.where(:region=>123).order("#{options[:order_col]} #{options[:order_type]}")
end
options[:order_col] ||= is really saying:
options[:order_col] = options[:order_col] || ""
which in english is saying set options[:order_col] to options[:order_col] if set, other wise "". We can set the order_type to "" because SQL will by default order results ASC.
Example:
get_users #=> return ordered by created ASC
get_users(:order_col => "sales_count") #=> return order by sales count ASC
get_users(:order_col => "sales_count", :order_type => "DESC") #=> sales_count, DESC
# etc

Related

ActiveRecord select with OR and exclusive limit

I have the need to query the database and retrieve the last 10 objects that are either active or declined. We use the following:
User.where(status: [:active, :declined]).limit(10)
Now we need to get the last 10 of each status (total of 20 users)
I've tried the following:
User.where(status: :active).limit(10).or(User.where(status: : declined).limit(10))
# SELECT "users".* FROM "users" WHERE ("users"."status" = $1 OR "users"."status" = $2) LIMIT $3
This does the same as the previous query and returns only 10 users, of mixed statuses.
How can I get the last 10 active users and the last 10 declined users with a single query?
I'm not sure that SQL allows doing what you want. First thing I would try would be to use a subquery, something like this:
class User < ApplicationRecord
scope :active, -> { where status: :active }
scope :declined, -> { where status: :declined }
scope :last_active_or_declined, -> {
where(id: active.limit(10).pluck(:id))
.or(where(id: declined.limit(10).pluck(:id))
}
end
Then somewhere else you could just do
User.last_active_or_declined()
What this does is to perform 2 different subqueries asking separately for each of the group of users and then getting the ones in the propper group ids. I would say you could even forget about the pluck(:id) parts since ActiveRecord is smart enough to add the proper select clause to your SQL, but I'm not 100% sure and I don't have any Rails project at hand where I can try this.
limit is not a permitted value for #or relationship. If you check the Rails code, the Error raised come from here:
def or!(other) # :nodoc:
incompatible_values = structurally_incompatible_values_for_or(other)
unless incompatible_values.empty?
raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}"
end
# more code
end
You can check which methods are restricted further down in the code here:
STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references]
def structurally_incompatible_values_for_or(other)
STRUCTURAL_OR_METHODS.reject do |method|
get_value(method) == other.get_value(method)
end
end
You can see in the Relation class here that limit is restricted:
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache,
:skip_preloading]
So you will have to resort to raw SQL I'm afraid
I don't think you can do it with a single query, but you can do it with two queries, get the record ids, and then build a query using those record ids.
It's not ideal but as you're just plucking ids the impact isn't too bad.
user_ids = User.where(status: :active).limit(10).pluck(:id) + User.where(status: :declined).limit(10).pluck(id)
users = User.where(id: user_ids)
I think you can use UNION. Install active_record_union and replace or with union:
User.where(status: :active).limit(10).union(User.where(status: :declined).limit(10))

how to select distinct fields grouped by one field and concat one field with delimiter in ruby

I have an table that includes these field :
table phonebook
id
user_id
number
name
added
card_id
speeddial
updated_at
sms_group_name
some records have same sms_group_name and number,but there is some duplicate number for same sms_group_name. First I want to take distinct number for each sms_group_name and group_concat with , delimiter.
query result must be like this :
user_id, number,number,number,number,sms_group_name
select where condition is user_id
I tried all of them :
# #a = Phonebook.select(["DISTINCT number","sms_group_name"]).where(user_id: session[:user_id]).order(:sms_group_name).distinct
# #a = Phonebook.where(user_id: session[:user_id])
# Product.where.not(restaurant_id: nil).select("DISTINCT ON(name) name, restaurant_id, price, updated_at").order("name, updated_at")
# #a = Phonebook.where(user_id: session[:user_id]).select("DISTINCT ON(number) number, added, user_id, speeddial, updated_at,sms_group_name").order("sms_group_name")
# Phonebook.select("DISTINCT(number), *").where("user_id = ?", session[:user_id]).order("sms_group_name ASC").group_by(&:sms_group_name)
# Location.where("calendar_account_id = ?", current_user.calendar_accounts.first).group(:alias).order("alias ASC").group_by(&:category)
# #a = Phonebook.where("user_id = ?", session[:user_id]).order("sms_group_name ASC").group(:sms_group_name)
# #a = Phonebook.select("DISTINCT(number), sms_group_name").where("user_id = ?", session[:user_id]).order("sms_group_name ASC").group_by(&:sms_group_name)
# #a = Phonebook.select(:number)distinct.where("user_id = ?", session[:user_id]).order("sms_group_name ASC").group_by(&:sms_group_name)
# #a = Phonebook.select("DISTINCT(number), sms_group_name").group("sms_group_name")
most of them give error or does not work.
how can achive this ?
I tried select all values into object array ,after that I tried to eliminate them, but there is no suitable solution for now.
what can be solutions for both:
1- solution by using query and may be added one block
2- solution by using hash or array
best regards,

Why does .order doesn't get executed in my query?

I have slightly modified version for Kaminari to find on which page my record is (original code is here: https://github.com/kaminari/kaminari/wiki/FAQ#how-can-i-know-which-page-a-record-is-on):
module KaminariHelper
extend ActiveSupport::Concern
# Detecting page number in pagination. This method allows you to specify column name, order,
# items per page and collection ids to filter specific collection.
#
# Options:
# * :by - specifies the column name. Defaults to :id.
# * :order - specifies the order in DB. Defaults to :asc.
# * :per - specifies amount of items per page. Defaults to object model's default_per_page.
# * :nulls_last - if set to true, will add "nulls last" into the order.
# * :collection_ids - array of ids for the collection of current class to search position in specific collection.
# If it's ommited then search is done across all objects of current object's model.
#
def page_num(options = {})
column = options[:by] || :id
order = options[:order] || :asc
per = options[:per] || self.class.default_per_page
nulls_last = options[:nulls_last] == true ? "nulls last" : ""
operator = (order == :asc ? "<=" : ">=")
data = if options[:collection_ids].present?
self.class.where(id: options[:collection_ids])
else
self.class
end
# 1. Get a count number of all the results that are listed before the given record/value.
# 2. Divide the count by default_per_page or per number given as an option.
# 3. Ceil the number to get a proper page number that the record/value is on.
(
data.where("#{column} #{operator} ?", read_attribute(column))
.order("#{column} #{order} #{nulls_last}").count.to_f / per
).ceil
end
end
However when I test it for some weird reasons .order doesn't seem to be executed. Here is the sql output in rails console:
2.3.1 :005 > email.page_num(by: :sent_at, order: :desc, per: 25, collection_ids: user.emails.ids, nulls_last: true)
(1.1ms) SELECT "emails"."id" FROM "emails" WHERE "emails"."deleted_at" IS NULL
AND "emails"."user_id" = $1 [["user_id", 648]]
(1.5ms) SELECT COUNT(*) FROM "emails" WHERE "emails"."deleted_at" IS NULL AND
"emails"."id" IN (35946, 41741) AND (sent_at >= '2016-01-22 14:04:26.700352')
=> 13
Why does .order is not applied in the final SQL query? Is there a way to execute it? Otherwise the code doesn't make sense since there are no guarantee it'll give me proper page number.
Rails does ignore the order clause when counting.
Instead of relying on Rails' count method, try counting manually:
data.where("#{column} #{operator} ?", read_attribute(column))
.order("#{column} #{order} #{nulls_last}")
.select('COUNT(*) c').first.c.to_f / per

Random ActiveRecord with where and excluding certain records

I would like to write a class function for my model that returns one random record that meets my condition and excludes some records. The idea is that I will make a "random articles section."
I would like my function to look like this
Article.randomArticle([1, 5, 10]) # array of article ids to exclude
Some pseudo code:
ids_to_exclude = [1,2,3]
loop do
returned_article = Article.where(published: true).sample
break unless ids_to_exclude.include?(returned_article.id)
do
Lets look at DB specific option.
class Article
# ...
def self.random(limit: 10)
scope = Article.where(published: true)
# postgres, sqlite
scope.limit(limit).order('RANDOM()')
# mysql
scope.limit(limit).order('RAND()')
end
end
Article.random asks the database to get 10 random records for us.
So lets look at how we would add an option to exclude some records:
class Article
# ...
def self.random(limit: 10, except: nil)
scope = Article.where(published: true)
if except
scope = scope.where.not(id: except)
end
scope.limit(limit).order('RANDOM()')
end
end
Now Article.random(except: [1,2,3]) would get 10 records where the id is not [1,2,3].
This is because .where in rails returns a scope which is chain-able. For example:
> User.where(email: 'test#example.com').where.not(id: 1)
User Load (0.7ms) SELECT "users".* FROM "users" WHERE "users"."email" = $1 AND ("users"."id" != $2) [["email", "test#example.com"], ["id", 1]]
=> #<ActiveRecord::Relation []>
We could even pass a scope here:
# cause everyone hates Bob
Article.random( except: Article.where(author: 'Bob') )
See Rails Quick Tips - Random Records for why a DB specific solution is a good choice here.
You can use some like this:
ids_to_exclude = [1,2,3,4]
Article.where("published = ? AND id NOT IN (?)", true , ids_to_exclude ).order( "RANDOM()" ).first

How to query by ActiveRecord depending If the value of an attribute is empty or not?

I have a variable that could be set or left empty. In the first case the query looks fine, but in the second one I can't find how it works.
Checking If :
if params['customer_id'] == ""
#customer_id = "";
else
#customer_id = params['customer_id']
end
The query
User.where("customer_id = ?", #customer_id)
The problem is that If "" ,the query returns nothing. I could write it as
if params['customer_id'] == ""
User.all
else
User.where("customer_id = ?", params['customer_id'])
end
but first this is not DRY and second my query will include 10 * where's so this is not a very smart way to accomplish it.
You will refactor your query as:
#users = User.all
#users = #users.where(customer_id: params['customer_id']) if params['customer_id'].present?
Example:
#users = User.all
# User Load (6.8ms) SELECT "users".* FROM "users" WHERE "users"."deleted_at" IS NULL
#users.where(email: 'arup').count
# (1.2ms) SELECT COUNT(*) FROM "users" WHERE "users"."deleted_at" IS NULL AND "users"."email" = $1 [["email", "arup"]]
update
with scope
scope :with_or_without_customer, ->(customer_id) do
customer_id.present? ? where(customer_id: customer_id) : all
end
Note:
Model.all now returns an ActiveRecord::Relation, rather than an array of records. Use Relation#to_a if you really want an array.
In some specific cases, this may cause breakage when upgrading. However in most cases the ActiveRecord::Relation will just act as a lazy-loaded array and there will be no problems.
You can try this way.
#condition = "1"
#condition = {:customer_id => params['customer_id']} if params['customer_id'].present?
You can create your condition first then fire the query on database so that it will fire query only single time on database:
#users = User.where(#condition)

Resources