Does has_many realation require foreign key? - ruby-on-rails

I wan to make simple has_many and belongs_to relation between my models.
I have User model and Quote model. And each quote belongs to user and evry user has many quotes.
I've studied this: http://guides.rubyonrails.org/association_basics.html
But I still don't know what about foreign key. Does Rails do it automatically in background (when I add has_many and belongs_to) or should I add manually migration with user_id column added to Quotes table?
Should I have user_id column in my db_schema?

The foreign keys should be defined in your migration. If you use the generator, Rails will generate a migration, which maybe looks like the following:
class CreateQuotes < ActiveRecord::Migration
def change
create_table :quotes do |t|
t.string :title, null: false
t.text :content, null: false
t.references :user
t.timestamps
end
end
end
The statement t.references :user will generate your foreign key column, which is called user_id in this case.
Here is a quote from the Rails Guides:
Using t.integer :supplier_id makes the foreign key naming obvious and explicit. In current versions of Rails, you can abstract away this implementation detail by using t.references :supplier instead.

Related

Referencing a column on a table to a column on another table Ruby on Rails

I was reading another question on here regarding referencing columns from two separate tables but was a little confused if it addressed my issue. What's going on is I have two tables, Destination and Booking. The Destination table has a column for location_id, and the Booking has a column for location, and I am trying to reference location in Booking table from location_id column in Destination table.
Here is my table for Booking(migration)
class CreateBookings < ActiveRecord::Migration[6.1]
def change
create_table :bookings do |t|
t.string :name
t.string :start_date
t.string :end_date
t.string :email
t.integer :location
t.timestamps
end
end
end
and here is my table(Migration) for Destination
class CreateDestinations < ActiveRecord::Migration[6.1]
def change
create_table :destinations do |t|
t.string :address
t.string :city
t.string :state
t.string :zip
t.integer :location_id
t.timestamps
end
end
end
My Models are setup currently as
class Booking < ApplicationRecord
# belongs_to :reservation, optional: true
has_many :destinations, :class_name => 'Destination', :foreign_key=> 'location_id'
validates :name, :start_date, :end_date, :email, presence: true
end
and
class Destination < ApplicationRecord
has_many :bookings, :class_name => 'Booking', :foreign_key=> 'location'
end
Am I currently referencing the columns correctly, or is there something else I should be doing?
How you should write your migrations depends on the association between your models. Foreign keys go onto tables that have a belongs_to association.
Can a single Booking have multiple Destinations? If the answer is no, you need to change the association in your Booking model to belongs_to :destination and then put a :destination_id on your bookings table (you can give it a custom name like :location_id if you want but the convention is to use the model name).
If a single Booking can have multiple Destinations, and surely a single Destination can have multiple Bookings, then you have a many-to-many relationship. In that case you will not put foreign keys on the destinations table, nor the bookings table. Instead you will need a join table between them and that's where the foreign keys go.
Rails gives 2 different ways to declare many-to-many relationships. See https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many.
If you want to use has_and_belongs_to_many, your models would look like this:
class Booking < ApplicationRecord
has_and_belongs_to_many :destinations
end
class Destination < ApplicationRecord
has_and_belongs_to_many :bookings
end
And the migration would look like this:
class CreateBookingsAndDestinations < ActiveRecord::Migration[6.0]
def change
create_table :bookings do |t|
# ...
end
create_table :destinations do |t|
# ...
end
create_table :bookings_destinations, id: false do |t|
t.belongs_to :booking
t.belongs_to :destination
end
end
end
Caveat: Based on your question I'm assuming you want a booking to have a destination. If you want a destination to many bookings and vise-versa, Sean's answer is great.
I think you're misunderstanding how foreign keys / associations work in databases.
It sounds like you want a column in the bookings table to "reference" a value column in the destinations table (or maybe the opposite), as in:
bookings.location -> destinations.location_id or maybe destinations.location_id -> bookings.location.
That's not typically what we mean by "reference" in a relational database. Instead, when you say that a table (for example, a 'comments' table) references another table (for example, a comments table references a user table), what we typically mean is that we're storing the primary key column of the referenced table (e.g. the user's id) in a column in the first table (e.g. comments.user_id --> users.id).
From an english language standpoint I expect that you want a booking to refer to a destination, so I'm going to assuming we want a the booking table to reference/refer to the destinations table, like this:
booking.location -> destinations.id
In Ruby on Rails, the convention is to name a column that stores an association with the same as the table it references, plus _id, like so the convention would be this:
booking.destination_id -> destinations.id
A common way to create this in a migration would be with:
add_reference :bookings, :destination
When adding a reference in a database you almost always want to index by that value (so that you can do Bookings.where(destination_id: #destination.id) and not kill your database). I am also a strong advocate for letting your database enforce referential integrity for you, so (if your database supports it) i'd recommend the following:
add_reference :destinations, :booking, index: true, foreign_key: true
This would prevent someone from deleting a destination that has a booking associated with it.

Ruby on Rails Foreign Keys Issue

I am trying to get a handle on how to use foreign keys in Rails,
$ rails g scaffold categories cat:string value:integer
$ rails db:migrate
Then create a new table with a foreign key connecting to the first table categories,
$ rails g scaffold subcategories subcats:string subcatsvalue:integer categories:references
$ rails db:migrate
Then I append /categories to the url and the form is there as expected and I can do all CRUD operations.
Then I append /subcategories to the url and try to add some data to the form such as,
Subcats: blah
Subcatsvalue: 123
Categories: cat1
should this be the id of the category or the name of the category?
/RubyLearningApp/db/migrate/20200413195730_create_categories.rb
class CreateCategories < ActiveRecord::Migration[5.0]
def change
create_table :categories do |t|
t.string :cat
t.integer :value
t.timestamps
end
end
end
/RubyLearningApp/db/migrate/20200413200303_create_subcategories.rb
class CreateSubcategories < ActiveRecord::Migration[5.0]
def change
create_table :subcategories do |t|
t.string :subcats
t.integer :subcatsvalue
t.references :categories, foreign_key: true
t.timestamps
end
end
end
Is this correct way to set up a foreign key between tables?
When I fill in the Categories with 'cat1' I get the following error,
Schema.rb
ActiveRecord::Schema.define(version: 20200413200303) do
create_table "categories", force: :cascade do |t|
t.string "cat"
t.integer "value"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "subcategories", force: :cascade do |t|
t.string "subcats"
t.integer "subcatsvalue"
t.integer "categories_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["categories_id"], name: "index_subcategories_on_categories_id"
end
end
The model files:
category.rb
class Category < ApplicationRecord
end
subcategory.rb
class Subcategory < ApplicationRecord
belongs_to :categories
end
Any help would be greatly appreciated,
Thanks,
There are a few things wrong with your scaffolds that may be causing the problems. The correct way to generate a scaffold is to use a singular scaffold name:
rails g scaffold Category
and
rails g scaffold SubCategory
This will use Rails built in Inflector to pluralize the names where necessary.
When using references, you should also use the singular:
category:references
This is the Rails Way and it will sort out most of the problems you are having. The other issue is if you want to add the category to the url, you should nest your routes:
resources :categories do
resources :sub_categories
end
This will allow you to use routes like
http://localhost:3000/categories/1/subcategories
and
http://localhost:3000/categories/1/subcategories/1
The first number (the one closest to the left) is the category id and can be access by using params[:category_id] in the sub_categories_controller.rb file. The second number (the one closest to the right) is the sub_category id and can be accessed by params[:id] in the sub_categories_controller.rb file.
Well, after spending two days stuck figuring out how to solve the foreign key issue in Rails 6+ - even though i read a lot of comments from S.O which did not do much help. I finally found the solution.
Using add_reference in your migration, you can easily solve this.
Let's pick it up from where you have model files untouched and Rails generated.
For your Category Model, you should have:
class Category < ApplicationRecord
has_many :subcategories, foreign_key: :categories_id
end
And for your Subcategory Model, you should have:
class SucCategory < ApplicationRecord
belongs_to :category. foreign_key: :categories_id
end
This creates an Association atrribute that tells rails that a Category has many Subcategories that can be identified in the categories table by a foreign key found in the subcategories table known as categories_id
Then in your console, now run the command rails generate migration AddSubcategoriesToCategories to create a migration file. Within the generated migration file, be sure to have the change method;
class AddSubcategoriesToCategories < ActiveRecord::Migration[6.0]
def change
add_references :categories, :categories, references: :subcategories, type: :integer, index: false
end
end
This would create a categories_id column in your categories table and tells ActiveRecord to reference the values(s) from the subcategories table, automatically making it a foreign key.
Funny enough, the reason why the option :categories appears a second time is because ActiveRecord by default, looks for the column named id within the table from which the foreign key is taken - as it is the default index on creating tables. But as a different column with a different name is defined as the index, you will have to specify the name of the column (eg. keyname) in the add_reference function to make ActiveRecord append the phrase _id to what you just defined as the column name and find that column - now named 'keyname_id', else you'll receive errors that specify that the column 'id' referenced in foreign key constraint does not exist or if you specify the full column name as 'keyname_id' in your add_reference function, you'll receive errors that specify that the column 'keyname_id' referenced in foreign key constraint does not exit
So in this case the second :categories in the function is the first part of the name of the column to which ActiveRecord appends the remaining part '_id' to become :categories_id.
Drawback: All your foreign keys would then have to be snakecased as 'whateverkeyname_id' in your tables

Rails migration t.belongs_to :user, add custom column name seller_id?

I am trying to add seller_id to my items model in a migration by doing the following:
rails g model Item title:string description:text price:bigint status:integer published_date:datetime seller:belongs_to
This didn't work of course...
ultimately I want to see seller_id in my items table, which relates the item model to the user model.
Presently my migration looks like this:
class CreateItems < ActiveRecord::Migration
def change
create_table :items do |t|
t.string :title
t.text :description
t.bigint :price
t.integer :status
t.datetime :published_date
t.belongs_to :user, index: true, foreign_key: true
t.timestamps null: false
end
end
end
How can I modify this migration to create a foreign key "seller_id" that relates to the user model.
How can I auto generate a migration that includes edits from 1., from the console? I.E modify the rails g model ... to generate the migration I need.
Huge Thanks!
Just explicitly define what you want in the migration file and add the necessary relation to your model, instead of t.belongs_to you could just use:
t.integer :seller_id, index: true, foreign_key: true
and in your models you could go about this a few ways, if you also want reference your relation as seller on item instances then in the Item class make the relation:
belongs_to :seller, class_name: 'User'
and in the User class:
has_many :items, foreign_key: :seller_id
or if you want to reference it as user from the items then:
belongs_to :user, foreign_key: :seller_id
In terms of editing the generator, that is the default way that a model generator works and in my opinion I like to keep the defaults the way they are. However I do recommend creating your own rails generators or your own rake tasks for when you want to create some custom / special case stuff. For looking at creating your own generator I would point you to the ruby on rails official guide for creating generators to get you started: http://guides.rubyonrails.org/generators.html
Inside your migration you can use
t.belongs_to :seller, foreign_key: {to_table: :users}
If you don't want it to be specifically called seller_id then what you have is enough. Once you run your migration using rake db:migrate you will see the actual table structure in your db/schema.rb file with a foreign key user_id
You can simply edit the generated migration file in your favorite text editor to edit the migration before running rake db:migrate

rails nested resource unknown attribute error

I have a Contract and a Task_Order model. I keep getting an unknown attribute error for contract_id Each Contract has many Task Orders. I have read other nested models unknown attribute error questions but they haven't been able to help me. Please keep in mind I am pretty new to Rails and would greatly appreciate any help I can get. I am using Rails 4.0
Contract Model:
has_many :task_orders
Contract schema:
create_table "contracts", force: true do |t|
t.string "contractId"
t.string "contractName"
end
Task Order Model:
belongs_to :contracts
Task Order Schema:
create_table "task_orders", force: true do |t|
t.string "contract_Id"
t.string "task_orderId"
t.string "task_orderName"
end
When I click Show Contract, I get the error:
unknown attribute: contract_id
This is the line that gets highlighted:
<%= form_for([#contract, #contract.task_orders.new]) do |f| %>
I can tell that Rails is trying to print out contract_id, which is not in my Contract model... so how can I get it to print out contractId instead - which is in my Contract model?
Thanks!!
Task Order Model should have this line belongs_to contract
belongs_to association should be declared as a singular of corresponding model
Also there should be contract_id column within task_orders table.
Diagram below explains default behavior of belongs_to in Rails
Something you need to be aware of is the foreign_key of Rails (and relational databases in general):
Foreign Key
Rails' standard foreign_key is to use snake_case (contract_id), however, you can use non-conventional foreign_keys like this:
#app/models/order.rb
belongs_to :contract, foreign_key: "contract_Id"
#schema SHOULD be:
create_table "orders", force: true do |t|
t.integer "contract_id" #-> should
t.string "contract_Id" #-> your current
end
Primary Key
create_table "contracts", force: true do |t|
t.string "contractId" #-> don't need
t.string "contractName" #-> your current
t.string "name" #-> should be
end
Your primary_key is almost always going to be the id column. You should remove your contractId column from the contracts db!
Task Orders
You'll need to do this:
#app/models/order.rb
belongs_to :contracts
has_many :task_orders
You'll then need another model at app/models/task_order.rb
Form
Your form is showing the error. This is because you're trying to create an ActiveRecord in the view itself. You'll be much better using the standard accepts_nested_attributes_for method of passing nested model data through a form:
#app/models/contract.rb
def new
#contract = Contract.new
#contract.task_orders.build
end
#app/views/contracts/new.html.erb
<%= form_for #contract do |f| %>
Firstly,use singular names for belongs_to
Class TaskOrder < ActiveRecord::Base
belongs_to :contract
end
Secondly,try changing your contract_Id in your task_orders table to contract_id.
Rails by default look for model_name_id(in your case contract_id) foreign key unless if any of the custom foreign keys defined in your model.
And finally,specify the data type integer for default foreign key.In your case it should be t.integer contract_id
However if you want contract_Id as foreign key,you should define it as custom foreign key in the Contract model itself like this
Class Contract < ActiveRecord:Base
has_many :task_orders,:foreign_key => "contract_Id"
end

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