Include with condition, but always show the primary table results - ruby-on-rails

I have a function like :
# get all locations, if the user has discovered them or not
def self.getAll(user)
self.find(:all, :order => 'min_level asc', :include => 'discovered_locations',
:conditions => [ "discovered_locations.user_id = ? OR discovered_locations.id is null", user.id] )
end
self is actually the BossLocation model. I want to get a result set of the bosslocation and the discovered location IF that location was discovered by my user. However, if it was not discovered, i still need the bosslocation and no object as a discovered location. With the above code, if the user has not discovered anything, i don't get the bosslocations at all.
EDIT :
My associations are like :
class BossLocation < ActiveRecord::Base
has_many :discovered_locations
has_many :users, :through => :discovered_locations
class DiscoveredLocation < ActiveRecord::Base
belongs_to :user
belongs_to :boss_location
class User < ActiveRecord::Base
has_many :discovered_locations
has_many :boss_locations, :through => :discovered_locations

I think the problem is that you specify the user_id in the where conditions and not in the join condition. Your query will only give you the BossLocation if the user has discovered it or if no user at all has discovered it.
To make the database query match your need, you could change the include to the following joins:
:joins => "discovered_locations ON discovered_locations.boss_location_id = boss_locations.id
AND discovered_locations.user_id = '#{user.id}'"
BUT, I don't think it would help that much since the eager loading of Rails will not work when using joins like this instead of include.
If I where to do something similar, I would probably split it up. Perhaps by adding associations for user like this:
class User < ActiveRecord::Base
has_many :disovered_locations
has_many :discovered_boss_locations, :through => :discovered_locations
Update:
That way, in your Controller you can get all BossLocations and all discovered BossLocations like this:
#locations = BossLocation.all
#discovered = current_user.discovered_locations.all.group_by(&:boss_location_id)
To use these when you loop through them, do something like this:
<% #locations.each do |location| %>
<h1><%= location.name %></h1>
<% unless #discovered[location.id].nil? %>
<p>Discovered at <%= #discovered[location.id].first.created_at %></p>
<% end %>
<% end %>
What this does is it groups all discovered locations into a hash where the key is the boss_location_id. So when you loop through all boss_locations, you just see if there is an entry in the discovered hash that matches the boss_id.

"a result set of the bosslocation and the discovered location IF that location was discovered by my user."This is not a left outer join.You will get all bosslocations anytime.So,your conditions are wrong!This will get the bosslocation that it's discovered_locations.user_id = user.id OR discovered_locations.id is null".In this condition, this may be difficult for one sql statement. Also you can use union in your find_by_sql,but i suggest you use two find function.

Related

How do I do eager loading for multiple associations?

I am using public_activity and one of my partials I have this call:
<%= link_to "#{activity.trackable.user.name} " + "commented on " + "#{activity.trackable.node.name}", node_path(activity.trackable.node.family_tree_id,activity.trackable.node) %>
That is executed by this call:
<% #unread_act.each do |friend| %>
<li><%= render_activity friend %> </li>
<% end %>
That instance variable is assigned in my ApplicationController here:
before_filter :handle_nofications
def handle_nofications
if current_user.present?
#unread_act = Notification.where(owner_id: current_user).includes(:trackable => :user).unread_by(current_user)
end
end
I am using the gem Bullet, to find N+1 queries and the feedback I get is this:
N+1 Query detected
Comment => [:node]
Add to your finder: :include => [:node]
N+1 Query method call stack
I initially got that message for :trackable and for :user. Both of which I am now eager loading via includes within that assignment statement.
However, I have no idea how to go 3 levels deep on an eager loading - rather than just two.
Given that the node is called like activity.trackable.node.name, I imagine some appropriate eager loading assignment would look something like:
#unread_act = Notification.where(owner_id: current_user).includes(:trackable => [:user, :node]).unread_by(current_user)
But the error I get is this:
ActiveRecord::AssociationNotFoundError at /
Association named 'node' was not found on Node; perhaps you misspelled it?
I even get that when I simply do:
#unread_act = Notification.where(owner_id: current_user).includes(:trackable => :node).unread_by(current_user)
So I suspect something else is going wrong here.
Any ideas?
Edit 1
See Node & Notification models and associations below.
Node.rb
class Node < ActiveRecord::Base
include PublicActivity::Model
tracked except: :update, owner: ->(controller, model) { controller && controller.current_user }
belongs_to :family_tree
belongs_to :user
belongs_to :media, polymorphic: true, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :node_comments, dependent: :destroy
Notification.rb
class Notification < PublicActivity::Activity
acts_as_readable :on => :created_at
end
This will hopefully be fixed in rails 5.0. There is already an issue and a pull request for it.
https://github.com/rails/rails/pull/17479
https://github.com/rails/rails/issues/8005
I have forked rails and applied the patch to 4.2-stable and it works great for me. Feel free to use it, even though I cannot guarantee to sync with upstream on a regular basis.
https://github.com/ttosch/rails/tree/4-2-stable
It seems the correct way to approach this is by using an array on the trackable attribute like so:
#unread_act = Notification.where(recipient_id: current_user).includes(:trackable => [:user, :node]).unread_by(current_user).order("created_at desc")
The main part is:
includes(:trackable => [:user, :node])
This seems to have worked beautifully.

