Rails - Building a Query - Named Scope or? - ruby-on-rails

I have the following models: Users, Groups, Conversations, ConversationParticipants( has a read boolean)
What I want to do is get an unread Count for a particular user in a particular group.
Should I be using a named_scope for this? If so, which model would this belong in (not sure)...
Also, I can do: #user.conversation_participations which then has the read field, problem is it does not have the group field as conversation_participations links to conversations (which has the group_id) via a conversation_id key.
Thoughts?
Thanks

You didn't show the code for the models, so I made some assumptions. Here's one way:
class Conversation < ActiveRecord::Base
belongs_to :group
has_many :conversation_participants
has_many :participants, :through => :conversation_participants,\
:source => :user
scope :unread, lambda { |user,group| includes(:group,:conversation_participants).\
where(:group_id => group.id,\
:conversation_participants => {:read => false,:user_id => user.id})
}
end
You're asking for "unread conversations belonging to a specific user and group". Since the thing being asked for is a set of Conversations, that's a natural place to put the scope.
EDIT
I see you wanted the count, not the result set. Just add .count to the scope:
Conversation.unread(user,group).count
EDIT 2
is it possible to do something like
this instead to get the #,
current_user.unread(group).count ..?
Add an instance method on User:
def unread(group)
Conversation.unread(self,group)
end
Now you can call current_user.unread(group).count

If I understand the question correctly. I would use a named_scope in the ConversationParticipants called something like in_group:
scope :in_group, lambda do
|group| joins(:conversation).where('conversation.group_id = ?', group.id)
end
I'm assuming the ConversationParticipants has belongs_to :conversation.
Now you can do:
#user.conversation_participations.in_group( some_group )

Related

Rails ActiveRecord how to order by a custom named association

