Rails activerecord find objects with specific children associations - mysql

I have a model User who can have many Features:
class User << ActiveRecord::Base
has_many :features, dependent: :destroy
end
class Feature << ActiveRecord::Base
belongs_to :user
# columns id, user_id, name
end
I have 2 features I can put on a user called "feat1" and "feat2" The 2 features can combine for a total of 4 types of users:
User has feat1 ONLY
User has feat2 ONLY
User has BOTH feat1 and feat2
User has NEITHER feat1 and feat2
I want to create scopes on user to scope out the 4 types of users.
User.only_feat1
User.only_feat2
User.both_feats
User.no_feats
I've been playing around with .merge, .uniq, .joins, .includes, but can't seem to figure out the activerecord way.

So here's my solution. It involves a lot of raw SQL, but it gets the job done. The problem is if I join the same table (features) to my users table more then once, then the where clauses get overwritten and I can't do multiple joins.
So to combat this problem, I wrote a lot of raw sql which explicitly aliases the table on the join forcing the double join table.
scope :without_feature, ->(feat) { joins("LEFT OUTER JOIN features af ON users.id = af.user_id AND af.name = '#{feat}'").where(af: {id: nil}) }
scope :feat1, -> { joins("INNER JOIN features rs ON users.id = rs.user_id AND rs.name = 'feat1'") }
scope :feat2, -> { joins("INNER JOIN features rr ON users.id = rr.user_id AND rr.name = 'feat2'") }
scope :both_feats, -> { feat1.feat2 }
scope :only_feat1, -> { feat1.without_feature('feat2') }
scope :only_feat2, -> { feat2.without_feature('feat1') }
Hope this helps anyone else.

Related

convert mysql query into rails query

