How do you persist a derived attribute which depends on the value of id in rails? The snippet below seems to work-- Is there a better rails way?
class Model < ActiveRecord::Base
....
def save
super
#derived_attr column exists in DB
self.derived_attr = compute_attr(self.id)
super
end
end
Callbacks are provided so you should never have to override save. The before_save call in the following code is functionally equivalent to all the code in the question.
I've made set_virtual_attr public so that it can be calculated as needed.
class Model < ActiveRecord::Base
...
# this one line is functionally equivalent to the code in the OP.
before_save :set_virtual_attr
attr_reader :virtual_attr
def set_virtual_attr
self.virtual_attr = compute_attr(self.id)
end
private
def compute_attr
...
end
end
I think the more accepted way to do this would be to provide a custom setter for the virtual attribute and then provide an after_create hook to set the value after the record is created.
The following code should do what you want.
class Virt < ActiveRecord::Base
def after_create()
self.virtual_attr = nil # Set it to anything just to invoke the setter
save # Saving will not invoke this callback again as the record exists
# Do NOT try this in after_save or you will get a Stack Overflow
end
def virtual_attr=(value)
write_attribute(:virtual_attr, "ID: #{self.id} #{value}")
end
end
Running this in the console shows the following
v=Virt.new
=> #<Virt id: nil, virtual_attr: nil, created_at: nil, updated_at: nil>
>> v.save
=> true
>> v
=> #<Virt id: 8, virtual_attr: "ID: 8 ", created_at: "2009-12-23 09:25:17",
updated_at: "2009-12-23 09:25:17">
>> Virt.last
=> #<Virt id: 8, virtual_attr: "ID: 8 ", created_at: "2009-12-23 09:25:17",
updated_at: "2009-12-23 09:25:17">
Related
Let's say you had a user with an age attribute that could not be negative
class User < ActiveRecord::Base
validates :age, numericality: { greater_than: 0 }
end
If you attempt to update attributes to a negative number validation will fail but the instance will still have a negative age value
#<User id: 1, age: 5, created_at: "2014-11-08 20:14:12", updated_at: "2014-11-08 20:14:12">
user.update_attributes!(:age => -5)
#<User id: 1, age: -5, created_at: "2014-11-08 20:14:12", updated_at: "2014-11-08 20:14:12">
Other than catching ActiveRecord::RecordInvalid and reseting the value yourself is their a way to reset an instance if its validations fail?
thanks!
You can call model.reload if the validation fails. So it will look something like:
if #model.update_attributes(age: params[:age]) # params[:age] = -5 for example
# model is valid and saved, continue...
else # update_attributes return false and will not raise an exception if model is invalid
# model is invalid, reloading...
#model.reload
# if we call #model.age now, it will return previous value
end
Anyway, update_attributes will set attributes even model is becoming invalid after that update, although it will not persist invalid attributes to database. But remember it will reset all other changes which may have been performed inside this call, so update_attributes(name: params[:name], age: params[age]) will reset both name and age even name is valid.
I would say you need a custom validator, e.g:
class MyValidator < ActiveModel::Validator
def validate(record)
unless record.age.to_i > 0
record.errors[:name] << 'Invalid!'
record.age = record.age_was # Rewrite new with old value
end
end
end
class Person
include ActiveModel::Validations
validates_with MyValidator
end
With ActiveModel::Dirty there's no need to reload.
I have a standard model with a few fields that are saved to a DB, and I need 1 field that doesn't have to be saved.
I tried attr_accessor but that doesn't cover it. Using Attr_accessor I can set and get the field, but it is not part of the model. If I add the models to an array and then see what is in the virtual field is not part of it. I also tried to add the field :headerfield to attr_accessible but that didn't change anything.
How can I get a field that is part of the model but not saved to the database?
The model
class Mapping < ActiveRecord::Base
attr_accessible :internalfield, :sourcefield
attr_accessor :headerfield
end
console output:
1.9.3-p194 :001 > m = Mapping.new
=> #<Mapping id: nil, internalfield: nil, sourcefield: nil, created_at: nil, updated_at: nil, data_set_id: nil>
1.9.3-p194 :002 > m.headerfield = "asef"
=> "asef"
1.9.3-p194 :003 > m
=> #<Mapping id: nil, internalfield: nil, sourcefield: nil, created_at: nil, updated_at: nil, data_set_id: nil>
Because ActiveRecord::Base has custom implementations for the standard serializiation methods (including to_s and as_json), you will never see your model attributes that do not have backing database columns unless you intervene in some way.
You can render it to JSON using the following:
render json: my_object, methods: [:virtual_attr1, :virtual_attr2]
Or you can use the as_json serializer directly:
my_object.as_json(methods: [:virtual_attr1, :virtual_attr2])
The return you see in the console is nothing else but the value of to_s. For this case, code should be better than natural language, take a look in the following code and see if you understand
class A
end
=> nil
A.new
=> #<A:0xb73d1528>
A.new.to_s
=> "#<A:0xb73d1528>"
class A
def to_s
"foobar"
end
end
=> nil
A.new
=> ble
A.new.to_s
=> "ble"
You can see this output because ActiveRecord::Base defines a method to_s that take into account only the attributes that are defined in the database, not the attr_accessor methods, maybe using the attributes call.
I am defining #foo as a class instance attribute, and using the after_initialize callback to set the value of this when a record is created/loaded:
class Blog < ActiveRecord::Base
#foo = nil
after_initialize :assign_value
def assign_value
#foo = 'bar'
end
end
However, when I inspect a Blog object, I am not seeing the #foo attribute:
> Blog.first.inspect
=> "#<Blog id: 1, title: 'Test', created_at: nil, updated_at: nil>"
What do I need to do to get inspect to include this? Or conversely, how does inspect determine what to output?
Thanks.
Active record determines which attributes to show in inspect based on the columns in the database table:
def inspect
attributes_as_nice_string = self.class.column_names.collect { |name|
if has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
end
}.compact.join(", ")
"#<#{self.class} #{attributes_as_nice_string}>"
end
Lifted from base.rb on github
To change the output of inspect you'll have to overwrite it with your own method e.g.
def inspect
"#{super}, #foo = #{#foo}"
end
Which should output:
> Blog.first.inspect
=> "#<Blog id: 1, title: 'Test', created_at: nil, updated_at: nil>, #foo = 'bar'"
I would like to create a virtual attribute that will always be included when you do model_instance.inspect. I understand that attr_reader will give me the same thing as just defining an instance method, but I would like this attribute to be part of the object's "make up"
How can I accomplish this?
Thanks!
UPDATE
Here is what is not working in more detail:
class Product < ActiveRecord::Base
attr_reader :less_secure_asset_url
def less_secure_asset_url
self.asset.url
end
end
>> p = Product.find(:all)[1]
=> #<Product id: 49, asset_file_name: "Etrade_Trade_Conf_Request.docx", asset_content_type: "application/vnd.openxmlformats-officedocument.wordp...", asset_file_size: 38152, asset_updated_at: "2010-05-04 17:45:46", created_at: "2010-05-04 17:45:46", updated_at: "2010-05-04 17:45:46", owner_id: 345, product_type_id: 1>
As you can see, when I use the console it returns no "less_secure_asset_url" attribute
Your attribute is in there, even if it doesn't show up. Rails overrides the definition of the inspect method of Object in ActiveRecord::Base to something like:
def inspect
attributes_as_nice_string = self.class.column_names.collect { |name|
if has_attribute?(name) || new_record?
"#{name}: #{attribute_for_inspect(name)}"
end
}.compact.join(", ")
"#<#{self.class} #{attributes_as_nice_string}>"
end
Basically, if it's not a column_name, it's not going to show up. I haven't tried this, but you might be able to call something like p.as(Object).inspect to get to the super classes inspect method. You have to require 'facets' to get as. See docs here.
I'm trying to setup single table inheritance in my Rails app for a User model and its subclasses Member, Subscriber, and Staff.
I have a model file for each: user.rb, member.rb, etc
The user model is defined: class User < ActiveRecord::Base; end;
I subclassed the other models as such: class Member < User; end; and so on.
In my users table I have all the fields every class needs plus the type field. Now when I go to the console and try to create a new instance of say member or subscriber i get the following error:
TypeError: can't dup NilClass
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2184:in 'dup'
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2184:in 'scoped_methods'
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2188:in 'current_scoped_methods'
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2171:in 'scoped?'
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2439:in 'send'
from /Library/Ruby/Gems/1.8/gems/activerecord-2.3.4/lib/active_record/base.rb:2439:in 'initialize'
from (irb):6:in 'new'
from (irb):6
Rails know the subclasses models are there because in the console when I simply call Member or Subscriber, i get the class definition returned.
I've read the simple documentation, but I must be missing something?
I tried on my side starting from a scratch application and it works
Here is my User model (User.rb)
class User < ActiveRecord::Base
end
My member model (Member.rb)
class Member < User
end
I have one migration file to create my users table which contains:
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :name
t.timestamps
end
end
def self.down
drop_table :users
end
end
Now launching the console:
➜ ./script/console
Loading development environment (Rails 2.3.4)
>> u = User.new
=> #<User id: nil, name: nil, created_at: nil, updated_at: nil>
>> m = Member.new
=> #<Member id: nil, name: nil, created_at: nil, updated_at: nil>
>> m.name="hop"
=> "hop"
>> m.save
=> true
However I did not manage to reproduce your error :(
Do you have a type column of type varchar (string in ruby)? Try the following commands (in a new rails project)
class Member < User
end
C:\projects\test\sti>ruby script\generate model user name:string type:string membertype:string
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/user.rb
create test/unit/user_test.rb
create test/fixtures/users.yml
create db/migrate
create db/migrate/20091019051506_create_users.rb
C:\projects\test\sti>rake db:migrate
(in C:/projects/test/sti)
== CreateUsers: migrating ====================================================
-- create_table(:users)
-> 0.0000s
== CreateUsers: migrated (0.0000s) ===========================================
C:\projects\test\sti>ruby script\console
Loading development environment (Rails 2.3.4)
>> u = User.new
=> #<User id: nil, name: nil, type: nil, membertype: nil, created_at: nil, updated_at: nil>
>> m = Member.new
=> #<Member id: nil, name: nil, type: "Member", membertype: nil, created_at: nil, updated_at: nil>
>> m.name = 'fred'
=> "fred"
>> m.save
=> true
>> u.name = 'rader'
=> "rader"
>> u.save
=> true
>> User.find :all
=> [#<Member id: 1, name: "fred", type: "Member", membertype: nil, created_at: "2009-10-19 05:17:11", updated_at: "2009-10-19 05:17:11">, #<User id: 2, name: "rader", type: nil, membertype: nil, created_at: "2009-10-19 05:17:24", updated_at: "2009-10-19 05:17:24">]
>>
Check this page, there are more than few solutions to this problem (even in comments).
http://strd6.com/2009/04/cant-dup-nilclass-maybe-try-unloadable/
I'm thinking that the problem is in one of your model definitions because of the stack trace you show. If you still are having a problem, pastie your code, and i'm sure you'll get a good answer.
I hade exactly this problem, after I extracted some functionality to a plugin.
But i my case it worked from the console, so i made sure id reloaded, with this line in init.rb
ActiveSupport::Dependencies.load_once_paths.delete(
File.expand_path(File.dirname(__FILE__))+'/app/models')
I ran into something similar a while back and this website helped:
http://www.dansketcher.com/2009/05/11/cant-dup-nilclass/
class User < ActiveRecord::Base
unloadable
...
end
Not sure why this occurs as I could not track down anything abnormal. I do believe it was a STI situation though.