Get associated ActiveRecord::Relation from complex query - sql

I have the following AR models:
class Checkin < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :checkins
end
Let's say I have a complex query on checkins and users, for example, Checkin.nearby.today and User.friends_of(john). Is there a straightforward way I can derive an ActiveRecord::Relation of Users? The end result would be friends of John who have checked in nearby today.
I would like the end result to be an instance of ActiveRecord::Relation.
Thanks!

This should do it:
users = User.friends_of(john)
users = users.joins(:checkins).where(checkins: { checkin_date: Date.today })
users = users.where( # your logic to determine the nearby )
As you can see, there is the logic about the nearby scope missing.
In a custom method:
def self.friends_checked_nearby_at_date(friend, nearby = true, date = Date.today)
users = User.friends_of(friend)
users = users.joins(:checkins).where(checkins: { checkin_date: date })
users = users.where( # your logic for the nearby scope ) if nearby.present?
return users
end
# usage:
User.friends_checked_nearby_at_date( User.first )
# or
User.friends_checked_nearby_at_date( User.first, true, Date.today-1.week )
# or
User.friends_checked_nearby_at_date( User.first, false )
# etc.

Related

Rails: loading association at once vs multiple tiny queries

I've got a situation. I am rendering a paginated collection of resources(let's say Posts). I need to show whether the post is liked by the current user. So basically we have three tables: posts, users and likes(a join table o indicate if users like some posts).
I came up with three solutions:
class Post
# 1
def liked_by?(user)
likes.exists?(user: user) # usage: Post.limit(50).map { |post| [p.id, post.liked_by?(current_user)] }
end
# 2
def liked_by?(user)
likes.any? { |like| like.user_id == user.id } # usage: Post.limit(50).includes(:likes).map { |post| [p.id, post.liked_by?(current_user)] }
end
# 3
def liked_by?(user)
user.likes.exists?(post: self) # usage: Post.limit(50).includes(:likes).map { |post| [p.id, post.liked_by?(current_user)] }
end
# 4
def liked_by?(user)
user.likes.any? { |like| like.user_id == user.id } # usage: Post.limit(50).map { |post| [p.id, post.liked_by?(current_user)] }
end
end
I can imagine the advantages and disadvantages of each of them but would be happy to hear any thoughts from you guys.
Usually, we render 50 posts at a time and the current user has about a hundred likes, but some users have thousands of likes so it's hard to say that there is a general solution for this. Anyway, I'm open to hear your proposals. Probably there's way # 5...(like denormalizing tables, materialized views and etc).
None of the above as they will all create a N+1 query issue. Also interrogation methods (methods that end with ?) should by convention return a boolean in Ruby.
You can use EXISTS with a subquery:
SELECT "posts".* FROM "posts"
WHERE EXISTS (SELECT "likes".* FROM "likes" WHERE "likes"."user_id" = $1 AND "likes"."post_id" = "posts"."id")
class User < ApplicationRecord
has_many :likes
end
class Post < ApplicationRecord
has_many :likes
# returns posts liked by user
def self.liked_by_user(user)
where(
user.likes
.where(Like.arel_table[:post_id].eq(arel_table[:id]))
.arel.exists
)
end
end
If you want all the posts regardless if the have been liked or not and a boolean indicating if they have been liked by the user can put that same EXISTS into the SELECT clause:
class Post < ApplicationRecord
# ...
# selects posts and a liked_by_user boolean
def self.with_likes_by_user(user)
columns = self.respond_to?(:arel) ? arel.projections : arel_table[Arel.star]
select(
columns,
user.likes
.where(Like.arel_table[:post_id].eq(arel_table[:id]))
.arel.exists.as('liked_by_user')
)
end
end
SELECT
"posts".*,
EXISTS (
SELECT "likes".*
FROM "likes" WHERE "likes"."user_id" = $1
AND "likes"."post_id" = "posts"."id"
) AS liked_by_user
FROM "posts"

Find cards with today date when user receive mails in RoR

Every day I need to send letters to users with today's tasks.
For do this I need to find all users who are allowed to send letters, and among these users to find all cards that have a deadline today. The result is three array elements with a nil value. How is this better done and right?
#users = User.all {|a| a.receive_emails true}
#user_cards = []
#users.each_with_index do |user, index|
#user_cards[index] = user.cards.where(start_date: Date.today).find_each do |card|
#user_cards[index] = card
end
end
My user model:
class Card < ApplicationRecord
belongs_to :user
# also has t.date "start_date"
end
My card model:
class User < ApplicationRecord
has_many :cards, dependent: :destroy
# also has t.boolean "receive_emails", default: false
end
Something like #cards_to_send = Card.joins(:users).where("users.receive_emails = true").where(start_date: Date.today)
Have a look at https://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-the-joined-tables for the docs on how to query on a joined table.
You could do this with a SQL join like this
User.joins(:cards).where(receive_emails: true, cards: { start_date: Date.today })
https://guides.rubyonrails.org/active_record_querying.html#joining-tables

Get count of one-to-many records efficiently

I have two tables: App (with a release_id field) and User.
Models
class App < ActiveRecord::Base
has_one :user
end
class User < ActiveRecord::Base
belongs_to :app
end
Class
class Stats::AppOverview
def initialize(from:, apps:)
#from = from || Date.new(2010)
#apps = apps
end
def total_count
{ apps: { total: total_app, with_user: app_with_user } }
end
private
def app_since
#apps.where('created_at >= ?', #from)
end
def total_app
devices_since.count
end
def app_with_user
User.where(app: app_since).count
end
end
I would like to return
the number of app records for a given array of release_id
the number of app records that belong to each user, satisfying other criteria
This is how I use the class
Stats::AppOverview.new(from: 1.month.ago.iso8601,
apps: App.where(release_id: [1,2,3,4,5,19,235]).total_count
#=> { apps: { total: 65, with_user: 42 } }
For the moment I do it in two queries, but is it possible to put them in the same query? Is this possible using active record?

Rails: saving filter conditions on attributes and checking whether conditions pass?

I have a SaaS application where an Account has many Users.
In an Account, the account owner can specify certain filters on the users that are applying to be part of the account. For instance, he can set that these requirement conditions must be met in order for user to be accepted into account : user.age > 21, user.name.count > 4.
I was thinking of creating a FilterCondition model, which belongs to Account, where a row could look like account_id: 1, attribute: "age", condition_string: "> 21" or account_id: 1, attribute: "phone_type", condition_string: "== iphone"
and then when I want to only accept users that meet these conditions do something like
#User.rb
def passes_requirements?
account.filter_conditions.each do |filter_conditions|
attr = filter_condition.attribute
return false if self.attr != filter_condition.condition
end
true
end
I know that is completely wrong syntax, but it should get the point across on what I would like to do.
Any suggested ways to allow accounts to save requirement conditions, and then check on Users to see if they meet those requirements?
It will be easier if the condition string is separated into a comparator (e.g >) and the value to be compared:
class FilterCondition < ActiveRecord::Base
belongs_to :account
validates :attribute, presence: true
validates :comparator, presence: true
validates :value, presence: true
def matching_users(query_chain = User.where(account: account))
query_chain.where("#{attribute} #{safe_comparator} ?", value)
end
private
def safe_comparator
safe_values = ['=', '>', '>=', '<', '<='] # etc
return comparator if safe_values.include? comparator
''
end
end
The safe_comparator method reduces the risk of SQL injection into the query. Chaining a collection of filters is a bit tricky, but something like the following idea should work.
class Account < ActiveRecord::Base
#....
has_many :filter_conditions
def filtered_users
query = User.where(account: self)
filter_conditions.each do |filter_condition|
query = filter_condition.matching_users(query)
end
query
end
end
account = Account.first
filter_1 = FilterCondition.create(
account: account,
attribute: :age,
comparator: '>=',
value: 21
)
filter_2 = FilterCondition.create(
account: account,
attribute: :age,
comparator: '<=',
value: 99
)
account.filtered_users

Find top scored from parent and all their children in Rails

I have a model called User, which has self-join association as this:
has_many :children, class_name: "User",
foreign_key: "parent_id"
belongs_to :parent, class_name: "User"
And it also has an association with a Post model:
User has_many post
Each Post object has a score attribute, and I am trying to find the posts for a given user and their children which have a highest score, which score is bigger than 0, and which satisfy a particular attribute. So right now, I have this method in my Post model:
def self.top_related_scored_by_user_id(user_id, max)
where(:user_id => user_id).
where(:related_image => true).
where('score > 0').
order(score: :desc).
first(max)
end
But, I would like to be able to look not just for the User with user_id, but also for their children. How can I do that?
Thanks
Its very simple:
def self.top_related_scored_by_user_id(user_ids, max = nil)
user_ids = user_ids.kind_of?(Array) ? user_ids : [user_ids]
scope = where(:user_id => user_ids)
scope = scope.where(:related_image => true)
scope = scope.where('score > 0')
scope = scope.order(score: :desc)
scope = scope.limit(max) if max.present?
scope
end
You can give an array of ids to the where clause, it will generate a condition like this:
WHERE id IN (1, 2, 3, 4)
A little improvement of your method, to make it more flexible:
def self.top_related_scored_by_user_id(user_ids, options = {})
options = { limit: 10, order: 'score DESC',
score: 0, related_image: true }.merge(options)
user_ids = user_ids.kind_of?(Array) ? user_ids : [user_ids]
scope = where(:user_id => user_ids)
scope = scope.where(:related_image => options[:related_image])
scope = scope.where('score > ?', options[:score])
scope = scope.order(options[:order])
scope = scope.limit(options[:limit])
scope
end
This way you can easily set options with the same function, and it has default values that can be overridden if needed.