Rails View model data relating to another model - ruby-on-rails

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

Related

How to set references in Rails when using custom primary keys

I'm trying to save data fetched from Sellix API into the db in my Rails application.
Basically, there are 4 models: products, coupons, orders, and feedback.
Sellix has its own unique id on every object called "uniqid" so I decided to use it as the primary key in my models as well.
For some models, I want to save references for other tables. For example, I want to have a coupon as a reference for orders to find out which coupon has been used when placing that order.
This is how two schemas are now:
create_table "coupons", id: false, force: :cascade do |t|
t.string "uniqid", null: false
t.string "code"
t.decimal "discount"
t.integer "used"
t.datetime "expire_at"
t.integer "created_at"
t.integer "updated_at"
t.integer "max_uses"
t.index ["uniqid"], name: "index_coupons_on_uniqid", unique: true
end
create_table "orders", id: false, force: :cascade do |t|
t.string "uniqid", null: false
t.string "order_type"
t.decimal "total"
t.decimal "crypto_exchange_rate"
t.string "customer_email"
t.string "gateway"
t.decimal "crypto_amount"
t.decimal "crypto_received"
t.string "country"
t.decimal "discount"
t.integer "created_at"
t.integer "updated_at"
t.string "coupon_uniqid"
t.index ["uniqid"], name: "index_orders_on_uniqid", unique: true
end
The coupon_uniqid on orders table is the reference to the relevant coupon.
The order object on Sellix API already has that reference so currently I can save it this way.
But when I display all orders, I have to use Coupon.find_by(uniqid: order.coupon_uniqid) and it always iterate through every coupon record in the local db to find it as below.
CACHE Coupon Load (0.0ms) SELECT "coupons".* FROM "coupons" WHERE "coupons"."uniqid" = $1 LIMIT $2 [["uniqid", "62e95dea17de385"], ["LIMIT", 1]]
I can get rid of that if I can keep the coupon reference instead of the uniqid.
That's basically what I want to figure out.
YAGNI. A better approach that you should consider is to just have your own primary key and treat the uniqid as a secondary identifier to be used when looking up records based on their external id.
That way everything just works with minimal configuration.
If you really want to break the conventions you can configure the type and name of the primary key when creating the table:
create_table :orders, id: :string, primary_key: :uniqid do |t|
# ...
end
class Order < ApplicationRecord
self.primary_key = :uniqid
end
Since this column won't automatically generate primary keys you'll need to deal with that in all your tests as well.
You then have to provide extra configuration when creating foreign key columns so that they are the same type and point to the right column on the other table:
class AddOrderIdToCoupons < ActiveRecord::Migration[7.0]
def change
add_reference :coupons, :order,
type: :string, # default is bigint
null: false,
foreign_key: { primary_key: "uniqid" }
end
end
And you also need to add configuration to all your assocations:
class Coupon < Application
belongs_to :order, primary_key: "uniqid"
end
class Order < Application
has_many :coupons, primary_key: "uniqid"
end

Based on status_id want to populate an assigned string on an index page but it will only populate the status_id

I am using a drop down menu to determine the status of a Show is/was: "Watched," "Watching," or "To-Watch" I am trying to display the status on the shows page and it will only populate the status_id. This feels pretty basic, but I've tried many iterations and even did a nested attribute in my controller. The only way I can populate it is Status.last.watched, etc. Can anyone point me in the right direction?
***schema.rb***
create_table "statuses", force: :cascade do |t|
t.string "watched"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "show_id"
t.index ["show_id"], name: "index_statuses_on_show_id"
create_table "shows", force: :cascade do |t|
t.string "show_title"
t.integer "user_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "note_id"
t.integer "status_id"
end
***params in shows_controller.rb***
def show_params
params.require(:show).permit(:show_title, :status_id, status_attributes: [:watched])
end
***index.html.erb***
<% #shows.each do |s| %>
<h4><li><%= link_to s.show_title, show_path(s.id) %></h4>
<p><%= s.status_id %>
<% end %>
Unfortunately you kind of failed at the database design stage. If you have shows and users and want to keep track of which users have watched what you want to setup a join table and put the status there.
class User < ApplicationRecord
has_many :user_shows
has_many :shows, through: :user_shows
end
# rails g model UserShow user:belongs_to show:belongs_to status:integer
# adding a unique compound index on user_id and show_id is a good idea
# if you are using PG you can use a native Enum column type instead of an integer
class UserShow < ApplicationRecord
enum status: { watching: 0, watched: 1, to_watch: 2 }
belongs_to :user
belongs_to :show
validates_uniqueness_of :show, scope: :user_id
end
class Show < ApplicationRecord
has_many :user_shows
has_many :shows, through: :user_shows
end
This creates a many to many association between Users and Shows. In your code a show can only be associated with a single status. That means you would have to duplicate everything on the shows table for every user.
I have no idea what you're trying to do with t.string "watched". A boolean would be slight improvement. But an ActiveRecord::Enum would let you keep track of the status without multiple boolean columns.
To get the status string to show in your view you want to change <%= s.status_id %> to <%= s.status.watched %>.
That being said, the way you have this setup, I would make another column in the shows table, call it status, and have it be of type string. Then you can set the status as one of three you listed. You could then get rid of the status_id column in shows and the whole statuses table.

