Rails instance variable for record - ruby-on-rails

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

Related

Overriding ApplicationRecord initialize, bad idea?

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

Bind value to ruby define_method

In my model, I'm trying to dynamically expose objects that are inside an array as a top level attribute. Here's the code snippet:
class Widget < ActiveRecord::Base
# attr_accessor :name
end
class MyModel < ActiveRecord::Base
has_many :widgets
#attr_accessor :widgets
after_initialize :init_widgets
def init_widgets
widgets
widgets.each_with_index do |widget, index|
define_method(widget.name) do
widgets[index]
end
end
end
end
Is there any way for me to define the value of index into the new method I am creating so that it will be associated with the proper index?
I might create an accessor / assignment methods that overload the [] operator:
class BracketOperator
def initialize
#values = (1..100).to_a
end
def [](index)
#values[index]
end
def []=(index, value)
#values[index] = value
end
end
bo = BracketOperator.new
bo[3] # => 4
bo[3] = 17
bo[3] # => 17
So for reference, here's the answer I came up with. Apparently I don't understand scoping for ruby too well. The variable n somehow remains referenced to the n within the each loop. Hence for my original question, I can just use the index variable within the method and it will be mapped to what I am expecting it to be mapped to.
class Test
def setup
names = [ "foo","bar" ]
names.each do |n|
self.class.send :define_method, n do
puts "method is called #{n}!"
end
end
end
end

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

How to add a method for active record?

I would want to use my method getScheduleFixed() on a Active Record Istance
ps = ProgramSchedule.all
ps.getScheduleFixed
the important fact is how to access to "ps" array (active record) on my method declaration
class ProgramSchedule < ActiveRecord::Base
def getScheduleFixed
array = (HERE ARRAY RETURNED BY ACTIVE RECORD)
# some stuff...
return array
end
end
You are mixing things up here.
1) There are (instance) methods that you can use on a single ActiveRecord object.
# Returns an ARRAY with all programschedule instances
all_ps = ProgramSchedule.all
# You can now iterate over over the array
all_ps.each do |ps|
# in here you can call the instance method on the actual model instance
ps.instance_method
end
# Definition of this method in app/models/program_schedule.rb
class ProgramSchedule < ActiveRecord::Base
def instance_method
# Foo
end
end
2) There are class methods that you can run on the ActiveRecord model itself.
ProgramSchedule.class_method
# Definition of this method in app/models/program_schedule.rb
class ProgramSchedule < ActiveRecord::Base
def self.class_method
# Bar
end
end
When you do ProgramSchedule.all, you get an Array, not an instance of ProgramSchedule.
If your method will be called with all the records at all times, you can use a class method like this one :
class ProgramSchedule < ActiveRecord::Base
def self.getAllScheduleFixed
array = ProgramSchedule.all # or self.class.all could be used if you subclass this class
#some stuff
end
end
If you need to work with only a subset of ProgramSchedule's, that is conditions, you will need to pass conditions to this class method, or to directly pass the array of results to some class method.
I think you should use scope for this:
class ProgramSchedule < ActiveRecord::Base
scope :fixed, { all.map(&:getScheduleFixed) }
end
or
class ProgramSchedule < ActiveRecord::Base
def self.fixed
all.map(&:getScheduleFixed)
end
end
Now you just need to call ProgramSchedule.fixed. Both these methods can be chained on other scopes such as ProgramSchedule.latest.fixed. See more details here
def getScheduleFixed
array = User.all.map(&:name)
return array
end

Adding a method to an attribute in Ruby

How do you define a method for an attribute of an instance in Ruby?
Let's say we've got a class called HtmlSnippet, which extends ActiveRecord::Base of Rails and has got an attribute content. And, I want to define a method replace_url_to_anchor_tag! for it and get it called in the following way;
html_snippet = HtmlSnippet.find(1)
html_snippet.content = "Link to http://stackoverflow.com"
html_snippet.content.replace_url_to_anchor_tag!
# => "Link to <a href='http://stackoverflow.com'>http://stackoverflow.com</a>"
# app/models/html_snippet.rb
class HtmlSnippet < ActiveRecord::Base
# I expected this bit to do what I want but not
class << #content
def replace_url_to_anchor_tag!
matching = self.match(/(https?:\/\/[\S]+)/)
"<a href='#{matching[0]}'/>#{matching[0]}</a>"
end
end
end
As content is an instance of String class, redefine String class is one option. But I don't feel like to going for it because it overwrites behaviour of all instances of String;
class HtmlSnippet < ActiveRecord::Base
class String
def replace_url_to_anchor_tag!
...
end
end
end
Any suggestions please?
The reason why your code is not working is simple - you are working with #content which is nil in the context of execution (the self is the class, not the instance). So you are basically modifying eigenclass of nil.
So you need to extend the instance of #content when it's set. There are few ways, there is one:
class HtmlSnippet < ActiveRecord::Base
# getter is overrided to extend behaviour of freshly loaded values
def content
value = read_attribute(:content)
decorate_it(value) unless value.respond_to?(:replace_url_to_anchor_tag)
value
end
def content=(value)
dup_value = value.dup
decorate_it(dup_value)
write_attribute(:content, dup_value)
end
private
def decorate_it(value)
class << value
def replace_url_to_anchor_tag
# ...
end
end
end
end
For the sake of simplicity I've ommited the "nil scenario" - you should handle nil values differently. But that's quite simple.
Another thing is that you might ask is why I use dup in the setter. If there is no dup in the code, the behaviour of the following code might be wrong (obviously it depends on your requirements):
x = "something"
s = HtmlSnippet.find(1)
s.content = x
s.content.replace_url_to_anchor_tag # that's ok
x.content.replace_url_to_anchor_tag # that's not ok
Wihtout dup you are extending not only x.content but also original string that you've assigned.

Resources