rails 5 API low-level caching - ruby-on-rails

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

Related

Rails how to copy all active storage attachments to new object?

I have a function to clone records in a rails application. In addition to the form data I would like to copy/attach any active storage file uploads that are attached to the source object to the new object. Any ideas on how to do this? Here is my action:
def copy
#source = Compitem.find(params[:id])
#compitem = #source.dup
render 'new'
end
class Compitem < ApplicationRecord
belongs_to :user
has_many_attached :uploads, dependent: :destroy
end
I ended up getting this working by using the https://github.com/moiristo/deep_cloneable gem. Final action:
def copy
#source = Compitem.find(params[:id])
#compitem = #source.deep_clone(include: :uploads_blobs)
#compitem.save
render 'new'
end
Just did this in one of my applications - it was a has_one rather than has_many but I think something like this should work for you, without adding any additional dependencies, in Rails 6+:
#compitem = #source.dup
#source.uploads.each do |original_file|
#compitem.uploads.attach(io: StringIO.new(original_file.download),
filename: original_file.filename,
content_type: original_file.content_type)
end
#compitem.save

Rails includes cache not being used in model method

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.

Is there a more direct way to do a pub/sub pattern in Rails than Observers?

I have a model which has a dependency on a separate, joined model.
class Magazine < ActiveRecord::Base
has_one :cover_image, dependent: :destroy, as: :imageable
end
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
end
Images are polymorphic and can be attached to many objects (pages and articles) not just magazines.
The magazine needs to update itself when anything about its associated image changes
The magazine also stores a screenshot of itself that can be used for publicising it:
class Magazine < ActiveRecord::Base
has_one :cover_image, dependent: :destroy, as: :imageable
has_one :screenshot
def generate_screenshot
# go and create a screenshot of the magazine
end
end
Now if the image changes, the magazine also needs to update its screenshot. So the magazine really needs to know when something happens to the image.
So we could naively trigger screenshot updates directly from the cover image
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
after_save { update_any_associated_magazine }
def update_any_associated_magazine
# figure out if this belongs to a magazine and trigger
# screenshot to regenerate
end
end
...however the image shouldn't be doing stuff on behalf of the magazine
However the image could be used in lots of different objects and really shouldn't be doing actions specific to the Magazine as it's not the Image's responsibility to worry about. The image might be attached to pages or articles as well and doesn't need to be doing all sorts of stuff for them.
The 'normal' rails approach is to use an observer
If we were to take a Rails(y) approach then we could create a third party observer that would then trigger an event on the associated magazine:
class ImageObserver < ActiveRecord::Observer
observe :image
def after_save image
Magazine.update_magazine_if_includes_image image
end
end
However this feels like a bit of a crappy solution to me.
We've avoided the Image being burdened by updating the magazine which was great but we've really just punted the problem downstream. It's not obvious that this observer exists, it's not clear inside the Magazine object that the update to the Image will in fact trigger an update to the magazine and we've got a weird floating object which has logic that really just belongs in Magazine.
I don't want an observer - I just want one object to be able to subscribe to events on another object.
Is there any way to subscribe to one model's changes directly from another?
What I would much rather do is have the magazine subscribe directly to events on the image. So the code would instead look like:
class Magazine < ActiveRecord::Base
...
Image.add_after_save_listener Magazine, :handle_image_after_save
def self.handle_image_after_save image
# determine if image belongs to magazine and if so update it
end
end
class Image < ActiveRecord::Base
...
def self.add_after_save_listener class_name, method
##after_save_listeners << [class_name, method]
end
after_save :notify_after_save_listeners
def notify_after_save_listeners
##after_save_listeners.map{ |listener|
class_name = listener[0]
listener_method = listener[1]
class_name.send listener_method
}
end
Is this a valid approach and if not why not?
This pattern seems sensible to me. It uses class variables and methods so doesn't make any assumptions of particular instances being available.
However, I'm old enough and wise enough now to know that if something seemingly obvious hasn't been done already in Rails there's probably a good reason for it.
This seems cool to me. What's wrong with it though? Why do all the other solutions I see all draft in a third party object to deal with things? Would this work?
I use Redis:
In an initializer I set up Redis:
# config/initializers/redis.rb
uri = URI.parse ENV.fetch("REDISTOGO_URL", 'http://127.0.0.1:6379')
REDIS_CONFIG = { host: uri.host, port: uri.port, password: uri.password }
REDIS = Redis.new REDIS_CONFIG
It'll default to my local redis installation in development but on Heroku it'll use Redis To Go.
Then I publish using model callbacks:
class MyModel < ActiveRecord::Base
after_save { REDIS.publish 'my_channel', to_json }
end
Then I can subscribe from anywhere, such as a controller I'm using to push events using Event Source
class Admin::EventsController < Admin::BaseController
include ActionController::Live
def show
response.headers["Content-Type"] = "text/event-stream"
REDIS.psubscribe params[:event] do |on|
on.pmessage do |pattern, event, data|
response.stream.write "event: #{event}\n"
response.stream.write "data: #{data}\n\n"
end
end
rescue IOError => e
logger.info "Stream closed: #{e.message}"
ensure
redis.quit
response.stream.close
end
end
Redis is great for flexible pub/sub. That code I have in the controller can be placed anywhere, let's say in an initializer:
# config/initializers/subscribers.rb
REDIS.psubscribe "image_update_channel" do |on|
on.pmessage do |pattern, event, data|
image = Image.find data['id']
image.imageable # update that shiz
end
end
Now that will handle messages when you update your image:
class Image < ActiveRecord::Base
belongs_to :imageable, polymorphic: true
after_save { REDIS.publish 'image_update_channel', to_json }
end
There is ActiveSupport Notifications mechanism for implementing pub/sub in Rails.
First, you should define instrument which will publish events:
class Image < ActiveRecord::Base
...
after_save :publish_image_changed
private
def publish_image_changed
ActiveSupport::Notifications.instrument('image.changed', image: self)
end
end
Then you should subscribe for this event (you can put this code in initializer):
ActiveSupport::Notifications.subscribe('image.changed') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
image = event.payload[:image]
# If you have no other cases than magazine, you can check it when you publish event.
return unless image.imageable.is_a?(Magazine)
MagazineImageUpdater.new(image.imageable).run
end
I'll give it a shot...
Use public_send to notify the parent class of a change:
class BaseModel < ActiveRecord::Base
has_one :child_model
def respond_to_child
# now generate the screenshot
end
end
class ChildModel < ActiveRecord::Base
belongs_to :base_model
after_update :alert_base
def alert_base
self.base_model.public_send( :respond_to_child )
end
end

