ActiveRecord - Group by attribute of included table - sql

I have the following two models
post_comment.rb
class PostComment < ActiveRecord::Base
has_many :replies, dependent: :destroy
...
end
reply.rb
class Reply < ActiveRecord::Base
belongs_to :post_comments
belongs_to :reply
...
end
I have the following query that gets all the post_comments and includes their respective replies:
PostComment.all.includes(:replies)
I want to, however, also group the replies of by the reply_id attribute of the reply model
Ideally I'd like to end up with something like the following straight out of the DB:
[
<Post 1:
replies: { nil: [...], 1: [...], 2: [...], ... }
...
>,
<Post 2:
replies: { nil: [...], 123: [...], 341: [...], ... }
...
>,
]
Thanks!

I don't think ActiveRecord can do that
We can group relies like this,but maybe it isn't the way you want
posts = PostComment.all.includes(:replies)
for post in posts
...
replies_map = post.replies.group_by(&:reply_id)
...
end
you can add a virtual column to model

To group the replies by the reply_id attribute of the reply model, you can do this:
PostComment.joins(:replies).group('replies.reply_id')

Related

Rails find substring in jsonb multiple where conditions

Let's say I have the table recipes with the column ingredients, jsonb column type.
Example record:
{
id: 1,
ingredients: [
'eggs',
'fragrant bread',
'fresh tomatoes'
]
}
How can I retrieve the record with substring in where conditions?
For example:
ingredients = ['egg', 'tomato']
Recipe.where('ingredients ?& array[:keys]', keys: ingredients)
I was trying:
ingredients = ['egg', 'tomato']
Recipe.where("ingredients #> ARRAY[?]::varchar[]", ingredients).count
But I'm getting this error:
ERROR: operator does not exist: jsonb #> character varying[] (PG::UndefinedFunction)
I would really consider just using two tables instead as this stinks of the unnesiccary JSON antipattern.
class Recipe
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
def self.with_ingredients(*ingredients)
binds = Array.new(ingredients.length, '?')
sql = "ingredients.name ILIKE ANY(ARRAY[#{binds.join(,)}])"
joins(:ingredients)
.where(
sql,
*ingredients.map { |i| "'%#{i}%'" }
)
end
end
class RecipeIngredient
belongs_to :ingredient
belongs_to :recipe
end
class Ingredient
has_many :recipe_ingredients
has_many :recipes: through: :recipe_ingredients
end
This lets you use an effective index on recipies.name, sane queries and avoids denormalization.

Rails - Serialize related data

