Why isn't Rails automatically creating join table entries? - ruby-on-rails

I have a simple has_many through relationship set up:
class Tag < ActiveRecord::Base
has_many :profile_tags
has_many :profiles, :through => :profile_tags
end
class ProfileTags < ActiveRecord::Base
belongs_to :profile
belongs_to :tag
end
class Profile < ActiveRecord::Base
has_many :profile_tags
has_many :tags, :through => :profile_tags
end
From my view I am accepting a set of tags (just strings), and am iterating over them in my controller and calling Tag.create( ... ) on each of them, and pushing them into an array. This all works fine.
So I get to a point where I have an Array of Tag objects (tags) which were each returned by the call to create, and variable #profile which was created by doing Profile.new
I would like to do: #profile.tags = tags
Doing this causes this error on the line where I try the assignment:
uninitialized constant Profile::ProfileTag
Rails is acting like I need to manually create and assign the join table association, even though here http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association it states that when you do an assignment like this, new associations will be created and if some are gone they will be deleted.
Any ideas what I could be doing wrong here?

Rails assumes that model classes are named with the singular form, i.e. the class ProfileTags should be called ProfileTag.
Depending on which Rails version you are using, probably the easiest way to fix this is to re-create the model using script/destroy and script/generate in Rails 2.x or rails destroy and rails generate in Rails 3.
Alternatively, specifying the class name manually by adding :class_name => 'ProfileTags' to the has_many declarations should work too.

Related

Rails 4 Accept nested attributes with has_one association

I have a question about Rails Nested Attributes.
I'm using Rails 4 and have this model:
model Location
has_one parking_photo
has_many cod_photos
accepts_nested_attributes_for :parking_photo
accepts_nested_attributes_for :cod_photos
end
When I use for example:
Location.find(100).update(cod_photo_ids: [1,2,3]) it works.
But Location.find(100).update(parking_photo_id: 1) doesn't works.
I don't know what difference between nested attributes has_one and has_many.
Or do we have any solution for my case, when I already have child object and want to link the parent to the child and don't want to use child update.
Thank you.
The problem has nothing to do with nested attributes. In fact you're not even using nested attributes at all in these examples.
In this example:
Location.find(100).update(cod_photo_ids: [1,2,3])
This will work even if you comment out accepts_nested_attributes_for :cod_photos as the cod_photo_ids= setter is created by has_many :cod_photos.
In the other example you're using has_one where you should be using belongs_to or are just generally confused about how you should be modeling the association. has_one places the foreign key on the parking_photos table.
If you want to place the parking_photo_id on the locations table you would use belongs_to:
class Location < ActiveRecord::Base
belongs_to :parking_photo
# ...
end
class ParkingPhoto < ActiveRecord::Base
has_one :location # references locations.parking_photo_id
end
Of course you also need a migration to actually add the locations.parking_photo_id column. I would really suggest you forget about nested attributes for the moment and just figure out the basics of how assocations work in Rails.
If you really want to have the inverse relationship and put location_id on parking_photos you would set it up like so:
class Location < ActiveRecord::Base
has_one :parking_photo
# ...
end
class ParkingPhoto < ActiveRecord::Base
belongs_to :location
validates_uniqueness_of :location_id
end
And you could reassign a photo by:
Location.find(100).parking_photo.update(location_id: 1)

Build method is not associating in Rails

I'm still newbie in Rails, but got confused with the initialization of a HABTM association. Reading its documentation, it says
When initializing a new has_one or belongs_to association you must use the build_ prefix to build the association, rather than the association.build method that would be used for has_many or has_and_belongs_to_many associations.
So, basically, let's suppose we have two models:
class User < ApplicationRecord
has_and_belongs_to_many :organizations
end
class Organization < ApplicationRecord
has_and_belongs_to_many :users
end
Inside organization_controller, since I'm using Devise, my create method should have something like this:
#organization = current_user.organizations.build(organization_params)
#organization.save
However, it is not working. With byebug, I checked that for the current_user.organizations, the new organization was there, but, if I call #organization.users, there's an empty array. Looks like it's required to run current_user.save as well, is it correct? I was able to associate both models with this code:
#organization = Organization.new(organization_params)
#organization.users << current_user
#organization.save
You should highly consider using has_many, :through as that's the preferred way to do these kinds of relationships now in Rails.
having said that if you want to use has_and_belongs_to_many associations yes its get stored in join table on main object save.

has_one - how to assign_id?

