Related object not saved - ruby-on-rails

I have the models:
class Idea < ActiveRecord::Base
has_many :connections, :class_name => 'IdeaConnection', :foreign_key => 'idea_a_id', :dependent => :destroy
has_many :ideas, :through => :connections, :source => :idea_b, :dependent => :destroy
end
class IdeaConnection < ActiveRecord::Base
belongs_to :idea
belongs_to :idea_a, :class_name => 'Idea'
belongs_to :idea_b, :class_name => 'Idea'
belongs_to :relationship
end
class Relationship < ActiveRecord::Base
has_many :idea_connections
end
Idea, as you can see, own itself through Connections (join table). Each Connection entry belongs to Relationship. What I'm trying to do is to, after adding an Idea to another with:
Idea.find(1).ideas << Idea.find(2)
which is working and saving properly, get its connection on join table and update its relationship:
Ex:
Idea.find(1).connections.find_by_idea_b_id(Idea.f
ind(2).id).relationship = Relationship.find(1)
It processes correctly but it won't save.
Please, help, what am I missing?
ps: I don't want to do it by manually editting the relationship_id since it's ugly.
ps2: Before you answer, remember the fact that autosave:true do not work for belongs_to/has_many relationships.

You're working with connection object. Remember it.
Your problem is that you call find_by... method in has_many association. It returns one record BUT Idea model has no ruby attribute link to that. See here why. That's why Idea#save cannot call IdeaConnection#save (remember that for cascade saving connections realtion must have autosave: true if connection already exists).
So I suggest you two options:
Set :inverse_of on Idea#connections and IdeaConnection#idea_a relations and preload all records before mangling with Idea.find(1).connections.find_by_idea_b_id(Idea.f
ind(2).id).relationship = Relationship.find(1). But I don't recommend you to do so because:
As I said you're working with connection object. Just do so:
Idea.find(1).connections.create do |connection|
connection.idea_b = Idea.find(2)
connection.relationship = Relationship.first
end

This line of code won't save it
Idea.find(1).connections.first.relationship = Relationship.first
What you need to do is:
Method #1: ( add the connection to the has_many relation )
Relationship.first.idea_connections << Idea.find(1).connections.first
OR
Method #2: ( add relationship_id to the connection then manually save it)
connection = Idea.find(1).connections.first
connection.relationship_id = Relationship.first.id
connection.save

Assignments on the relational object level don't/won't automatically save; you have to tell them to. In ActiveRecord the push method (<<) has save built into it, which is why that was working for you. Setting a value (=) however does not have save built in, so you have to do it manually.
If you're interested, here's a link to another SO question where an answer talks about why push saves: Rails push into array saves object

Related

Rails one-to-one relationship

