Need help in understanding a rails active record query - ruby-on-rails

I have two models
Customer and Transaction
Customer
has_many :transactions
Transaction
belongs_to :customer
Now I need all the customers having
more than 1 transaction in last 30 days
exactly 1 transaction in last 30 days

Expanding on ScottJShea's answer, I'd use some scopes
scope :one_recent_transaction, :conditions => lambda {
includes(:transactions).where("transactions.date > ?", DateTime.now - 30.days).group("customer.id").having("COUNT(transactions.id) = 1")
}
scope :many_recent_transactions, :conditions => lambda {
includes(:transactions).where("transactions.date > ?", DateTime.now - 30.days).group("customer.id").having("COUNT(transactions.id) > 1")
}
Then use them like this
one_transaction = Customer.one_recent_transaction
many_transactions = Customer.many_recent_transactions

You want to use the HAVING clause. I suggest (I am guessing a bit not knowing your model exactly):
#exactly_one = Customer.where("transaction_date between ? and >", Date.now, Date.now - 30).group("customer.id").having("count(transaction.id) = 1")
#exactly_one = Customer.where("transaction_date between ? and >", Date.now, Date.now - 30).group("customer.id").having("count(transaction.id) > 1")

Related

Rails query where at least 1 of the associated records contains an attribute

