Optimizing purchase querying in Rails - ruby-on-rails

I am implementing an online application shop with Rails. Its data model is shown as follows:
class User < ActiveRecord::Base
has_many purchase_records
has_many items, :through => purchase_records
end
class Item < ActiveRecord::Base
has_many purchase_records
has_many users, :through => purchase_records
end
class PurchaseRecord < ActiveRecord::Base
belongs_to :user
belongs_to :item
end
It has a page showing available items and there prices, and if the user has purchased the item, the price will be a download link, just as App Store does. A view helper is written to help generate such links:
def download_link(item)
# generate a download link
end
def item_link(item)
if current_user and current_user.items.where(:id => item.id).first != nil
# User already purchased it
download_link(item, 'book-price')
else
# Not purchased yet, show price and link to its details
link_to item.price, item
end
end
current_user is defined by devise. It works fine except for it costs 20 extra database queries for a page with 20 items, since it needs to check if the user has purchased the item or not for every single item. I am wondering if it can be optimized, for example, to pre-load purchased items of current user, but I have no idea how to write it in a view helper.

I just implemented downloadable content for a client.
What I did was write an instance method on the user class that retrieves the user's purchased items, e.g.:
class User < ActiveRecord::Base
...
def downloads
self.orders.collect { |o| o.products }.flatten
end
end
You could use the include? method to check if the user purchased the item, e.g.:
def item_link(item)
if current_user && current_user.downloads.include?(item)
download_link(item, 'book-price')
else
link_to item.price, item
end
end
Unfortunately, while this is a bit more explicit, it will still loop through the user's orders every time item_link is hit. I would suggest optimizing this with Rails low-level caching where you may clear the cache every time the user logs in or completes a purchase.
A Rails low-level cache may look like this:
def downoads
Rails.cache.fetch("user-downloads-#{self.id}") do
self.orders.collect { |o| o.products }.flatten
end
end
And call the following to clear a cache:
Rails.cache.delete("user-downloads-#{self.id}")

You could set the user's purchased items to an instance variable in the controller. Then you're only hitting the database once:
# app/controllers/items_controller.rb
def index
#purchased_items = current_user.items
end
# app/helpers/items_helper.rb
def item_link(item)
if #purchased_items.include?(item)
download_link(...)
else
link_to ...
end
end

Well, you don't write that in a view helper. You make a scope on the user model called purchased_items where you would check all of the items a user has purchased.
Since you didn't put up the source code for User, Item and whatever their relationship is, I can only give you that general hint.

Related

Finding user object in view? Ruby on Rails

<% #review.each do |review|%>
<% if review.host_id == #host.id>
<%= #user = User.find(review.user_id) %>
<% end %>
<% end %>
So I'm a bit confused. I have a few things going on here. I'm doing a loop through all reviews of hosts and then checking if the stored host.id value is equal to the active #host object's id that is passed from the controller. Problem is.. Now I need get the user object from the user ID stored in the review but, I'm unsure exactly how to do it. I can't do it from the controller as all this is done in the loop. As you can see I tried to do it with the code above but, I highly doubt I did it right. Please help me out on this. Thanks.
You should pre-load users with loading reviews, in controller. First, you should have belongs_to association, like this:
class Review < ActiveRecord::Base
belongs_to :user
# ...
end
then, in controller, you could use includes, this way:
#reviews = Review.includes(:user)
Now, for every review record in #reviews relation, to get associated user you can call user method, like this:
review.user
What's more, (and that's advantage of using includes) it doesn't fire new SQL query for every single review, so you avoid quite common N + 1 problem.
You can make a relationship in
class Review < ActiveRecord::Base
belongs_to :user
end
and then in view
review.user #gives you user
Put association in Review Model
class Review < ActiveRecord::Base
.
.
.
belongs_to :user
.
.
.
end
After putting association you can directly call association to find user object using Review object.
review.user
But this will raise N+1 query problem, so better user include user while finding review, It will execute only two queries one for finding reviews and another for finding users.
#reviews = Review.includes(:user)

Conditional email trigger on Model attribute update (Devise & Rails)

