Any way to run a block right after Chewy `udpate_index`? - ruby-on-rails

In a Rails app Chewy gem is used to handle ElasticSearch indexing.
I have a block of code in an after_commit and I need it to be run once a new record of the RDB is indexed in our NRDB. it looks like:
class User < ApplicationRecord
update_index('USER') { self }
after_commit :run_this_block, on: :create
def run_this_block
index = UsersIndex.find id
'do something with index'
end
end
seems the after_commit is called before update_index!!!
Nothing is found in chewy gem,
Anyone with any idea?

That could be a possible solution, though it doesn’t work in my specific case:
There is leave method in atomic strategy of Chewy which let bypass the main atomic update_index action:
class Atomic < Base
def initialize
#stash = {}
end
def update(type, objects, _options = {})
#stash[type] ||= []
#stash[type] |= type.root.id ? Array.wrap(objects) : type.adapter.identify(objects)
end
def leave
#stash.all? { |type, ids| type.import!(ids) }
end
end
It is possible to prepend leave method in chewy.rb:
# /config/initializers/chewy.rb
Chewy::Strategy::Atomic.prepend(
Module.new do
def leave
#stash.all? do |type, ids|
if INTENDED_INDICES_NAMES.include?(type.name)
type.import!(sliced_ids)
<here goes the code where one have access to recently indexed records>
else
type.import!(ids)
end
true
end
end
end

Related

Rails N+1 query : monkeypatching ActiveRecord::Relation#as_json

Situation
I have a model User:
def User
has_many :cars
def cars_count
cars.count
end
def as_json options = {}
super options.merge(methods: [:cars_count])
end
end
Problem
When I need to render to json a collection of users, I end up being exposed to the N+1 query problem. It is my understanding that including cars doesn't solve the problem for me.
Attempted Fix
What I would like to do is add a method to User:
def User
...
def self.as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
That way all cars counts would be queried in a single query.
Remaining Issue
ActiveRecord::Relation already has a as_json method and therefore doesn't pick the class defined one. How can I make ActiveRecord::Relation use the as_json method from the class when it is defined? Is there a better way to do this?
Edits
1. Caching
I can cache my cars_count method:
def cars_count
Rails.cache.fetch("#{cache_key}/cars_count") do
cars.count
end
end
This is nice once the cache is warm, but if a lot of users are updated at the same time, it can cause request timeouts because a lot of queries have to be updated in a single request.
2. Dedicated method
Instead of calling my method as_json, I can call it my_dedicated_as_json_method and each time I need to render a collection of users, instead of
render json: users
write
render json: users.my_dedicated_as_json_method
However, I don't like this way of doing. I may forget to call this method somewhere, someone else might forget to call it, and I'm losing clarity of the code. Monkey patching seems a better route for these reasons.
Have you considered using a counter_cache for cars_count? It's a good fit for what you're wanting to do.
This blog article also offers up some other alternatives, e.g. if you want to manually build a hash.
If you really wanted to continue down the monkey patching route, then ensure that you are patching ActiveRecord::Relation rather than User, and override the instance method rather than creating a class method. Note that this will then affect every ActiveRecord::Relation, but you can use #klass to add a condition that only runs your logic for User
# Just an illustrative example - don't actually monkey patch this way
# use `ActiveSupport::Concern` instead and include the extension
class ActiveRecord::Relation
def as_json(options = nil)
puts #klass
end
end
Option 1
In your user model:
def get_cars_count
self.cars.count
end
And in your controller:
User.all.as_json(method: :get_cars_count)
Option 2
You can create a method which will get all the users and their car count. And then you can call the as_json method on that.
It would roughly look like:
#In Users Model:
def self.users_with_cars
User.left_outer_joins(:cars).group(users: {:id, :name}).select('users.id, users.name, COUNT(cars.id) as cars_count')
# OR may be something like this
User.all(:joins => :cars, :select => "users.*, count(cars.id) as cars_count", :group => "users.id")
end
And in your controller you can call as_json:
User.users_with_cars.as_json
Here is my solution in case someone else is interested.
# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
# config/initializers/core_extensions.rb
require 'core_extensions/active_record/relation/serialization'
ActiveRecord::Relation.include CoreExtensions::ActiveRecord::Relation::Serialization
# lib/core_extensions/active_record/relation/serialization.rb
require 'active_support/concern'
module CoreExtensions
module ActiveRecord
module Relation
module Serialization
extend ActiveSupport::Concern
included do
old_as_json = instance_method(:as_json)
define_method(:as_json) do |options = {}|
if #klass.respond_to? :collection_as_json
scoping do
#klass.collection_as_json options
end
else
old_as_json.bind(self).(options)
end
end
end
end
end
end
end
# app/models/user.rb
def User
...
def self.collection_as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
Thanks #gwcodes for pointing me at ActiveSupport::Concern.

How to add errors before updating attributes?

I'm trying to handle the situation where the user has entered info incorrectly, so I have a path that follows roughly:
class Thing < AR
before_validation :byebug_hook
def byebug_hook
byebug
end
end
thing = Thing.find x
thing.errors.add(:foo, "bad foo")
# Check byebug here, and errors added
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
byebug for byebug_hook> errors.messages #=> {}
Originally I thought that maybe the model was running its own validations and overwriting the ones I added, but as you can see even when I add the before hook the errors are missing, and I'm not sure what's causing it
ACTUAL SOLUTION
So, #SteveTurczyn was right that the errors needed to happen in a certain place, in this case a service object called in my controller
The change I made was
class Thing < AR
validate :includes_builder_added_errors
def builder_added_errors
#builder_added_errors ||= Hash.new { |hash, key| hash[key] = [] }
end
def includes_builder_added_errors
builder_added_errors.each {|k, v| errors.set(k, v) }
end
end
and in the builder object
thing = Thing.find x
# to my thinking this mirrors the `errors.add` syntax better
thing.builder_added_errors[:foo].push("bad foo") if unshown_code_does_stuff?
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
update_attributes will validate the model... this includes clearing all existing errors and then running any before_validation callbacks. Which is why there are never any errors at the pont of before_validation
If you want to add an error condition to the "normal" validation errors you would be better served to do it as a custom validation method in the model.
class Thing < ActiveRecord::Base
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo")
end
end
If you want some validations to occur only in certain controllers or conditions, you can do that by setting an attr_accessor value on the model, and setting a value before you run validations directly (:valid?) or indirectly (:update, :save).
class Thing < ActiveRecord::Base
attr_accessor :check_foo
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo") if check_foo
end
end
In the controller...
thing = Thing.find x
thing.check_foo = true
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end

Active Record Clear instance of model after_find callback

I have defined a callback after_find for checking some settings based on the retrieved instance of the model. If the settings aren't fulfilled I don't want the instance to be return from the find method. Is that possible?
an example
the controller looks like:
class UtilsController < ApplicationController
def show
#util = Util.find(params[:id])
end
end
the model:
class Util < ActiveRecord::Base
after_find :valid_util_setting
def valid_util_setting
# calculate_availability? complex calculation
# that can not be part of the sql statement or a scope
unless self.setting.calculate_availability?(User.current.session)
#if not available => clear the record for view
else
#nothing to do here
end
end
end
Instead of trying to clear the record, you could just raise an exception?
E.g.
unless self.setting.calculate_availability?(User.current.session)
raise ActiveRecord::RecordNotFound
else
...
I'm afraid you can't clear found record in this callback
Maybe you should find in scope with all your options from the beginning?
I.e. #util = Util.scoped.find(params[:id])
I found a solution
def valid_util_setting
Object.const_get(self.class.name).new().attributes.symbolize_keys!.each do |k,v|
begin
self.assign_attributes({k => v})#, :without_protection => true)
rescue ActiveModel::MassAssignmentSecurity::Error => e; end
end
end
With this I'm able to create an almost empty object

Rails Validations with Redis

I have a bunch of Redis getters and setters like
def share_points= p
$redis.set("store:#{self.id}:points:share", p) if valid?
end
The thing is, ActiveRecord's validation doesn't stop the values from being inserted into redis. How do I go about doing this without adding if valid? on every setter? valid? calculates the validation every time it is called.
What about switching to an after_save callback approach, where you store all the fields that have been changed and just persist them all at once to redis.
Something like:
class Foo < ActiveRecord::Base
after_save :persist_to_redis
attr_accessor :redis_attributes
def share_points=(p)
#redis_attributes ||= {}
#redis_attributes[:share_points] = p
end
def something_else=(p)
#redis_attributes ||= {}
#redis_attributes[:something_else] = p
end
private
def redis_store_share_points(value)
$redis.set("store:#{self.id}:key", value)
end
def redis_store_something_else(value)
$redis.set("something_else:#{self.id}", value)
end
def persist_to_redis
$redis.multi do
#redis_attributes.each_pair do |key, value|
send("redis_store_#{key}".to_sym, value)
end
end
end
end
I think even this could be refactored and cleaned up but you get the idea.
If the model you're editing is derived from active_record, then you probably want to have a specific, wrapped call to redis that does the validation for you. e.g.
class Foo < ActiveRecord::Base
def rset(key,value)
$redis.set("store:#{self.id}:key", value) if valid?
end
def share_points=(p)
rset("points:share", p)
end
end
You could also put that in a module and include it.
If you're not deriving from AR:Base, you might want to come up with a more AR::Base-like structure using ActiveModel as described here: http://purevirtual.de/2010/04/url-shortener-with-redis-and-rails3/

Rails Metaprogramming: How to add instance methods at runtime?

I'm defining my own AR class in Rails that will include dynamically created instance methods for user fields 0-9. The user fields are not stored in the db directly, they'll be serialized together since they'll be used infrequently. Is the following the best way to do this? Alternatives?
Where should the start up code for adding the methods be called from?
class Info < ActiveRecord::Base
end
# called from an init file to add the instance methods
parts = []
(0..9).each do |i|
parts.push "def user_field_#{i}" # def user_field_0
parts.push "get_user_fields && #user_fields[#{i}]"
parts.push "end"
end
Info.class_eval parts.join
One nice way, especially if you might have more than 0..9 user fields, would be to use method_missing:
class Info
USER_FIELD_METHOD = /^user_field_(\n+)$/
def method_missing(method, *arg)
return super unless method =~ USER_FIELD_METHOD
i = Regexp.last_match[1].to_i
get_user_fields && #user_fields[i]
end
# Useful in 1.9.2, or with backports gem:
def respond_to_missing?(method, private)
super || method =~ USER_FIELD_METHOD
end
end
If you prefer to define methods:
10.times do |i|
Info.class_eval do
define_method :"user_field_#{i}" do
get_user_fields && #user_fields[i]
end
end
end
Using method_missing is very difficult to maintain and unnecessary. The other alternative using define_method is better but leads to poorly performing code. The following 1 liner is all you need:
class Info
end
Info.class_eval 10.times.inject("") {|s,i| s += <<END}
def user_field_#{i}
puts "in user_field_#{i}"
end
END
puts Info.new.user_field_4

Resources