I have a GroupMeetings table and a GroupMeetingsUser table which is a join table between User and GroupMeetings. I want to find all the GroupMeetings where at least 1 of the GroupMeetingsUser has an attribute.
Right now, this works:
#group_meetings = GroupMeeting.where('lang_one_id IN (?) AND lang_two_id IN (?) AND meeting_time >= ?', #user.languages.pluck(:id), #user.languages.pluck(:id), Date.today)
#new_group_meetings_id = []
#group_meetings.each do |meeting|
meeting.group_meetings_user.each do |user|
if(user.user.location === #user.location)
#new_group_meetings_id.push(meeting.id)
end
end
end
#group_meetings = GroupMeeting.where('id IN (?)', #new_group_meetings_id)
But how can I include the .each loop in original GroupMeetings query instead? Like using .joins(:group_meetings_user) to find all the records where at least 1 of the users has an attribute?
class GroupMeeting < ApplicationRecord
has_many :group_meetings_users
has_many :users, through: :group_meetings_users
end
class GroupMeetingsUser < ApplicationRecord
belongs_to :user
belongs_to :group_meeting
validates_presence_of :user_id, :group_meeting_id
validates :user_id, :uniqueness => {:scope => :group_meeting_id, :message => 'can only join each group once.'}
end
UPDATE 1:
GroupMeeting.joins(:users).where(group_meeting: { lang_one_id: #user.languages.pluck(:id), lang_two_id: #user.languages.pluck(:id), meeting_time: DateTime.now..DateTime::Infinity.new}, user: { location: #user.location })
gives the error:
no such column: group_meeting.lang_one_id: SELECT "group_meetings".* FROM "group_meetings" INNER JOIN "group_meetings_users" ON "group_meetings_users"."group_meeting_id" = "group_meetings"."id" INNER JOIN "users" ON "users"."id" = "group_meetings_users"."user_id" WHERE "group_meeting"."lang_one_id" IN (29, 30, 31, 22) AND "group_meeting"."lang_two_id" IN (29, 30, 31, 22) AND ("group_meeting"."meeting_time" >= ?) AND "user"."location" = ?
I suggest the following:
GroupMeeting
.joins('INNER JOIN languages l1 ON languages l1.id = group_meetings.lang_one_id')
.joins('INNER JOIN languages l2 ON languages l2.id = group_meetings.lang_two_id')
.joins(:group_meeting_users => :users)
.where('meeting_time >= ?', Date.today)
.select('group_meetings.*')
.group('group_meetings.id')
.having('users.location = ?', #user.location)
It should be faster than doing sub-selects (using languages.id IN (?) and pluck)
Let me know if that works and/or if you have questions.

Better solution for "one, other or both" cases

I was checking some code, and something similar to the following showed up:
def between_dates(date_1, date_2)
if date_1 && date_2
conditions "created_at >= date_1 AND created_at <= date_2"
elseif date_1
conditions "created_at >= date_1"
elseif date_2
conditions "created_at <= date_2"
end
end
It looked the kind of code that could be improved, but I couldn't find a more elegant solution for such a trivial and common conditional statement.
I'm looking for a better answer for this problem when we must return a value for one, other or both.
Rails lets you build a query dynamically. Here's an example using scopes and a class method. Since scopes always return an ActiveRecord::Relation object (even if the block returns nil), they are chainable:
class Event < ApplicationRecord
scope :created_before, -> (date) { where('created_at <= ?', date) if date }
scope :created_after, -> (date) { where('created_at >= ?', date) if date }
def self.created_between(date_1, date_2)
created_after(date_1).created_before(date_2)
end
end
Example usage:
Event.created_between(nil, Date.today)
# SELECT `events`.* FROM `events` WHERE (created_at <= '2018-05-15')
Event.created_between(Date.yesterday, nil)
# SELECT `events`.* FROM `events` WHERE (created_at >= '2018-05-14')
Event.created_between(Date.yesterday, Date.today)
# SELECT `events`.* FROM `events` WHERE (created_at >= '2018-05-14') AND (created_at <= '2018-05-15')
I'd use something like this:
def between_dates(date_1, date_2)
parts = []
if date_1
parts << "created_at >= date_1"
end
if date_2
parts << "created_at <= date_2"
end
full = parts.join(' AND ')
conditions(full)
end
This can be further prettified in many ways, but you get the idea.
def between_dates(date_1, date_2)
date_conditions = []
date_conditions << 'created_at >= date_1' if date_1
date_conditions << 'created_at <= date_2' if date_2
conditions date_conditions.join(' AND ') unless date_conditions.empty?
end
I am not sure if this is more elegant, but I always do reduce everything to avoid typos:
[[date_1, '>='], [date_2, '<=']].
select(&:first).
map { |date, sign| "created_at #{sign} #{date}" }.
join(' AND ')

Order a model with conditions in Rails (3)

Lets say there is a model called Event. I want to display important and current events first, so i have the attributes important (boolean) and enddate (date).
Now I want to get all events where important == true and where enddate >= today first, all others should be ordered by created_at.
I want to avoid doing events = important_events + not_important_events as this would return an array insted of an activerecord. Does anyone knows an elegant way to order a model in rails?
Try (for ActiveRecord 5+):
#events = Event.where(:important => true, 'enddate >= ?', Date.today).or(Event.where.not(:important => true, 'enddate >= ?', Date.today).order(:created_at => :desc))
Try this, It will return newest record first and fulfil your conditions.
important == true
enddate >= today
#events1 = Event.where(:important => true).where('enddate >= ?', Date.today).order(:created_at => :desc)
#events2 = Event.where(:important => false).where('enddate <= ?', Date.today).order(:created_at => :desc)
#events = #events1.or(#events2)
"OR" works only in ActiveRecod(5+).

Rails - Activerecord conditions with enum

I have an Item model and a UserItem model. An Item has many UserItems.
Both models use Enum:
enum status: [ :pending, :approved]
I need to query Item to return user_items where item.id != 1, item.status is approved, and user_items.status is pending. I am having trouble with the correct syntax
Item.joins(:user_items).where( "items.id != ? and items.status = ? and user_items.status = ?", 1, ???, ???)
What is the correct way to write this query?
Since you're selecting UserItems, and assuming you have belongs_to :item on the other side of has_many relation query will look like
UserItem.pending.joins(:item).merge(Item.approved.where.not(id:1))
Try this-
UserItem.joins("LEFT OUTER JOIN items.id = user_items.id").where("items.id != ? AND items.status = ? AND user_items.status = ?", 1, "approved","pending")
In case if you don't want to use scope
Item.joins(:user_item).where( "items.id != ? and items.status = ? and user_items.status = ?", 1, Item.statuses[:pending], UserItem.statuses[:approved])
But it is always nicer to use scopes.

How do I search trough query results, to prevent making to many requests

I have the below set of queries, but I'm sure this isn't DRY. However, I can't find out how to filter trough the deals var instead of querying again for each var. Is it possible?
deals = Deal.all
won = Deal.find( :all, :conditions => ["status = 'won'"] ).count
pending = Deal.find( :all, :conditions => ["status = 'pending'"] ).count
lost = Deal.find( :all, :conditions => ["status = 'lost'"] ).count
Use GROUP BY SQL clause:
Hash[Deal.all(:select => 'status, count(*) as count', :group => 'status').map{|e|
[e.status, e.count]
}]
Edit: I forgot that you already have all the records loaded. In that case, you can get counts per status this way:
Hash[deals.group_by(&:status).map{|k,v| [k,v.count]}]
You can use following:-
Deal.find(:all, :select => 'status, count(id) as deal_count', :group => 'status')
You can use Array#select:
deals = Deal.all
won = deals.select { |deal| deal.status == 'won' }.length
# similar for pending and lost
I think you can use Ruby's inject function for this:
won = deals.inject(0) {|total, deal| deal.status == 'won' ? total + 1 : total }
if your Deal objects are ActiveRecord objects (which is typically the case for models), you can launch the count on the data base:
won = Deal.count_by_sql("select count(*) from deals where status = 'won'")
Another way to do it would be to write the sql query that would do the all the count for you, and group them by status:
count_by_status = Deal.find_by_sql("select status,count(*) from deals group by status;")
Then you can use the result (which will be an array of hashes I think).

Resources