I've got two models:
class Continent < ActiveRecord::Base
has_many :countries
end
class Country < ActiveRecord::Base
belongs_to :continent
end
I created controller like:
class ContinentsController < ApplicationController
def index
#continents = Continent.all
render json: #continents
end
end
and serializer:
class ContitnentSerializer < ActiveModel::Serializer
attributes :name, :countries
end
Here my issue begins. I'd like to serialize only countries with given condition where value comes from HTTP GET params. E.g country inside serializer should be displayed only if population is more than params[:population]. The problem is inside serializer we don't have access to params to examine that.
[
{
name: 'Europe'
countries: [
{
name: 'Italy',
population: 1000000
}
]
},
{
name: 'Africa'
countries: [
]
}
]
I've tried to join table with condition but it seems be not working.
#continents = Continent.all.joins("LEFT JOIN countries ON countries.continent_id = continents.id AND countries.population > #{params[:population]}")
Create a scope and call the scope with param value from controller:
scope :population_more_than, ->(population) {all.joins("LEFT JOIN countries ON countries.continent_id = continents.id AND countries.population > ?", population)}
Now call it from controller instead of Continent.all
Continent.population_more_than(params[:population])
You can try
#continents = Continent.all
#continents.num_population = params[:population]
render json: #continents.to_json(methods: :countries_with_population_gt)
in your Continent model
attr_accessor :num_population
def countries_with_population_gt(num_population=0)
countries.where('population > ?', #num_population)
end
Basically, you need to select only Continents that fall under specific rule. If this is a frequently used filter, then I would go with the Babar's suggestion and create a scope.
If this is a one time selection, then I prefer simply do filtering right there without cluttering up my models with non-frequently used scopes.
Continent.joins(:countries).where("countries.population > :population", population: params[:population])
# Or event shorter
Continent.joins(:countries).where("countries.population > :population", params)

which rails query/chain is better?

I have a rails app with the models below. I have both assigned_tasks and executed_tasks for a given user. I would like to know which option is better for getting all the tasks (executed and assigned as well) for that given user.
task.rb
belongs_to :assigner, class_name: "User"
belongs_to :executor, class_name: "User"
user.rb
has_many :assigned_tasks, class_name: "Task", foreign_key: "assigner_id", dependent: :destroy
has_many :executed_tasks, class_name: "Task", foreign_key: "executor_id", dependent: :destroy
Solution 1:
task.rb
scope :completed, -> { where.not(completed_at: nil) }
scope :uncompleted, -> { where(completed_at: nil) }
user.rb
def tasks_uncompleted
tasks_uncompleted = assigned_tasks.uncompleted.order("deadline DESC")
tasks_uncompleted += executed_tasks.uncompleted.order("deadline DESC")
tasks_uncompleted.sort_by { |h| h[:deadline] }.reverse!
end
tasks_controller:
#tasks = current_user.tasks_uncompleted.paginate(page: params[:page], per_page: 12)
Solution 2:
task.rb
scope :completed, -> { where.not(completed_at: nil) }
scope :uncompleted, -> { where(completed_at: nil) }
scope :alltasks, -> (u) { where('executor_id = ? OR assigner_id = ?', u.id, u.id) }
tasks_controller
#tasks = Task.alltasks(current_user).uncompleted.order("deadline DESC").paginate(page: params[:page], per_page: 12)
You should define an association on User that will return all of the Tasks associated by either executor_id or assigner_id:
class User < ActiveRecord::Base
has_many :assigned_and_executed_tasks,
->(user) { where('executor_id = ? OR assigner_id = ?', user, user) },
class_name: 'Task',
source: :tasks
end
user = User.find(123)
user.assigned_and_executed_tasks
# => SELECT tasks.* FROM tasks WHERE executor_id = 123 OR assigner_id = 123;
Then you can do as you do in "Solution 2," but instead of the unfortunate Task.alltasks(current_user) you can just do current_user.assigned_and_executed_tasks (of course you could give it a shorter name, but descriptive names are better than short ones):
#tasks = current_user.assigned_and_executed_tasks
.uncompleted
.order("deadline DESC")
.paginate(page: params[:page], per_page: 12)
Solution 2 will be the more efficient way of retrieving the records from your database. In most Rails apps, calls to the database are a frequent cause of bottlenecks, and in solution 2 you make one call to the database to retrieve all the records, but in solution 1 you make two calls to the database to retrieve the same information.
Personally, I also think this solution is much more readable, easily testable, and maintainable, so solution 2 is better in many ways beyond speed!

Rails 3: Polymorphic conditioned query

I've got the following models:
class Notification < ActiveRecord::Base
belongs_to :notificatable, polymorphic: true
end
class BounceEmailNotification < ActiveRecord::Bas
has_one :notification, :as => :notificatable, :dependent => :destroy
end
class UserNotifierEmailNotification < ActiveRecord::Base
has_one :notification, :as => :notificatable, :dependent => :destroy
end
As you can see, a notification can be of type "bounce email notification" or "user notifier email notification". The BounceEmailNotification model has a event string attribute. What if I want retrieve all user notifier email notifications and all bounce email notifications which have a specific event value, ordered by created_at?
Something like this (using squeel):
(Notification.joins{ notificatable(BounceEmailNotification) }.where('bounce_email_notifications.event' => 'error') + Notification.joins { notificatable(UserNotifierEmailNotification) }).sort_by { |n| n.created_at }
will work, but I don't want to use Ruby to order the notifications. What can I do? Thanks
Try this.
Notification.joins('left outer join bounce_email_notifications on notifications.notificatable_id = bounce_email_notifications.id and notificatable_type = "BounceEmailNotification"').
where("bounce_email_notifications.event = ? or notificatable_type = ?",'error',UserNotifierEmailNotification.to_s).
order('notifications.created_at')
This is using active record 3.2 but I guess it should work in rails 3.
I have not used squeel cant comment much on how to use it with squeel but this can give you a brief idea.
In squeel I came up with something like this ( NOT TESTED) created by using squeel docs
Notification.notificatable{notable(BounceEmailNotification).outer}. where{
{ ('bounce_email_notifications.event'=>'error') | (:notificatable_type=>UserNotifierEmailNotification.to_s) }
Conceptual steps
left join on the bounce_email_notifications to get all bounce_email_notifications and and non bounce_email_notifications notification in result
check if the bounce_email_notifications.event = event
or
notificatable_type = 'UserNotifierEmailNotification' for all UserNotifierEmailNotification records
sort the records by notifications.created at
Hope this helps.

Why are individual SELECT queries running when an all-encompassing SELECT already ran? (Rails/ActiveRecord)

I have the following code (note the includes and the .each):
subscribers = []
mailgroup.mailgroup_members.opted_to_receive_email.includes(:roster_contact, :roster_info).each { |m|
subscribers << { :EmailAddress => m.roster_contact.member_email,
:Name => m.roster_contact.member_name,
:CustomFields => [ { :Key => 'gender',
:Value => m.roster_info.gender.present? ? m.roster_info.gender : 'X'
} ]
} if m.roster_contact.member_email.present?
}
subscribers
Correspondingly, I see the following in my logs (i.e. select * from ROSTER_INFO ... IN (...)):
SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` IN ('1450', '1000', '1111')
Yet immediately after that there are select * from ROSTER_INFO for each ID already specified in the IN list of the previous query:
RosterInfo Load (84.8ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1450' LIMIT 1
RosterInfo Load (59.2ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1000' LIMIT 1
RosterInfo Load (56.8ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1111' LIMIT 1
If select * had already been done on ROSTER_INFO on all IDs of interest (IN (...)), why is another select * being done again for each of the same IDs? Doesn't ActiveRecord already know all the ROSTER_INFO columns for each ID?
(Meanwhile, there are no individual queries for ROSTER_CONTACT, yet if I remove :roster_contact from the includes method, then ROSTER_INFO is not queried again, but ROSTER_CONTACT is.)
RosterInfo model (abridged)
class RosterInfo < ActiveRecord::Base
self.primary_key = 'ID'
end
RosterContact model (abridged)
class RosterContact < ActiveRecord::Base
self.primary_key = 'ID'
has_many :mailgroup_members, foreign_key: 'rosterID'
has_many :mailgroups, through: :mailgroup_members
has_one :roster_info, foreign_key: 'ID' # can use this line
#belongs_to :roster_info, foreign_key: 'ID' # or this with no difference
def member_name # I added this method to this
roster_info.member_name # question only *after* having
end # figured out the problem.
end
RosterWeb model (abridged)
class RosterWeb < ActiveRecord::Base
self.primary_key = 'ID'
end
Mailgroup model (abridged)
class Mailgroup < ActiveRecord::Base
self.primary_key = 'ID'
has_many :mailgroup_members, foreign_key: 'mailCatID'
has_one :mailing_list, foreign_key: :legacy_id
end
MailgroupMember model (abridged)
class MailgroupMember < ActiveRecord::Base
self.primary_key = 'ID'
belongs_to :mailgroup, foreign_key: 'mailCatID'
belongs_to :roster_contact, foreign_key: 'rosterID'
belongs_to :roster_info, foreign_key: 'rosterID'
belongs_to :roster_web, foreign_key: 'rosterID'
scope :opted_to_receive_email, joins(:roster_web).where('ROSTER_WEB.receiveEmail=?', 1)
end
The issue turned out to be related to m.roster_contact.member_name -- unfortunately I made member_name a method of roster_contact that itself (indirectly) queried roster_info.member_name. I resolved this by changing the line
:Name => m.roster_contact.member_name,
to directly query roster_info as follows
:Name => m.roster_info.member_name,
I am sorry for the trouble!
I'm going to stick my neck out and say that this is probably an in-flight optimization by your query engine. The 'IN' is typically used to compare large sets of keys, the most efficient way of resolving three keys (assuming ID is the key) would be to retrieve each row by key, as has happened.
class RosterInfo < ActiveRecord::Base
has_one :roster_contact, foreign_key: 'ID'
end
class RosterContact < ActiveRecord::Base
has_one :roster_info, foreign_key: 'ID'
end
I don't know what is the premise for having bi-directional has_one, but I suspect it will turn out badly. Probably change one of them to belongs_to. Do the same for the other bi-directional has_one associations.
Another thing is that you are using 'ID' for the foreign_key column, where the usual practice is roster_contact_id or whichever class you are referencing.
Edit:
On closer examination, RosterInfo, RosterContact, RosterWeb look like separate tables for what should be a single record since they are all having the same set of mutual has_one associations. This is something that should be addressed on the schema level, but right now you should be able to drop the has_one associations from one of the three models to solve your immediate problem.