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.
Related
So, I have read through quite a few rails active records pages, stack O questions and answers (about 12 hours of time) trying to figure out how the heck to tie all of these things together into a single query to display them on my page.
Here is my page view
Secrets with owner info
</h3>
<% #secretInfo.each do |i| %>
<p><%= i.content %> - <%= i.first_name %></p>
<p><%= i.created_at %></p>
--> "this is where I'd like to have likes for post" <--
<% end %>
and here is my controller
def show
#user = User.find(params[:id])
#secrets = Gossip.all
#mySecrets = Gossip.where(user_id: [params[:id]])
#secretInfo = Gossip.joins(:user).select("content", "first_name", "created_at")
#secretWLikesNInfo = WTF MATE?
end
Also, may help to see my models and schema so here are those
class User < ActiveRecord::Base
attr_accessor :password
has_many :gossips
has_many :likes
has_many :liked_secrets, :through => :gossips, :source => :gossip
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :gossip
class Gossip < ActiveRecord::Base
belongs_to :user
has_many :likes
has_many :liking_users, :through => :likes, :source => :user
I don't know why this seems so impossible or it could be something very simple that I am just overlooking. This all was very easy in PHP/MySQL. All help is appreciated.
Additional points for coming up with a query that allows me to see all posts that I as a user has created AND liked!
Well, what you want to do is eager loading: load data associated with a record in a single roundtrip to the database. For example, i think you can load all your data like this:
#user = User.where(id: params[:id])
.joins(:liked_secrets)
.includes(:liked_secrets => :likes)
.first!
#secretInfo = #user.liked_secrets.map do |secret|
OpenStruct.new(
content: secret.content,
first_name: user.first_name,
created_at: secret.created_at,
likes: secret.likes
)
end
This works by including in the data fetched from the database in the first query all the data associated included in the include parameter. So, calling #user.liked_secrets will return the secrets but won't call the database because that information already came from the database in the first query. The same happens if you do #user.liked_secrets.first.likes because of the :linked_secrets => :likes parameter on the initial query.
I'll let a link to a good blog post about this here:
http://blog.arkency.com/2013/12/rails4-preloading/.
And, if you feel the Rails ORM (ActiveRecord) doesn't really works for your use case, you can just use sql in a string or fallback to use another Ruby ORM out there (like Sequel).
I read this interesting article about Using Polymorphism to Make a Better Activity Feed in Rails.
We end up with something like
class Activity < ActiveRecord::Base
belongs_to :subject, polymorphic: true
end
Now, if two of those subjects are for example:
class Event < ActiveRecord::Base
has_many :guests
after_create :create_activities
has_one :activity, as: :subject, dependent: :destroy
end
class Image < ActiveRecord::Base
has_many :tags
after_create :create_activities
has_one :activity, as: :subject, dependent: :destroy
end
With create_activities defined as
def create_activities
Activity.create(subject: self)
end
And with guests and tags defined as:
class Guest < ActiveRecord::Base
belongs_to :event
end
class Tag < ActiveRecord::Base
belongs_to :image
end
If we query the last 20 activities logged, we can do:
Activity.order(created_at: :desc).limit(20)
We have a first N+1 query issue that we can solve with:
Activity.includes(:subject).order(created_at: :desc).limit(20)
But then, when we call guests or tags, we have another N+1 query problem.
What's the proper way to solve that in order to be able to use pagination ?
Edit 2: I'm now using rails 4.2 and eager loading polymorphism is now a feature :)
Edit: This seemed to work in the console, but for some reason, my suggestion of use with the partials below still generates N+1 Query Stack warnings with the bullet gem. I need to investigate...
Ok, I found the solution ([edit] or did I ?), but it assumes that you know all subjects types.
class Activity < ActiveRecord::Base
belongs_to :subject, polymorphic: true
belongs_to :event, -> { includes(:activities).where(activities: { subject_type: 'Event' }) }, foreign_key: :subject_id
belongs_to :image, -> { includes(:activities).where(activities: { subject_type: 'Image' }) }, foreign_key: :subject_id
end
And now you can do
Activity.includes(:part, event: :guests, image: :tags).order(created_at: :desc).limit(10)
But for eager loading to work, you must use for example
activity.event.guests.first
and not
activity.part.guests.first
So you can probably define a method to use instead of subject
def eager_loaded_subject
public_send(subject.class.to_s.underscore)
end
So now you can have a view with
render partial: :subject, collection: activity
A partial with
# _activity.html.erb
render :partial => 'activities/' + activity.subject_type.underscore, object: activity.eager_loaded_subject
And two (dummy) partials
# _event.html.erb
<p><%= event.guests.map(&:name).join(', ') %></p>
# _image.html.erb
<p><%= image.tags.first.map(&:name).join(', ') %></p>
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 for me. Feel free to use my fork, even though I cannot guarantee to sync with upstream on a regular basis.
https://github.com/ttosch/rails/tree/4-2-stable
You can use ActiveRecord::Associations::Preloader to preload guests and tags linked, respectively, to each of the event and image objects that are associated as a subject with the collection of activities.
class ActivitiesController < ApplicationController
def index
activities = current_user.activities.page(:page)
#activities = Activities::PreloadForIndex.new(activities).run
end
end
class Activities::PreloadForIndex
def initialize(activities)
#activities = activities
end
def run
preload_for event(activities), subject: :guests
preload_for image(activities), subject: :tags
activities
end
private
def preload_for(activities, associations)
ActiveRecord::Associations::Preloader.new.preload(activities, associations)
end
def event(activities)
activities.select &:event?
end
def image(activities)
activities.select &:image?
end
end
image_activities = Activity.where(:subject_type => 'Image').includes(:subject => :tags).order(created_at: :desc).limit(20)
event_activities = Activity.where(:subject_type => 'Event').includes(:subject => :guests).order(created_at: :desc).limit(20)
activities = (image_activities + event_activities).sort_by(&:created_at).reverse.first(20)
I would suggest adding the polymorphic association to your Event and Guest models.
polymorphic doc
class Event < ActiveRecord::Base
has_many :guests
has_many :subjects
after_create :create_activities
end
class Image < ActiveRecord::Base
has_many :tags
has_many :subjects
after_create :create_activities
end
and then try doing
Activity.includes(:subject => [:event, :guest]).order(created_at: :desc).limit(20)
Does this generate a valid SQL query or does it fail because events can't be JOINed with tags and images can't be JOINed with guests?
class Activity < ActiveRecord::Base
self.per_page = 10
def self.feed
includes(subject: [:guests, :tags]).order(created_at: :desc)
end
end
# in the controller
Activity.feed.paginate(page: params[:page])
This would use will_paginate.
I have a survey and I would like to add participants to a Participant model whenever a user answers to a question for the first time. The survey is a bit special because it has many functions to answer questions such as Tag words, Multiple choices and Open Question and each function is actually a model that has its own records. Also I only want the Participant to be saved once.
The Participant model is fairly simple:
class Participant < ActiveRecord::Base
belongs_to :survey
attr_accessible :survey_id, :user_id
end
The Survey model is also straightforward:
class Survey < ActiveRecord::Base
...
has_many :participants, :through => :users
has_many :rating_questions, :dependent => :destroy
has_many :open_questions, :dependent => :destroy
has_many :tag_questions, :dependent => :destroy
belongs_to :account
belongs_to :user
accepts_nested_attributes_for :open_questions
accepts_nested_attributes_for :rating_questions
accepts_nested_attributes_for :tag_questions
...
end
Then you have models such as rating_answers that belong to a rating_question, open_answers that belong to open_questions and so on.
So initially I thought for within my model rating_answers I could add after_create callback to add_participant
like this:
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
In this case, I didn't know how to find the survey_id, so I tried using the params but I don't think that is the right way to do it. regardles it returned this error
NameError (undefined local variable or method `current_user' for #<RatingAnswer:0x0000010325ef00>):
app/models/rating_answer.rb:25:in `add_participant'
app/controllers/rating_answers_controller.rb:12:in `create'
Another idea I had was to create instead a module Participants.rb that I could use in each controllers
module Participants
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
and in the controller
class RatingAnswersController < ApplicationController
include Participants
def create
#rating_question = RatingQuestion.find_by_id(params[:rating_question_id])
#rating_answer = RatingAnswer.new(params[:rating_answer])
#survey = Survey.find(params[:survey_id])
if #rating_answer.save
add_participant
respond_to do |format|
format.js
end
end
end
end
And I got a routing error
ActionController::RoutingError (uninitialized constant RatingAnswersController::Participants):
I can understand this error, because I don't have a controller for participants with a create method and its routes resources
I am not sure what is the proper way to add a record to a model from a nested model and what is the cleaner approach.
Ideas are most welcome!
current_user is a helper that's accessible in views/controller alone. You need to pass it as a parameter into the model. Else, it ain't accessible in the models. May be, this should help.
In the end I ended up using the after_create callback but instead of fetching the data from the params, I used the associations. Also if #participant.nil? didn't work for some reason.
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => self.user.id, :survey_id => self.rating_question.survey.id)
unless #participant.any?
#new_participant = Participant.create(:user_id => self.user.id, :survey_id => self.survey.rating_question.id)
end
end
end
The cool thing with associations is if you have deeply nested associations for instead
Survey has_many questions
Question has_many answers
Answer has_many responses
in order to fetch the survey id from within the responses model you can do
self.answer.question.survey.id
very nifty!
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.
In Rails 3, I am having a problem accessing a helper method from within a model
In my ApplicationController I have a helper method called current_account which returns the account associated with the currently logged in user. I have a project model which contains projects that are associated with that account. I have specified 'belongs_to :account' in my project model and 'has_many' in my account model. All of this seems to be working correctly and my projects are being associated with the right accounts.
However at the moment when I use 'Project.all', all of the project records are being returned as you would expect. I would like it to automatically filter the projects so that only those associated with the specified account are returned and I would like this to be the default behaviour
So I tried using 'default_scope'. The line in my model looks something like this
default_scope :order => :name, :conditions => ['account_id = ?', current_account.id]
This throws an error saying
Undefined local variable or method current_account for #
If I swap the call to current_account.id for an integer id - eg
default_scope :order => :name, :conditions => ['account_id = ?', 1]
Everything works correctly. How do I make my current_account method accessible to my model
Many Thanks
You can't access the session from models. Instead, pass the account as a parameter to a named scope.
#controller
Model.my_custom_find current_account
#model.rb
named_scope :my_custom_find, lambda { |account| { :order => :name, :conditions => ['account_id = ?', account.id]}}
I haven't used rails 3 yet so maybe named_scopes have changed.
The association setup is enough to deal with scoping on controller and view levels. I think the problem is to scope, for instance, finds in models.
In controller:
#products = current_account.products.all
Fine for view scoping, but...
In model:
class Inventory < ActiveRecord::Base
belongs_to :account # has fk account_id
has_many :inventory_items, :dependent => :destroy
has_many :products, :through => :inventory_items
end
class Product < ActiveRecord::Base
has_many :inventory_items
def level_in(inventory_id)
inventory_items.where(:inventory_id => inventory_id).size
end
def total_level
# Inventory.where(:account_id => ?????) <<<<<< ISSUE HERE!!!!
Inventory.sum { |inventory| level_in(inventory.id) }
end
end
How can this be scoped?
+1 for mark's answer. This should still work in Rails 3.
Just showing you the rails 3 way with scope and new query api:
scope :my_custom_find, lambda { |account| where(:account_id=>account.id).order(:name) }
You've got the association set up, so couldn't you just use:
#projects = current_account.projects.all
...in your controller?
I've not adjusted the syntax to be Rails 3 style as I'm still learning it myself.