Rails has_many relation: a look at object_ids - ruby-on-rails

Something strange is happening in Rails 2.3.14. Any ideas why the following happens?
Example
Suppose we have classes like
class Article < ActiveRecord::Base
has_many :prices
end
class Price < ActiveRecord::Base
end
Then in a irb session the following happens.
>> a = Article.first
=> #<Article id: 980190962>
>> a.prices.first.object_id
=> 97498070
>> a.prices.first.object_id
=> 97470500
>> a.prices.first.object_id
=> 97451010
>> a.valid?
=> true
>> a.prices.first.object_id
=> 97374790
>> a.prices.first.object_id
=> 97374790
So at first the object_id changes each time the record is accessed (yes, it's always the same one). Later after a call to #valid? this behavior stops. Instead everything is fine. You get the same object on each call.
Why is this important?
Let's assume you add a validation to Price
class Price < ActiveRecord::Base
validates_presence_of :amount
end
Then you want to change a price of an article.
>> a = Article.first
=> #<Article id: 980190962>
>> p = a.prices.first
=> #<Price id: 280438907, amount: 1.0, article_id: 980190962>
>> p.amount = nil # oops, accidentally we assigned nil
=> nil
>> p.valid?
=> false
>> a.valid?
=> true
What's that? The price is invalid, but the article is not? This shouldn't have happend, because by default the :validate-option of has_many is set to true. This happens because of the changing object_ids

Every new context(request/response) will regenerate the object IDs. As you are running the query a.prices.first.object_id it will hit the query to get the first price every time and as each hit will generate new object IDs.
But in second case you have extracted first price into a instance variable and working on that so no new request/response is going to database hence it is giving same object IDS.

Related

Rails after_create created_at not set on created objects

I have on my Message model, an after_create which creates a new instance of a Notification like such.
after_create :send_notification
def send_notification
n = Notification.new :name => "#{self.sender.smart_name} sent you a message:", :user_id => self.receiver_id, :notification_type => 'message', :subject => self.subject
n.save
end
However, the objects that are created all have their created_at and updated_at set to nil.
#<Notification:0x0000000c486208
id: 123123,
user_id: 3423,
name: "I sent you a message:\n" + "10:27",
notification_type: "message",
created_at: nil,
updated_at: nil>
I've checked to see that the model.record_timestamps is set to true based on this answer.
I don't have anything set on active_record as suggested here.
I'm using Mysql on Rails 4.
You should call n.reload after n.save just to get the timestamps read after save

Rails ActiveRecord callback messup

This is driving nuts. I have a dead simple callback functions to initialize and validate a class children as such:
class A < ActiveRecord::Base
has_many :bs
after_initialize :add_t_instance
validate :has_only_one_t
protected
def add_t_instance
bs << B.new(:a => self, :type => "T") unless bs.map(&:type).count("T") > 0
end
def has_only_one_t
unless bs.map(&:type).count("T") < 2
errors.add(:bs, 'has too many Ts")
end
end
end
and now, here comes the magic at runtime:
a = A.new
>>[#<A>]
a.bs
>> [#<T>]
a.save
>> true
a.id
>> 15
so far it's all going great, but:
s = A.find(15)
s.bs
>>[#<T>,#<T>]
s.bs.count
>> 2
s.valid?
>> false
s.errors.full_messages
>> "Too many Ts"
What the heck am I missing here?!?! What in the world could be adding the second #T?
Confusingly (to me at least) after_initialize is called whenever an active record object is instantiated, not only after creating a new instance, but also after loading an existing one from the database. So you create the second B when you run A.find(15).
You could solve the problem by checking whether you are dealing with a new record in your callback, e.g.
def add_t_instance
if new_record?
bs << B.new(:a => self, :type => "T") unless bs.map(&:type).count("T") > 0
end
end
or you could place a condition on the before_initialize declaration itself, or perhaps try using a before_create callback.

In Rails, how do I limit which attributes can be updated, without preventing them from being created?

I have a situation where an attribute can be created through a JSON API. But once it is created, I want to prevent it from ever being updated.
This constraint causes my first solution, which is using attr_accessible, to be insufficient. Is there a nice way to handle this type of situation in rails, or do I have to perform a manual check in the update method?
You can use attr_readonly, this will allow the value to be set on creation, but ignored on update.
Example:
class User < ActiveRecord::Base
attr_accessible :name
attr_readonly :name
end
> User.create(name: "lorem")
> u = User.first
=> #<User id: 1, name: "lorem">
> u.name = "ipsum"
=> "ipsum"
> u.save
=> true
> User.first.name
=> "lorem"
There is not a nice way to do that as far as I know, you have to write a custom filter
before_update :prevent_attributes_update
def prevent_attribute_updates
%w(attr1, attr2).each do |a|
send("#{attr1}=", send("#{attr1}_was")) unless self.send("#{attr1}_was").blank?
end
end

invalid decimal becomes 0.0 in rails

I have the following rails model:
class Product < ActiveRecord::Base
end
class CreateProducts < ActiveRecord::Migration
def self.up
create_table :products do |t|
t.decimal :price
t.timestamps
end
end
def self.down
drop_table :products
end
end
But when I do the following in the rails console:
ruby-1.9.2-p180 :001 > product = Product.new
=> #<Product id: nil, price: nil, created_at: nil, updated_at: nil>
ruby-1.9.2-p180 :002 > product.price = 'a'
=> "a"
ruby-1.9.2-p180 :003 > product.save
=> true
ruby-1.9.2-p180 :004 > p product
#<Product id: 2, price: #<BigDecimal:39959f0,'0.0',9(9)>, created_at: "2011-05-18 02:48:10", updated_at: "2011-05-18 02:48:10">
=> #<Product id: 2, price: #<BigDecimal:3994ca8,'0.0',9(9)>, created_at: "2011-05-18 02:48:10", updated_at: "2011-05-18 02:48:10">
As you can see, I wrote 'a' and it saved 0.0 in the database. Why is that? This is particularly annoying because it bypasses my validations e.g.:
class Product < ActiveRecord::Base
validates :price, :format => /\d\.\d/
end
anything that is invalid gets cast to 0.0 if you call to_f on it
"a".to_f #=> 0.0
you would need to check it with validations in the model
validates_numericality_of :price # at least in rails 2 i think
i dont know what validating by format does, so i cant help you there, but try to validate that it is a number, RegExs are only checked against strings, so if the database is a number field it might be messing up
:format is for stuff like email addresses, logins, names, etc to check for illegeal characters and such
You need to re-look at what is your real issue is. It is a feature of Rails that a string is auto-magically converted into either the appropriate decimal value or into 0.0 otherwise.
What's happening
1) You can store anything into an ActiveRecord field. It is then converted into the appropriate type for database.
>> product.price = "a"
=> "a"
>> product.price
=> #<BigDecimal:b63f3188,'0.0',4(4)>
>> product.price.to_s
=> "0.0"
2) You should use the correct validation to make sure that only valid data is stored. Is there anything wrong with storing the value 0? If not, then you don't need a validation.
3) You don't have to validate that a number will be stored in the database. Since you declared the db field to be a decimal field, it will ONLY hold decimals (or null if you let the field have null values).
4) Your validation was a string-oriented validation. So the validation regexp changed the 0.0 BigDecimal into "0.0" and it passed your validation. Why do you think that your validation was bypassed?
5) Why, exactly, are you worried about other programmers storing strings into your price field?
Are you trying to avoid products being set to zero price by mistake? There are a couple of ways around that. You could check the value as it comes in (before it is converted to a decimal) to see if its format is right. See AR Section "Overwriting default accessors"
But I think that would be messy and error prone. You'd have to set the record's Error obj from a Setter, or use a flag. And simple class checking wouldn't work, remember that form data always comes in as a string.
Recommended Instead, make the user confirm that they meant to set the price to 0 for the product by using an additional AR-only field (a field that is not stored in the dbms).
Eg
attr_accessor :confirm_zero_price
# Validate that when the record is created, the price
# is either > 0 or (price is <= 0 && confirm_zero_price)
validates_numericality_of :price, :greater_than => 0,
:unless => Proc.new { |s| s.confirm_zero_price},
:on => :create
Notes The above is the sort of thing that is VERY important to include in your tests.
Also I've had similar situations in the past. As a result of my experiences, I now record, in the database, the name of the person who said that the value should indeed be $0 (or negative) and let them have a 255 char reason field for their justification. Saves a lot of time later on when people are wondering what was the reason.

