HABTM and Naming conventions - ruby-on-rails

I've been racking my head around a rails issue for a while and wanted to verify my findings. I was trying to get the Has_and_belongs_to_many relationship working, but couldn't connect my two classes, auctionItem and category. First of all, here was my migration file and the two classes before solving the issue:
Migration file:
class AuctionItemsCategories < ActiveRecord::Migration
def up
create_table 'auction_items_categories', :id=>false do |t|
t.reference :auctionItem_id
t.references :category_id
end
end
def down
drop_table 'auction_items_categories'
end
end
Category.rb
class Category < ActiveRecord::Base
has_and_belongs_to_many :auction_items
end
auction_item.rb
class AuctionItem < ActiveRecord::Base
has_and_belongs_to_many :categories
end
After creating an instance of AuctionItem, I tried
auction_item = AuctionItem.last
auction_item.categories
...and got the following error:
NoMethodError: undefined method `categories' for #<AuctionItem:0x0000010521b870>
After some research, I found adding the specific class to the has_and_belongs_to_many helped:
Category Model
has_and_belongs_to_many :auction_items , :class_name => 'AuctionItem'
auction_item Model
has_and_belongs_to_many :categories , :class_name => 'Category'
This solved that issue and I was able to access the categories table. I went on to try to append a category to the auction item:
auction_item.categories << category
I then got received the following error:
SELECT "auction_items".* FROM "auction_items" INNER JOIN "auction_items_categories" ON "auction_items"."id" = "auction_items_categories"."auction_item_id" WHERE "auction_items_categories"."category_id" = 2
SQLite3::SQLException: no such column: auction_items_categories.auction_item_id: SELECT "auction_items".* FROM "auction_items" INNER JOIN "auction_items_categories" ON "auction_items"."id" = "auction_items_categories"."auction_item_id" WHERE "auction_items_categories"."category_id" = 2
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: auction_items_categories.auction_item_id: SELECT "auction_items".* FROM "auction_items" INNER JOIN "auction_items_categories" ON "auction_items"."id" = "auction_items_categories"."auction_item_id" WHERE "auction_items_categories"."category_id" = 2
If you notice, the query is trying to get auction_item.id rather than AuctionItem.id. To get the connection to work, I had to change my migration file to the following:
class AuctionItemsCategories < ActiveRecord::Migration
def up
create_table 'auction_items_categories', :id=>false do |t|
t.integer :auction_item_id
t.integer :category_id
end
end
def down
drop_table 'auction_items_categories'
end
end
So long story short/TL DR version: For me, it seems that when naming your class with multiple words and using camel case, rails does not singularize your pluralized class name back to it's original state if it has an underscore. So for example, my class name was AuctionItem which became auction_items for the model. Rather than search for auctionitem.id, the sql call that was looked for was auction_item.id, which is the singularize version of auction_items. Why didn't it search for auctionitem.id? In the future, when I am making association tables with multi word classes, do I use the singular underscore id version of the model name?

Your original migration was incorrect. It should have been as follows:
class AuctionItemsCategories < ActiveRecord::Migration
def up
create_table 'auction_items_categories', :id => false do |t|
t.references :auction_item
t.references :category
end
end
def down
drop_table 'auction_items_categories'
end
end
You should specify the symbolized model name when using references.

Related

Rails5 Category with Parent Category Id on the same table

Ok so what I have been doing for categories is I have had 2 tables Categories(id, name) and SubCategories(id, name, category_id) that are related through the models. I set inverse_of as you will see below.
My question is when I use to code in PHP years ago we use to have one table "Categories" and it had id, name, parent_id(0 by default) we then used that one table to control the outputs of navigations, breadcrumbs, and other navigational elements.
class CreateCategories < ActiveRecord::Migration[5.1]
def change
create_table :categories do |t|
t.string :name
t.timestamps
end
end
end
class CreateSubCategories < ActiveRecord::Migration[5.1]
def change
create_table :sub_categories do |t|
t.string :name
t.integer :category_id
t.timestamps
end
end
end
class Category < ApplicationRecord
has_many :sub_categories, inverse_of: :category
end
class SubCategory < ApplicationRecord
belongs_to :categories, inverse_of: :sub_category
end
I have been programming in Ruby on Rails now for over 4 years and I have yet to find a real nice "Rubyway" to achieve this. Over the years I have seen examples of using what I am already using with the 2 table method, however this does not seem very intuitive because when the system gets many categories and sub categories like 100's the page load time will be impacted in processing. Is anyone useing or know of a one table method like
class CreateCategories < ActiveRecord::Migration[5.1] def change
create_table :categories do |t|
t.string :name
t.integer :parent_id
t.timestamps
end
end
end
The problem I have always had is the model and how to get the system to realize that a record can belong to a record on the same table. I have been able to achieve it manually but I have not found a way to set it up where formtastic and other gems like rails admin would play nice with it.
Seems that you're looking for a tree structure. acts_as_tree has been around for some time. If you're using PostgreSQL, the ltree extension may be of interest as well (along with the pg_ltree gem).

Spree Relation Issue

I'm trying to add product attachment functionality to a Spree store. E.g. a product has many attached documents: brochures, instruction manuals, etc. I can't get the relationship between documents and products to work.
I can use the Paperclip gem for the attachment functionality since Spree already uses it for images.
I have the "document" model: models/spree/document.rb:
class Spree::Document < ActiveRecord::Base
belongs_to :products, class_name: "Spree::Product"
has_attached_file :pdf
end
Then I try to relate the document model to the Spree::Product model in models/spree/product_decorator.rb:
Spree::Product.class_eval do
has_many :documents, dependent: :destroy
end
Then I add migrations:
class CreateDocuments < ActiveRecord::Migration
def change
create_table :spree_documents do |t|
t.timestamps
end
end
end
class AddPdfToDocuments < ActiveRecord::Migration
def self.up
add_attachment :spree_documents, :pdf
end
def self.down
remove_attachment :spree_documents, :pdf
end
end
Now I go into the rails console to see if it worked:
#=> prod = Spree::Product.first
#=> prod.document
#=> PG::UndefinedColumn: ERROR: column spree_documents.product_id does not exist
#=> LINE 1: ..."spree_documents".* FROM "spree_documents" WHERE "spree_doc...
^
#=> : SELECT "spree_documents".* FROM "spree_documents" WHERE "spree_documents"."product_id" = $1
Seems like I'm not defining the relationship between documents and products correctly, but I'm not sure what the issue is.
It looks like you never added a product_id column to your Spree::Documents table. When you define a model belongs_to another model, it tells ActiveRecord that the first one will be a [relation]_id column in its table.
You just need to make sure to add t.references :product in your migration, so it'd look like:
class CreateDocuments < ActiveRecord::Migration
def change
create_table :spree_documents do |t|
t.references :product
t.timestamps
end
end
end

Rails belongs_to relationship not working

Currently I am having issues with the belongs_to relationship between my Page model and my Companies model.
I receive the following error message when I am on the Page show.erb.html and I am accessing #page.company.id: undefined method `id' for nil:NilClass
This error message with and without the attr_accessor method in the companies model.
Page Model:
class Page < ActiveRecord::Base
belongs_to :company
end
Company Model:
class Company < ActiveRecord::Base
attr_accessor :id, :name
has_many :pages
end
Companies Migration:
class CreateCompanies < ActiveRecord::Migration
def change
create_table :companies do |t|
t.string :name
t.timestamps
end
end
end
Pages migration:
class CreatePages < ActiveRecord::Migration
def change
create_table :pages do |t|
t.string :name
t.references :companies, index: true
t.timestamps
end
end
end
As a page has a single company, your page migration should use:
t.references :company, index: true
(singular) instead of :companies (plural). This should allow rails to populate it properly.
Associations
As Rails is designed to run on a relational database stack, you have to appreciate the role of this structure plays in defining & creating the ActiveRecord associations in your models.
To give you some ideas - ActiveRecord is actually something known as an "ORM" (Object Relationship Mapper) - meaning that it will manage the relationships between "objects" in your app. As Rails, by virtue of being built on top of Ruby, is object orientated, ActiveRecord plays an integral role in managing the relationships between your objects
The problem you're referring to is how to construct these relationships correctly:
Models
The answer to your question is:
#app/models/page.rb
Class Page < ActiveRecord::Base
#fields id | company_id | other | attributes | created_at | updated_at
belongs_to :company
end
#app/models/company.rb
Class Company < ActiveRecord::Base
#fields id | title | other | page | attributes | created_at | updated_at
has_many :pages
end
To demonstrate what the tables should look like, you can use this Rails documentation:
This means that in reference to your question, you can either use Martin's answer, or just add the company_id column manually (we do that):
class CreatePages < ActiveRecord::Migration
def change
create_table :pages do |t|
t.string :name
t.string :company_id
t.timestamps
end
end
end
Datatables
Some other information information you should know:
Rails builds your associations with foreign_keys - whenever you create a "relationship" in your Models, all it does it trigger ActiveRecord to identify the foreign_key for the association:
Finally, you also need to appreciate that the identification of "records" in a datatable is determined by the primary key it has. Every time you load an object in Rails, it's actually looking into the database for the corresponding id it has
This means if you wish to identify records in Rails, you need to make sure they have a primary key in the database

