how do rails associations get set? - ruby-on-rails

I always seem to have trouble with this concept. I get what associations allow you to do, I just never seem to be able to tell if the associations are set in an application.
For example, I generated a scaffold for line_items and before I ran my migration, I set the belongs_to and has_many methods in the correct models, and then ran my migration.
After running my migration, I look at my schema and I can't tell if there are any associations set. To me it does not seem like it because I don't see the schema setting any relationships.
Do the has_many and belongs_to methods actually set the association? Or are they there for developers reading the code to understand the relationship?
How would my schema look if the associations were set properly? Do I need to rollback my last migration and include the correct indexes?
create_table "carts", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "line_items", force: :cascade do |t|
t.integer "product_id"
t.integer "cart_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "products", force: :cascade do |t|
t.string "title"
t.text "description"
t.string "image_url"
t.decimal "price", precision: 8, scale: 2
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

it's very simple to tell if the association is set up properly or not through your schema. basically the model has belongs_to should have the corresponding table that contains the foreign_id. For example in the schema you post, it's easy to see that the lineItem belongs to 'Product' and Cart, because it has cart_id and product_id(two foreign id).
activerecord is not a black magic, all has_many and belongs_to do is just dynamically adds a method to the model class, which translate the query to raw sql for you and map the result to ruby object. however it is your responsibility to set up the database table correctly. Because after all, activerecord use SQL to query data following rails convention.
update
I think what you mean is the helper method in your migration file such as
t.belongs_to product, index: true, foreign_key: true
this line of code is also not a black magic, it is a helper method rails provide to make your life much easier. It is equivalent to do three simple things to create your database table.
create a foreign_id depends on your input, in the above example, it will be product_id. equivalent to t.integer product_id
add an index on the foreign_id, because usually you query out the associated data a lot by using the foreign_id, add an index will improve your efficiency. equivalent to add_index "xxx", ["product_id"], name: "index_xxxs_on_product_id"
at the end the foreign_key: true is going to set up an database constrains for your foreign_id, so that if there is still one row in your child table related to one row in your parent table, the row in your parent table will not be accidentally deleted. this is equivalent to add_foreign_key :xxxs, :products
so as a conclusion, using add_foreign_key :articles, :authors will not make anything look special in your schema, because ActiveRecord is just a translator to make your life easier when you deal with sql database, it can only do the thing sql database can do, not anything special. the idea of association in database is just saving a foreign_id in one table so that you can query the related data in another table by using the foreign_id.

The "association" is a rails concept that is implemented by has_many and belongs_to... so those lines are not just for documentation, they create the association.
You have product_id and cart_id in the line_items table and I assume
class LineItem
belongs_to :cart
belongs_to :product
...
end
class Product
has_many :line_items
...
end
class Cart
has_many :line_items
...
end
The has_many and belongs_to do set the associations for rails and means rails now knows that there are the associations...
my_line_item.cart
my_line_item.product
my_cart.line_items
my_product.line_items
If you didn't have the has_many and belongs_to it wouldn't work.
The columns card_id and product_id are needed for the associations to work as they're the way that records are linked and need to be present on the belongs_to side of the relationship. They don't have to be called what they're called but if you don't specifically use a different foreign_key name in the belongs_to and has_many then these field names are what will be expected by rails. And it's best to use the expected names... "Convention over configuration" is the preference.
If you did decide to call the foreign key vegetable_id instead of product_id that's fine, but then you'd define the association as
class LineItem
belongs_to :product, foreign_key: :vegetable_id
and
class Product
has_many :line_items, foreign_key: :vegetable_id
You can go an extra step and have...
class Cart
has_many :line_items
has_many :products, through: :line_items
...
end
This auto-magically gives you the ability to do..
my_cart.products
As rails knows how to build the SQL command to get all products for a cart via the line_items.
Because you need the foreign key in the belongs_to table, you would need to specify that foreign key when you create the table.
create_table :line_items do |t|
t.integer :product_id
There is a helper_method called references (also aliased as belongs_to) which basically does the same thing... creates the :product_id field...
create_table :line_items do |t|
t.belongs_to :product
or
create_table :line_items do |t|
t.references :product
The three versions above basically do the same thing... they create the integer column :product_id
You can also index on the :product_id field to improve retrieval performance, so you'll occastionally see index: true but this isn't essential.

