今天來建造三個表單

首先是一對一的has_one

何謂一堆一呢 用店長、商店來舉例(我們先假設今天店長手頭比較緊,只會有一間商店)

一個店長只會有一間店 一間店只會有一個店長

has_one 一對ㄧ

Owner

來先想一下店長表單有包含哪些欄位,店長model的欄位有,姓名、信箱、電話
完整的資訊就像下列表格

欄位名稱 資料型態 說明
name string 姓名
email string email
phone string 電話

因此我們來產生model

> rails g model Owner name email phone 

Store

接著來產生商店表單,一樣先想店家會有哪些欄位,應該會有店家姓名、地址、電話 還有一個最重要的,跟店長(owner_id)的連結 一樣用表格來顯示清楚一點

欄位名稱 資料型態 說明
title string 店名
address string 地址
phone string 電話
owner_id string 這家店的店長

整理完後,來產生 model

> rails g model Store title address phone owner:references

兩個都產生完後,記得讓他具現化,也就是輸入

> rails db:migrate

此時我們先來看到schema檔案,這裡有存放所有表格的真實情況
這裡可以看到,我們剛剛在store增加的owner:references欄位,他多了幾行額外的東西

> ActiveRecord::Schema.define(version: 2022_11_16_025143) do
> 
>   create_table "owners", force: :cascade do |t|
>     t.string "name"
>     t.string "email"
>     t.string "phone"
>     t.datetime "created_at", precision: 6, null: false
>     t.datetime "updated_at", precision: 6, null: false
>   end
> 
>   create_table "stores", force: :cascade do |t|
>     t.string "title"
>     t.string "address"
>     t.string "phone"
>     t.integer "owner_id", null: false                             # owner:references 增加的東西
>     t.datetime "created_at", precision: 6, null: false
>     t.datetime "updated_at", precision: 6, null: false
>     t.index ["owner_id"], name: "index_stores_on_owner_id"        # owner:references 增加的東西
>   end
> 
>   add_foreign_key "stores", "owners"                              # owner:references 增加的東西
> end

寫進table後,還要記得做一件事,到個表單的model,寫上關聯
首先我們先到store寫上關聯

> store 的 model         

> belongs_to :owner      # 剛剛 owner:references 指令幫我們做的

> 他會讓store new出來的實體產生兩種方法
> (1)owner
> (2)owner=

接著到owner寫上關聯

> owner 的 model

> has_one :store        # 這一行要自己寫

> 上面那一行的意思是,讓owner產生的新實體,新增四種方法
> (1) store
> (2) store=
> (3) create_store
> (4) build_store

不過我們剛剛生出了一堆方法,那些方法是什麼?會用在哪裡?我們實際舉個例子看看

> rails c           # 先到rails c的環境

今天我想先新增一個新老闆(owner)

> o1 = Owner.new(name: "o1")
> o1.save                           # true

> Ps. 用new方法產生物件,他只會暫時存在記憶體,記得.save才能寫進資料庫

接著新增一間商店

> s1 = Store.new(title: "s1")
> s1.save                           # false, 此時.save會發生錯誤,原因我們可以把它印出來看看

> s1.errors.full_messages           # ["Owner must exist"], 系統說此欄位必須存在

會發生上面的錯誤的原因,就是因為我們一開始在設定關聯表的時候
store在寫入資料庫前,一定要有店長(沒有店長,商店怎麼開得成呢!!!?)
因此沒有店長的情況下,當然無法.save,那要怎麼解決這件事

第一個解決方法 -> 在new實體的時候,直接帶店長給他

> s1 = Store.new(title: "s1", owner: o1)        # 開新店時,直接帶一個店長給他
> s1.save                                       # true, 這樣就成功寫進資料庫了

第二個解決方法 -> 開好一間店後,在.save前,指派一位店長給這間店

> s1 = Store.new(title: "s1")                   # 創建一間店 
> o1 = Owner.create(name: "o1")                 # 新增一個店長

> s1.owner = o1                                 # 指派一位店長給這一間商店
> s1.save                                       # 指派完後,記得寫進資料庫

第三個解決方法 -> 直接從店長的角度,開一間新的店

這是什麼意思呢???我們直接來寫看看

> o1.create_store(title: "s1")                  # 直接由一號店長創造一間店


build_store Vs. create_store
build跟new一樣,都是先暫存在記憶體位置,要.save後,才能存進資料庫
如果今天是用create,會直接把.save一起做完,直接存進資料庫


rails db:rollback step=2
由於現在資料庫有點多,直接用rollback把資料清掉
Ps. 要記得使用rollback會清掉資料庫,由於現在是在練習,所以才會使用,工作上盡量不要使用rollback

has_many 一對多

