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
Related
First off, i'm using Rails 3.2.1 and ruby 1.9.3p392
I have two models, ad_user and device
An ad_user has many devices, and a device belongs to an ad_user.
My models are as follow:
class AdUser < ActiveRecord::Base
has_many :devices
class Device < ActiveRecord::Base
belongs_to :device_type
belongs_to :device_status
belongs_to :ad_user
validates_presence_of :name
validates_uniqueness_of :name
validates_presence_of :serial
validates_uniqueness_of :serial
validates_presence_of :device_type_id
validates_presence_of :device_status_id
validates_presence_of :ad_user_id
before_update :before_update_call
before_save :before_save_call
before_create :before_create_call
before_validation :before_validation_call
protected
def before_update_call
p self.name
p self.ad_user_id
p "-=-=-=-=-=-=-=-=-"
p "before_update_call"
self.ad_user_id = 1 if self.ad_user_id.nil? || self.ad_user_id.blank?
end
def before_save_call
p self.name
p self.ad_user_id
p "-=-=-=-=-=-=-=-=-"
p "before_save_call"
self.ad_user_id = 1 if self.ad_user_id.nil? || self.ad_user_id.blank?
end
def before_create_call
p self.name
p self.ad_user_id
p "-=-=-=-=-=-=-=-=-"
p "before_create_call"
self.ad_user_id = 1 if self.ad_user_id.nil? || self.ad_user_id.blank?
end
def before_validation_call
p self.name
p self.ad_user_id
p "-=-=-=-=-=-=-=-=-"
p "before_validation_call"
self.ad_user_id = 1 if self.ad_user_id.nil? || self.ad_user_id.blank?
end
When i assign devices to a user using
u = AdUser.first
u.device_ids=[1,2]
I can see the before_validation_call, before_save_call and before_update_call printing to the console, however when I unassign these devices from the user with:
u.device_ids=[]
It results in a simple:
SQL (2.0ms) UPDATE "devices" SET "ad_user_id" = NULL WHERE "devices"."ad_user_id" = 405 AND "devices"."id" IN (332, 333)
None of the callbacks are called and my devices end up with having a nil ad_user_id despite the fact that the model should validate the presence. I planned to use the callbacks to check that ad_user_id is not nil before saving or updating but they are not even called.
Am I doing anything wrong here ?
Unfortunately, I think this is an expected behavior. You shouldn't rely on callbacks being fired when you update associations, so underlying model is being updated implicitly.
What you can do though, try to access devices field instead. device_ids performs differently, on a lower level.
Also consider using association-callbacks, if it fits.
P.S. just a small note: in Rails you can use self.ad_user.present? instead of self.ad_user_id.nil? || self.ad_user_id.blank?. Also, you can merge validates_presence_of statements.
Allright then, in my update method of the ad_user controller, before the update_attributes I added:
old_device_ids = #ad_user.device_ids
the after the update_attributes I added:
(old_device_ids - AdUser.find(params[:id]).device_ids).each do |device|
Device.find(device).update_attributes(:ad_user_id => 1)
end
I had already done that earlier, but I wanted to find a proper "Rails way" of doing it.
I have a model
class Transaction < ActiveRecord::Base
end
I have a transaction_type column which is an integer.
How can I create an enumeration that I could map values to names like:
one_time = 1
monthly = 2
annually = 3
So in the db column, the values would be 1, 2 or 3.
Also, whenever I create a new instance, or save a model and the field wasn't set like:
#transaction = Transaction.new(params)
It should default to 1 (on_time).
I'm not sure how I can do this?
basically the same answer as Amit, slight variation
class TransactionType
TYPES = {
:one_time => 1,
:monthly => 2,
:annually => 3
}
# use to bind to select helpers in UI as needed
def self.options
TYPES.map { |item| [item[0], item[1].to_s.titleize] }
end
def self.default
TYPES[:one_time]
end
end
one way to control the default value
class Transaction < ActiveRecord::Base
before_create :set_default_for_type
def set_default_for_type
type = TransactionType.default unless type.present?
end
end
but - best way is to just apply the defaults on your database column and let ActiveRecord get it from there automatically
NOTE: it might also make sense to just have a TransactionType ActiveRecord object instead of above, depends on your situation, i.e.
# on Transaction with type_id:integer
belongs_to :type, class_name: "TransactionType"
You can map the values by creating a constant either in the same Transaction model or by creating a new module and place it inside that as explained by #KepaniHaole
In Transaction model, you can do it like :
class Transaction < ActiveRecord::Base
TRANSACTION_TYPES = { 'one_time' => 1, 'monthly' => 2, 'monthly' => 3 }
end
You can access these values by accessing the constant as
Transaction::TRANSACTION_TYPES['one_time'] # => 1
Transaction::TRANSACTION_TYPES['monthly'] # => 2
Transaction::TRANSACTION_TYPES['monthly'] # => 3
To add a default value to transaction_type column just create a new migration with :
def up
change_column :transactions, :transaction_type, :default => Transaction::TRANSACTION_TYPES['one_time']
end
With this, every time you create a Transaction object without passing transaction_type, the default value 1 with be stored in it.
Maybe you could try something like this? Ruby doesn't really support c-style enums..
module TransactionType
ONCE = 1
MONTHLY = 2
ANUALLY = 3
end
then you could access their values like so:
#transaction = Transaction.new(TransactionType::ONCE)
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.
** update **
it all seems to be related to a custom validator: if I remove it, it works as expected. see code at the end
**
I have a model budget that has many multi_year_impacts
in the console, if I run:
b = Budget.find(4)
b.multi_year_impacts.size #=> 2
b.update_attributes({multi_year_impacts_attributes: {id: 20, _destroy: true} } ) #=> true
b.multi_year_impacts.size #=> 1 (so far so good)
b.reload
b.multi_year_impacts.size #=> 2 What???
and if before b.reload I do b.save (which shouldn't be needed anyway), it's the same.
Any idea why my child record doesn't get destroyed?
Some additional information, just in case:
Rails 3.2.12
in budget.rb
attr_accessible :multi_year_impacts_attributes
has_many :multi_year_impacts, as: :impactable, :dependent => :destroy
accepts_nested_attributes_for :multi_year_impacts, :allow_destroy => true
validates_with MultiYearImpactValidator # problem seems to com from here
in multi_year_impact.rb
belongs_to :impactable, polymorphic: true
in multi_year_impact_validator.rb
class MultiYearImpactValidator < ActiveModel::Validator
def validate(record)
return false unless record.amount_before && record.amount_after && record.savings
lines = record.multi_year_impacts.delete_if{|x| x.marked_for_destruction?}
%w[amount_before amount_after savings].each do |val|
if lines.inject(0){|s,e| s + e.send(val).to_f} != record.send(val)
record.errors.add(val.to_sym, " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts")
end
end
end
end
it might depend on your rails version, however, comparing your code to the current docs:
Now, when you add the _destroy key to the attributes hash, with a
value that evaluates to true, you will destroy the associated model:
member.avatar_attributes = { :id => '2', :_destroy => '1' }
member.avatar.marked_for_destruction? # => true
member.save
member.reload.avatar # => nil
Note that the model will not be destroyed until the parent is saved.
you could try with:
b.multi_year_impacts_attributes = {id: 20, _destroy: true}
b.save
So it looks like the culprit was here
if lines.inject(0){|s,e| s + e.send(val).to_f} != record.send(val)
record.errors.add(val.to_sym, " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts")
end
changing this to the slightly more complex
total = 0
lines.each do |l|
total += l.send(val).to_f unless l.marked_for_destruction?
end
if total != record.send(val)
record.errors[:amount_before] << " please check \"Repartition per year\" below: the sum of all lines must be equal of total amounts"
end
solved the problem.
I'm learning new tricks all the time and I'm always on the lookout for better ideas.
I have this rather ugly method. How would you clean it up?
def self.likesit(user_id, params)
game_id = params[:game_id]
videolink_id = params[:videolink_id]
like_type = params[:like_type]
return false if like_type.nil?
if like_type == "videolink"
liked = Like.where(:user_id => user_id, :likeable_id => videolink_id, :likeable_type => "Videolink").first unless videolink_id.nil?
elsif like_type == "game"
liked = Like.where(:user_id => user_id, :likeable_id => game_id, :likeable_type => "Game").first unless game_id.nil?
end
if liked.present?
liked.amount = 1
liked.save
return true
else # not voted on before...create Like record
if like_type == "videolink"
Like.create(:user_id => user_id, :likeable_id => videolink_id, :likeable_type => "Videolink", :amount => 1)
elsif like_type == "game"
Like.create(:user_id => user_id, :likeable_id => game_id, :likeable_type => "Game", :amount => 1)
end
return true
end
return false
end
I would do something like:
class User < ActiveRecord::Base
has_many :likes, :dependent => :destroy
def likes_the(obj)
like = likes.find_or_initialize_by_likeable_type_and_likeable_id(obj.class.name, obj.id)
like.amount += 1
like.save
end
end
User.first.likes_the(VideoLink.first)
First, I think its wrong to deal with the "params" hash on the model level. To me its a red flag when you pass the entire params hash to a model. Thats in the scope of your controllers, your models should have no knowledge of the structure of your params hash, imo.
Second, I think its always cleaner to use objects when possible instead of class methods. What you are doing deals with an object, no reason to perform this on the class level. And finding the objects should be trivial in your controllers. After all this is the purpose of the controllers. To glue everything together.
Finally, eliminate all of the "return false" and "return true" madness. The save method takes care of that. The last "return false" in your method will never be called, because the if else clause above prevents it. In my opinion you should rarely be calling "return" in ruby, since ruby always returns the last evaluated line. In only use return if its at the very top of the method to handle an exception.
Hope this helps.
I'm not sure what the rest of your code looks like but you might consider this as a replacement:
def self.likesit(user_id, params)
return false unless params[:like_type]
query = {:user_id => user_id,
:likeable_id => eval("params[:#{params[:like_type]}_id]"),
:likeable_type => params[:like_type].capitalize}
if (liked = Like.where(query).first).present?
liked.amount = 1
liked.save
else # not voted on before...create Like record
Like.create(query.merge({:amount => 1}))
end
end
I assume liked.save and Like.create return true if they are succesful, otherwise nil is returned. And what about the unless game_id.nil? ? Do you really need that? If it's nil, it's nil and saved as nil. But you might as well check in your data model for nil's. (validations or something)