When I write ActiveRecord models in rails I want to make sure that local variables I use in my method will remain local to the scope of the method I write, and I will not be accidentally overwriting model property in future.
For example:
class User < ActiveRecord::Base
after_save :do_something
def do_something
# ... some logic
group = "some value"
# ... more logic
end
end
How do I make sure that if I happen to add group property to my model in future - my method will not accidentally overwrite it?
I thought about class variables, but it's not what they are for, and I think using them will be a bad practice.
In ruby you can define local variables and instance variables with the same name.
class Example
#instance = 4
def example_method
#instance += 1
end
def example_2
instance = 0
10.times { instance += 1 }
end
end
e = Example.new
e.example_2 # => 10
e.instance # => 4
e.example_method # => 5
e.instance # => 5
these will not conflict
example_2
is returning the local variable which is then destroyed if not used
example_method
is setting the instance variable which will save per instance of the class or in this case for active record will save to database.
Also note you can be more explicit when referring to instance variables and say
self.instance
EDIT
your group variable inside your do_something is a local variable and will not cause conflict with #group which is a instance variable of a higher scope
EDIT2
I have a projects model in my application
class Project < ActiveRecord::Base
VALID_NAME_REGEX = /(\A[\w ]+)/
VALID_URL_REGEX = URI::regexp(["http", "https"])
validates :name, :presence => true,
:uniqueness => true,
:length => {
:maximum => 24
},
:format => {
:with => VALID_NAME_REGEX,
:message => "Only allow numbers letters hyphens and underscores"
}
after_save :test
def test
name = 'bob'
end
end
after setting my model to this and creating a new instance of it on my website I can confirm this will not set my projects name to bob
Using a local variable, that is a variable not prefixed by # or ## will not cause a namespace collision with future model attributes. Those attributes would have to be referenced in such a way similar to self.group. You could run into trouble if your code in your model tweaks the class by adding fields or members to the object at runtime, or if you mess with the class definition, but just declaring a local variable in a method will not collide with an instance variable that corresponds to an ORM field.
Additionally, if you've got methods to mutate fields in your model and you try to access them by saying
def getgroup
group
end
this won't work, either. group in this context is undefined, because it doesn't refer to anything else in the local scope
Related
This is the first time I am writing test cases on a rails project which is using RSpec and FactoryGirl
When I run the test case i get the following error
wrong number of arguments (given 0, expected 1)
I have gone through other posts at stack over flow and they are not much helpful in my case.
What I have tried
I am writing a test case on a Model which is called ImportFeed and it looks something like as following
class ImportFeed < ApplicationRecord
belongs_to :staffroom
belongs_to :user, optional: true # We don't have to have a user
validates_presence_of :url, :feed_type
validates :enabled, presence: true, allow_blank: true
def initialize(params)
super(params)
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
end
This is what my test case looks like
require 'rails_helper'
describe JobImporters::JoraJobImporter, '.run' do
it 'should create an instance of ImportFeed' do
feed = ImportFeed::new FactoryGirl.create(:import_feed, :import1)
expect(feed).to be_a ImportFeed
end
end
This is the factory
FactoryGirl.define do
factory :import_feed do
trait :import1 do
enabled true
feed_type 'example'
staffroom_id 7526
url Faker::Internet::url
end
end
end
When I run this I get the error mentioned at the beginning of this question,
If I pass the data to the test case without FactoryGirl then my test case works and passes for example if I replace
feed = ImportFeed::new FactoryGirl.create(:import_feed, :import1)
with
feed = ImportFeed::new enabled: true, staffroom_id: 7526, feed_type: 'example', url: Faker::Internet::url
the test case passes.
I will really appreciate if someone can point to me what am I doing wrong here.
Because you're overriding initialize method, so you got unexpected exception.
Don't override initialize on ActiveRecord objects
ActiveRecord::Base doesn't always use new to create objects, so initialize might not be called. [link]
In order to solve your problem, you should set your attributes in callback instead
class ImportFeed < ApplicationRecord
# ...
after_initialize :set_my_attributes
private
def set_my_attributes
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
end
One more thing:
You're testing creating an instance of ImportFeed functionality, so you should either pass params to new or create methods to test it, but you pass an instance of ImportFeed to it (from FactoryGirl).
According to the docs, ActiveRecord#new accepts Hash only (the default argument is {} if you don't pass anything).
If you pass an object to it, you'll get ArgumentError exception along with "When assigning attributes, you must pass a hash as an argument" message
def assign_attributes(new_attributes)
if !new_attributes.respond_to?(:stringify_keys)
raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
end
return if new_attributes.empty?
attributes = new_attributes.stringify_keys
_assign_attributes(sanitize_for_mass_assignment(attributes))
end
I think you are just using the initialize method to change the values according to the conditions:
def initialize(params)
super(params)
self.enabled = false if self.enabled.blank?
self.default_radius = DEFAULT_RADIUS if self.default_radius.blank?
self.default_days = DAYS_DEFAULT if self.default_days.blank?
end
You should not override it (my suggestion) as it may break many things. So instead you may change the values on a callback (before_validation, before_save, before_create, after_initialize whichever suits you) like this:
before_create :set_default_radius, if: proc { |feed| feed.default_radius.blank? }
def set_default_radius
self.default_radius = DEFAULT_RADIUS
end
And the best way to do this is having the default value in database itself. You can define that in migration:
def up
change_column :import_feeds, :default_radius, :integer, default: 0
end
def down
change_column :import_feeds, :default_radius, :integer, default: nil
end
So if the value is not defined it will always set to the default value mentioned in the migration file.
Also you may have a read of several question related to this which has some very good answers and explanation:
How to override "new" method for a rails model
Why is overriding ActiveRecord::Base.initialize wrong?
Overriding ApplicationRecord initialize, bad idea?
I have some variables in a instance variable (for other methods can access the variable) which type is hash.
if I don't want all members in hash #iw2 applied attr_accessor
only #iw2[:dir] can be modified by others.
#iw2 ={}
#iw2[:dir] = "#{Rails.root}/#{ENV["module_handy_network_tools_src_path"]}"
#iw2[:prog_path] ="#{#iw2[:dir]}/#{ENV["module_handy_network_tools_prog_path"]}"
So I wrote that way,
attr_accessor :iw2[:dir]
But I got the error
TypeError (can't convert Symbol into Integer):
app/helpers/handy_network_tools_helper.rb:8:in `[]'
How to fix the problem, thanks in advance.
[2] pry(#<HandyNetworkToolsController>)> #iw2.class
=> Hash
Edit
When you find yourself having many methods with the same prefix (iw2 in this case), it is a sign that there's a hidden object in there. How about this? Better?
class Iw2
def initialize(hash)
#dir = hash[:dir]
#prog_path = hash[:prog_path]
end
attr_accessor :dir, :prog_path
end
class MyClass
def initialize
#iw2 = Iw2.new(:dir => "a rails path",
:prog_path => "some another rails path")
end
delegate :dir, :prog_path, :to => :#iw2
end
mc = MyClass.new
mc.dir # => "a rails path"
mc.prog_path # => "some another rails path"
Original answer
Well, attr_accessor doesn't work like that. You can always use old-fashioned getters/setters.
def iw2_dir
#iw2[:dir]
end
def iw2_dir=(dir)
#iw2[:dir] = dir
end
You can then implement your own attr_sub_accessor that will generate such methods for you.
attr_sub_accessor :iw2, :dir
attr_sub_accessor :iw2, :prog_path
(I think explicit getters/setters are better in this case)
I am new to ruby and programming in general and am trying to grasp a few key concepts.
Given I have a class Dog, with the below characteristics.
class Dog
attr_accessor :type, :popularity, :total
def initialize(type = nil)
#type = type
end
def total_dogs
Dog.count
end
def total
Dog.where(:type => self.type).size
end
def popularity
total.to_f/total_dogs
end
end
What I am trying to understand, is how ruby persists attributes to an instance via getter/setter methods. Its clear to me that if I instantiate a new instance and then save attributes to that instance, those attributes are tied to that instance because if I look at the object the attributes appear as such:
#dog = Dog.new
#dog
=> #<Dog:0x007fa8689ea238 #type=nil>
Its easy for me to understand that as I pass the #dog object around its always going to have the #type attribute as nil.
However, the situation I am having trouble understanding is if I pass this #dog object to another class. Like if I did:
Owner.new(#dog)
When I am in the owner class and I call #dog.popularity how does it know the value of popularity for that instance? At runtime are all methods processed and then that instance just always is tied to the value at the time? Apologies if this makes no sense or I am way off.
When you do
#dog = Dog.new
You do two spearate things
1) Create an instance variable #dog for whatever object your code is currently inside
2) Instantiate a new instance of Dog (with all its methods and attributes) and assign a reference to it to #dog
#dog is a variable, that just happens to point at the Dog instance ("instance of class" generally same meaning as "object") you created at that point. You can set other variables to point to the same instance, and in Ruby this is generally how you pass data around. Objects contain instance variables, and those instance variables point to yet more objects.
Using the assignment operator (i.e "=") you can point a variable at any other object.
To answer your questions in turn:
When I am in the owner class and I call #dog.popularity how does it
know the value of popularity for that instance?
You have to be careful in Ruby (and OO languages in general) to differentiate between class and object in your descriptions and questions. Ruby I'm assuming you are referring to a line of code in the Owner class, and that you intend it to work with an owner object. I'd also assume that #dog is an attribute you have added to Owner.
In which case, Ruby knows because #dog points to the Dog object that you added to owner. Each Dog object has its own copy of all of Dog's instance variables. You do need to take care in Ruby though, because variables point to objects, that you aren't simply passing in the same Dog object to all the owners (i.e. they all effectively share a single dog). So you need to understand when you are creating new instances (via new) and when you are simply handling existing references.
At runtime are all methods processed and then that instance just
always is tied to the value at the time?
No. At runtime, basic Ruby will only perform the assignments that you have coded. Instance variables may not even exist until the code that assigns them has been run. If you use attr_reader etc methods, then the variables will at least exist (but will be nil unless you assign something during initialize)
Niel has a great answer for this, I just want to add something to it.
Counting Dogs :)
You need a class variable to do this..
class Dog
##count = 0 # this is a class variable; all objects created by this class share it
def initialize
##count += 1 # when we create a new Dog, we increment the count
end
def total
##count
end
end
There is another way to do this with "instance variables of the Class object" but that's a bit of an advanced topic.
Accessing Instance Variables
In Ruby, variables are really just references to objects / instances.
> x = 1
=> 1
> x.class
=> Fixnum
> 1.instance_variables
=> []
x is a reference to the object '1', which is an instance of class Fixnum.
The '1' object is an instance of Fixnum which does not contain any instance variables.
It is not different in any way from a reference to a new "Dog" instance.
Similarly, you can say x = Dog.new , then x is a reference to an instance of class Dog.
class Dog
attr_accessor :legs # this defines the 'legs' and 'legs=' methods!
end
x = Dog.new
x.instance_variables
=> [] # if you would assign legs=4 during "initialize", then it would show up here
x.legs = 4 # this is really a method call(!) to the 'legs' method
x.instance_variables # get created when they are first assigned a value
=> [:legs]
It does not matter if you pass such a reference to a method call, or to another class or just evaluate it by itself - Ruby knows it's an object reference, and looks inside the object and it's inheritance chain on how to resolve things.
Resolving Method Names
That was only the partial truth :) When interpreting x.legs , Ruby checks if there is a method in the class-inheritance chain of the object, which responds to that name 'legs'.
It is not magically accessing the instance variable with the same name!
We can define a method 'legs' by doing "attr_reader :legs" or "attr_accessor :legs", or by defining the method ourselves.
class Dog
def legs
4 # most dogs have 4 legs, we don't need a variable for that
end
end
x.legs # this is a method call! it is not directly accessing a :legs instance variable!
=> 4
x.instance_variables
=> [] # there is no instance variable with name ":legs"
and if we try to implement it as a method and an instance variable, this happens: :)
class Dog
attr_accessor :legs # this creates "def legs" and "def legs=" methods behind the scenes
def legs # here we explicitly override the "def legs" method from the line above.
4
end
end
x = Dog.new
x.legs # that's the method call we implemented explicitly
=> 4
x.legs = 3 # we can still assign something to the instance_variable via legs=
=> 3
x.legs # the last definition of a method overrides previous definitions
# e.g. it overrides the automatically generated "legs" method
=> 4
attr_accessor :legs is just a short hand notation for doing this:
class Dog
def legs
#legs
end
def legs=(value)
#legs = value
end
end
there is no magic way instance variable get automatically accessed. They are always accessed through a method, which can be overridden later.
I hope that makes sense to you
When you create an object, you don't need to use the # symbol. The variable is the object. So if you have multiple dogs, you'd do:
myDog = Dog.new(brown)
yourDog = Dog.new(white)
From there, you can say:
yourDog.type #white
myDog.type #brown
What you would NOT do is:
#dog = Dog.new #myDog
#dog = Dog.new #yourDog
If you need multiple versions of an object, you just give them different names. So if you create multiple dogs and pass them to other objects, they will work. For example:
Say your owner class is:
Class Owner
def initialize(pet)
puts "my pet is #{pet.type}"
end
Then using the instance variable would be:
me = Owner.new(myDog) #my pet is brown
you = Owner.new(yourDog) #my pet is white
Both "type" and "popularity" are methods on "dog" instances. Their definition is as follows:
class Dog
# getter
def type
#type
end
def popularity
total.to_f/total_dogs
end
end
This is roughly equivalent to:
class Dog
attr_accessor :type
def popularity
total.to_f/total_dogs
end
end
Note that attr_accessor is just a shortcut for defining a getter method. If you define a method yourself it is pointless to use attr_accessor:
class Dog
attr_accessor :popularity
# this will override getter defined by attr_accessor
def popularity
total.to_f/total_dogs
end
end
Back to your question: #dog.type invokes type method on #dog which returns its instance variable; #dog.popularity invokes popularity method on #dog which does calculations (as defined by you) on the fly and returns the result. No magic here!
This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
attr_accessor default values
I am using Ruby on Rails 3.0.9 and I would like to initialize some attr_accessor attribute values in my class\model that inherits from ActiveRecord::Base. That is,
... in my module I have:
class User < ActiveRecord::Base
attr_accessor :attribute_name1,
:attribute_name2,
:attribute_name3,
...
end
and I would like to set to true all attr_accessor attribute values. How can I do that?
P.S.: Of course I would like to solve the above issue approaching "à la Ruby on Rails Way". I know about the after_initialize callback but by using that method I should repeat each attribute_name<N> statement for which I would like to set the value to true inside that after_initialize statement (... and this is not DRY - Don't Repeat Yourself). Maybe there is a better way to achieve this. Is there a way to set attr_accessor attribute values "on the fly" when you state those attributes? That is, I expect to declare and set attr_accessor attributes at once!
Did you try:
class User < ActiveRecord::Base
attr_accessor :attribute_name1,
:attribute_name2,
:attribute_name3,
...
after_initialize :set_attr
def set_attr
#attribute_name1 = true
...
end
end
For Rails 3.2 or earlier, you could use attr_accessor_with_default:
class User < ActiveRecord::Base
attr_accessor_with_default :attribute_name1, true
attr_accessor_with_default :attribute_name2, true
attr_accessor_with_default :attribute_name3, true
...
end
Since your default value is an immutable type (boolean), this form of the method is safe to use here. But don't use it if the default value is a mutable object, including an array or string, because all of your new model objects will share the exact same instance, which is probably not what you want.
Instead, attr_accessor_with_default will accept a block, where you can return a new instance each time:
attr_accessor_with_default(:attribute_name) { FizzBuzz.new }
I would just define a getter that lazily loads the value you are interested in, and use attr_writer to define the setter. For instance,
class Cat
attr_writer :amount_of_feet
def amount_of_feet; #amount_of_feet ||= 4; end # usually true
end
Which, if you really mean it, can be rewritten with some meta-programming:
class Cat
# The below method could be defined in Module directly
def self.defaulted_attributes(attributes)
attributes.each do |attr, default|
attr_writer attr
define_method(attr) do
instance_variable_get("##{attr}") ||
instance_variable_set("##{attr}", default)
end
end
end
defaulted_attributes :amount_of_feet => 4
end
calin = Cat.new
print "calin had #{calin.amount_of_feet} feet... "
calin.amount_of_feet -= 1
puts "but I cut one of them, he now has #{calin.amount_of_feet}"
This works because, usually, computing the default value won't have any side effect making the order matter and it won't be needed to compute the value until you first try to access it.
(Câlin is my cat; he's doing well, and still has the four of his feet)
Brutal solution
class User < ActiveRecord::Base
##attr_accessible = [:attribute_name1, :attribute_name2, :attribute_name3]
attr_accessor *##attr_accessible
after_initialize :set_them_all
def set_them_all
##attr_accessible.each do |a|
instance_variable_set "##{a}", true
end
end
end
or little more conceptual: Ruby: attr_accessor generated methods - how to iterate them (in to_s - custom format)?
While creating new records. I need to create more records for the same model.
Example ::
class XYZ < ActiveRecord
def before_save
# At this point object is already initialized ..
# And it's containing values.
# At this point i want to create 10 more records for the same class.
# something like this
XYZ.new(:att1 => value1,:att2 => value2,:att3 => self.att1)
end
end
How may i handle this type of scenario ?
On which call back i have to create more records for the same model ?
First, this sounds like bad engineering, try to rethink your model in a way that makes what you need.
maybe if you need to create 10 models of something, do not use the activerecord hooks, otherwise you might incur in infine loops.
I would recommend
class XYZ < ActiveRecord
def self.create10(original_xyz)
10.times do
clone = original_xyz.clone
clone.save
end
end
end
and where in your controller or wherever you have the need to create 10 more, call:
new_xyz = XYZ.new(:att1 => value1,:att2 => value2,:att3 => self.att1)
new_xyz.save
XYZ.create10(new_xyz)
but if you really need to create 10 more on a hook (like before save), do:
class XYZ < ActiveRecord
before_save create10
attr_acessor :cloned
def create10
return if cloned # this will prevent infinit loooooooooooooooop
10.times do
clone = self.clone
clone.cloned = true
clone.save
end
end
end
I did not run this, so, try it first.
class XYZ < ActiveRecord
def after_initialize
# At this point object is already initialized ..
# And it's containing values.
# At this point i want to create 10 moew records for the same class.
# something like this
#XYZ.new(:att1 => value1,:att2 => value2,:att3 => self.att1)
x = 10 #an integer
x.times do |task|
Model.create(:key => :value)
end
end
end