Why are associated objects through a belongs_to not equal to themselves?

I have two classes with a has_many and belongs_to association:
class Employee < ActiveRecord::Base
has_many :contracts
end
class Contract < ActiveRecord::Base
belongs_to :employee
end
I expect that the employee returned by the #employee method of the Contract class would be equal to itself, which means that the following unit test would pass.
class EmployeeTest < ActiveSupport::TestCase
test "an object retrieved via a belongs_to association should be equal to itself" do
e = Employee.new
e.contracts << Contract.new
assert e.save
a = e.contracts[0].employee
assert a.equal? a
end
end
However, it fails. I do not understand. Is this a bug in ActiveRecord?
Thanks for helping out.
This has to do with object equality. consider this IRB session
irb(main):010:0> Foo = Class.new
=> Foo
irb(main):011:0> f = Foo.new
=> #<Foo:0x16c128>
irb(main):012:0> b = Foo.new
=> #<Foo:0x1866a8>
irb(main):013:0> f == b
=> false
By default, == will test that the two objects have the same type, and same object_id. In activerecord, it is hitting up the database for the first employee, and hitting it up again for the employee through the referencial method, but those are two different objects. Since the object_ids are different, it doesn't matter if they have all the same values, == will return false. To change this behavior, consider this second IRB session
irb(main):050:0> class Bar
irb(main):051:1> attr_accessor :id
irb(main):052:1> def ==(compare)
irb(main):053:2> compare.respond_to?(:id) && #id == compare.id
irb(main):054:2> end
irb(main):055:1> end
=> nil
irb(main):056:0> a = Bar.new
=> #<Bar:0x45c8b50>
irb(main):057:0> b = Bar.new
=> #<Bar:0x45c2430>
irb(main):058:0> a.id = 1
=> 1
irb(main):059:0> b.id = 1
=> 1
irb(main):060:0> a == b
=> true
irb(main):061:0> a.id = 2
=> 2
irb(main):062:0> a == b
=> false
Here I defined the == operator to compare the .id methods on the two objects (or to just return false if the object we are comparing doesn't have an id method). If you want to compare Employees by value like this, you will have to define your own == method to implement it.
That is probably because the rails internal cached differently in two association calls. try to do
a.reload.equal? a.reload
this will get rid of the caching and should return true

Resources