ActiveRecord: sort of complicated query with nested associations and sorting - sql

Alrigth, so I'm not sure if this isn't too specific... But I really don't have a clue how to construct such a query, neither in AR, nor in SQL. So here's the situation:
I have a User model. User has_many Projects. A Project, in turn, have a following associations:
Project has_one BasicEvent
Project had_many AdditionalEvents
BasicEvent and AdditionalEvent classes are built on inheritance from a AR model class, Event
Now, the goal is this: on my view, I need:
access to all Projects count per User
access to all Events count, where happened_at attribute in nil
have all the Users sorted, by the count of Events, where happened_at attribute in nil
I made a couple of attempts at it so far, but didn't really get too far... I will appreciate any help with this complicated (at least from my perspective) query.

You can start from code like:
# -*- frozen-string-literal: true -*-
class UserStatService
SELECT_CLAUSE = <<~SQL
(SELECT count(1) FROM projects WHERE user_id=users.id) AS projects_count,
(SELECT count(1) FROM events WHERE (happened_at IS NULL)
AND (project_id IN (SELECT id FROM projects WHERE user_id=users.id)
) AS events_count,
users.*
SQL
def call
User
.select( SELECT_CLAUSE )
.order( 'events_count' )
end
end
Each User instance in resultset will have projects_count and events_count attributes.
Also any kind of cache (counter_cache or handmade) is recommended. Because the query is not expected to be fast.

Projects count per User: Project.group(:user_id).count
Events count, where happened_at attribute in nil: Event.where(happened_at: nil).count
Users sorted, by the count of Events, where happened_at attribute in nil: This is the trickiest but it's also not particularly difficult:
User.
select("users.*, (SELECT COUNT(*) FROM events WHERE user_id = users.id AND happened_at IS NULL) AS unhappened_events_count").
order("unhappened_events_count DESC")
This is not a particularly efficient query but it should do its job well for several thousand records (easily) – especially if you set an index on user_id and happened_at in the events table.

Assuming basic_events and additional_events have essentially the same columns, a great solution to this problem would be to change your database/app to use single table inheritance.
Assuming you're using Rails 5.x your models would look like this:
class User < ApplicationRecord
has_many :projects
has_many :basic_events, through: :projects
has_many :additional_events, through: :projects
end
class Project < ApplicationRecord
belongs_to :user
has_one :basic_event
has_many :additional_events
end
class Event < ApplicationRecord
belongs_to :project
end
class BasicEvent < Event
belongs_to :project
end
class AdditionalEvent < Event
belongs_to :project
end
This way, there are only three tables involved users, projects and events. Your events table will have a :type column where you can specify if it's basic or additional.
For your queries:
access to all Projects count per User
User
.select("users.*, count(projects.id) AS projects_count")
.joins(:projects)
.group('users.id')
.order('projects_count DESC')
Using select like this give you access to a :projects_count method on each User object returned in this active record relation, so if you assigned this query to a variable called users_with_projects_count you could do users_with_projects_count.first.projects_count and it would return the number of projects associated with that user.
access to all Events count, where happened_at attribute in nil
User
.select("users.*, count(events.id) AS events_count")
.joins(:events)
.where('events.happened_at IS NULL')
.group('users.id')
You can access :events_count the same way you did :projects_count in the last example.
have all the Users sorted, by the count of Events, where happened_at attribute in nil
User
.select("users.*, count(events.id) AS events_count")
.joins(:events)
.where('events.happened_at IS NULL')
.group('users.id')
.order('events_count DESC')
You use the same query as the last example just add an :order.

Related

How to reduce queries on associated object with Rails ActiveRecord

I have the following problem.
I have an Organization class that returns and array of "authorized user" emails and their associated User ID.
class Organization < ApplicationRecord
...
has_many :authorized_users
def authorized_user_opts
self.authorized_users.map do |authorized_user|
[authorized_user.email, authorized_user.user.id]
end
end
end
Then the AuthorizedUser class - note that we lookup the user via a find_by:
class AuthorizedUser < ApplicationRecord
...
def user
User.find_by(email: email)
end
end
And the User model:
class User < ApplicationRecord
validates :email, presence: true
end
This creates an extra query for each user to get their ID. Is there a way I can improve this query?
I thought about migrating the AuthorizedUser class to add a user_id field, but I'm wondering if there's a way to improve this just SQL instead of adding another field.
I think the addition of a user_id to AuthorizedUser is a decent choice, but if you must do it without, you should be able to use joins and includes:
authorized_users.joins("inner join users on users.email = authorized_users.email").includes(:users)
joins here is doing what an ActiveRecord association would do under the hood, and then includes eager loads the user objects in one query so that you don't have N queries for N users.
You might also be able to mess with the options on belongs_to which lets you specify the key that it uses under the hood. Something like:
# authorized_user.rb
belongs_to :user, foreign_key: 'email', primary_key: 'email'

Add Arbitrary Attribute to SQL Query from Joins Record without WHERE clause (Active record)

I'm trying to create an attribute in my select statement that depends on whether or not an association exists. I'm not sure if it's possible with a single query, and the goal is to not have to iterate a list afterward.
Here is the structure.
class Project < ApplicationRecord
has_many :subscriptions
has_many :users, through: :subscriptions
end
class User < ApplicationRecord
has_many :subscriptions
has_many :projects, through: :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :project
belongs_to :user
end
Knowing a project, the goal of the query is to return ALL users and include on them a new attribute call subscribed - denoting whether or not they are subscribed.
non-working code (pseudo code):
project = Project.find_by(name: 'has_subscribers')
query = 'users.*, (subscriptions.project_id = ?) AS subscribed'
users = User.includes(:subscriptions).select(query, project.id)
user.first.subscribed
# => true or false
I'm open to whether or not there is a better way of going about this. However, the information is:
You know the project record.
You query a list of ALL users
Each user record has a subscribed attribute, denoting whether its
subscribed to the given project
Solution:
I was able to figure out a straight forward solution using the bool_or aggregate method. Coalesce ensures that the value returned is false instead of nil, should no subscriptions exists.
query = "users.*, COALESCE(bool_or(subscriptions.project_id = '#{project_id}'::uuid), false) as subscribed"
User.left_outer_joins(:subscriptions)
.select(query)
.group('users.id')
Yep, you can do this:
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id])
Which will result in a SQL query like this:
SELECT *, "subscriptions"."project_id" FROM "users" INNER JOIN "subscriptions" ON "subscriptions"."user_ud" = "users"."id";
If you want to specify a specific project (i.e. use an expression), you can do it with Arel like this:
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id].eq(42))
Unfortunately, you won't have a column name alias, and you can't call as on an Arel::Nodes::Equality instance. I don't know enough about the internals of Arel to have a way out of that box. But you can do this if you want the composability of Arel (e.g. if this is going to be something that needs to work with multiple models or columns):
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id].eq(42).to_sql + " as has_project")
This is a bit clunky, but it works and provides a user.has_project method that returns a boolean. You can pretty it up like so:
class User
scope :with_project_status, lambda do |project_id|
has_project =
Subscription.arel_table[:project_id].
eq(project_id).to_sql + " as has_project"
joins(:projects).select(Arel.star, has_project)
end
end
User.with_project_status(42).where(active: true)