Related

How to have a rails model have mulitple user_id's in one table?

I'm developing an assigning system. I need to be able to access the Assignment of the referees to the Game model. The Assignment can have up to 4 referees who are all Users. I believe my associations are correct. My questions are:
Do I need to add an assignment_id to the Game table?
To test my system I will need to seed data eventually. How do I seed the data/setup the Assignment so that each attribute (center_referee, assistant_referee) is a User? I would like this form to be a drop-down inevitably if it makes a difference.
For seed Data, I'm looking for something along the lines of the following (ignore that I'm using name instead of first_name):
Assignment.create(center_referee: user.name, assistant_referee_1: user.name, assistant_referee_2: "user.name", fourth_official: "user.name", game_id: 1)
Do I need to setup up accepts_nested_attributes on the model associations to accomplish all this?
Models:
class User < ApplicationRecord
has_many :assignments
has_many :games, through: :assignments
end
class Game < ApplicationRecord
has_one :assignment
has_many :users, through: :assignments
end
class Assignment < ApplicationRecord
belongs_to :game
belongs_to :user
end
Schema:
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "first_name"
t.string "last_name"
t.string "role"
end
create_table "games", force: :cascade do |t|
t.string "home_team"
t.string "away_team"
end
create_table "assignments", force: :cascade do |t|
t.string "center_referee"
t.string "assistant_referee_1"
t.string "assistant_referee_2"
t.string "fourth_official"
t.integer "game_id"
end
I know this is somewhat of a loaded question but I've been scratching my head over this for quite sometime now.
I would like to add to what #Vijay said,
As you want to have multiple user_ids in single table, I would suggest you to name the association, for example: In assignment.rb as belongs_to :center_referee, class_name: 'User'
You can also keep the association optional by using optional: true.
Do I need to add an assignment_id to the Game table?
No, Here Assignment belongs_to Game so id should be present in the Assignment model(as an assignment cannot exist without a game but the game is independent of assignment).
If this was about accessing the assignment for a given game, you can try game.assignment.
Do I need to setup up accepts_nested_attributes on the model associations to accomplish all this?
accepts_nested_attributes is used if you want to accept attributes for the associated model(Assignment) while accepting attributes for the model(Game).
for more info checkout
To test my system I will need to seed data eventually. How do I seed the data/setup the Assignment so that each attribute (center_referee, assistant_referee) is a User? I would like this form to be a drop-down inevitably if it makes a difference.
I don't quiet get your this question, but checkout if this helps:
f.collection_select(:center_referee_id, User.all, :id, :name )
Hope this helped.
In your Assignment class, the belongs_to for game is correctly correlated to the database schema. But, the belongs_to :user is incorrect - there is no assignments.user_id column in the db.
I think you want 4 columns, but each of them referring back to the users table with a different name - is that correct? If so, then your model should have the belongs_to with the appropriate option for class.
Since you mention that these are optional, you will have to also specify the allow_null appropriately.
btw, your games table and the corresponding Game model are also not correlating correctly.
I think you need to start with only 2 tables, logically model them correctly to satisfy the one-to-one, one-to-many relationships. Once this is done, then "translate" that into the rails model DSL. currently, this all seems very hay-wire.
create_table "assignments", force: :cascade do |t|
t.string "center_referee"
t.string "assistant_referee_1"
t.string "assistant_referee_2"
t.string "fourth_official"
t.integer "game_id"
end
Looks like you currently have this. What you need to do is to have a change of the types to as below:
create_table "assignments", force: :cascade do |t|
t.integer "center_referee_id"
t.integer "assistant_referee_1_id"
t.integer "assistant_referee_2_id"
t.integer "fourth_official_id"
t.integer "game_id"
end
This is going to work because, as you are passing the belongs_to :center_referee, class_name: "User" to the model, ActiveRecord is intelligent enough to query the ids on the user table.

Rails View model data relating to another model