I have the following:
class User < ActiveRecord::Base
has_one :car, :class_name => 'Car', :foreign_key => 'user_id'
class Car < ActiveRecord::Base
belongs_to :worker, :class_name => 'User', :foreign_key => 'user_id'
It is basically a one-to-one relationship between a user and a car.
What I want is for the User to be able to have one and only one car. That implies the fact that if he creates a car assigned to him, he won't be able to create the second.
How could this be done?
There are certainly a couple different ways of accomplishing this. I would suggest creating a composite key index on that table to ensure that the user_id is unique in the table. This will ensure that it will only be present once. In a migration, you could write something like this.
add_index(:cars, :worker_id, :unique => true)
The first argument is the table name (don't forget this is generally the pluralized version of the class name). The field name comes second. The unique true is what will prevent you from inserting an extra row.
Note: This is a database level constraint. If you hit this because validations didn't catch it, it will throw an error.
In addition to this solution, you will want to add a validation to the Car model itself.
validates_uniqueness_of :worker_id, message: "can not have more than one car"
You'll see this error come through with something like "Worker ID can not have more than one car". You will most likely want to customize the "Worker ID" section of this. Refer to this post for instructions on how to do that.
You certainly don't have to do the db constraint, but in case anyone else inserts into the DB, it's a good idea. Otherwise, you'll have "invalid" data as far as Rails is concerned.
Change the definition of the relationship slightly:
class User < ActiveRecord::Base
has_one :car
class Car < ActiveRecord::Base
belongs_to :worker, :class_name => 'User', :foreign_key => 'user_id'
And you'll establish what you're looking for. See: http://guides.rubyonrails.org/association_basics.html#the-has-one-association
why don't you just test before the user tries to add a car?
if worker.car
raise "sorry buddy, no car for you"
else
car = Car.create(user_id: worker.id)
end

How to combine duplicate rails objects and update all references

I'm working on a Rails app (Ruby 1.9.2 / Rails 3.0.3) that keeps track of people and their memberships to different teams over time. I'm having trouble coming up with a scalable way to combine duplicate Person objects. By 'combine' I mean to delete all but one of the duplicate Person objects and update all references to point to the remaining copy of that Person. Here's some code:
Models:
Person.rb
class Person < ActiveRecord::Base
has_many :rostered_people, :dependent => :destroy
has_many :rosters, :through => :rostered_people
has_many :crews, :through => :rosters
def crew(year = Time.now.year)
all_rosters = RosteredPerson.find_all_by_person_id(id).collect {|t| t.roster_id}
r = Roster.find_by_id_and_year(all_rosters, year)
r and r.crew
end
end
Crew.rb
class Crew < ActiveRecord::Base
has_many :rosters
has_many :people, :through => :rosters
end
Roster.rb
class Roster < ActiveRecord::Base
has_many :rostered_people, :dependent => :destroy
has_many :people, :through => :rostered_people
belongs_to :crew
end
RosteredPerson.rb
class RosteredPerson < ActiveRecord::Base
belongs_to :roster
belongs_to :person
end
Person objects can be created with just a first and last name, but they have one truly unique field called iqcs_num (think of it like a social security number) which can be optionally stored on either the create or update actions.
So within the create and update actions, I would like to implement a check for duplicate Person objects, delete the duplicates, then update all of the crew and roster references to point to the remaining Person.
Would it be safe to use .update_all on each model? That seems kind of brute force, especially since I will probably add more models in the future that depend on Person and I don't want to have to remember to maintain the find_duplicate function.
Thanks for the help!
The 'scalable' way to deal with this is to make the de-duplication process part of the app's normal function - whenever you save a record, make sure it's not a duplicate. You can do this by adding a callback to the Person model. Perhaps something like this:
before_save :check_for_duplicate
def check_for_duplicate
if iqcs_num
dup = Person.find_by_iqcs_num(self.iqcs_num)
if dup && dup.id != self.id
# move associated objects to existing record
dup.crews = dup.crews + self.crews
# update existing record
dup.update_attributes(:name => self.name, :other_field => self.other_field)
# delete this record
self.destroy
# return false, so that no other callbacks get triggered
return false
end
end
end
You'll want to make sure that you index the table you store Person objects in on the iqcs_num column, so that this lookup stays efficient as the number of records grows - it's going to be performed every time you update a Person record, after all.
I don't know that you can get out of keeping the callback up-to-date - it's entirely likely that different sorts of associated objects will have to be moved differently. On the other hand, it only exists in one place, and it's the same place you'd be adding the associations anyway - in the model.
Finally, to make sure your code is working, you'll probably want to add a validation on the Person model that prevents duplicates from existing. Something like:
validates :iqcs_num, :uniqueness => true, :allow_nil => true

Best way to specify a default record for a has_many relationship in rails

I have Accounts and AccountAddressess. An account can have many AccountAddressess and I would like to specify one as the "default_account_address", so in the Account table, I have a column named "default_account_address_id". Here is the current state of the models.
class Account < ActiveRecord::Base
has_many :account_addresses
belongs_to :default_account_address,
:class_name => "AccountAddress",
:inverse_of => :account_assigned_to_as_default
end
class AccountAddress < ActiveRecord::Base
belongs_to :accounts
has_one :account_assigned_to_as_default,
:class_name => "Account",
:foreign_key => :default_account_address_id,
:inverse_of => :default_account_address
end
This works fine except for the fact that #account.default_account_address returns an account address and #account.account_addresses returns an empty array.
So, the issue is that the default account address is not included in #account.account_addresses.
Any ideas on the best way to approach this issue? I considered habtm, but it doesn't seem appropriate. I considered using has_one :default_account_address, but this doesn't make sense because the default_account_address_id column is on the account table. Thanks.
There is probably a better way, but here is something that came to mind:
class Account < ActiveRecord::Base
has_many :account_addresses
def default_address
account_addresses.find_by_default true
end
end
class AccountAddress < ActiveRecord::Base
belongs_to :accounts
end
This of course assumes you have a boolean column named default in AccountAddress. I would probably add validation to AccountAddress that would check that there is only 1 AccountAddress marked as default for a given account_id. You could also create a method in AccountAddress that not only marks an address as default, but also unmarks all associated addresses for you.
Like I said, there is probably something better out there, but this should allow the default address to also show in #account.account_addresses.
Another solution:
class Account < ActiveRecord::Base
has_many :account_addresses
has_one :default_account_address, -> { find_by_default true },
class_name: 'AccountAddress'
end
class AccountAddress < ActiveRecord::Base
belongs_to :accounts
end
I don't know if this is best practice, however, I would just add an attribute "mainaddress" to the table and use just has_many/belongs_to in the relations. Into the account model I would put a function that fetches the main address using a simple query where mainaddress is true.
Another idea is to use https://github.com/swanandp/acts_as_list and then treat default address as the top one, then, setting some address as default would be as simple as:
address.move_to_top if params[:set_as_default]
Especially if all you need is to show it the first in some list or combo box.
Querying default address is also easy, since it always first in the scope of "position".
This gem set_as_primary solves the same issue but in a different way.

How to destroy a record with has_many, :dependent => :destroy

I've built a Rail 3 AuditLog with the help of a few plugins, that store data in an AuditLog Table with the following fields for identification (feeded_id, feeded_type)
So in my case, I have a photoalbum that has_many photos.
class PhotoAlbum < ActiveRecord::Base
has_many :photos, :dependent => :destroy
when I delete a photoalbum (id=2) this works very well to delete all associated photos, but it doesn't delete items from the AuditLog that are like this: (feeded_id = 2, feeded_type = PhotoAlbum)
Given that the AuditLog table doesn't have a "photo_album_id" column, and can't, is there a way to setup a dependent > Destory with Rails to delete all associated items in teh AuditLog when a PhotoAlbum is deleted?
Thanks, I know this one's a little more complicated than most. Thanks for reading through it!
I think what you are looking for is the combination of
belongs_to :feeded, :polymorphic => true
in your Audit log class and
has_many :logs, :as => :feeded, :dependent => :destroy
in your PhotoAlbum class.
If you do not have a class to represent your audit log, you should be able to add the belongs_to to the existing class (in your plugins perhaps?).
I'm not 100% sure about the :as => :feeded option, you will have to name that symbol correctly and I am not sure what ActiveRecord will expect, but the belongs_to relationship will look for feeded_id and feeded_type, so when the 'parent' object is a PhotoAlbum it will join correctly on photo_album.id = audit_logs.feeded_id AND audit_logs.feeded_type = 'PhotoAlbum'. Since this doesn't require any changes to your database, all your existing code should continue to work.
You can read up on the options for associations here.

belongs_to not using primary key option

I've been struggling with this for a while, and decided to throw it out there:
I have 3 models, User, Connection, Suspect
A User has many Connections,
A Connection has one Suspect, linked via case_id
A User has many Suspects through its Connections.
The code is as follows:
class User < ActiveRecord::Base
has_many :followers
has_many :suspects, :through => :followers
end
class Connection < ActiveRecord::Base
belongs_to :user
belongs_to :suspect, :primary_key => :case_id , :foreign_key => :case_id
end
class Suspect < ActiveRecord::Base
belongs_to :connection, :primary_key => :case_id , :foreign_key => :case_id
end
The problem is that the belongs_to seems to ignore the :primary key.
If I do
u = User.find(:first)
u.suspects
The SQL generated is:
SELECT `suspects`.* FROM `suspects` INNER JOIN `connections` ON `suspects`.id = `connections`.case_id WHERE ((`followers`.user_id = 1))
However it should be:
SELECT `suspects`.* FROM `suspects` INNER JOIN `connections` ON `suspects`.case_id = `connections`.case_id WHERE ((`followers`.user_id = 1))
Can someone point me in the right direction?
James
The docs for belongs_to say that you can use the :primary_key option, but we have never been able to get it to work either. That said the latest version of rails, 2.3.3, specifically has a fix for this
After more testing, #pallan is correct, this was fixed in 2.3.3 but only for the first level of association, not for has_many :through.
For example:
Connection.suspects
Uses the correct SQL, but
User.connections.suspects
Does not.
I'm going to open a ticket for it now.
JP

Resources