Ok so have created 2 models User and Following. Where User has a username attribute and Following has 2 attributes which are User associations: user_id, following_user_id. I have set up these associations in the respective models and all works good.
class User < ActiveRecord::Base
has_many :followings, dependent: :destroy
has_many :followers, :class_name => 'Following', :foreign_key => 'following_user_id', dependent: :destroy
end
class Following < ActiveRecord::Base
belongs_to :user
belongs_to :following_user, :class_name => 'User', :foreign_key => 'following_user_id'
end
Now I need to order the results when doing an ActiveRecord query by the username. I can achieve this easily for the straight-up User association (user_id) with the following code which will return to me a list of Followings ordered by the username of the association belonging to user_id:
Following.where(:user_id => 47).includes(:user).order("users.username ASC")
The problem is I cannot achieve the same result for ordering by the other association (following_user_id). I have added the association to the .includes call but i get an error because active record is looking for the association on a table titled following_users
Following.where(:user_id => 47).includes(:user => :followers).order("following_users.username ASC")
I have tried changing the association name in the .order call to names I set up in the user model as followers, followings but none work, it still is looking for a table with those titles. I have also tried user.username, but this will order based off the other association such as in the first example.
How can I order ActiveRecord results by following_user.username?
That is because there is no following_users table in your SQL query.
You will need to manually join it like so:
Following.
joins("
INNER JOIN users AS following_users ON
following_users.id = followings.following_user_id
").
where(user_id: 47). # use "followings.user_id" if necessary
includes(user: :followers).
order("following_users.username ASC")
To fetch Following rows that don't have a following_user_id, simply use an OUTER JOIN.
Alternatively, you can do this in Ruby rather than SQL, if you can afford the speed and memory cost:
Following.
where(user_id: 47). # use "followings.user_id" if necessary
includes(:following_user, {user: :followers}).
sort_by{ |f| f.following_user.try(:username).to_s }
Just FYI: That try is in case of a missing following_user and the to_s is to ensure that strings are compared for sorting. Otherwise, nil when compared with a String will crash.

Merge results from two has_many associations with the same model

I have users. Users can poke other users, as well as poke themselves. Each poke is directional, and group pokes don't exist. I want to list all pokes (incoming or outgoing) for a given user, without duplicating self-pokes (which exist as both incoming_ and outgoing_pokes).
Here are my models:
class User < ActiveRecord::Base
has_many :outgoing_pokes, :class_name => "Poke", :foreign_key => :poker_id
has_many :incoming_pokes, :class_name => "Poke", :foreign_key => :pokee_id
end
class Poke < ActiveRecord::Base
belongs_to :poker, :class_name => "User"
belongs_to :pokee, :class_name => "User"
end
I tried creating a method in the User model to merge the pokes:
def all_pokes
outgoing_pokes.merge(incoming_pokes)
end
but that returns only the self-pokes (those that are both incoming_ and outgoing_pokes). Ideas? Is there a clean way to do this using associations directly?
Also, in the merged list, it'd be great to have two booleans for each poke to record how they're related to the current user. Something like outgoing and incoming.
The reason your all_pokes method is returning only self-pokes is because outgoing_pokes is not an array yet, but an AR relationship that you can chain on. merge, in this case, combines the queries before executing them.
What you want is to actually perform the queries and merge the result sets:
def all_pokes
(outgoing_pokes.all + incoming_pokes.all).uniq
end
...or you could write your own query:
def all_pokes
Poke.where('poker_id = ? OR pokee_id = ?', id, id)
end
Determining whether it's incoming or outgoing:
# in poke.rb
def relation_to_user(user)
if poker_id == user.id
poker_id == pokee_id ? :self : :poker
elsif pokee_id == user.id
:pokee
else
:none
end
end
Now that Rails 5 has OR queries, there is a very readable solution.
def pokes
outgoing_pokes.or(incoming_pokes)
end
I left off the all in the method name since it is now returning an ActiveRelation and other methods can be chained.
#user.pokes.where(...).includes(...)

has_many :through with instance specific conditions

Is it possible to set an instance-level constraint on a has_many, :through relationship in rails 3.1?
http://guides.rubyonrails.org/association_basics.html#the-has_many-association
Something like:
Class A
has_many :c, :through => :b, :conditions => { "'c'.something_id" => #a.something_id }
The documentation gives me hope with this, but it doesn't work for me:
If you need to evaluate conditions dynamically at runtime, you could
use string interpolation in single quotes:
class Customer < ActiveRecord::Base
has_many :latest_orders, :class_name => "Order",
:conditions => 'orders.created_at > #{10.hours.ago.to_s(:db).inspect}'
end
That gives me "unrecognized token '#'" on rails 3.1. Wondering if this functionality doesn't work anymore?
EDIT
Want to clarify why I don't think scopes are the solution. I want to be able to get from an instance of A all of the Cs that have a condition (which is based on an attribute of that instance of A). These are the only Cs that should EVER be associated with that A. To do this with scopes, I would put a scope on C that takes an argument, and then have to call it from #a with some value? I don't get why that's better than incorporating it into my has_many query directly.
Use a scope on the orders model:
class Order < ActiveRecord::Base
belongs_to :customer
scope :latest, lambda { where('created_at > ?', 10.hours.ago) }
end
And then call it with:
#customer.orders.latest
And if you really want to use latest_orders, you can instead add this to the Customer model:
def latest_orders
orders.where('created_at > ?', 10.hours.ago)
end

Has many through associations with conditions

I am trying to add a condition to a has many through association without luck. This is the association in my video model:
has_many :voted_users, :through => :video_votes, :source => :user
I want to only get the voted_users whose video_votes have a value equal to 1 for that video. How would I do this?
I would suggest creating a model method within the video model class
Something like:
def users_with_one_vote
self.voted_users, :conditions => ['value = ?', 1]
end
Then in the controller use video.users_with_one_vote
Then testing is easier too.
Any chance you can change that column name from 'value'. Might give some issues (reserved?).
I'd do this in 2 stages:
First, I'd define the has_many :through relationship between the models without any conditions.
Second, I'd add a 'scope' that defines a where condition.
Specifically, I'd do something like:
class User < ActiveRecord::Base
has_many :video_votes
has_many :votes, :through=>:video_votes
def self.voted_users
self.video_votes.voted
end
end
class VideoVote
def self.voted
where("value = ?", 1)
end
end
class Video
has_many :video_votes
has_many :users, :through=>:video_votes
end
Then you could get the users that have voted using:
VideoVote.voted.collect(&:user).uniq
which I believe would return an array of all users who had voted. This isn't the exact code you'd use -- they're just snippets -- but the idea is the same.
Would
has_many :voted_users, :through => :video_votes, :source => :user, :conditions => ['users.votes = ?', 1]
Do the trick?
I found that defining this method in my model works:
def upvoted_users
self.voted_users.where("value = 1")
end
and then calling #video.upvoted_users does the trick.
The best way to do this without messing with the relations is by crafting a more complex query. Relations is not the best thing to use for this particular problem. Please understand that relations is more a "way of data definition" then a way of "bussiness rules definition".
Bussiness logic or bussiness rules must be defined on a more specifically layer.
My suggestion for your problem is to create a method to search for users who voted on your video only once. something like:
class Video < ActiveRecord::Base
def voted_once()
User.joins(:video_votes).where("video_votes.value == 1 AND video_votes.video_id == ?", this.id)
end
Rails is magical for many things, but complex queries still have to be done in a "SQL" way of thinking. Don't let the illusional object oriented metaphor blind you
As long as we are throwing around ideas, how about using association extensions.
class VideoVote
scope :upvotes, where(:value => 1)
end
class Video
has_many :voted_users, :through => :video_votes, :source => :user do
def upvoted
scoped & VideoVote.upvotes
end
end
end
Then you feel good about making a call with absolutely no arguments AND you technically didn't add another method to your Video model (it's on the association, right?)
#video.voted_users.upvoted

In Rails 3 how can I select items where the items.join_model.id != x?

In my Rails models I have:
class Song < ActiveRecord::Base
has_many :flags
has_many :accounts, :through => :flags
end
class Account < ActiveRecord::Base
has_many :flags
has_many :songs, :through => :flags
end
class Flag < ActiveRecord::Base
belongs_to :song
belongs_to :account
end
I'm looking for a way to create a scope in the Song model that fetches songs that DO NOT have a given account associated with it.
I've tried:
Song.joins(:accounts).where('account_id != ?', #an_account)
but it returns an empty set. This might be because there are songs that have no accounts attached to it? I'm not sure, but really struggling with this one.
Update
The result set I'm looking for includes songs that do not have a given account associated with it. This includes songs that have no flags.
Thanks for looking.
Am I understanding your question correctly - you want Songs that are not associated with a particular account?
Try:
Song.joins(:accounts).where(Account.arel_table[:id].not_eq(#an_account.id))
Answer revised: (in response to clarification in the comments)
You probably want SQL conditions like this:
Song.all(:conditions =>
["songs.id NOT IN (SELECT f.song_id FROM flags f WHERE f.account_id = ?)", #an_account.id]
)
Or in ARel, you could get the same SQL generated like this:
songs = Song.arel_table
flags = Flag.arel_table
Song.where(songs[:id].not_in(
flags.project(:song_id).where(flags[:account_id].eq(#an_account.id))
))
I generally prefer ARel, and I prefer it in this case too.
If your where clause is not a typo, it is incorrect. Code frequently uses == for equality, but sql does not, use a single equals sign as such:
Song.joins(:accounts).where('account_id = ?', #an_account.id)
Edit:
Actually there is a way to use activerecord to do this for you, instead of writing your own bound sql fragments:
Song.joins(:accounts).where(:accounts => {:id => #an_account.id})

Resources