Confusion about ActiveRecord associations and foreign keys

In the below example, do I have to create employee_id in the Office model, or is it created automatically by db:migrate?
class Employee < ActiveRecord::Base
has_one :office
end
class Office < ActiveRecord::Base
belongs_to :employee # foreign key - employee_id
end
Feels like I'm missing something fundamental. I'm trying to get a basic one to many relationship working, where I can use a drop-down select of objects from the one side. Are there any good basic tuts explaining how this works?
I had to create _ids in all the models where I wanted this to work, but it doesn't seem right from examples I've looked at.
two steps.
firstly, you have to create an employee_id field in the office table in the migration file. you will have something like that :
class CreateOffices < ActiveRecord::Migration
def change
create_table :offices do |t|
t.string :name
t.integer :employee_id
t.timestamps
end
end
end
secondly, you have to define the association in the model. by convention, if you name the foreign_key field employee_id, you don't have to specify the name of it in the model.
class Office < ActiveRecord::Base
belongs_to :employee
end
should be enough.
Associations in ActiveRecord comprise two parts. Hooking together the model objects (like you've done) and setting up the database. So you'll need to define the association in your migration like so:
def change
create_table :offices do |t|
# Other migrations
t.references :employee
end
end
Alternatively you can do t.integer :employee_id which will achieve the same end too.

Rails Many to Many SQLite3 error

I created a many-to-many relationship in rails, here's my models and migrations
class Channel < ActiveRecord::Base
has_and_belongs_to_many :packages
validates_presence_of :name
end
class Package < ActiveRecord::Base
has_and_belongs_to_many :channels
validates_presence_of :name
end
class CreateChannelsPackages < ActiveRecord::Migration
def change
create_table :channels_packages, :id => false do |t|
t.references :channel
t.references :package
t.timestamps
end
add_index :channels_packages, :channel_id
add_index :channels_packages, :package_id
end
end
Then i have a multiple select, but when i try to save i get this error
SQLite3::ConstraintException: constraint failed: INSERT INTO "channels_packages" ("package_id", "channel_id") VALUES (1, 1)
I tried to remove the indexes from the migration but it didn't solve it, did somebody else have this problem?
Btw i'm using Rails 3.2.6 and sqlite3 1.3.6
I think gabrielhilal's answer is not quite correct: use of extra attributes in the join table is deprecated, thus you need to remove the timestamp in your migration, then it should work just fine with the has_and_belongs_to_many wich itself is not deprecated.
If you do need additional attributes in your join table, though, has_many :through is the way to go.
There is also another question with good answers on this topic:
Rails migration for has_and_belongs_to_many join table
I don't know if it is the reason of your problem, but the has_and_belongs_to_many association is deprecated.
According to the Rails Guide:
The use of extra attributes on the join table in a has_and_belongs_to_many association is deprecated. If you require this sort of complex behavior on the table that joins two models in a many-to-many relationship, you should use a has_many :through association instead of has_and_belongs_to_many.
I know that you are not adding any extra attribute to the join table, but try changing your migration to the below, which I think is the default:
class CreateChannelPackageJoinTable < ActiveRecord::Migration
def change
create_table :channels_packages, :id => false do |t|
t.integer :channel_id
t.integer :package_id
t.timestamps
end
end
end

Resources