I'm still learning Rails, and using Devise. Currently I am working on a bug/ticket logging system. I'n this system we have tickets created by a user, assigned to another user and all users that can view it can post a reply on it.
I want to trigger an email when a user changes the status of a ticket to closed.
HOWEVER, if you are the creator (of the ticket) and you closed it, you do not want an email, but you want to email the user its assigned to. Likewise, if you are the assignee and you close it, you do not want to email, but you do want to email the creator. If you are neither creator or assignee, you still do not want an email, but you do want to email the other two.
The email will be a small notification noting ticket #_ is closed.
I am a bit tripped up as to where this code should go. There is no new code in the controller but I added a before_update :email_update in my ticket model.
def email_update
#status field is changed
if status_changed? && status.description == "Closed"
if(current_user != assigned_to)
UserMailer.new_ticket_admin(assigned_to, self).deliver
end
if(current_user != user)
UserMailer.new_ticket_admin(user, self).deliver
end
end
end
But, is this not bad practice to access the current user in one of the models? What would be a better approach?
Pretty sure, but I don't think that you can access current_user in the model. Even if you could, might I suggest an alternative. Instead, I would use a closed_by_id attribute where it is the current_user's ID. This way you can also track who closed a ticket. From here, you can check to see if the ticket is closed and if the creator of the ticket's ID is equal to the closed_by_id.
As you mentioned you have a creator and a 'closer' (or whatever you want to call that user). Within your user model you want to have something like this:
class Ticket < ActiveRecord::Base
belongs_to :requested_by, class_name: 'User' # foreign_key requested_by_id
belongs_to :closed_by, class_name: 'User' # foreign_key closed_by_id
def close(user)
self.closed_by = user
self.save
end
# bonus method
def closed?
closed_by?
end
end
def User < ActiveRecord::Base
has_many :tickets, foreign_key: 'requested_by_id'
has_many :closed_tickets, foreign_key: 'closed_by_id'
end
And for your controller something like:
class TicketController < ApplicationController
def create
#ticket = current_user.tickets.build params[:ticket]
end
def close
#ticket = Ticket.find(params[:id])
#ticket.close current_user
end
end
This way there is no need to have current_user within your model. Which probably solves your challege.

How to access a model from another model?

I have two models in ROR, one which is Note and another one which is Access. Each Access has a Note field and a user field. In my index action of the notes controller I want to filter notes owned by the user (done) as well as notes accessible to the user, which I named #accessible_notes.
The following code gives me the correct notes owned by the user, however I cannot get the notes accessible to the user.
Basically, I need to find all the Accesses in which the user is involved and then fetch the corresponding notes. How can I do that?
def index
#notes = Note.where(user: current_user)
#personal_access = Access.where("user_id = ?",current_user.id)
#accessible_notes = []
#personal_access.each do |accessible|
tnote = Note.find(accessible.note_id)
#accessible_notes += tnote if tnote
end
end
class User < ActiveRecord::Base
has_many :accessible_notes, :through => :accesses, :source => :notes
end
#accessible_notes = current_user.accessible_notes
How about
#personal_access.each do |accessible|
#accessible_notes << accessible.note
end
#accessible_notes.flatten!
There might be a faster way using Active Record queries.
And that faster way is in depa's answer.

Ruby on Rails model association issue

