Overriding ApplicationRecord initialize, bad idea? - ruby-on-rails

I am creating a foo obeject like this:
#foo = Foo.new(foo_params)
#foo.bar = Bar.where(name: "baz").first_or_create
But there are other objects that I will need to do this as well. So, I thought of overriding the Foo initialize method to do something like this:
class Foo < ApplicationRecord
def initialize(*args, BarName)
#foo = super
#foo.bar = Bar.where(name: BarName).first_or_create
end
end
and call it like this:
#foo = Foo.new(foo_params, "baz")
But Foo is an ApplicationRecord and it seems that it's not recommended to override the ApplicationRecord initialize method.
So how could I do this? Any other ideas? Would this initialize overriding thing work?

You can use active record callbacks for that. However you won't be able to to specify bar_name and will somehow need to find it dynamically from Foo attributes.
If that option works you. Add to your model something like the the following code.
after_initialize :set_bar
# some other code
def set_bar
name = # dynamicly_find_name
self.bar = Bar.where(name: name).first_or_create
end
In case you really need to specify bar_name, I would suggest to create a method for it.
Foo.new(params).with_bar
def with_bar(bar_name)
self.bar = Bar.where(name: BarName).first_or_create
end

You can make use of the after_initialize callback and use transients if necessary:
class Foo < ApplicationRecord
after_initialize :custom_initialization
attr_accessor :barname
def custom_initialization()
self.bar = Bar.where(name: self.barname).first_or_create
end
end
The application records own initialisation should take care of setting barname providing it is in the params

Related

ruby monkey patching on the fly

Is there a way to implement monkey patching while an object is being instantiated?
When I call:
a = Foo.new
Prior to the instance being instantiated, I would like to extend the Foo class based on information which I will read from a data store. As such, each time I call Foo.new, the extension(s) that will be added to that instance of the class would change dynamically.
tl;dr: Adding methods to an instance is possible.
Answer: Adding methods to an instance is not possible. Instances in Ruby don't have methods. But each instance can have a singleton class, where one can add methods, which will then be only available on the single instance that this singleton class is made for.
class Foo
end
foo = Foo.new
def foo.bark
puts "Woof"
end
foo.bark
class << foo
def chew
puts "Crunch"
end
end
foo.chew
foo.define_singleton_method(:mark) do
puts "Widdle"
end
foo.mark
are just some of the ways to define a singleton method for an object.
module Happy
def cheer
puts "Wag"
end
end
foo.extend(Happy)
foo.cheer
This takes another approach, it will insert the module between the singleton class and the real class in the inheritance chain. This way, too, the module is available to the instance, but not on the whole class.
Sure you can!
method_name_only_known_at_runtime = 'hello'
string_only_known_at_runtime = 'Hello World!'
test = Object.new
test.define_singleton_method(method_name_only_known_at_runtime) do
puts(string_only_known_at_runtime)
end
test.hello
#> Hello World!
Prior to the instance being instantiated, I would like to extend
Given a class Foo which does something within its initialize method:
class Foo
attr_accessor :name
def initialize(name)
self.name = name
end
end
And a module FooExtension which wants to alter that behavior:
module FooExtension
def name=(value)
#name = value.reverse.upcase
end
end
You could patch it via prepend:
module FooPatcher
def initialize(*)
extend(FooExtension) if $do_extend # replace with actual logic
super
end
end
Foo.prepend(FooPatcher)
Or you could extend even before calling initialize by providing your own new class method:
class Foo
def self.new(*args)
obj = allocate
obj.extend(FooExtension) if $do_extend # replace with actual logic
obj.send(:initialize, *args)
obj
end
end
Both variants produce the same result:
$do_extend = false
Foo.new('hello')
#=> #<Foo:0x00007ff66582b640 #name="hello">
$do_extend = true
Foo.new('hello')
#=> #<Foo:0x00007ff66582b280 #name="OLLEH">

Set activerecord model defaults before mass assignment initialize

I need defaults (from a remote service) in ModelA to be set before the object is passed to the view from ModelAController#new. I have used after_initialize for this. However, in #create I have a problem. If I use model_b.create_model_a(some_attributes), the attributes are passed in during initialization and then overwritten by the after_initialize call:
class ModelA < ActiveRecord::Base
after_initialize :set_defaults, if: :new_record?
def set_defaults
self.c = "default"
#actually a remote call, not something can be set as database default
end
end
class ModelB < ActiveRecord::Base
belongs_to :model_a
end
class ModelAController < ApplicationController
#ModelA is nested under ModelB in routes.rb
#GET /model_bs/:model_b_id/model_as/new
def new
model_b = ModelB.find(params[:model_b_id])
#no problem
respond_with model_b.build_model_a
end
#POST /model_bs/:model_b_id/model_as
def create
model_b = ModelB.find(params[:id])
#problem:
respond_with model_b.create_model_a({c: "not default"})
#at this point the model_a instance still has attribute c set to "default"
end
...
end
I could separate the create steps:
model_b = ModelB.find(params[:id])
model_a = model_b.build_model_a #should fire after_initialize
model_a.update_attributes({c: "not default"}) #overwrite default c value
But I feel like this makes the lifecycle of ModelA a bit of a trap for other programmers. This looks like an obvious candidate for refactoring the last two lines into one, but that would create this problem again. Is there a neater solution?
Make a conditional assignment:
def set_defaults
self.c ||= "default"
end
Alternatively instead of after_initialize hook set a default in attribute reader. That way you only set the default when you actually need attribute value, so it saves you a remote call if you don't need it:
def c
super || self.c = 'default'
end