剛剛我們做好了店長和商店了,現在我們來做產品
一家店會有很多產品對吧!!所以我們現在來新增產品model,並讓產品跟商店的id有連結

一樣先來想一下產品的表格

欄位名稱 資料型態 說明
name string 商品名稱
description text 商品描述
price decimal 商品價格
store_id integer 商店編號

設定好後,我們來產生產品的model

> rails g model name description:text price:decimal store:references

輸入好後,記得要具現化表格

> rails db:migrate

現在來看一下schema的變化狀況

> ActiveRecord::Schema.define(version: 2022_11_16_045940) do
> 
>   create_table "owners", force: :cascade do |t|
>     t.string "name"
>     t.string "email"
>     t.string "phone"
>     t.datetime "created_at", precision: 6, null: false
>     t.datetime "updated_at", precision: 6, null: false
>   end
> 
>   create_table "products", force: :cascade do |t|
>     t.string "name"
>     t.text "description"
>     t.decimal "price"s
>     t.integer "store_id", null: false                          # store:references 增加的東西 
>     t.datetime "created_at", precision: 6, null: false
>     t.datetime "updated_at", precision: 6, null: false
>     t.index ["store_id"], name: "index_products_on_store_id"   # store:references 增加的東西 
>   end
> 
>   create_table "stores", force: :cascade do |t|
>     t.string "title"
>     t.string "address"
>     t.string "phone"
>     t.integer "owner_id", null: false
>     t.datetime "created_at", precision: 6, null: false
>     t.datetime "updated_at", precision: 6, null: false
>     t.index ["owner_id"], name: "index_stores_on_owner_id"
>   end
> 
>   add_foreign_key "products", "stores"                        # store:references 增加的東西 
>   add_foreign_key "stores", "owners"
> end

寫進table後,一樣要記得到model那邊寫上關聯
首先我們先到product寫上關聯

> product 的 model         

> belongs_to :store                                            # 剛剛 store:references 指令幫我們做的

> 他會讓product new出來的實體產生兩種方法
> (1)store
> (2)store=

接著到store寫上關聯

> store 的 model

> has_many :products        # 這一行要自己寫

> 上面那一行的意思是,讓store產生的新實體,新增四種方法
> (1) products
> (2) products=
> (3) create_products
> (4) build_products

寫好了關聯,我們一樣來看看剛剛那些產生的新方法,要怎麼使用

> rails c           # 先到rails c的環境

前置動作 -> 先產生一位店長、一家店,等等我們要做的事情就是把商品塞進這家店裡面

> o1 = Owner.create(name: "o1")         # 產生店長
> o1.create_store(title: "s1")          # 產生店家    
> s1 = Store.first                      # s1 = 剛剛產生的店家

接著來產生商品

> p1 = Product.new(name: "p1")
> p2 = Product.new(name: "p2")

> s1.products = [p1, p2]                # 一次把多個商品塞進商店

> s1.products << p1                     # 也可以用這個方式,一個一個塞進去
> s1.products << p2 

has_many :through 多對多

這邊我們要來改一下商店和產品間的關聯
畢竟一間店會有很多類型的產品
一個產品也一定會在很多商店一起賣(總不可能這件商品只在這一家店上架,除非這家店獨佔)

所以我們要讓兩個商品用多對多的方法來連結
不過這邊建立多對多關聯表比較特別,舉個例子好了

大家在看醫生的時候,應該知道門診人員都有一個約診簿,裡面會紀錄醫生有哪些客人,那又如果你今天只是小感冒的話,
每次去這家診所不會是同一位醫生看病,所以有機會被很多位醫生看過

把上面那一大串簡化就會是
一位醫生會有很多病人,一位病人也會有很多不同醫生看診的紀錄,而這些都記錄在約診簿上

因此我們來設計的這約診簿,不過是產品、商店的約診簿
我們幫這個約診簿取名為Book

欄位名稱 資料型態 說明
store_id string 商品編好
product_id text 產品編號

規劃好後,開始來把model建出來

> rails g model Book store:references product:references

建好後,記得具現化

> rails db:migrate

接著我們看一下schema的樣子,剛剛創立約診簿後,變得怎麼樣了