So I am just learning Ruby on Rails and I have come across an issue relating to viewing model data.
I have 2 models, Product and Review. As a product can have many reviews the relationship is set to product has_many reviews and reviews has_one product.
I am trying to display all the reviews for a product on the product details page. I have added a table of reviews to the show view for products. I then added #reviews=#product.reviews to my definition for show.
What is happening is that I am receiving an error for the loop that runs through each review <% #reviews.each do |review| %> stating Unknown column 'reviews.product_id'.
In my product model, I have a column named ProductId which I thought would be how the application retrieves the list of reviews for a product but it's not. Is this product_id just a unique value created by the framework?
Just wondering if I have done something wrong or if it is something I haven't implemented.
Show.html.erb;
Show.html.erb
Show definition;
Show definition in controller
My review table definition is schema.rb;
create_table "reviews", charset: "utf8mb4", force: :cascade do |t|
t.integer "ProfileId"
t.integer "ProductId"
t.string "Author"
t.integer "ProductRating"
t.string "ReviewText"
t.date "DateofReview"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
My product table definition is schema.rb;
create_table "products", charset: "utf8mb4", force: :cascade do |t|
t.string "pName"
t.string "pBrand"
t.integer "pCost"
t.string "pCategory"
t.datetime "pDate"
t.string "pDescription"
t.string "pPhoto"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
the convention and default columns of a database in rails are snake_case https://edgeguides.rubyonrails.org/active_record_basics.html#schema-conventions even though it does not fail when using camelCase like in your case there is a lot of areas where you may need to change and in your case here the has_many is looking for a foreign key based on the class + _id to fix this you need to specify the foreign key on there:
has_many :reviews, foreign_key: :ProductId
and a foreign_key on the has_one, also note that this should be a belongs_to in your case as the key lives inside the reviews table https://guides.rubyonrails.org/association_basics.html#the-belongs-to-association
belongs_to :product, foreign_key: :ProductId
Your migration is incorrect. Rails works on conventions. It does not use StudlyCaps, it uses snake_case. It expects the column to be product_id, not ProductId. Unless you're adapting Rails to an existing schema, it's best to follow Rails conventions.
The migrations should look like so:
# Prefer Symbols to Strings.
# See https://medium.com/#lcriswell/ruby-symbols-vs-strings-248842529fd9
# force: :cascade is questionable.
create_table :reviews, charset: :utf8mb4 do |t|
# Declare profile_id and product_id as indexed foreign keys
# Declare them `not null`, a review must have a reviewer and product.
t.belongs_to :profile, foreign_key: true, null: false
t.belongs_to :product, foreign_key: true, null: false
# Assuming Profile is the author of the review, Author is redundant.
# Drop the "product" prefix, we know from context what its rating.
# Presuming a review needs a rating, set it `not null`.
t.integer :rating, null: false
# Presuming a review needs a body, set it `not null`.
# Drop the "review" prefix from "review_text". "text" is too generic.
# It's the body of the review.
t.text :body, null: false
# DateOfReview is redundant with the created_at timestamp
# The standard created_at and updated_at timestamps
t.timestamps
end
# Don't prefix columns, its confusing and obfuscating,
# especially when applied inconsistently.
# Use table aliases and fully qualified column names instead.
# In Rails you will rarely be writing SQL by hand anyway.
create_table :products, charset: :utf8mb4 do |t|
# Presuming a product requires a name and cost, make them `not null`.
t.string :name, null: false
t.integer :cost, null: false
# Consider referencing categories and brands tables instead. It would make
# listing them and querying products by them faster and easier, and
# protects against typos.
t.string :brand
t.string :category
# Large blocks of text should be "text" not "string".
# string is varchar, text is text.
t.text :description
# Do not store binaries in the database. It is slow and bloated.
# Use ActiveStorage instead.
# https://guides.rubyonrails.org/active_storage_overview.html
t.string :photo
# pDate is redundant with created_at
t.timestamps
end
has_one is inappropriate here. That's for when you could have many, but only have one. You would use it if, for example, a Product could only have one Review.
Instead, for a simple one-to-many relationship a Review belongs_to Product.
class Review < ApplicationRecord
belongs_to :product
end
class Product < ApplicationRecord
has_many :reviews
end
In general, generate your models to get the basics, and then edit them.
For example, you might generate Product and Review like so and then edit the generated files.
$ rails g model Product name:string cost:integer description:text
$ rails g model Review product:belongs_to rating:integer