rails callback before 'new' of a model?

I have a model base_table, and I have a extended_table which has extra properties to further extend my base_table. (I would have different extended_tables, to add different properties to my base_table, but that's non-related to the question I'm asking here).
The model definition for my base_table is like:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def self.included(base)
base.belongs_to :base_table, autosave:true, dependent: :destroy
# do something more when this module is included
end
end
end
And the model definition for my extended_table is like:
class TennisQuestionaire < ActiveRecord::Base
include BaseTable::BaseTableInclude
end
Now I what I want is the code below:
params = {base_table: {name:"Songyy",age:19},tennis_ball_num:3}
t = TennisQuestionaire.new(params)
When I created my t, I want the base_table to be instantiated as well.
One fix I can come up with, is to parse the params to create the base_table object, before TennisQuestionaire.new was called upon the params. It's something like having a "before_new" filter here. But I cannot find such kind of filter when I was reading the documentation.
Additionally, I think another way is to override the 'new' method. But this is not so clean.
NOTE: There's one method called accepts_nested_attributes_for, seems to do what I want, but it doesn't work upon a belongs_to relation.
Any suggestions would be appreciated. Thanks :)
After some trails&error, the solution is something like this:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def initialize(*args,&block)
handle_res = handle_param_args(args) { |params| params[:base_table] = BaseTable.new(params[:base_table]) }
super(*args,&block)
end
private
def handle_param_args(args)
return unless block_given?
if args.length > 0
params = args[0]
if (params.is_a? Hash) and params[:base_table].is_a? Hash
yield params
end
end
end
end
end

Rails instance variable for record

Is there any built-in way to attach an instance variable to a record? For example, suppose I have a User class with a foo attr_accessor:
class User < ActiveRecord::Base
...
attr_accessor :foo
end
If I do
u = User.first
u.foo = "bar"
u.foo # -> "bar"
User.find(1).foo # => nil
This is the correct behavior and I understand why: the instance variable exists for the instance (in memory) and not for the record. What I want is something like a record variable, so that in the above example u.foo and User.find(1).foo return the same value within the same instance of my application. I don't want persistence: it's not appropriate for foo to be a column in the table, it's just appropriate for foo to return the same value for the same record during the life cycle of e.g., a controller action, console session, etc. Nor do I want a class variable via cattr_accessor, because there's no reason that User.find(1).foo should be the same as User.find(2).foo.
The best bet I can come up with is to fake it with a class variable array and instance methods to get/set the appropriate element of the array:
class User < ActiveRecord::Base
cattr_accessor :all_the_foos
self.all_the_foos = Array.new
def foo
self.all_the_foos[id]
end
def foo= new_foo
self.all_the_foos[id] = new_foo
end
end
This ALMOST works, but it doesn't work with un-saved records, e.g. User.new.foo = "bar" fails.
You can use Thread.current to set variables that are active within the context of a controller action invocation. Your current implementation doesn't guarantee context reset across calls.
class User < ActiveRecord::Base
after_create :set_thread_var
def foo
new_record? ? #foo : Thread.current["User-foo-#{id}"]
end
def foo=(val)
new_record? ? (#foo = val) : (Thread.current["User-foo-#{id}"] = val)
end
private
def set_thread_var
Thread.current["User-foo-#{id}"] = #foo if defined?(#foo)
end
end
I can't think of a better idea than your class variable solution. To solve your "new" problem I'd use after_initialize.
class User < ActiveRecord::Base
after_initialize :init_foos
cattr_accessor :all_the_foos
def foo
self.all_the_foos[id]
end
def foo= new_foo
self.all_the_foos[id] = new_foo
end
private
def init_foos
##all_the_foos ||= []
end
end

Executing Rails virtual attribute setters in order

I have an ActiveRecord model with several virtual attribute setters. I want to build an object but not save it to the database. One setter must execute before the others. How to do?
As a workaround, I build the object in two steps
#other_model = #some_model.build_other_model
#other_model.setup(params[:other_model)
Where setup is:
class OtherModel < ActiveRecord::Base
def setup(other_params)
# execute the important_attribute= setter first
important_attribute = other_params.delete(:important_attribute)
# set the other attributes in whatever order they occur in the params hash
other_params.each { |k,v| self.send("#{k}=",v) }
end
end
This seems to work, but looks kludgy. Is there a better way?
EDIT
per neutrino's suggestion, I added a method to SomeModel:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = OtherModel.new(:some_model=>self)
other_model.setup(other_params)
other_model
end
end
It's a good thing that you have this manipulations done in an OtherModel's method, because you can just call this method and not worry about the order of assignments. So I would leave this part but just call it from a SomeModel's method:
class SomeModel < ActiveRecord::Base
def build_other_model(other_params)
other_model = build_other_model
other_model.setup(other_params)
other_model
end
end
So then you would have
#other_model = #some_model.build_other_model(params[:other_model])
I took your idea of deleting the important attribute first in your setup method, but used alias_chain_method instead to make it more of a transparent process:
def attributes_with_set_important_attribute_first=(attributes = {})
# Make sure not to accidentally blank out the important_attribute when none is passed in
if attributes.symbolize_keys!.include?(:important_attribute)
self.important_attribute = attributes.delete(:important_attribute)
end
self.attributes_without_set_important_attribute_first = attributes
end
alias_method_chain :attributes=, :set_important_attribute_first
This way none of your code should change from the normal Rails style
#other_model = #some_model.other_models.build(params[:other_model])

Resources