Injecting custom callback in rails 3 - ruby-on-rails

I have a custom module which sets up a hash to be stored in my sql. As part of this it rolls a its own _changed accessor.
module MyAwesomeCustomModule
extend ActiveSupport::Concern
included do
after_save: wipe_preferences_changed
end
module ClassMethods
def blah
end
etc
end
end
and then in my model:
class MyModel < ActiveRecord::Base
include MyAwesomeCustomModule
after_save :something_that_expects_preferences_changed_to_be_available
blah
end
unfortunately, the after_save defined in the custom module runs before the one defined in the model. Is there a way to get the array of all callbacks and append to it? Is there a way to write a custom after_after_save callback? Is there a way to specify priority/ordering of after_save callbacks?
What would be a good way to resolve this race condition?

In spite of the order of model callbacks, the current design makes the module and the class very coupled.
To solve the current problem as well as improve design, you can define an expected callback in the module's method, and then the class who includes this module is free to respond it or not.
module MyAwesomeCustomModule
extend ActiveSupport::Concern
included do
after_save: wipe_preferences_changed
end
def wipe_preferences_changed
# previous logic to wipe
process_further if respond_to :process_further
end
end
class MyModel < ActiveRecord::Base
include MyAwesomeCustomModule
# Feel free to write this or not
# The content is the previous
# :something_that_expects_preferences_changed_to_be_available
def process_further
end
end

If you want to keep your original strategy (2 after_save callbacks) all you should need to do is move the include statement below the model after_save.
class MyModel < ActiveRecord::Base
after_save :something_that_expects_preferences_changed_to_be_available
include MyAwesomeCustomModule
blah
end
Callbacks are executed in the order they are defined. The include statement acts (very roughly) like you had copy and pasted the code from the module at that point, so by putting the include statement above the after_save in your model you were causing that callback to execute first.

Related

Ruby/Rails: Circular dependency when including concern in ApplicationRecord