How to combine two scopes with OR

I have a tag feed and a friend feed.
I want to combine these two and build the ultimate "all" feed.
For friend feed:
class Post < ActiveRecord::Base
scope :friendfeed, lambda{|x| followed_by}
def self.followed_by(user)
where("user_id IN (?) OR user_id = ?", user.watched_ids, user.id)
end
end
For tag feed:
class Post < ActiveRecord::Base
scope :tagfeed, lambda{|x| infatuated_with}
def self.infatuated_with(user)
joins(:attachments).where("attachments.tag_id IN (?)", user.tags).select("DISTINCT pages.*")
end
end
And I would call something like this from the controller (I'm using Kaminari gem for pagination):
#tag_feed = Post.tagfeed(current_user).page(params[:page]).per(21)
#friend_feed = Post.friendfeed(current_user).page(params[:page]).per(21)
Now I want to have a universal feed, but I'm lost. Scopes are meant for narrowing down, but in this case I'm trying to do an OR operation. Doing stuff like
#mother_of_all_feed = #tag_feed + #friend_feed
would be redundant, and I wouldn't be able to control the number of posts appearing on a single page. How can I go about doing this? Thanks!
By the way, for tags I have association set up like this:
class Post < ActiveRecord::Base
has_many :attachments
has_many :tags, :through => :attachments
end
class Tag < ActiveRecord::Base
has_many :attachments
has_many :posts, :through => :attachments
end
class Attachment < ActiveRecord::Base
belongs_to :tag
belongs_to :post
end
There's a rails pull request for this feature (https://github.com/rails/rails/pull/9052), but in the meantime, some one has created a monkey patch that you can include in your initializers that will allow you to or scopes and where clauses in one query and still give you an ActiveRecord::Relation:
https://gist.github.com/j-mcnally/250eaaceef234dd8971b
With that, you'd be able to OR your scopes like this
Post.tagfeed(current_user).or.friendfeed(current_user)
or write a new scope
scope :mother_of_all_feed, lambda{|user| tagfeed(user).or.friendfeed(user)}
Answering my own question. I think I figured out a way.
where("pages.id IN (?) OR pages.id IN (?)",
Page.where(
"user_id IN (?) OR user_id = ?",
user.watched_ids, user.id
),
Page
.joins(:attachments)
.where("attachments.tag_id IN (?)", user.tags)
.select("DISTINCT pages.*")
)
It seems to be working so far, hope this is it!
Here's an example of how I combined two scopes.
scope :reconcilable, -> do
scopes = [
with_matching_insurance_payment_total,
with_zero_insurance_payments_and_zero_amount
]
where('id in (?)', scopes.flatten.map(&:id))
end

How do I access records from a 2 level nested model

I have 3 models User,Listing and Message. What I want is for an authenticated user to have many listings. The listings then can have multiple messages. So the messages are tied to the user through the listing model. I am able to get a users listings but not able to get the users messages which he owns through the listings. Here are the associations that I currently have.
class User < ActiveRecord::Base
has_many :listings, :dependent => :destroy
end
class Listing < ActiveRecord::Base
belongs_to :user
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :listing
end
To create a message I simply do this;
#listing = Listing.find(params[:listing_id])
#message = #listing.messages.build(params[:message])
And getting the user's listing i have this;
#user_listings = Listing.user_listings(current_user)
But getting the messages tied to the user's listings proves to be elusive. What am I doing wrong or how do I go about this? help appreciated.
Still not sure where user_listings comes from but why not this:
#user = User.find(params[:user_id], :include => {:listings => :messages})
#user.listings.each do |listing|
listing.messages.each do |message|
#or
#user.listings.collect(&:messages).each do |message|
#or (just read about using authenticated user so the same as above like this
current_user.listings(:all, :include => :messages)...
Include prefetches all the listings' associated messages in one query in order that they're not fetched in the loop causing n+1 querying.
----------
Or another approach, if you don't need the listings data.
#messages.rb
def self.user_messages user_id
find(:all, :joins => :listings, :conditions => ["listings.user_id = ?", user_id])
#with pagination
def self.user_messages user_id, page
paginate(:all, :joins => :listings,
:conditions => ["listings.user_id = ?", user_id],
:per_page => 10, :page => page)
updated regarding your comment.
You may want to just add has_many :messages to the user class as well and add a user_id column to Message. Then you could just do current_user.messages
How about something like this:
class User < ActiveRecord::Base
has_many :listings, :dependent => :destroy
has_many :listing_messages, :through => :listings
That way you dont have to "tie" the messages with the user because it is always accessed through the listing association:
current_user.listing_messages.all
Or have I misunderstood your question?
If you have current_user pulled already. You can just access listings directly by calling
current_user.listings
instead of
#user_listings = Listing.user_listings(current_user)

Limiting a search to records from last_request_at

I am trying to figure out how to display a count for records that have been created in a table since the last_request_at of a user.
In my view I am counting the notes of a question with the following code:
<% unless #questions.empty? %>
<% #questions.each do |question| %>
<%= h(question.notes.count) %>
end
end
This is happening in the /views/users/show.html.erb file. Instead of counting all the notes for the question, I would only like to count the notes that have been created since the users last_request_at datetime. I don't neccessarily want to scope notes to display this 'new notes' count application wide, just simply in this one instance.
To accomplish I am assuming I need to create a variable in the User#show action and call it in the view but not really sure how to do that.
Other information you may need:
class Note < ActiveRecord::Base
belongs_to :user
belongs_to :question
end
class Question < ActiveRecord::Base
has_many :notes, :dependent => :destroy
belongs_to :user
end
Just create a named scope and then use it only when it applies:
class Note < ActiveRecord::Base
named_scope :added_since, lambda { |time| {
:conditions => time && [ 'created_at>=?', time ]
}}
end
This should only enforce conditions if a time is provided. If you submit a nil time, the default behavior is to scope all notes.
This way you can do something along the lines of:
#new_notes = #user.notes.added_since(#user.last_login_at)
Adding a named scope does not alter the default scope.

how to access rails join model attributes when using has_many :through

I have a data model something like this:
# columns include collection_item_id, collection_id, item_id, position, etc
class CollectionItem < ActiveRecord::Base
self.primary_key = 'collection_item_id'
belongs_to :collection
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :collection_items
has_many :collections, :through => :collection_items, :source => :collection
end
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position
end
An Item can appear in multiple collections and also more than once in the same collection at different positions.
I'm trying to create a helper method that creates a menu containing every item in every collection. I want to use the collection_item_id to keep track of the currently selected item between requests, but I can't access any attributes of the join model via the Item class.
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.each do |item|
# !!! FAILS HERE ( undefined method `collection_item_id' )
do_something_with( item.collection_item_id )
end
end
end
I tried this as well but it also fails with ( undefined method `collection_item' )
do_something_with( item.collection_item.collection_item_id )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
I have also tried to access other attributes in the join model, like this:
do_something_with( item.position )
and:
do_something_with( item.collection_item.position )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
but they also fail.
Can anyone advise me how to proceed with this?
Edit: -------------------->
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
Currently I am working on amending my Collection model like this:
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position, :include => :item
...
end
and changing the helper to use coll.collection_items instead of coll.items
Edit: -------------------->
I've changed my helper to work as above and it works fine - (thankyou sam)
It's made a mess of my code - because of other factors not detailed here - but nothing that an hour or two of re-factoring wont sort out.
In your example you have defined in Item model relationship as has_many for collection_items and collections the generated association method is collection_items and collections respectively both of them returns an array so the way you are trying to access here is wrong. this is primarily case of mant to many relationship. just check this Asscociation Documentation for further reference.
do_something_with( item.collection_item_id )
This fails because item does not have a collection_item_id member.
do_something_with( item.collection_item.collection_item_id )
This fails because item does not have a collection_item member.
Remember that the relation between item and collection_items is a has_many. So item has collection_items, not just a single item. Also, each collection has a list of collection items. What you want to do is probably this:
colls = Collection.find :all
colls.each do |coll|
coll.collection_items.each do |collection_item|
do_something_with( collection_item.id )
end
end
A couple of other pieces of advice:
Have you read the documentation for has_many :through in the Rails Guides? It is pretty good.
You shouldn't need the :source parameters in the has_many declarations, since you have named your models and associations in a sensible way.
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
I recommend you stick with has_many :through, because has_and_belongs_to_many is more confusing and doesn't offer any real benefits.
I was able to get this working for one of my models:
class Group < ActiveRecord::Base
has_many :users, :through => :memberships, :source => :user do
def with_join
proxy_target.map do |user|
proxy_owner = proxy_owner()
user.metaclass.send(:define_method, :membership) do
memberships.detect {|_| _.group == proxy_owner}
end
user
end
end
end
end
In your case, something like this should work (haven't tested):
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position do
def with_join
proxy_target.map do |items|
proxy_owner = proxy_owner()
item.metaclass.send(:define_method, :join) do
collection_items.detect {|_| _.collection == proxy_owner}
end
item
end
end
end
end
Now you should be able to access the CollectionItem from an Item as long as you access your items like this (items.with_join):
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.with_join.each do |item|
do_something_with( item.join.collection_item_id )
end
end
end
Here is a more general solution that you can use to add this behavior to any has_many :through association:
http://github.com/TylerRick/has_many_through_with_join_model
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position, :extend => WithJoinModel
end

Resources