How to manually join two different table with different attribute name in Ruby on Rails controller

I am currently making a website that runs on Ruby on Rails. I am facing some issues while I was trying to join two tables, Rates and Locations, that I have with two different attributes name.
Rates: id rater_id rateable_id (and a few more attributes in this table)
Locations: id title body user_id (and a few more attributes in this table)
Here is the query that I am trying to do in SQL.
SELECT *
FROM rates, locations
WHERE rates.rater_id = locations.user_id AND rates.rateable_id = locations.id
I have read the official active record documents that provided by rubyonrails.org. I have tried doing these, but it does not work. Here is the code that I am trying to implant in app\controllers\users_controller.rb
#join_rating = Rate.joins(:locations).where("rates.rateable_id = locations.id AND rates.rater_id = locations.id")
#all_rating = #all_rating.where(rater_id: #user)
#count_all_rating = #all_rating.count
#join_rating, is trying to join the attributes with different names.
#all_rating, is trying to filter which location to show using the user ID
#join_rating, is trying to calculate the total numbers of locations that are rated by the user
Assume that everything is setup correctly and the only error is in the query that I am trying to do, how should I rewrite the statement so that I am able to show the locations that the user has rated using #all_rating.
Thank you!
A few points:
When in ActiveRecord you're starting a statement with the Rate class, it means the result is going to be a collection of Rate objects. So if you're trying to show locations, you should start with a Location class.
#locations_user_rated = Location.joins('INNER JOIN rates ON
rates.rateable_id = locations.id').where('rates.rater_id' => #user)
And if your ActiveRecord associations are well defined, you could simply do:
#locations_user_rated = Location.joins(:rates).where('rates.rater_id' => #user)
"Well defined" simply means you'll need to do something like the following. Note that I am not sure I understand your model relationships correctly. I assume below that every location has multiple rates, and that the reason your Rate model has the field called rateable_id instead of a location_id is because you want :rateable to be polymorphic. This means you probably also have a rateable_type field in rates table.
class Location < ActiveRecord::Base
has_many :rates, as: :rateable
end
class Rate < ActiveRecord::Base
belongs_to :rateable, polymorphic: true
end
If this polymorphism is not the case, things should actually be simpler, and I highly recommend that you follow Rails's conventions and simply name the relationship field location_id on your Rate model instead of rateable_id. Then you can do:
class Location < ActiveRecord::Base
has_many :rates
end
class Rate < ActiveRecord::Base
belongs_to :location
end
If still you are not convinced about the field name, you can customize things and do:
class Location < ActiveRecord::Base
has_many :rates, foreign_key: :rateable_id
end
class Rate < ActiveRecord::Base
belongs_to :location, foreign_key: :rateable_id
end
You can find more about how to customize associations here, and here.
I highly recommend taking advantage of ActiveRecord's has_many, belongs_to, and has_many through: functionality.
If you set up a model for each of these tables, with the correct relationships:
class User < ActiveRecord::Base
has_many :ratings, foreign_key: :rater_id
has_many :rated_locations, through: ratings, class_name: Location.name, source: :rater
end
class Rating < ActiveRecord::Base
belongs_to :rater, class_name: User.name
belongs_to :location
end
class Location < ActiveRecord::Base
has_many :ratings
end
Then to access the locaitons that a user has rated, you just call
user.rated_locations