There is a cool feature for has_many in Rails. I can write
class Article < AR::Base
has_many :comments
has_one :another_association
and voila! method comment_ids= created, which I can use in strong parameters and mass assignment. Somethind like #article.comment_ids = [1,2,3]
I need something similar for has_one, like #article.another_association_id = 1. But I get NoMethodError exception. Is there any way to make this method works?
Has one has a different syntax.
#article.build_another_association
#article.create_another_association
See the guides for more info
Use attr_accessible
Specifies a white list of model attributes that can be set via mass-assignment
So you can add it like this assuming you created and ran the migration already:
class Article < AR::Base
has_many :comments
has_one :another_association
attr_accessible :another_association_id
But if it's Rails 4 you may need to handle it in the controller.
You have the direction of the association reversed.
With has_one, the other class should have an article_id to refer to a record in your articles table.
If you want articles.another_association_id, then you should specify belongs_to :another_association.
This method should only be used if the other class contains the foreign key. If the current class contains the foreign key, then you should use belongs_to instead.
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_one
If you want to simulate the way has_many works you could try this:
class Article < AR::Base
has_one :page
def page_id=(int_id)
self.page = Page.find(int_id)
end
end
#article.page_id = 3

How does attribute class_name effect my model?

In my web dev class we were given the following code:
class User < ActiveRecord::Base
has_many :shouts
has_many :followed_user_relationships, class_name: "FollowingRelationship"
has_many :followed_users, through: :followed_user_relationships
def follow(other_user)
followed_users << other_user
end
end
I understand everything except what class_name does. Does it add information to the model relationship somehow or does it just make this relationship an alias of that class name? I checked out the documentations and it seemed pretty worthless for someone new to rails.
Rails needs to know which class to instantiate for an association. It does this by guessing based on the association name, but it can only do this when your class and association are named predictably. Specifically, it uses String#classify to turn an association name into a class name. classify converts from underscore case to camel case, and singularizes the word:
"some_kind_of_records".classify => "SomeKindOfRecord"
In your particular case, your association name and class name aren't related this way.
:followed_user_relationships would cause Rails to look for a class called FollowedUserRelationship, which isn't the right class, FollowingRelationship.
Because you've deviated slightly from this convention, you have to explicitly tell Rails the name of the class involved. That's all class_name does. It tells Rails the name of the class to use, when Rails isn't able to guess correctly.
Consider a simpler example:
class User < ActiveRecord::Base
end
class Post < ActiveRecord::Base
belongs_to :author, class_name: 'User'
end
How could Rails know that a Post is associated with a User, if all we had written was belongs_to :author?

Rails app has trouble with inter-model saving

I'm working on an app that downloads meta tags from websites and saves then. The downloading happens in a model called Site. I'd like to save off the downloaded robots meta tags into a model called robots_tag which is connected to sites via a join table called meta_tag_sites.
But the method that I've written in the sites model to do this isn't working. When I try to call the method in the console, I get the following error.
undefined method `robots_meta=' for []:ActiveRecord::Relation
Any idea what I'm doing wrong?
class Site < ActiveRecord::Base
attr_accessible :domain
belongs_to :user
has_many :meta_tag_sites
has_many :robots_tags, through: :meta_tag_sites
accepts_nested_attributes_for :robots_tags
# ...
def download_robots_meta_tags
robots_tags = Nokogiri::HTML(Net::HTTP.get(self.domain, "/")).xpath("//meta[#name='robots']")
robots_tags.each do |tag|
self.robots_tags.robots_meta = tag
end
end
# ...
end
class RobotsTag < ActiveRecord::Base
attr_accessible :robots_meta
has_many :meta_tag_sites
has_many :sites, through: :meta_tag_sites
end
class MetaTagSite < ActiveRecord::Base
attr_accessible :site_id, :meta_tag_id
belongs_to :site
belongs_to :robots_tag
end
(BTW, this post is related to an earlier post: Web-scraping Rails App Getting Over-Modelled?).
The problem is here:
self.robots_tags.robots_meta = tag
self.robots_tags is a collection of objects defined by has_many :robots_tags, and you're attempting to assign a specific attribute to that entire collection. You can't do this. If you want to assign to an attribute on a specific object, you have to either iterate over the collection, or select a specific object from the collection via first or last or any of the other Enumerable methods.
By inspection, the offending line appears to be:
self.robots_tags.robots_meta = tag
You should iterate over self.robots_tags instead, with something like:
self.robots_tags.each do |robot_tag|
robot_tag.robots_meta = tag
end

Resources