Hi all I have a problem converting mysql query into rails query.
I have these models -
class User < ApplicationRecord
has_many :comments, foreign_key: "commenter_id"
end
class Comment < ApplicationRecord
belongs_to :commenter, class_name: "User"
end
Can anyone help me out with converting following query into rails query-
UPDATE comments
INNER JOIN users on comments.commenter_id = users.id
SET comments.deleted_at = users.deleted_at
WHERE users.deleted_at IS NOT NULL
I am trying to make soft-delete comments whose commenter was softly deleted.
UPDATE 1:
so far I can able to do it by using this-
User.only_deleted.includes(:comments).find_each do |u|
u.comments.update_all(deleted_at: u.deleted_at)
end
But I want to do this on single query without having to iterate over the result.
UPDATE 2:
I am using acts_as_paranoid gem, so unscoping user is needed and my final query became:
User.unscoped{Comment.joins(:commenter).where.not(users: {deleted_at: nil}).update_all("comments.deleted_at = users.deleted_at")
This should work on MySQL:
Comment
.joins(:user)
.where.not(users: { deleted_at: nil })
.update_all("comments.deleted_at = users.deleted_at")
This won't work on Postgres since its missing a FROM clause for users.
A less performant but polyglot option is:
Comment
.joins(:user)
.where.not(users: { deleted_at: nil })
.update_all("deleted_at = ( SELECT users.deleted_at FROM users WHERE comments.id = users.id )")
This is still probably an order of magnitude better than iterating through the records in Ruby since you eliminate the traffic delay between your app server and the db.
From your comments, I think this is what you want:
Comment.where.not(user_id: nil).each { |comment| comment.update_attributes(deleted_at: comment.user.deleted_at)
Or slightly more readable:
Comment.all.each do |comment|
next unless comment.user.present?
comment.update_attributes(deleted_at: comment.user.deleted_at)
end
The code below should execute number of queries corresponding to deleted_users and without loading User and any associated Comments in memory
deleted_users_data_arr = User.only_deleted.pluck(:id, :deleted_at)
deleted_users_data_arr.each do |arr|
deleted_user_id = arr[0]
user_deleted_at = arr[1]
Comment.where(commenter_id: deleted_user_id).update_all(deleted_at: user_deleted_at)
end

ActiveRecord- Inner Join two SQL tables + Where clauses

I have two tables in MySQL and I would like to call a SQL query using an inner join and multiple Where clauses based on a form's input. I am aware that you can execute a raw SQL Query via ActiveRecord::Base.connection, but I'd like to learn how to do so using ActiveRecord objects. The schema for the two MySQL tables are like so:
Describe Options;
'VIN','varchar(45)','NO','PRI',NULL,''
'LEATHER','varchar(45)','YES','',NULL,''
'4WD','varchar(45)','YES','',NULL,''
'FOGLIGHTS','varchar(45)','YES','',NULL,''
'DVD','varchar(45)','YES','',NULL,''
'SURROUND','varchar(45)','YES','',NULL,''
and
Describe Inventory;
'VIN','varchar(30)','NO','PRI',NULL,''
'MAKE','varchar(30)','NO','',NULL,''
'MODEL','varchar(30)','NO','',NULL,''
'TYPE','varchar(50)','NO','',NULL,''
I would like to execute a SQL script like so:
Select Inventory.* from Inventory
INNER JOIN Options
ON Inventory.VIN = Options.VIN
WHERE Inventory.Make = "Toyota"
AND Options.Leather = "Yes";
My Ruby classes in ActiveRecord are like so:
class Option < ActiveRecord::Base
self.table_name = "Options"
end
class Inventory < ActiveRecord::Base
self.table_name = "INVENTORY"
end
Again, I know that I can just input the query as a script, but I'd like to learn how to do it via Ruby best practices
You can change into this:
Inventory.joins("INNER JOIN Options ON Inventory.VIN = Options.VIN")
.where("Inventory.Make = ? AND Options.Leather = ?", "Toyota", "YES")
In my opinion, I'd say that you have to change you table into inventories and options for model Inventory and Option so you don't need to use set_table_name in each model. It's about rails convention style code. Then you can see the model like this.
class Inventory < ActiveRecord::Base
has_many :options, foreign_key: "VIN"
end
class Option < ActiveRecord::Base
belongs_to :inventory, foreign_key: "VIN"
end
I hope this help you.

ActiveRecord custom has_many association makes multiple calls to the database

I have a pair of ActiveRecord objects that have a belongs_to ... has_many association, with the has_many association being custom-made. Example:
First AR object:
class Car < Vehicle
has_many :wheels, class_name: "RoundObject", foreign_key: :vehicle_id, conditions: "working = 1"
validates_presence_of :wheels
...
end
Second AR object:
class RoundObject < ActiveRecord::Base
belongs_to :vehicle
...
end
Please note that the above is not indicative of my app's function, simply to outline the association between my two AR objects.
The issue I'm having is that, when I reset the cache (and thus my Rails app re-caches all AR objects in the database), when it comes time for the RoundObject object to get re-cached, it makes multiple calls to the database, one for each unique vehicle_id associated with the collection of RoundObjects. The SQL commands being run are output to the console, so this is what my output looked like:
RoundObject Load (2.0ms) SELECT `round_objects`.* FROM `round_objects` WHERE `round_objects`.`vehicle_id` = 28 AND (active = 1)
RoundObject Load (1.0ms) SELECT `round_objects`.* FROM `round_objects` WHERE `round_objects`.`vehicle_id` = 29 AND (active = 1)
RoundObject Load (2.0ms) SELECT `round_objects`.* FROM `round_objects` WHERE `round_objects`.`vehicle_id` = 30 AND (active = 1)
My app has several other AR objects that use the built-in has_many association without any modifications, and I notice that they only hit the database once when resetting the cache. For instance:
Micropost Load (15.0ms) SELECT `microposts`.* FROM `microposts` INNER JOIN `posts` ON `posts`.`id` = `microposts`.`post_id` WHERE `microposts`.`active` = 1 AND `posts`.`active` = 1
My question is, how can I make my AR object only hit the database once on cache reset, while still maintaining the custom has_many association I need? Can I manually force a join on the SQL query being called, and will this help?
Thank you!
You can use includes method while calling your Vehicle object to include the RoundObject.
It will go like this:
Vehicle.where(conditions_for_getting_data).includes(:round_object)

How do I make a newsfeed without many join statements

What is the best way to approach making a newsfeed?
Currently I have an observer that creates a new newsfeedactivity record everytime someone creates a record. But, to deal with privacy I end up have 7 or 8 joins to get the appropriate output.
It seems like this is going to be slow and inefficient. What's another strategy for pulling out the right newsfeedactivity records as a scope?
More details:
Currently I have a site to help users track projects that they're working on. There are public and private projects (where people can invite collaborators).
I want my newsfeed to include when public projects are created. When you are invited to a private project. When a user follows a project. And then all of the actions of the other users that you're following. Then for the private projects I have another join table to determine who has access to the projects. (There are also comments on each of these projects that I want to show up in the newsfeed as well).
All of the following relationships are currently in join tables, which is why I have a lot of joins.
To get an idea of the type of query - I'm thinking it would look something like this:
SELECT news_feed_activities.* FROM news_feed_activities LEFT JOIN
user_following_relationships ON
user_following_relationships.following_id =
news_feed_activities.user_id LEFT JOIN
user_project_relationships ON
user_project_relationships.project_id =
news_feed_activities.responding_to_id AND
news_feed_activities.responding_to_type = 'Project' WHERE
(user_following_relationships.user_id = 1 OR
user_project_relationships.user_id = 1 OR
news_feed_activities.user_id = 1 OR
up2.user_id = 1) GROUP BY news_feed_activities.id ORDER BY
news_feed_activities.id DESC
EDIT:
I think I'm probably going to end up using Redis along these lines http://blog.waxman.me/how-to-build-a-fast-news-feed-in-redis
As RoR.
In your controller:
#user = current_user # (?)
recent_since = 24.hours.ago
#news_feed = []
# 1) I want my newsfeed to include when public projects are created.
#news_feed += Project.recent.open
# 2) When you are invited to a private project.
#news_feed += #user.invites.received.pending
# 3) When a user follows a project.
#news_feed += #user.user_following_relationships.recent
# 4) And then all of the actions of the other users that you're following.
#news_feed += #user.follows.collect(&:activities)
# 5) Then for the private projects I have another join table to determine who has access to the projects. (There are also comments on each of these projects that I want to show up in the newsfeed as well).
#news_feed += #user.projects.closed
#news_feed.sort!{ |a,b| a.created_at <=> b.created_at }
I did some sample scopes for you too.
project.rb
scope :recent, :conditions => ["created_at >= ?", 24.hours.ago]
scope :open, :conditions => "publicity = 'Public'"
scope :closed, :conditions => "publicity = 'Private'"
This is based on the precept that your news feed is actually a summary of recent activity across models rather than having a 'newsfeed' model.

Rails query/scope: exclude objects according to attribute of joined model

My models look like this:
class Ticket < ActiveRecord::Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :tickets
end
I want to have a scope that gives me all of the distinct Tickets that are not tagged unresolved (as in tag.name != "unresolved")
How would I go about doing this? For example, if 1 ticket has 6 tags (one of which being unresolved) I only want to return 1 instance of that ticket, not 5 in the scope. I've managed to do the opposite (all Tickets that are tagged unresolved) as such:
scope :unresolved, :select => "DISTINCT tickets.*", :joins => :tags, :conditions => "tags.name = 'unresolved'"
Depending on how flexible you want the scope to chain you have two options.
Use uniq within the scope (note: this may have a negative impact on chaining this scope with others, especially when adding more complex conditionals):
scope :unresolved, -> { joins(:tags).where(tags: { name: 'unresolved' }).uniq }
Use a LEFT OUTER join via includes(:tags) instead of the INNER join default used by joins(:tags)
scope :unresolved, -> { includes(:tags).where(tags: { name: 'unresolved' }) }