So in a rails-api I'm working on, we're currently trying to optimize some of the longer running calls, and I'm having an issue with the .includes functionality. I've got it working in most situations, but there's one particular situation where it's not working in the way that I want it to.
Here's an example:
User class
class User < ActiveRecord::Base
has_many :images
has_one :active_image, -> { where(images: { active_image: true })}, class_name: 'Image'
has_many :facebook_auth
def get_profile_image
if active_image
active_image.image.url(:profile)
else
facebook = facebook_auth.last
if facebook
"https://graph.facebook.com/#{facebook.provider_user_id}/picture?width=150&height=150"
end
end
nil
end
end
Controller:
class UserController < BaseAPIController
def get_user_image
user_id = params[:user_id]
user = User.includes(:active_image, :facebook_auth).find(user_id)
render json: user.get_profile_image
end
end
With this, I would assume that the .includes(:active_image, :facebook_auth) would cache the data so that when I call them in the get_profile_image method, it doesn't make any more db calls, but this isn't the case. What am I doing wrong here?
Thanks,
Charlie
You where almost there!
Try this approach:
class User < ApplicationRecord
has_many :images, dependent: :destroy
has_one :active_image,
-> { where(active: true) },
class_name: 'Image'
has_many :facebook_auths, dependent: :destroy
has_one :active_facebook_auth,
-> { order("created_at desc") },
class_name: 'FacebookAuth'
scope :eager_load_image_data,
-> { includes(:active_image).includes(:active_facebook_auth) }
def profile_image_url
if active_image
active_image.url
elsif active_facebook_auth
"https://graph.facebook.com/#{active_facebook_auth.provider_user_id}/picture?width=150&height=150"
else
nil
end
end
end
Then in your controller or whenever you want to eager load images:
# for one user, with id 2:
User.eager_load_image_data.find(2).profile_image_url
# for a collection (using 'all' here):
User.eager_load_image_data.all.map{ |user|
[user.name, user.profile_image_url]
}
This way the image data is eagerloaded, both from the Image class and the FacebookAuth class.
There where also some other issues in your method User#get_profile_image that I have fixed:
It always returns nil. I am sure in your real code you have early returns.
For collections, it does a N+1 query if looking for facebook_auth_tokens.
Well, I wanted to comment, but couldn't put code into the comments, so I'm giving a non-answer...
I don't see anything obviously wrong, but as a work around, you could do this in User or somewhere:
def self.user_profile_image(user_id)
active_image = Images.where(user_id: user_id).where(active_image: true).first
if active_image
active_image.image.url(:profile)
else
facebook = FaceBookAuth.where(user_id: user_id).last
if facebook
"https://graph.facebook.com/#{facebook.provider_user_id}/picture?width=150&height=150"
end
end
nil
end
And just call/cache the image in your controller, if that's not overly simplistic...
def get_user_image
render json: User.user_profile_image(params[:user_id])
end
That makes at most 2 relatively efficient queries. It doesn't needlessly load user, etc.
Related
I am a bit confused regarding Rails API caching. I am using JSONAPI spec and fast_jsonapi gem and trying to cache the vehicle itself on show action and if there are params coming over like include=service_notes,service_alerts then I would like to cache those too. This is my initial approach but not sure if it is right.
I have 2 main issues:
For the vehicle caching itself is there a better approach than my vehicle = Vehicle.find_cached(params[:id]). This is not using updated_at but an after save callback to update the cache if vehicle has been updated. I just don't see if I could somehow use sth like Rails.cache.fetch(["vehicles", vehicle], version: vehicle.updated_at) as it is proposed here: https://github.com/rails/rails/pull/29092 since this needs the vehicle instance. As you see the set_vehicle controller method is pretty awkward.
Does this Rails.cache.fetch(['vehicles', vehicle, include_params], version: vehicle.updated_at) make any sense? I am trying to cache the query based on the different include params. Maybe it is overkill and I could just include everything and cache it that way like:
Rails.cache.fetch(['vehicles', vehicle, 'with_includes'], version: vehicle.updated_at) do
Vehicle.includes(:vehicle_alerts, :service_notes, :service_intervals).find(params[:id])
end
What is the correct way to handle caching here?
service_note.rb setup same for service_interval.rb and vehicle_alert.rb
class ServiceNote < ApplicationRecord
belongs_to :vehicle, touch: true
end
vehicle.rb
class Vehicle < ApplicationRecord
after_save :update_cache
has_many :vehicle_alerts, dependent: :delete_all
has_many :service_notes, dependent: :delete_all
has_many :service_intervals, dependent: :delete_all
def update_cache
Rails.cache.write(['vehicles', vehicle_id], self)
end
def self.find_cached(vehicle_id)
Rails.cache.fetch(['vehicles', vehicle_id]) { find(vehicle_id) }
end
end
vehicles_controller.rb
before_action :set_vehicle, only: [:show]
def show
render json: VehicleSerializer.new(#vehicle, options).serialized_json
end
private
def set_vehicle
vehicle = Vehicle.find_cached(params[:id])
#vehicle = Rails.cache.fetch(['vehicles', vehicle, include_params], version: vehicle.updated_at) do
Vehicle.includes(include_params).find(params[:id])
end
authorize #vehicle
end
vehicle_serializer.rb (with fast_jsonapi gem)
# same for :service_notes and :vehicle_alerts
has_many :service_intervals do |vehicle, params|
if params[:include] && params[:include].include?(:service_intervals)
vehicle.service_intervals
end
end
So i'm trying to override a relation has_many in a rails 4.2 application.
I've a model Event who has_many :picturables.
The Event can also has_one :contact and this Contact also has_many :picturables.
What I try to achieve is that when we call event.picturables, is returns the picturables for the Event and also those of the Contact.
I've tried to do this with the 'extend association' :
has_many :picturables, ->{ order(:position) }, as: :picturable, class_name: "Picturable", dependent: :delete_all do
def <missing_name>
event = proxy_association.owner
event.contact.present? ? event.picturables + event.contact.picturables : event.picturables
end
def poney
event = proxy_association.owner
event.contact.present? ? event.picturables + event.contact.picturables : event.picturables
end
end
So when I do event.picturables.poney it works like I want.However, I'd like to override the event.picturables directly.
I tried to name the method self or itself but it doesn't work.
Is what i want to achieve possible the way I tried, or do I instead need to declare a 'normal' method?
Can you guys help me?
Thanks in advance
Edit :
The solution i choose :
I wasn't able to find the solution the way i search, so i implemented an other idea :
def picturables
unless contact.nil?
Picturable.where('(picturable_id = ? AND picturable_type = ?) OR (picturable_id = ? AND picturable_type = ?)', id, 'Event', contact.id, 'Contact')
else
super
end
end
def pictures
Picture.where(id: picturables.map(&:picture_id))
end
The advantage of making a 'where' by hand is that we still have a ActiveRecord:RelationProxy as return, because when i made event.picturables + event.contact.picturables it's return an array
I have 2 models:
Invoice has_many :lines
Line belongs_to :invoice
I want to ensure that the sum of the Line for a given Invoice match the total of the related Invoice.
I've tried this:
validate :total_amount
def total_amount
inv_id = self.invoice_id
target_amount = Invoice.find(inv_id).total
total_lines = Line.where(invoice_id: inv_id).sum(:line_value)
errors.add(:total, " should be lower or equal to the total amount of the invoice") if total_lines > target_amount
end
But
it doesn't work for new objects (just updates)
even for updates it systematically throws an error
I've also seen a question talking about AssociatedValidator, but I haven't been able to grasp how to use that :(
It's not clear what exactly you want to validate, since your example is different from what you were describing prior to that.
I think something like this should work, using a before_add callback:
class Invoice < AR::Base
has_many :lines, :before_add => :validate_total
def validate_total(invoice, line)
totals = invoice.lines.sum(:line_value)
if totals + line.line_value > invoice.total
invoice.errors.add(:total, " should be lower or equal to the total amount of the invoice")
return false # I think you can alternatively raise an exception here
end
...
I might be interpreting it wrong, but if total is a column in the invoices table, I suggest removing it. Instead, have it as a method and have the method add up the Line prices plus any adjustments. Otherwise, you have duplication in the database. And that way you won't need to validate anything anyway :)
On a more general note, adding validations on associated models in ActiveRecord is not working very well. In some cases it's almost impossible, in other - pretty hard to get right. I think you've seen that it goes wrong easily. I suggest avoiding it and trying to design your database so that you won't need to (having Invoice#total as a method in this case).
It took a little while to find an question/answer to a problem that cropped up using accepts_nested_attributes_for. But the answer just said It's hard, if not impossible!. accepts_nested_attributes_for is a somewhat complicated approach, but it works - unless you are trying to validate a model based on a calculation of the children model. I may have found a way to to overcome the calculation problem.
I'm working on a web based double entry accounting application that had the following basic models;
class Account < ApplicationRecord
has_many :splits
has_many :entries, through: :splits
end
class Entry < ApplicationRecord
has_many :splits, -> {order(:account_id)}, dependent: :destroy, inverse_of: :entry
validate :balanced?
end
class Split < ApplicationRecord
belongs_to :entry, inverse_of: :splits
belongs_to :account
validates_associated :account
validates_associated :entry
end
Entries(transactions) must have at least two Splits that the sum of the Amount attribute(or Debits/Credits) in the Splits must equal 0. I though the validate :balanced? would take care of it, but an apparent Javascript error allowed an unbalance entry. I've yet to track the bug down, but since the Entry was unbalanced, I could not update it since valid? does not work (returns false) on new Splits that I tried to add.
The ledger accepts_nested_attributes_for form has quit a bit of Javascript that is not supposed to allow an unbalanced transaction to be submitted. Balanced? did not set an error on create, but its there on update. My approach to fixing it is not used validations that don't work, but to rely on a method called in conjunction with #entry.update(entry_params):
class Entry < ApplicationRecord
has_many :splits, -> {order(:account_id)}, dependent: :destroy, inverse_of: :entry
# validate :balanced? # took this out since its after the fact, balanced? method can still be called
accepts_nested_attributes_for :splits,
:reject_if => proc { |att| att[:amount].to_i.zero? && att['account_id'].to_i.zero?},
allow_destroy: true
def valid_params?(params)
split_sum = 0
params_hash = params.to_h
params_hash[:splits_attributes].each{|k,s| split_sum += s[:amount].to_i if s[:_destroy].to_i.zero?}
unless split_sum.zero?
errors.add(:amount, "Unbalanced: debits, credits must balance")
return false
else
return true
end
end
end
end
# update action from Entry Controller
def update
respond_to do |format|
if #entry.valid_params?(entry_params) && #entry.update(entry_params)
format.html { redirect_to account_path(session[:current_acct]), notice: 'Entry was successfully updated.' }
format.json { render :show, status: :ok, location: #entry }
else
# ... errr
end
end
end
Again, this in nothing more than validating the params verses the model validation that does not work for this condition.
This may be about same as answer 2, but not using a callback, just calling in controller
I've got a multi-level nested form using formtastic_cocoon (jquery version of formtastic).
I am trying to do some validation in the sense of
if value is_numeric do
insert into database
else do
database lookup on text
insert id as association
end
I was hoping tha the accepts_nested_attributes_for would have an :if option, but apparently there is only the :reject_if.
Is there a way to create a validation like I describe as part of the accepts_nested_attributes_for??
-----------------------Updated as per Zubin's Response ---------------------------
I believe Zubin is on the right track with a method, but I can't seem to get it working just right. The method I am using is
def lookup_prereq=(lookup_prereq)
return if lookup_prereq.blank?
case lookup_prereq
when lookup_prereq.is_a?(Numeric) == true
self.task_id = lookup_prereq
else
self.task = Task.find_by_title(lookup_prereq)
end
end
When I trigger this function, the self.task_id is being put in the database as '0' rather than the Task.id.
I'm wondering if I'm missing something else.
I'm not completely sure that the method is actually being called. Shouldn't I need to say
lookup_prereq(attr[:prereq_id)
at some point?
-------------------further edit -----------------------
I think from what I can find that the method is called only if it is named with the same name as the value for the database, therefore I've changed the method to
def completed_task=(completed_task)
Unfortunately this is still resulting in 0 as the value in the database.
Sounds like you need a method in your nested model to handle that, eg:
class Post < ActiveRecord::Base
has_many :comments
accepts_nested_attributes_for :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
belongs_to :author
def lookup_author=(lookup_author)
return if lookup_author.blank?
case lookup_author
when /^\d+$/
self.author_id = lookup_author
else
self.author = Author.find_by_name(lookup_author)
end
end
end
I'm having some issues in RoR with some model methods I am setting. I'm trying to build a method on one model, with an argument that gets supplied a default value (nil). The ideal is that if a value is passed to the method, it will do something other than the default behavior. Here is the setup:
I currently have four models: Market, Deal, Merchant, and BusinessType
Associations look like this:
class Deal
belongs_to :market
belongs_to :merchant
end
class Market
has_many :deals
has_many :merchants
end
class Merchant
has_many :deals
belongs_to :market
belongs_to :business_type
end
class BusinessType
has_many :merchants
has_many :deals, :through => :merchants
end
I am trying to pull some data based on Business Type (I have greatly simplified the return, for the sake of brevity):
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum('price')
else
return self.deals(:conditions => ['market_id = ?',market]).sum('price')
end
end
end
So, if I do something like:
puts BusinessType.first.revenue
I get the expected result, that is the sum of the price of all deals associated with that business type. However, when I do this:
puts BusinessType.first.revenue(1)
It still returns the sum price of all deals, NOT the sum price of all deals from market 1. I've also tried:
puts BusinessType.first.revenue(market=1)
Also with no luck.
What am I missing?
Thanks!
Try this:
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.all.sum(&:price)
else
return self.deals.find(:all, :conditions => ['market_id = ?',market]).sum(&:price)
end
end
end
That should work for you, or at least it did for some basic testing I did first.
As I have gathered, this is because the sum method being called is on enumerable, not the sum method from ActiveRecord as you might have expected.
Note:
I just looked a bit further, and noticed you can still use your old code with a smaller tweak than the one I noted:
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum('price')
else
return self.deals.sum('price', :conditions => ['market_id = ?', market])
end
end
end
Try this!
class BusinessType
def revenue(market=nil)
if market.nil?
return self.deals.sum(:price)
else
return self.deals.sum(:price,:conditions => ['market_id = ?',market])
end
end
end
You can refer this link for other functions. http://en.wikibooks.org/wiki/Ruby_on_Rails/ActiveRecord/Calculations