How to override the default has_many condition for a relation in rails? - ruby-on-rails-3

I would like to enter my own condition for a has_many relationship in my ActiveRecord model.
I want my condition to override the default condition.
Class User < ActiveRecord::Base
has_many :notifs, :conditions =>
proc { "(notifs.user_id = #{self.id} OR notifs.user_id = 0)" }
And it generates:
Notif Load (0.2ms) SELECT notifs.* FROM notifs WHERE
notifs.user_id = 1 AND ((notifs.user_id = 1 OR notifs.user_id =
0))
I don't want the default condition of active record (the first WHERE notifs.user_id = 1 outside parens). I want only my own. How do I specify that ?

For rails 4.1+ you can use unscope inside the association scope.
class Post
belongs_to :user
end
class User
has_many :posts, -> { unscope(:where).where(title: "hello") }
end
User.first.posts
# => SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 [["title", "hello"]]

Replace :conditions with :finder_sqllike so:
has_many :notifs,
:finder_sql => proc { "(notifs.user_id = #{self.id} OR notifs.user_id = 0)" }
More details in the documentation: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_many

Related

How to write a Rails SQL query for finding an object where all children have an equal value

I've been reading this, but can't make sense of writing it into a Rails scope :
find all parent records where all child records have a given value (but not just some child records)
I have a Course, Section, and Quiz, object :
class Course < ActiveRecord::Base
has_many :course_members
has_many :members, through: :course_members
has_many :sections
has_many :quizzes, through: :sections
end
class Quiz < ActiveRecord::Base
belongs_to :member
belongs_to :section
end
class Section < ActiveRecord::Base
belongs_to :course
has_many :quizzes
end
I'd like to find all courses of a member, where all quizzes related to that course have the attribute completed = true.
So in my Member class, I'd ideally like to write something like :
has_many :completed_courses, -> {
joins(:courses, :quizzes, :sections)
# .select( 'CASE WHEN quizzes.completed = true then 1 end') ??? maybe ???
}, class_name: 'Course'
Haha! But barring that being too complicated. I've been trying to write this simply in the Course would also be fine.
class Member < ActiveRecord::Base
has_many :courses, through: :course_members
has_many :course_members
has_many :completed_courses,
-> { joins(:quizzes).where.not(quizzes: {completed: [false, nil]}) },
through: :course_members,
source: :course
end
If your completed boolean column is NOT NULL, then change [false, nil] above to just simply false
Usage Example
irb(main):002:0> Member.first.completed_courses
Member Load (0.2ms) SELECT "members".* FROM "members" ORDER BY "members"."id" ASC LIMIT 1
Course Load (0.1ms) SELECT "courses".* FROM "courses" INNER JOIN "sections" ON "sections"."course_id" = "courses"."id" INNER JOIN "quizzes" ON "quizzes"."section_id" = "sections"."id" INNER JOIN "course_members" ON "courses"."id" = "course_members"."course_id" WHERE (NOT (("quizzes"."completed" = 'f' OR "quizzes"."completed" IS NULL))) AND "course_members"."member_id" = ? [["member_id", 1]]

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!

ActiveRecord query based on multiple objects via has_many relationship

I have a Product class that has_many Gender through Connection class instances. I want to query to find products that have both end_a and end_b present. The current class method works with 2 caveats:
Fails to return correctly if searching where end_a and end_b are the same. Instead should search if product has 2 instances, not just one of object.
Returns an Array when I want an ActiveRecord_Relation.
The class method .query is below, any feedback or ideas are appreciated.
class Product < ActiveRecord::Base
has_many :connections, dependent: :destroy, as: :connectionable
has_many :genders, through: :connections
def self.query(end_a, end_b)
search_base = active.joins(:connections)
end_a_search = search_base.where(connections: { gender_id: end_a } )
end_a_search & search_base.where(connections: { gender_id: end_b } )
end
end
ps: Once this is figured out will likely move this to a scope for Product
class Product < ActiveRecord::Base
has_many :connections, dependent: :destroy, as: :connectionable
has_many :genders, through: :connections
scope :with_genders, -> (end_a, end_b) {
relation = joins('INNER JOIN connections c1 ON c1.connectionable_id = products.id AND c1.connectionable_type = \'Product\'')
.joins('INNER JOIN connections c2 ON c1.connectionable_id = c2.connectionable_id AND c2.connectionable_type = \'Product\'')
.where(c1: {gender_id: end_a}, c2: {gender_id: end_b})
.group('products.id')
end_a == end_b ? relation.having('COUNT(products.id) > 1') : relation
}
end

has_many through instead of join query