Rails does not save reference ID to record error: "Class must exist"

Issue is I can't find why reference column id can't be inserted when create new record.
I have 3 table shop_plan, shop and app
Below is tables schema:
create_table "shop_plans", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "shops", force: :cascade do |t|
t.string "url"
t.bigint "plan_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["plan_id"], name: "index_shops_on_plan_id"
end
create_table "apps", force: :cascade do |t|
t.bigint "shop_id"
t.binint "amount"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["app_id"], name: "index_apps_on_shop_id"
end
add_foreign_key "shops", "shop_plans", column: "plan_id"
add_foreign_key "apps", "shops"
And below is Model
class ShopPlan < ApplicationRecord
has_many :shop
end
class Shop < ApplicationRecord
belongs_to :shop_plan, class_name: 'ShopPlan', foreign_key: :plan_id
has_many :app
end
class App < ApplicationRecord
belongs_to :shop, class_name: 'Shop', foreign_key: :shop_id
end
There will be 1 default record added in seed.db for table shop_plan
ShopPlan.create(name: 'Basic')
ShopPlan and Shop are linked by plan_id column in Shop
Shop and App are linked by shop_id column in App
I pre-insert some value when user access index:
#basic_plan
#basicPlan = ShopPlan.where(name: "Basic").first
# if new shop registered, add to database
unless Shop.where(url: #shop_session.url).any?
shop = Shop.new
shop.url = #shop_session.url
shop.plan_id = #basicPlan.id
shop.save
end
This insert works well, however, when i run 2nd insert:
#shop= Shop.where(url: #shop_session.url).first
unless App.where(shop_id: #shop.id).any?
app = App.new
app.shop_id = #shop.id,
app.amount = 10
app.save
end
error occurs as somehow app.shop_id will not add in my #shop.id and it will return will error: {"shop":["must exist"]}
I even try hard-code app.shop_id =1 but it does not help and when I add in optional: true to app.db model, it will insert null
Appreciate if anyone can help point out why I get this error
EDIT: #arieljuod to be clear
1) I have to specific exact column class due to between Shop And Shop_Plan, i'm using a manual plan_id instead of using default shopplans_id columns.
2) I have update 1 column inside App and all that unless is just to do checking when debugging.
First of all, like #David pointed out, your associations names are not right. You have to set has_many :shops and has_many :apps so activerecord knows how to find the correct classes.
Second, you don't have to specify the class_name option if the class can be infered from the association name, so it can be belongs_to :shop and belongs_to :shop_plan, foreign_key: :plan_id. It works just fine with your setup, it's just a suggestion to remove unnecesary code.
Now, for your relationships, I think you shouldn't do those first any? new block manually, rails can handle those for you.
you could do something like
#basicPlan = ShopPlan.find_by(name: "Basic")
#this gives you the first record or creates a new one
#shop = #basicPlan.shops.where(url: #shop_session.url).first_or_create
#this will return the "app" of the shop if it already exists, and, if nil, it will create a new one
#app = #shop.app or #shop.create_app
I have found out the silly reason why my code does not work.
It's not because as_many :shops and has_many :app and also not because my code when creating the record.
It just due to silly comma ',' when creating new record in App at app.shop_id = #shop.id,, as I was keep switching between Ruby and JavaScript. Thank you #arieljuod and #David for your effort

how do rails associations get set?

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.

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.

Resources