> ActiveRecord::Schema.define(version: 2022_11_16_053913) do
> 
>   create_table "books", force: :cascade do |t|
>     t.integer "store_id", null: false
>     t.integer "product_id", null: false
>     t.datetime "created_at", precision: 6, null: false
>     t.datetime "updated_at", precision: 6, null: false
>     t.index ["product_id"], name: "index_books_on_product_id"         # product:references 增加的東西 
>     t.index ["store_id"], name: "index_books_on_store_id"             # store:references 增加的東西 
>   end
> 
>   create_table "owners", force: :cascade do |t|
>     t.string "name"
>     t.string "email"
>     t.string "phone"
>     t.datetime "created_at", precision: 6, null: false
>     t.datetime "updated_at", precision: 6, null: false
>   end
> 
>   create_table "products", force: :cascade do |t|
>     t.string "name"
>     t.text "description"
>     t.decimal "price"
>     t.integer "store_id", null: false
>     t.datetime "created_at", precision: 6, null: false
>     t.datetime "updated_at", precision: 6, null: false
>     t.index ["store_id"], name: "index_products_on_store_id"
>   end
> 
>   create_table "stores", force: :cascade do |t|
>     t.string "title"
>     t.string "address"
>     t.string "phone"
>     t.integer "owner_id", null: false
>     t.datetime "created_at", precision: 6, null: false
>     t.datetime "updated_at", precision: 6, null: false
>     t.index ["owner_id"], name: "index_stores_on_owner_id"
>   end
> 
>   add_foreign_key "books", "products"                                # product:references 增加的東西 
>   add_foreign_key "books", "stores"                                  # store:references 增加的東西 
>   add_foreign_key "products", "stores"
>   add_foreign_key "stores", "owners"
> end

接著重點來了!!!!!我們要來寫關聯表的設定,因為跟前面不太一樣,所以是重點
首先我們到store的model

> store model

> has_many :books                           # 這一行應該沒啥疑問,因為很多商店都會存在約診簿裡
> has_many :products, through: :books 

> 上面比較長的那一行意思就是,因為商店真正關心的是產品有沒有在我們商店賣,並不是約診簿,所以
> 寫上has_many :products,但是商品和產品又沒有直接關聯,所以要透過約診簿才行

首先我們到product的model

> product model                             # 這一行應該沒啥疑問,因為很多商店都會存在約診簿裡

> has_many :books
> has_many :stores, through: :books         

> 上面比較長的那一行意思就是,因為產品真正關心的是在哪些商店被賣,並不是約診簿,所以
> 寫上has_many :stores,但是產品和商店又沒有直接關聯,所以要透過約診簿才行

前置動作 -> 先產生一位店長、一家店,等等我們來開始實際把產品塞進商店中

> o1 = Owner.create(name: "o1")         # 產生店長
> o1.create_store(title: "s1")          # 產生店家    
> s1 = Store.first                      # s1 = 剛剛產生的店家

創造新商品

> p1 = Product.new(name: "p1")
> p2 = Product.new(name: "p2")

> s1.products = [p1, p2]               # 但,此時會噴出下面這個錯誤,寫不進去!!!!!!
> ActiveRecord::RecordInvalid (Validation failed: Store must exist)

為什麼會這樣呢??如果你今天是跟著本篇文章一路做下來,你也會遇到這個問題,也就是

  1. 先做了 一對一 > 商店對店長做references
  2. 再來做了 一對多 > 產品對商店做references
  3. 最後做 多對多 > 產品和商店多做一個約診簿來記錄多對多

為什麼會這樣呢?主要就是因為,在第二步驟的時候,product table的欄位store_id是必填欄位 所以我們在做產品寫進會失敗,原因就是因為在寫入前一定先有資料

實際來看一下schema

> create_table "products", force: :cascade do |t|
>   t.string "name"
>   t.text "description"
>   t.decimal "price"
>   t.integer "store_id", null: false                           # 這個如果沒有填寫的話,是false
>   t.datetime "created_at", precision: 6, null: false
>   t.datetime "updated_at", precision: 6, null: false
>   t.index ["store_id"], name: "index_products_on_store_id"
> end

那要怎麼解決這件事情呢?很簡單,把store_id改成純粹integer就好

> rails db:rollback STEP=2                => 倒退回product表單建立前(我這邊倒退兩步的原因是因為產品表是前前次建立的)

接著我們把當初創造product的migration打開,並看到references :store那一條

> class CreateProducts < ActiveRecord::Migration[6.1]
>   def change
>     create_table :products do |t|
>       t.string :name
>       t.text :description
>       t.decimal :price
>       t.references :store, null: false, foreign_key: true         # 這裡
> 
>       t.timestamps
>     end
>   end
> end

只要把那一條改掉,就可以把資料寫進去了

> t.integer :store                                               # 改成這樣

改好後,再把表單具現化

> rails db:migrate

前置動作 -> 先產生一位店長、一家店,等等我們來開始實際把產品塞進商店中

> o1 = Owner.create(name: "o1")         # 產生店長
> o1.create_store(title: "s1")          # 產生店家    
> s1 = Store.first                      # s1 = 剛剛產生的店家

這樣更改後,就可以把商品塞進去拉!!!!

> p1 = Product.new(name: "p1")
> p2 = Product.new(name: "p2")

> s1.products = [p1, p2]                # true