If I specify a has_many :through association in Ruby on Rails, do I have to create a model for the 3rd table?

Let's say I have
class CreateAppointments < ActiveRecord::Migration
def change
create_table :physicians do |t|
t.string :name
t.timestamps null: false
end
create_table :patients do |t|
t.string :name
t.timestamps null: false
end
create_table :appointments do |t|
t.belongs_to :physician, index: true
t.belongs_to :patient, index: true
t.datetime :appointment_date
t.timestamps null: false
end
end
end
1- Do I have to define again the associations on the model file? which leads me to my next question....
2- Do I have to create a model for this third table of appointments or just run the migration and the Active Record will take care of updating it everytime a doctor and a patient updates? when will the insert to this third table will be triggered in this type of association?
There is alot of magic inside of active record, so i understand where you are coming from.
Yes, The migration will not add the proper associations to your ActiveRecord models. Migrations are there to make changes to the database.
Yes, if you did not generate this using rails g scaffold or model, then you will need to create an Appointment class that inherits from ActiveRecord::Base in order to work with it through the orm. Then put the proper associations on them (patients have many appointments, they would also have many doctors through appointments. vice versa for doctors.

Rails: Add attribute values to several new records in a join table in a has_many :through association

I have a form that creates a new exercise showing which muscle groups are worked. Here's an example of what I want to enter into the DB:
New exercise: name => Pull-up, primary => back, secondary => [biceps, forearms, chest]
How should I set this up? I don't want to store primary and secondary as arrays in the muscle_groups_exercised table because I have future queries that will search for exercises based on a muscle_group that is primary to that exercise.
Here is the schema for this part of the app:
create_table "exercises", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "muscle_groups", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "muscle_groups_exercised", force: true do |t|
t.integer "muscle_group_id"
t.integer "exercise_id"
t.binary "primary"
t.binary "secondary"
end
add_index "muscle_groups_exercised", ["exercise_id"], name: "index_muscle_groups_exercised_on_exercise_id", using: :btree
add_index "muscle_groups_exercised", ["muscle_group_id"], name: "index_muscle_groups_exercised_on_muscle_group_id", using: :btree
Here are the models:
class Exercise < ActiveRecord::Base
has_many :muscle_groups, through: :muscle_groups_exercised
end
class MuscleGroup < ActiveRecord::Base
has_many :exercises, through: :muscle_groups_exercised
end
class MuscleGroupExercised < ActiveRecord::Base
belongs_to :exercise
belongs_to :muscle_group
end
I have an exercises_controller and muscle_groups_controller. I think this code should reside in the exercises_controller and muscle_groups_exercised model but not exactly sure how to do this.
What you are doing looks right to me. I imagine will want to apply muscle groups to an exercise, even if you are going to be searching within muscle groups for exercises later on. I would put the required code within exercises_controller.rb.
Check out Ryan Bates article and video on HAMBTM checkboxes for some help on how to do this. It isn't exactly what you are doing, you will need to apply an additional value for secondary or primary.
BTW, I wouldn't have two columns for secondary and primary, since there is only two values, just have the primary one and assume if it isn't true, then the muscle group is secondary.
Do you have a more specific question? Your schema seems appropriate for your requirements. You may not need both a t.binary primary and a t.binary secondary--you could probably get away with having just one of them to specify whether that entry is primary or secondary.

Creating a record and adding associated records to it

I have a following model
class Order < ActiveRecord::Base
has_many :products, :through => :line_items
end
class Product < ActiveRecord::Base
belongs_to :order
end
line_items is a table, that associates an Order with multiple products.
create_table "line_items", :force => true do |t|
t.integer "order_id"
t.integer "product_id"
t.integer "count"
t.datetime "created_at"
t.datetime "updated_at"
end
So, each order can have multilple products.
I need to create a form, that allows user to create an order and to include some products to it. For each product, the quantity can be setted.
I think, the classic solution of this problem, via keeping a cart (or basket) in session, not matches my problem, because i need to setup and send all the stuff once, without clicking on each product's buy button and waiting.
Are there any best practices to implement this ?
Check out these two:
http://railscasts.com/episodes/73-complex-forms-part-1
http://railscasts.com/episodes/196-nested-model-form-part-1

Resources