Sorry for the vague title, but it's a little much to explain in a sentence.
I've got three models, User, Device, and DeviceMessage. Their relationships are fairly simple:
a User has_many :devices,
a Device belongs_to :user,
a Device has_many :device_messages,
and a DeviceMessage belongs_to :device.
Rails provides ways to start playing with these associations quickly, like the ability to get all device messages that belong to a certain user (from any device).
In order to do this, I defined a method in the User model:
class User < ActiveRecord::Base
...
has_many :devices, :as => : owner #Other entities may "own" a device
def device_feed
DeviceMessage.that_belong_to_user(self)
end
end
And I define the called method in the DeviceMessage model:
class DeviceMessage < ActiveRecord::Base
...
belongs_to :device
def self.that_belong_to_user(user)
device_ids = "SELECT owner_id FROM devices WHERE owner_id = :user_id
AND owner_type = \"User\""
where("device_id IN (#{device_ids})", user_id: user.id)
end
end
I define a user page where they can associate a device to their account (the device has a name as well), and upon adding the device to the account, it will add the name to a list of device names in a pane to the left, while showing the user's device feed much like a twitter feed (yes, I followed Michael Hartl's RoR tutorial). At this point it is important to note that I am using helper functions to keep track of the current user so I can display this information when a user visits the root_path while logged in. When visiting the root_path, the controller for the root_path is defined so that:
if user_signed_in?
#device_feed_items = current_user.device_feed.paginate(page: params[:page])
end
And this all works perfectly!
So... what's the issue? When I create a new user via the signup page, and associate the device via the device-association page, I am redirected to the root_path, the device name is correctly displayed in the left pane (which mean the device is correctly associated with the new user), but the device_feed is not displayed.
I've used the Rails console to verify that the device messages should be showing (User.find(2).devices.first.device_messages.first displays the first message associated with the first device that is newly associated with the 2nd user), so I know that I need to reach down into the database to get a fresh rather than cached copy of the current_user, but I'm confused because it seems like that should be happening every time the user.device_feed method is called because of it's use of where() which is a part of the query API...
Any ideas? Thanks in advance for any and all answers.
-MM
I am just wondering why you have the device_feed function. For your feed display could you not just a loop like this one, this is
class Device < ActiveRecord::Base
scope :in_new_message_order, :joins => :device_messages, :order => "created_at DESC"
end
Added a joined scope
class User < ActiveRecord::Base
...
has_many :devices, :as => : owner #Other entities may "own" a device
scope :in_sort_order, order("message_date DESC")
def device_feed
DeviceMessage.that_belong_to_user(self)
end
end
Above I have added a scope to sort your messages
<% user.devices.in_new_message_order.each do |device| %>
<% device.device_messages_in_sort_order.each do |message| %>
<%= ....render out the message %>
<% end %>
<% end %>

Don't reshow seen posts in Rails

I'm currently developing an application whereby a user clicks a button and they're offered up a new page of content, and was wondering how I would go about hiding or skipping past those that the user has already interacted with (a separate table stores the post_id and user_id for each view).
I currently use this code in the model for displaying a random page:
def self.random
if (c = count) != 0
find(:first, :offset =>rand(c))
end
end
The user authentication system is built off of Authlogic, and I have User, Post and View models.
So if a user has already seen a post "foo", how would I not display that in the future and instead serve up a random "bar".
Thanks
Steve,
I would set a boolean field for each post called "read" (default => false).
Upon firing the "show" action of your controller (and any other action you consider the person seeing the post), you can automatically set that to true and perform a save without validation. When you then show your list of records, you can add the condition .where("read = ?", false).
Of course, you can decide whether you want to give users the flexibility of setting individual posts to 'unseen' or 'unread' - if you want to do that it's the subject of another question :).
You could store an array of viewed post ids on the session in the show action of the posts_controller. EDIT -- find random post not already viewed. Not tested, but the idea is here:
def show_random_post
while (id == nil || (session[:viewed_posts] ||= []).include?(id)) # initialize array if it hasn't been initialized
id = rand(Post.count) + 1
end
session[:viewed_posts] << id
#post = Post.find(id)
# etc.
end
Do you want to keep a record of viewed posts between sessions?
EDIT: If you want to keep a user-level record of viewed posts between sessions, you'll probably want to do it at the db level. Since this means a many-to-many relationship between users and posts, you'll likely want to manage that with a relational table, and the best way to do that in Rails is with has_many :through. Something like (again, not tested):
class ViewedPostRecord < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
class User < ActiveRecord::Base
has_many :viewed_post_records
has_many :viewed_posts, :class => 'Post', :through => :viewed_post_records
end
class PostsController < ApplicationController
def show_random_post
while (id == nil || current_user.viewed_posts.map(&:id).include?(id))
id = rand(Post.count) + 1
end
#post = Post.find(id)
current_user.viewed_posts << #post
# etc.
end
end

Resources