Rails 4 Not Updating Nested Attributes Via JSON

I've scoured related questions and still have a problem updating nested attributes in rails 4 through JSON returned from my AngularJS front-end.
Question: The code below outlines JSON passed from AngularJS to the Candidate model in my Rails4 app. The Candidate model has many Works, and I'm trying to update the Works model through the Candidate model. For some reason the Works model fails to update, and I'm hoping someone can point out what I'm missing. Thanks for your help.
Here's the json in the AngularJS front-end for the candidate:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}]}
Rails then translates this JSON into the following by adding the candidate header, but does not include the nested attributes under the candidate header and fails to update the works_attributes through the candidate model:
{"id"=>"13", "nickname"=>"New Candidate", "works_attributes"=>[
{"title"=>"Financial Analyst", "description"=>"I did things"},
{"title"=>"Accountant", "description"=>"I did more things"}],
"candidate"=>{"id"=>"13", "nickname"=>"New Candidate"}}
The candidate_controller.rb contains a simple update:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
def update
respond_with Candidate.update(params[:id], candidate_params)
end
private
def candidate_params
params.require(:candidate).permit(:nickname,
works_attributes: [:id, :title, :description])
end
end
The candidate.rb model includes the following code defining the has_many relationship with the works model:
class Candidate < ActiveRecord::Base
## Model Relationships
belongs_to :users
has_many :works, :dependent => :destroy
## Nested model attributes
accepts_nested_attributes_for :works, allow_destroy: true
## Validations
validates_presence_of :nickname
validates_uniqueness_of :user_id
end
And finally, the works.rb model defines the other side of the has_many relationship:
class Work < ActiveRecord::Base
belongs_to :candidate
end
I appreciate any help you may be able to provide as I'm sure that I'm missing something rather simple.
Thanks!
I've also been working with a JSON API between Rails and AngularJS. I used the same solution as RTPnomad, but found a way to not have to hardcode the include attributes:
class CandidatesController < ApplicationController
respond_to :json
nested_attributes_names = Candidate.nested_attributes_options.keys.map do |key|
key.to_s.concat('_attributes').to_sym
end
wrap_parameters include: Candidate.attribute_names + nested_attributes_names,
format: :json
# ...
end
Refer to this issue in Rails to see if/when they fix this problem.
Update 10/17
Pending a PR merge here: rails/rails#19254.
I figured out one way to resolve my issue based on the rails documentation at: http://edgeapi.rubyonrails.org/classes/ActionController/ParamsWrapper.html
Basically, Rails ParamsWrapper is enabled by default to wrap JSON from the front-end with a root element for consumption in Rails since AngularJS does not return data in a root wrapped element. The above documentation contains the following:
"On ActiveRecord models with no :include or :exclude option set, it will only wrap the parameters returned by the class method attribute_names."
Which means that I must explicitly include nested attributes with the following statement to ensure Rails includes all of the elements:
class CandidatesController < ApplicationController
before_filter :authenticate_user!
respond_to :json
wrap_parameters include: [:id, :nickname, :works_attributes]
...
Please add another answer to this question if there is a better way to pass JSON data between AngularJS and Rails
You can also monkey patch parameter wrapping to always include nested_attributes by putting this into eg wrap_parameters.rb initializer:
module ActionController
module ParamsWrapper
Options.class_eval do
def include
return super if #include_set
m = model
synchronize do
return super if #include_set
#include_set = true
unless super || exclude
if m.respond_to?(:attribute_names) && m.attribute_names.any?
self.include = m.attribute_names + nested_attributes_names_array_of(m)
end
end
end
end
private
# added method. by default code was equivalent to this equaling to []
def nested_attributes_names_array_of model
model.nested_attributes_options.keys.map { |nested_attribute_name|
nested_attribute_name.to_s + '_attributes'
}
end
end
end
end

Validate that the sum of the part is equal to the total with parent/children records

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

Resources