I have relationship between User models defined through Friendship model. (ROR 4)
User
class User < ActiveRecord::Base
has_many :friendships, ->(object) { where('user_id = :id OR friend_id = :id', id: object.id) }
has_many :friends, ->(object) { where(friendships: {status: 'accepted'}).where('user_id = :id OR friend_id = :id', id: object.id) }, through: :friendships, source: :friend
has_many :requested_friends, -> { where(friendships: {status: 'pending'}) }, through: :friendships, source: :friend
end
Friendship
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: 'User'
def self.request(user, friend)
unless user == friend or find_friendship(user, friend) != nil
create(user: user, friend: friend, status: 'pending')
end
end
def self.find_friendship(user, friend)
ids = [user.id, friend.id]
where(user_id: ids, friend_id: ids).first
end
end
However, this does not work and my tests are failing because of SQL queries produced.
Friendships relation
> user.friendships
Query:
SELECT "friendships".* FROM "friendships"
WHERE "friendships"."user_id" = ?
AND (user_id = 1 OR friend_id = 1) [["user_id", 1]]
So part of WHERE before AND "kills" my actual where. I made a workaround by making instance method:
def friendships
self.class
.select('friendships.* FROM `friendships`')
.where('user_id = :id OR friend_id = :id', id)
end
Is there a way I can remove my instance method and modify has_many relation to produce the SQL I want?
Requested_friends relation
> Friendship.request(user, friend)
> friend.requested_friends
Query:
SELECT "users".* FROM "users"
INNER JOIN "friendships" ON "users"."id" = "friendships"."friend_id"
WHERE "friendships"."status" = 'pending'
AND "friendships"."user_id" = ?
AND (user_id = 2 OR friend_id = 2) [["user_id", 2]]
It obviously isn't what I need so I made a workaround by removing has_many :requested_friends and making an instance method:
def requested_friends
self.class
.joins('JOIN `friendships` friendships ON users.id = friendships.user_id')
.where('friendships.status = ?', 'pending')
.where('friendships.friend_id = ?', id)
end
Is there any way I can modify my has_many :requested_friends relation to produce same SQL as my instance method?
Very confusing - I'd do something like this:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :friendships, class_name: "user_friendships", association_foreign_key: "user_id", foreign_key: "friend_id",
has_many :friends, class_name: "User", through: :friendships
end
#app/models/user_friendship.rb
Class UserFriendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, class_name: "User"
end
You'd have a join table which looks like this:
user_friendships
id | user_id | friend_id | other | info | created_at | updated_at
This should work (I'm not sure about the self referential association). If it does, it will allow you to call:
#user.friends
I hope this helps?
You might also benefit from this gem
you cannot achieve the SQL you want using has_many method with condition. The reason is that the block you pass to the method is only additional condition, on top of the standard query which checks if user_id = ?.
Instead you can simplify your instance method a little bit
def friendships
Friendship.where('user_id = :id or friend_id = :id', id)
end

Refactoring a find for children of parent created after a time

I am trying to find any comments created after a datetime that were not made by the current user.
At first I did this..
current_user.comments.find(:all, :conditions=>["created_at > ? AND user_id != ?",
current_user.last_checked_mail, current_user])
but the problem here was that because it begins with the user model, its only finding comments exclusively made by that user.
So instead, I began searching for all comments associated with the user, and those comments children so long as their user_id is not the current_user
Comment.find(:all, :conditions => ["user_id = ?", current_user]).select { |c|
c.comments.find(:all, :conditions => ["user_id != ?", current_user])
}
But it seems that still draws every single comment related to the current_user, and not their children.
My Comment Model :
belongs_to :commentable, :polymorphic => true
has_many :comments, :as => :commentable
So you have a parent relation in your comment model? Use that!
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :parent, :class_name => "Comment"
def self.after(date)
where "created_at > ?", date
end
def self.replies_for(comments)
where :parent_id => comments.map(&:id)
end
def self.exclude_user(user_id)
where "user_id != ?", user_id
end
end
class User < ActiveRecord::Base
has_many :comments
def new_comments
comments.after(last_check_mail)
end
def new_responses
Comment.replies_for(new_comments).after(last_check_mail).exclude_user(id)
end
end
current_user.new_responses
PS. This is for Rails 3.
Try:
Comment.find(:all, :conditions => ["created_at > ? AND user_id != ?", current_user.last_checked_mail, current_user])
Try this:
Comment.all(:conditions => ["user_id != ? AND parent_id = ? AND created_at > ?",
current_user.id, current_user.id, current_user.last_checked_mail])
Better solution is to create a named_scope on the Comment model.
class Comment
named_scope :other_comments, lambda {|u| { :conditions =>
["user_id != ? AND parent_id = ? AND created_at > ?",
u.id, u.id, u.last_checked_mail ] }}
end
Now you can get others comments as:
Comment.other_comments(current_user)
Edit 1
Updated answer based for Polymorphic associations:
class Comment
def self.other_comments(u)
Comment.all(
:joins => "JOIN comments A
ON A.commentable_type = 'User' AND
A.commentable_id = #{u.id} AND
comments.commentable_type = 'Comment' AND
comments.commentable_id = A.id",
:conditions => ["comments.user_id != ? AND comments.created_at > ?",
u.id, u.last_checked_mail]
)
end
end
Now you can get others comments as:
Comment.other_comments(current_user)