Querying for rows without matching ID in associated table

I have a very standard app backed by an SQL database with a User model, a Problem model, and a CompletedProblem model acting as a join table between the two.
I'm trying to create a method that returns all problems not solved by a particular user. I have run into a wall, however, and I would appreciate pointers on what my method should look like.
Below are the models as well as my latest (incorrect) pass at creating this method.
class User < ActiveRecord::Base
has_many :completed_problems
has_many :problems, :through => :completed_problems
def unsolved_problems
Problem.includes({:wall => :gym}, :completed_problems).
where('completed_problems.user_id != ? OR completed_problems.user_id IS NULL)', self.id)
end
end
class Problem < ActiveRecord::Base
has_many :completed_problems
has_many :users, :through => :completed_problems
end
class CompletedProblem < ActiveRecord::Base
belongs_to :user
belongs_to :problem
end
(For the curious: this method does work so long as there is only one user marking problems as solved. As soon as you add a second, each user starts to return only those problems that have been solved by other users, instead of those not solved by herself.)
Via a friend:
select * from problems where id not in (select problem_id from completed_problems where user_id = USER_ID))
Although I'd still be interested in hearing if there's a way in ActiveRecord to do this.
I think something like this will do it:
Problem.where(["id NOT IN (?)", self.problems.all.map(&:id)])

Rails - named scope issue (depends on number of children in a many_to_many relationship)

Let's say I have a many-to-many relationship between users and group. A user can be a member of a group, or his application can still be pending.
class User < ActiveRecord::Base
has_many :applications
has_many :groups, :through => :applications
end
class Group < ActiveRecord::Base
has_many :applications
has_many :users, :through => :applications
end
class Application < ActiveRecord::Base
belongs_to :user
belongs_to :group
attr_accessible :pending # boolean : is the application still pending or not
end
I want to add a scope to my Group class, to select the groups who have more than 10 non-pending users.
I can get those members like this
Group.joins(:applications).where('applications.pending = ?', false)
But I didn't find enough resources to make the scope which counts the numbers of results of this query, and returns the groups where this number is greater than 10
If you have a solution, or ressources on this topic, this would help me greatly
I didn't spec out your models in my own console, but wouldn't something along these lines work?
Group.joins(:applications).group('groups.id').having('COUNT(*) > 10').where(["applications.pending = ?", false])
Basically, once you include GROUP BY conditions in the underlying SQL, you can use HAVING on the aggregate results. The WHERE simply limits it to those results that you're looking for. You can achieve the same result using straight SQL:
Group.find_by_sql(["SELECT * FROM groups INNER JOIN applications ON applications.group_id = groups.id WHERE applications.pending = ? GROUP BY groups.id HAVING COUNT(*) > ?", false, 10])
Kind of a huge query to include in a named scope. You might want to consider breaking this into pieces - it might save lots of headaches later...