I have a concern that creates a class macro that I want available for all the models in my Rails application. So I'm including it in ApplicationRecord. The code is as follows:
# application_record.rb
class ApplicationRecord < ActiveRecord::Base
include ::TestConcern
end
# app/concerns/test_concern.rb
module TestConcern
extend ActiveSupport::Concern
class_methods do
def some_class_macro_all_models_must_have
User.some_class_instance_variable << self
end
end
included do
User.include(UserModule)
end
module UserModule
def self.included(base)
base.class_eval do
def self.some_class_instance_variable
#some_class_instance_variable ||= Set.new
end
end
end
end
end
As you can see, the class macro will actually interact with a class instance variable in the model User.
So that's why, on the included hook of the concern, I'm trying to class_eval the User model to have that class instance variable initialized. The plan was to do it like this because otherwise any model can be invoking the class macro BEFORE the class instance variable is initialized in the User model.
However, this errors out with Circular dependency detected while autoloading constant User. As far as I can understand, ApplicationRecord loads, it includes the module, the module included hooks is called, it references the User model, and so the User model is loaded, which inherits from ApplicationRecord (which didn't finish loading yet), so it causes the circular dependency.
How to avoid this circular dependency paradox, knowing that many models will invoke this class macro, and those classes might be loaded before the User class itself, so I can't even count on defining the some_class_instance_variable class method in the User model itself?
After giving it some extra thought, I decided to simply store the some_class_instance_variable in the concern itself, and since the model User also called the some_class_macro_all_models_must_have, I decided to include the UserModule when it was invoked, effectively eliminating both the circular dependency and the load order issue.
The real code is much more complex than this contrived example, but the end result was something like this:
module TestConcern
def self.some_class_instance_variable
#some_class_instance_variable ||= Set.new
end
extend ActiveSupport::Concern
class_methods do
def some_class_macro_all_models_must_have
User.include(UserModule) if self == User
TestConcern.some_class_instance_variable << self
end
end
included do
end
module UserModule
def self.included(base)
base.class_eval do
# Class macro invocations, class method and instance method definitions
end
end
end
end

Before Create in Active Record

I would like to setup a before_create for all of my modules
what i have been trying is:
module ActiveRecord
module UserMonitor
require 'securerandom'
before_create :attach_uuid
def attach_uuid
self.uuid = SecureRandom.uuid.gsub("-","")
end
end
end
This does not seem to be working.
if i go into each module and add it in there it works, but i want to do it on a global scale.
Any thoughts or ideas on how i can achieve this in this manner? i know i could do it in triggers and such but i don't want to go that route and i would like to avoid hitting every module/class in case i need to change something.
Currently using Ruby 1.9.3 Can not currently upgrade my app until i make future code changes.
Thanks!
An other solution - I use, is to put the logic for UUID in an own module, that you include. I already have some (class-) methods I add to my AR, like set_default_if, so it was a good place for me.
module MyRecordExt
def self.included base
base.extend ClassMethods # in my case some other stuff
base.before_create :attach_uuid # now add the UUID
end
def attach_uuid
begin
self.uuid = SecureRandom.uuid
rescue
# do the "why dont we have a UUID filed?" here
end
end
# some other things not needed for add_uuid
module ClassMethods
include MySpecialBase # just an eg.
def default_for_if(...)
...
end
end
end
and then
class Articel < ActiveRecord::Base
include MyRecordExt
...
end
In general I avoid doing something for ALL models modifying AR base - I made the first bad experience with adding the UUID to all, and crashed with devise GEMs models ...
If you define attach_uuid in the ActiveRecord module, can't you just call the before_create :attach_uuid at the top of each controller? This is DRY.
Is there a UserMonitor controller that you could add it to?
class UserMonitor < ActiveRecord::Base
before_create :attach_uuid
end

How to dynamically generate association names?

I am using Ruby on Rails 3.2.2 and the Squeel gem. I have following statements and I am trying to refactoring the my_squeel_query method in a Mixin module (since it is used by many of my models):
# Note: 'article_comment_associations' and 'model_as_like_article_comment_associations'
# refer to database table names.
class Article < ActiveRecord::Base
def my_squeel_query
commenters.
.where{
article_comment_associations.article_id.eq(my{self.id}) & ...
}
end
end
class ModelAsLikeArticle < ActiveRecord::Base
def my_squeel_query
commenters.
.where{
model_as_like_article_comment_associations.article_id.eq(my{self.id}) & ...
}
end
end
My problem is that I can not refactoring article_comment_associations and model_as_like_article_comment_associations statements by generating a dynamic name in the Mixin module. That is, if that was a String I could dynamically generate the related name by using something like "#{self.class.to_s.singularize}_comment_associations" as the following:
class Article < ActiveRecord::Base
include MyModule
end
class ModelAsLikeArticle < ActiveRecord::Base
include MyModule
end
module MyModule
def my_squeel_query
commenters.
.where{
# Note: This code doesn't work. It is just an sample.
"#{self.class.to_s.singularize}_comment_associations".article_id.eq(my{self.id}) & ...
}
end
end
But, since it is not my case, I cannot "build" the name and make the my_squeel_query to be "shared" across models.
How can I dynamically generate association names related to the Squeel gem? Should I think to refactoring in another way? What do you advice about?
Since the DSL is instance_evaled, you can actually say something like:
def my_squeel_query
base = self
commenters.
.where{
# Note: This code does work. Because it's awesome.
__send__("#{base.class.to_s.singularize}_comment_associations").
article_id.eq(my{self.id})
}
end
You can do this if you generate the methods dynamically. The Module.included method is provided for this purpose:
module ModuleAsLikeArticle
def self.included(base)
base.send(:define_method, "#{base.to_s.singularize}_comment_associations") do
# ...
end
end
end
This gets triggered when the module is imported with include and allows you to create methods specifically tailored for that.
As a note you might want to use base.name.underscore.singularize for a more readable method name. By convention, method names should not have upper-case in them, especially not as the first character.
Conventional Rails type applications use a different approach, though, instead defining a class method that can be used to create these on-demand:
module ModuleAsLikeArticle
def has_comments
base.send(:define_method, "#{base.to_s.singularize}_comment_associations") do
# ...
end
end
end
This would be used like this:
class ModelAsLikeArticle < ActiveRecord::Base
extend MyModule
has_comments
end
Since the method is not created until has_comments is called, you can safely extend ActiveRecord::Base and then insert the appropriate call in all the classes which require that functionality.
I think you might find what you need in the Rails Reflection class (http://api.rubyonrails.org/classes/ActiveRecord/Reflection/ClassMethods.html), which, as the page says, allows you to interrogate ActiveRecord classes about their associations and aggregations.

remote model callbacks - rails 3?

I'm not phrasing the question correctly in the title, but heres what I'd like to do.
I have a method, like such:
class User < ActiveRecord::Base
def myMethod(abc, xyz)
#do stuff
end
end
I want 5 different models in my app to call this function on their after_create callback.
It seems very anti-DRY to put a function in each of those models to call this function.
Is there a way in this model (above) that holds the method - to remotely use the callbacks of the other models?
Or can anyone suggest a different way I should be approaching something like this?
That's what I would do:
Create a module:
module MyCallbacks
extend ActiveSupport::Concern
included do
after_create :my_method
end
def my_method
#do stuff
end
end
And then, you just need to include this module in the models of your choice:
class MyModel < ActiveRecord::Base
include MyCallbacks
end

Ruby on Rails: shared method between models

If a few of my models have a privacy column, is there a way I can write one method shared by all the models, lets call it is_public?
so, I'd like to be able to do object_var.is_public?
One possible way is to put shared methods in a module like this (RAILS_ROOT/lib/shared_methods.rb)
module SharedMethods
def is_public?
# your code
end
end
Then you need to include this module in every model that should have these methods (i.e. app/models/your_model.rb)
class YourModel < ActiveRecord::Base
include SharedMethods
end
UPDATE:
In Rails 4 there is a new way to do this. You should place shared Code like this in app/models/concerns instead of lib
Also you can add class methods and execute code on inclusion like this
module SharedMethods
extend ActiveSupport::Concern
included do
scope :public, -> { where(…) }
end
def is_public?
# your code
end
module ClassMethods
def find_all_public
where #some condition
end
end
end
You can also do this by inheriting the models from a common ancestor which includes the shared methods.
class BaseModel < ActiveRecord::Base
def is_public?
# blah blah
end
end
class ChildModel < BaseModel
end
In practice, jigfox's approach often works out better, so don't feel obligated to use inheritance merely out of love for OOP theory :)

Resources