🔁 ทวนก่อนเริ่ม — คำพวกนี้จะโผล่ในบทนี้
- model
Post(บท 99) — class ในไฟล์app/models/post.rb·Post= บทความหนึ่งชิ้น ·posts= ชื่อตารางที่เก็บบทความหลายชิ้น class Post < ApplicationRecord(บท 95, 99) — Post สืบทอดจาก ApplicationRecord คือได้ความสามารถทุกอย่างของแม่แบบกลางนี้มาใช้ฟรีPost.new(title: "…")(บท 97, 99) — สร้างบทความหนึ่งชิ้นไว้ในมือ ยังไม่ได้บันทึกที่ไหนbin/rails console(บท 104) — ครัวทดลองหลังร้าน พิมพ์คำสั่ง Ruby ทีละบรรทัดโดยแอปทั้งตัวโหลดไว้แล้ว- GORM (บท 27) — ตัวช่วยของ Go ที่แปลง struct เป็นคำสั่งฐานข้อมูลให้ เช่น
db.Create(&user)
🧰 เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 17 · Go 1.26 + GORM v1.31.2
(สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — เรื่องในบทนี้ไม่ต่างกัน) · ผลรันทุกกล่องก็อปมาจากเครื่องจริง ·
เวลา (ms) · วันเวลา · เลข 0x… ของคุณจะไม่ตรงกับในบท
🗂 ข้อมูลตัวอย่าง: บทนี้ใช้แค่ตาราง posts ที่มีช่อง title กับ body ·
สร้างด้วยคำสั่งเดียวกับบท 99 (bin/rails generate model Post title:string body:text แล้ว bin/rails db:migrate) ·
ถ้าในเครื่องคุณยังไม่มี ภารกิจท้ายบทพาสร้างทีละขั้น — อ่านเนื้อหาตามไปก่อนได้เลย
1 · ทำไมทั้งวันพูดเรื่องข้อมูล
สไลด์หน้าแรกของ Day 2 เปิดด้วยประโยคนี้: "แอปพังเพราะโค้ดผิด แก้ 10 นาที · แอปพังเพราะข้อมูลผิด แก้เป็นอาทิตย์"
โค้ดผิดแก้ง่าย เพราะแก้ไฟล์เดียวแล้ว deploy ใหม่ (เอาโค้ดใหม่ขึ้นเครื่องที่ให้บริการจริง) ทุกคนได้โค้ดที่ถูกทันที ·
แต่ข้อมูลผิดถูกเก็บลงไปแล้ว อาจเป็นพันรายการ และบางรายการมีคนใช้ต่อไปแล้วด้วย
เช่น ระบบยอมให้สมัครด้วยอีเมลซ้ำมาหลายเดือน · วันที่รู้ตัว คุณต้องไล่ดูทีละคู่ว่าบัญชีไหนของจริง แล้วรวมบทความของสองบัญชีเข้าด้วยกัน
โค้ดผิด = สูตรอาหารพิมพ์ผิด ใส่เกลือ 5 ช้อน · แก้สูตรแล้ว จานถัดไปก็ถูก จานที่เสิร์ฟไปแล้วก็จบไป
ข้อมูลผิด = รับวัตถุดิบเสียเข้าห้องเก็บของไปแล้ว ปนอยู่กับของดีหลายร้อยกล่อง ·
ต้องไล่เปิดทีละกล่อง และบางกล่องถูกเอาไปปรุงแล้วด้วย
ที่ไม่เหมือนกัน: ของเสียในร้านจริงยังดมหรือดูออก · ข้อมูลผิดใน database หน้าตาปกติทุกอย่าง — อีเมลซ้ำสองแถวก็เป็นแค่ข้อความธรรมดาสองบรรทัด ไม่มีอะไรเตือน จนวันที่ลูกค้าล็อกอินแล้วเข้าผิดบัญชี
ทั้ง Day 2 จึงพูดเรื่องเดียว: ทำยังไงให้ข้อมูลที่เข้าห้องเก็บของถูกตั้งแต่แรก และดึงออกมาใช้ได้เร็ว · บทนี้เริ่มจากรู้จัก "คนดูแลสต็อก" ของ Rails ก่อน ว่าเขาทำงานยังไง
2 · แผนที่ของวันนี้
ไม่ต้องจำ แค่ดูว่าทั้งวันจะเดินไปทางไหน · ทุกบทใช้ภาพร้านอาหารชุดเดิมจาก Day 1 แล้วต่อยอดที่ "ห้องเก็บของ":
| บท | เรื่อง | ในร้านอาหาร |
|---|---|---|
| 106 | Active Record คืออะไร (บทนี้) | แฟ้มที่เดินเข้าลิ้นชักเองได้ |
| 107 | สร้าง อ่าน แก้ ลบ ใน console | ลองหยิบ-เก็บแฟ้มในครัวทดลอง |
| 108–109 | แก้หน้าตาตาราง (migration) โดยไม่ทำแอปค้าง | ใบสั่งช่างต่อเติมตู้ · ปรับปรุงร้านโดยไม่ปิดร้าน |
| 110–111 | กฎตรวจข้อมูล (validation) · กฎที่ตัว database | คนดูแลสต็อกตรวจแฟ้ม · ตู้ที่ใส่แฟ้มผิดแบบไม่ได้เลย |
| 112 | ขั้นตอนอัตโนมัติ (callbacks) · โค้ดควรอยู่ไหน | ขั้นตอนอัตโนมัติก่อน/หลังเก็บแฟ้ม |
| 113–114 | ค้นข้อมูล (query) · โค้ดที่ไปหา database บ่อยเกิน (N+1) | ใบเบิกของ · เดินไปตู้ 101 รอบ |
| 115–116 | บทความ ผู้เขียน คอมเมนต์ผูกกัน | ช่อง "เป็นของบทความเลขที่ …" บนใบคอมเมนต์ |
| 117 | เส้นทางมาตรฐาน (REST) · กันลูกค้าแอบกรอกช่องเกิน | พนักงานต้อนรับรับเฉพาะช่องบนแบบฟอร์ม |
| 118–119 | โค้ดชุดสำเร็จรูป (scaffold) · Lab 2 | ชุดเปิดแผนกสำเร็จรูป แล้วเติมกฎของร้านเรา |
สไลด์หน้า 3 บอกว่าถ้าจำได้แค่ 3 อย่าง ให้จำสามประโยคนี้ (ยังไม่ต้องเข้าใจตอนนี้ บท 119 จะกลับมาถามอีกที):
- แก้หน้าตาตารางได้โดยไม่ต้องปิดแอป — ทำทีละขั้น: ต่อของใหม่ก่อน → ย้ายข้อมูล → ค่อยรื้อของเก่า (บท 109)
- กฎตรวจข้อมูลในโค้ด Rails ยังไม่ใช่การรับประกัน — ต้องมีกฎที่ตัว database คู่กันด้วย (บท 111)
- โค้ดที่ไปหา database บ่อยเกินจำเป็น ดูจากโค้ดไม่ออก ต้องดูจาก log (บท 114)
3 · ตู้แฟ้มของร้าน — database · ตาราง · แถว · คอลัมน์ · SQL
ก่อนไปต่อ ต้องมีคำพื้นฐานห้าคำ · database คือโปรแกรมที่เก็บข้อมูลให้อยู่ถาวร ปิดแอปแล้วข้อมูลไม่หาย · แอปของเราใช้ PostgreSQL ตัวเดียวกับที่เคยใช้กับ Go
ข้างใน database แบ่งเป็นตาราง (table) ตามชนิดของข้อมูล เช่นตาราง posts เก็บบทความ ·
ข้อมูลหนึ่งชิ้นคือแถว (row) หนึ่งแถว · แต่ละแถวมีช่องเหมือนกันทุกแถว เรียกว่าคอลัมน์ (column)
นี่คือหน้าตาตาราง posts หลังบันทึกบทความไปสองชิ้น (ตัวอย่างจากเครื่องตรวจ · ตัดเวลาให้สั้น):
| id | title | body | created_at | updated_at |
|---|---|---|---|---|
| 1 | ต้มยำกุ้งน้ำข้น | เปรี้ยว เผ็ด หอมสมุนไพร | 2026-11-04 11:45 | 2026-11-04 11:45 |
| 2 | ส้มตำปูปลาร้า | (ว่าง — NULL) | 2026-11-04 11:46 | 2026-11-04 11:46 |
- หัวตาราง (
id·title· …) คือคอลัมน์ · หนึ่งบรรทัดข้างล่างคือแถว = บทความหนึ่งชิ้น id= เลขประจำแถวที่ database แจกให้เอง ไม่ซ้ำกัน ·created_at/updated_at= เวลาสร้าง / แก้ล่าสุด (มาจากt.timestampsในบท 99)- แถวที่ 2 ไม่ได้ใส่ body · ช่องนั้นจึงว่าง · database เรียกค่าว่างแบบนี้ว่า NULL (ฝั่ง Ruby เห็นเป็น
nil)
เราไม่ได้เปิดตารางด้วยมือ · ทุกครั้งที่อยากได้ข้อมูล ต้องสั่งเป็นภาษาเฉพาะของ database ชื่อ SQL ·
เช่นคำสั่งนี้ (Rails "ยิง" คือส่งคำสั่งนี้ไปให้ database เองตอนเราพิมพ์ Post.count — เดี๋ยวเห็นของจริงในหัวข้อ 4):
SELECT COUNT(*) FROM "posts"
SELECT= "ขอดู / ขอหยิบ" · เป็นคำขึ้นต้นของคำสั่งที่อ่านข้อมูล ไม่ได้แก้อะไรCOUNT(*)= "นับจำนวนแถว" (*= ทุกแถว ไม่เลือกคอลัมน์)FROM "posts"= "จากตาราง posts" · เครื่องหมาย" "ครอบ = บอก database ว่านี่คือชื่อตาราง ไม่ใช่คำสั่ง- รวมเป็นภาษาคน: "นับว่าในลิ้นชัก posts มีแฟ้มกี่แผ่น"
database = ตู้เก็บแฟ้มทั้งตู้ · ตาราง posts = ลิ้นชักที่ติดป้าย "posts" · แถว = แฟ้มหนึ่งแผ่น (บทความหนึ่งชิ้น) · คอลัมน์ = ช่องที่พิมพ์ไว้บนแบบฟอร์มของแฟ้ม (ช่องหัวข้อ · ช่องเนื้อหา) · SQL = ภาษาที่คนดูแลสต็อกใช้สั่งตู้ "หยิบแผ่นที่ …" "นับให้หน่อย"
ที่ไม่เหมือนกัน: ลิ้นชักจริงใส่กระดาษหน้าตาไหนก็ได้ · แต่ตารางบังคับให้ทุกแถวมีช่องชุดเดียวกันเป๊ะ — บทความที่ไม่มีเนื้อหาก็ยังมีช่อง body แค่ช่องว่าง (NULL) · และไม่มีใครเปิดลิ้นชักด้วยมือได้ ทุกอย่างต้องผ่านคำสั่ง SQL
- database · table · row · column
- ตู้ · ลิ้นชัก · แฟ้มหนึ่งแผ่น · ช่องบนแฟ้ม · ในทีมมักเรียก row ว่า "record" ด้วย · ถาม senior ได้ว่า "ตาราง posts ตอนนี้มีกี่ row แล้ว และมี column อะไรบ้าง" — คำถามแรกที่ควรถามเวลาเข้าโปรเจกต์ใหม่
- SQL · query
- SQL = ภาษาที่ใช้สั่ง database (อ่านว่า เอส-คิว-แอล หรือ ซีเควล) · query = คำสั่ง SQL หนึ่งคำสั่ง · พิมพ์บอก AI ได้ว่า "explain this SQL line by line in plain words" แล้วแปะบรรทัดจาก log ไปทั้งบรรทัด
- NULL
- ค่าว่างในช่องของ database · ฝั่ง Ruby เห็นเป็น
nil· ถาม senior ได้ว่า "ช่องนี้ปล่อยให้ NULL ได้ไหม" — เป็นคำถามสำคัญมากในบท 111
4 · คำถามจากบท 99 — class ว่างเปล่า แต่รู้จัก title
บท 99 เราเห็นว่า app/models/post.rb มีแค่สองบรรทัด แต่ post.title ใช้ได้ ·
คำตอบสั้น ๆ คือ Post ไปถามตาราง posts เองว่ามีคอลัมน์อะไร แล้วสร้าง method ตามนั้น ·
วันนี้ดูให้ชัดขึ้นโดยวางเทียบกับ GORM ที่เคยเขียนในบท 27 (โค้ดจากสไลด์หน้า 5 ปรับให้ตรงกับตาราง posts ของเรา)
Go + GORM
// 1) ต้องประกาศทุกช่องเอง
type Post struct {
ID uint
Title string
Body string
CreatedAt time.Time
UpdatedAt time.Time
}
// 2) สร้าง 3) ยื่นให้ db บันทึก 4) เช็ค err เอง
p := Post{Title: "ต้มยำกุ้งน้ำข้น", Body: "เปรี้ยว เผ็ด หอมสมุนไพร"}
if err := db.Create(&p).Error; err != nil {
return err
}
Ruby + Active Record
# 1) ไม่ประกาศช่องเลย
class Post < ApplicationRecord
end
# 2) สร้าง 3) สั่งตัวมันเองให้บันทึก
post = Post.new(title: "ต้มยำกุ้งน้ำข้น", body: "เปรี้ยว เผ็ด หอมสมุนไพร")
post.save!
ฝั่ง Go ตัดมาจากไฟล์ที่มี package main + import ครบ และ go build ผ่าน ผมรันกับ PostgreSQL ตัวเดียวกันจริง ·
สไลด์ตั้งชื่อตัวแปรว่า p · ผมใช้ post ฝั่ง Ruby เพราะ p เป็นชื่อคำสั่งพิมพ์ค่าที่เราใช้มาตั้งแต่บท 93 อ่านแล้วจะงง
เทียบทีละงาน:
| งาน | Go + GORM | Rails |
|---|---|---|
| บอกว่าบทความมีช่องอะไร | เขียนเองใน struct ทุกช่อง | ไม่เขียน · Rails ไปถามตาราง posts ตอนรัน |
| สร้างบทความหนึ่งชิ้น (ยังไม่บันทึก) | p := Post{Title: …} | post = Post.new(title: …) |
| บันทึก | db.Create(&p) — ยื่นบทความให้ db เป็นคนบันทึก (&p = ส่งตัวจริงไป GORM จึงเติม ID กลับมาให้ได้) | post.save! — สั่งบทความตัวเองให้บันทึก |
| ถ้าบันทึกไม่ได้ | ได้ .Error กลับมา ต้องเขียน if err != nil เอง | save! โยน error ออกมาเอง (! = "ถ้าทำไม่ได้ให้ระเบิด" · แบบไม่มี ! รอบท 110) |
มาดูแถวแรกด้วยตาตัวเอง (บท 97 เคยลองแบบเดียวกันมาแล้ว) · เปิด bin/rails console แล้วถามว่า Post รู้จักช่องอะไรบ้าง:
wongnok-blog(dev):001> Post.column_names
=> ["id", "title", "body", "created_at", "updated_at"]
column_names = "บอกชื่อคอลัมน์ทั้งหมดของตารางเรา" · ได้กลับมาเป็น Array ของข้อความ ·
ไม่มีชื่อไหนเขียนอยู่ใน post.rb เลย · ทั้งห้าชื่อมาจากตาราง posts ใน database ล้วน ๆ
ร้าน Go (เปิดร้านเองตั้งแต่ศูนย์): เราต้องพิมพ์แบบฟอร์มแฟ้มเองว่ามีช่องหัวข้อ ช่องเนื้อหา … (นั่นคือ struct) แล้วหวังว่าตรงกับลิ้นชัก ·
ร้าน Rails (แฟรนไชส์): คนดูแลสต็อกของ Post เดินไปเปิดลิ้นชัก posts ดูว่าแฟ้มในนั้นมีช่องอะไร แล้วทำแฟ้มเปล่าตามแบบนั้นให้เลย ·
ลิ้นชักมีช่องเพิ่ม แฟ้มก็มีช่องเพิ่มเอง
ที่ไม่เหมือนกัน: คนจริงเห็นแบบฟอร์มตั้งแต่ก่อนเริ่มงาน · Rails ไปดูลิ้นชักตอนโปรแกรมรัน ไม่ใช่ตอนเราเขียนโค้ด — ถ้าพิมพ์ชื่อช่องที่ลิ้นชักไม่มี จะรู้ตัวตอนรันเท่านั้น (ลองทายในกล่องท้ายหัวข้อนี้)
ต่อด้วยแถวที่สองและสาม — สร้างบทความ แล้วสั่งบันทึก (ผลจริง · 0x… คือที่อยู่ในหน่วยความจำ ของคุณจะต่าง):
wongnok-blog(dev):002> post = Post.new(title: "ต้มยำกุ้งน้ำข้น", body: "เปรี้ยว เผ็ด หอมสมุนไพร")
=>
#<Post:0x000000012595f3c8
...
wongnok-blog(dev):003> post
=>
#<Post:0x000000012595f3c8
id: nil,
title: "ต้มยำกุ้งน้ำข้น",
body: "เปรี้ยว เผ็ด หอมสมุนไพร",
created_at: nil,
updated_at: nil>
wongnok-blog(dev):004> post.id
=> nil
wongnok-blog(dev):005> post.save!
TRANSACTION (0.9ms) BEGIN /*application='WongnokBlog'*/
Post Create (2.7ms) INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES ('ต้มยำกุ้งน้ำข้น', 'เปรี้ยว เผ็ด หอมสมุนไพร', '2026-11-04 12:25:36.162255', '2026-11-04 12:25:36.162255') RETURNING "id" /*application='WongnokBlog'*/
TRANSACTION (1.0ms) COMMIT /*application='WongnokBlog'*/
=> true
wongnok-blog(dev):006> post.id
=> 1
wongnok-blog(dev):007> Post.count
Post Count (3.1ms) SELECT COUNT(*) FROM "posts" /*application='WongnokBlog'*/
=> 1
บรรทัด ... หลัง post = Post.new(…) ไม่ใช่ error · console ย่อผลของบรรทัดที่มี = ให้สั้นลงเอง ·
อยากเห็นครบ ให้พิมพ์ชื่อตัวแปรเปล่า ๆ (post) อย่างบรรทัด 003
อ่านทีละบรรทัด:
| บรรทัด | แปลเป็นภาษาคน |
|---|---|
Post.new(…) → id: nil |
สร้างแฟ้มเปล่าแล้วกรอกช่องในมือ · ยังไม่ได้เข้าลิ้นชัก จึงยังไม่มีเลขแฟ้ม (id เป็น nil) · ยังไม่มี SQL สักบรรทัด |
TRANSACTION … BEGIN |
"เริ่มงานชุดหนึ่ง" · Rails ห่องานบันทึกไว้ในชุด ถ้าพังกลางทางจะไม่มีอะไรค้างครึ่ง ๆ (เรียกว่า transaction = ทำทั้งหมดหรือไม่ทำเลย · ละเอียดในบท 111) |
Post Create (2.7ms) |
ป้ายที่ Rails แปะหน้าบรรทัด: "คนดูแลสต็อกของ Post กำลังสร้างแถว ใช้เวลา 2.7 มิลลิวินาที" · ข้างหลังคือ SQL ที่ยิงจริง |
INSERT INTO "posts" (…) VALUES (…) |
INSERT INTO = "ใส่แถวใหม่ลงตาราง" · วงเล็บแรก = ช่องที่จะกรอก (ชื่อช่องอยู่ใน " ") · VALUES = ค่าที่กรอกเรียงตามลำดับช่อง (ค่าที่เป็นข้อความอยู่ใน ' ') ·
ภาษาคน: "ใส่แฟ้มใหม่ลงลิ้นชัก posts กรอกหัวข้อ เนื้อหา เวลาสร้าง เวลาแก้ ตามนี้" · สังเกตว่า Rails เติมเวลาให้เอง |
RETURNING "id" |
"ใส่เสร็จแล้วบอกเลขแฟ้มกลับมาด้วย" · นี่คือเหตุที่หลังบันทึก post.id กลายเป็น 1 |
/*application='WongnokBlog'*/ |
คอมเมนต์ที่ Rails แปะท้าย SQL บอกว่าแอปไหนส่งมา · database ไม่สนใจ · ข้ามได้ |
TRANSACTION … COMMIT → => true |
"ยืนยันงานชุดนี้" · ข้อมูลเข้าตู้จริงแล้ว · save! ตอบ true |
เทียบกับ Go: ผมเปิดโหมดโชว์ SQL ของ GORM (db.Debug()) แล้วรันโค้ดฝั่งซ้าย ·
ได้ SQL หน้าตาเกือบเหมือนกันทุกตัวอักษร (ผลจริง ตัด path ด้านหน้าออก):
2026/11/04 18:48:29 …/gormdemo/main.go:30
[7.862ms] [rows:1] INSERT INTO "posts" ("title","body","created_at","updated_at") VALUES ('ต้มยำกุ้งน้ำข้น','เปรี้ยว เผ็ด หอมสมุนไพร','2026-11-04 18:48:29.883','2026-11-04 18:48:29.883') RETURNING "id"
saved id: 1
บรรทัดแรก = วันเวลา + ไฟล์และบรรทัดในโค้ด Go ที่สั่ง · [7.862ms] = ใช้เวลาเท่าไร · [rows:1] = ใส่ไป 1 แถว ·
saved id: 1 คือบรรทัดที่โปรแกรม Go ของผมพิมพ์เองหลังบันทึกเสร็จ
แปลว่า ทั้งสองฝั่งทำงานเดียวกันเป๊ะ: แปลงคำสั่งในภาษาโปรแกรมเป็น SQL แล้วส่งให้ PostgreSQL · ตัวแปลงแบบนี้เรียกว่า ORM (เจอคำนี้ในบท 27) · ที่ต่างกันคือใครถือคำสั่งบันทึก — หัวข้อถัดไป
# ตาราง posts ของเรามีแค่ id title body created_at updated_at
# สไลด์หน้า 5 เขียนแบบนี้ (มีช่อง user_id ด้วย)
wongnok-blog(dev):001> Post.new(title: "Hello", user_id: 1)
ตารางของเราไม่มีช่อง user_id · คิดว่าได้ Post ที่ไม่มี user_id · ได้ error · หรือ Rails สร้างช่องให้เอง?
(wongnok-blog):1:in '<main>': unknown attribute 'user_id' for Post. (ActiveModel::UnknownAttributeError)
raise UnknownAttributeError.new(self, name)
^^^^^
…/attribute_methods.rb:512:in 'ActiveModel::AttributeMethods#method_missing': undefined method 'user_id=' for an instance of Post (NoMethodError)
from …
ได้ error ตั้งแต่ Post.new ยังไม่ทันถึง save! ·
unknown attribute 'user_id' for Post = "Post ไม่รู้จักช่องชื่อ user_id" (attribute = ช่องของข้อมูลฝั่ง model)
บรรทัดล่างบอกเหตุผลลึกกว่านั้น: undefined method 'user_id=' — Rails สร้าง method สำหรับกรอกค่าเฉพาะช่องที่มีในตารางจริง ·
ไม่มีช่อง ก็ไม่มี method ให้กรอก · และ Rails ไม่สร้างช่องในตารางให้เอง (นั่นเป็นงานของ migration — บท 108)
ฝั่ง GORM ก็คล้ายกัน: ผมลองใส่ UserID uint ใน struct ทั้งที่ตารางไม่มีช่องนี้ · go build ผ่าน แต่พังตอนยิง INSERT ด้วย ERROR: column "user_id" of relation "posts" does not exist ·
ทั้งสองฝั่งรู้ว่าช่องไม่ตรงก็ต่อเมื่อไปถึงตาราง · ช่อง user_id จะมาในบทถัด ๆ ไปของ Day 2 ตอนมีตาราง users
- ORM (Object-Relational Mapping)
- ตัวแปลงระหว่าง object ในโค้ดกับแถวในตาราง · GORM คือ ORM ของ Go · Active Record คือ ORM ของ Rails · ถาม senior ได้ว่า "ขอดู SQL ที่ ORM ยิงจริงหน่อย" — ใน Rails ดูได้จาก log ของ console หรือ server เลย
- attribute
- ช่องของข้อมูลฝั่ง model (
post.title) · ตรงกับคอลัมน์ฝั่งตาราง · ใน Rails มาจากตารางตอนรัน ไม่ได้ประกาศในไฟล์ · พิมพ์บอก AI ได้ว่า "I get ActiveModel::UnknownAttributeError for user_id — does the posts table have that column?" - bang method (
save!) - method ที่ชื่อลงท้ายด้วย
!· ใน Active Record แปลว่า "ทำไม่ได้ให้โยน error" ·saveไม่มี!ตอบtrue/falseแทน (บท 110) · ถาม senior ได้ว่า "ตรงนี้ทีมใช้ save หรือ save! — อยากให้พังดัง ๆ หรือเช็คผลเอง"
5 · Active Record คือชื่อ "วิธีคิด" ไม่ใช่แค่ชื่อของใน Rails
คำว่า Active Record ที่เห็นในชื่อ ActiveRecord::Base จริง ๆ เป็นชื่อ pattern — แบบแผนการออกแบบที่มีคนตั้งชื่อไว้ให้คุยกันรู้เรื่อง ·
Rails Guides เล่าว่า Martin Fowler เป็นคนอธิบาย pattern นี้ไว้ในหนังสือ Patterns of Enterprise Application Architecture
(สไลด์หน้า 6 อ้างว่าหนังสือออกปี 2002 ก่อน Rails 2 ปี)
แก่นของ pattern นี้มีประโยคเดียว:
object หนึ่งตัว = หนึ่งแถวในตาราง · และ object ตัวนั้นรู้วิธีอ่าน เขียน ลบ ตัวเองลง database
ดังนั้น post หนึ่งตัวไม่ได้ถือแค่ข้อมูล · มันถือของห้าอย่างไว้ในตัว (สไลด์หน้า 6) ซึ่งเราจะได้ลงมือทีละอย่างทั้งวัน:
ของที่ post แบกไว้ | ตัวอย่าง | เจอในบท |
|---|---|---|
| ข้อมูล — ค่าของแต่ละช่อง | post.title | บทนี้ · 107 |
| การเดินทางไป database — บันทึก ลบ โหลดใหม่ | post.save! · post.destroy | 107 |
| กฎตรวจข้อมูล | "ไม่มีหัวข้อ ไม่รับ" | 110 |
| ขั้นตอนอัตโนมัติก่อน/หลังบันทึก | "ติดป้ายก่อนเข้าตู้ทุกครั้ง" | 112 |
| วิธีหาข้อมูลที่เกี่ยวข้อง | post.comments | 115 |
ในร้าน Rails คนดูแลสต็อกฝังอยู่ในแฟ้มทุกแผ่น · แฟ้มบทความหนึ่งแผ่นรู้ว่าตัวเองมีช่องอะไร ·
สั่ง post.save! แฟ้มก็เดินเข้าลิ้นชัก posts เอง · สั่ง post.destroy ก็เดินออกไปทิ้งเอง ·
ไม่ต้องมีใครถือไปให้
ที่ไม่เหมือนกัน: แฟ้มไม่ได้ "เดิน" ไปจริง — save! แค่แปลเป็นคำสั่ง SQL (INSERT INTO …) แล้วยื่นให้ PostgreSQL ทำ ·
และกระดาษจริงเป็นแฟ้มได้แม้ไม่มีตู้ · แต่ Post ต้องมีตู้ก่อนถึงจะสร้างแฟ้มเปล่าได้ เพราะต้องไปดูช่องจากลิ้นชัก (หัวข้อ 7 พิสูจน์ให้ดู)
- pattern (design pattern)
- วิธีแก้ปัญหาที่คนใช้ซ้ำกันบ่อยจนตั้งชื่อเรียกได้ · พูดชื่อเดียวแล้วทั้งทีมเห็นภาพตรงกัน ·
ได้ยินคำว่า "Active Record" ให้ดูบริบท: คุยเรื่องออกแบบ = ชื่อ pattern · คุยเรื่อง Rails = ส่วนของ Rails ที่คุยกับ database
(เป็น gem จริงชื่อ
activerecordอยู่ในใบเสร็จGemfile.lockของคุณ — บท 98) - Active Record pattern
- object หนึ่งตัวแทนหนึ่งแถว และมี method บันทึก/ลบตัวเอง (
post.save!) · พิมพ์บอก AI ได้ว่า "explain the difference between the Active Record and Data Mapper patterns with a Rails example"
6 · อีกสายหนึ่ง: Data Mapper — แฟ้มเป็นกระดาษเปล่า มีพนักงานคลังแยก
Active Record ไม่ใช่ทางเดียว · สไลด์หน้า 7 แบ่งโลกของ ORM เป็นสองสาย ด้วยคำถามข้อเดียว: "ใครควรเป็นคนที่รู้เรื่อง database"
Active Record — แฟ้มรู้เอง
post ← ถือข้อมูล + รู้วิธีเก็บตัวเอง
│
│ post.save!
▼
ตาราง posts
Data Mapper — มีคนกลาง
post ← ถือแค่ข้อมูล ไม่รู้จัก database
│
│ ยื่นให้
▼
mapper ← พนักงานคลัง: repo.save(post)
│
▼
ตาราง posts
ฝั่งขวา post เป็นกระดาษเปล่าธรรมดา · งานบันทึก/ค้นหาทั้งหมดอยู่ที่ "พนักงานคลัง" อีกตัว (มักเรียกว่า mapper หรือ repository) ·
repo.save(post) ข้างบนเป็นแค่ภาพประกอบ ไม่ใช่โค้ดของ library ไหน
เทียบกับ Go: คุณเคยทำหน้าตาแบบขวามาแล้วในบท 27 — struct User ใน model.go ถือแค่ข้อมูล ·
ส่วน repository.go เป็นคนเดียวที่เรียก GORM (r.db.WithContext(ctx).First(&u, id)) ·
struct ไม่มีวันสั่ง save ตัวเอง
ร้านแบบ Data Mapper: แฟ้มบทความเป็นกระดาษเปล่าที่กรอกข้อมูลอย่างเดียว ไม่รู้ว่าตู้อยู่ไหน · มีพนักงานคลังแยกหนึ่งคน คอยรับแฟ้มไปเก็บ ไปหยิบ ไปทิ้ง · ร้านแบบ Active Record ไม่มีพนักงานคนนี้ เพราะแฟ้มทำเองได้
ที่ไม่เหมือนกัน: ในร้านจริง การมีพนักงานเพิ่มหนึ่งคนแปลว่าต้องจ้างเพิ่ม · ในโค้ด "พนักงานคลัง" คือไฟล์และโค้ดที่ต้องเขียนเพิ่ม (ข้อเสียที่สไลด์บอกว่า "ไฟล์เยอะกว่า") · แลกกับการที่กระดาษเปล่าเอาไปทดสอบหรือใช้ที่อื่นได้โดยไม่ต้องมีตู้
- Data Mapper · repository
- แยก object ข้อมูลล้วนออกจากตัวที่คุยกับ database · repository คือชื่อเรียกตัวกลางที่พบบ่อย (บท 27 ใช้คำนี้) · ถาม senior ได้ว่า "โปรเจกต์นี้ให้ model คุยกับ database ตรง ๆ หรือผ่าน repository" — ตอบคำถามนี้ได้ จะรู้ทันทีว่าโค้ดบันทึกข้อมูลอยู่ไฟล์ไหน
7 · ราคาที่ต้องจ่าย
ข้อดีของ Active Record เห็นแล้ว: เขียนสั้น เริ่มเร็ว ของทุกอย่างอยู่ที่เดียว · สไลด์หน้า 6 บอกราคาที่ต้องจ่ายไว้สามข้อ · แปลเป็นภาษาคน:
- model อ้วนขึ้นเรื่อย ๆ — กฎตรวจข้อมูล ขั้นตอนอัตโนมัติ วิธีค้นหา ทุกอย่างมากองที่
post.rb· โปรเจกต์อายุสองปี ไฟล์นี้อาจยาวหลายร้อยบรรทัด (บท 112 คุยว่าจะย้ายไปไว้ไหน) - ทดสอบยากกว่า — แค่สร้างแฟ้มเปล่าก็ต้องมี database แล้ว (ผลจริงข้างล่าง)
- ย้ายไปใช้ database แบบอื่นยากกว่า — เพราะโค้ดทั้งแอปเรียก Active Record ตรง ๆ ไม่มีคนกลางคั่น
ข้อสองพิสูจน์ได้ · ผมชี้ DATABASE_URL ไปที่ database ชื่อ w106_nope ที่ไม่มีอยู่จริง แล้วสั่งแค่ Post.new (ยังไม่ save) · ผลจริง ตัดบรรทัดคำแนะนำที่ตามมาออก:
$ bin/rails runner 'p Post.new(title: "x")'
…/postgresql_adapter.rb:63:in 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new_client': Database not found: w106_nope. Available database configurations can be found in config/database.yml. (ActiveRecord::NoDatabaseError)
bin/rails runner '…' = รันโค้ด Ruby บรรทัดเดียวโดยโหลดแอปให้ก่อน (บท 99) ·
ผลคือพังตั้งแต่ Post.new เพราะ Post ต้องไปถามตารางก่อนว่ามีช่องอะไร ·
ฝั่ง Go Post{Title: "x"} สร้างได้เสมอ ไม่ต้องมี database เลย
Rails ยอมรับข้อเสียพวกนี้โดยตั้งใจ แลกกับความเร็วในการเขียน · ไม่ใช่ว่าแบบไหนถูกแบบไหนผิด · แค่ต้องรู้ว่าจ่ายอะไรไป จะได้ไม่แปลกใจตอนเทสต์ต้องเปิด database (Day 4)
- fat model
- model ที่มีโค้ดกองอยู่มากเกินไปจนอ่านยาก · คำนี้ได้ยินบ่อยในทีม Rails · ถาม senior ได้ว่า "Post เริ่ม fat แล้ว ทีมเราแยกโค้ดออกไปไว้ที่ไหน"
- coupled to the database (ผูกกับ database)
- โค้ดที่ทำงานไม่ได้ถ้าไม่มี database · Active Record model เป็นแบบนี้โดยธรรมชาติ · พิมพ์บอก AI ได้ว่า "this Rails model test needs the database — is that expected or can I avoid it?"
8 · ApplicationRecord และการเดาชื่อตาราง
ทวนบท 99 สั้น ๆ: Post < ApplicationRecord < ActiveRecord::Base (เขียนย่อให้เห็นว่าใครสืบทอดจากใคร ไม่ใช่โค้ดที่พิมพ์ได้จริง) ·
ActiveRecord::Base คือของจาก Rails ที่คุยกับ database ได้ทุกอย่าง · ApplicationRecord คือแม่แบบกลางของแอปเรา
ไว้ใส่ของที่ model ทุกตัวต้องมี (ตอนนี้ยังว่าง) · Post จึงได้ความสามารถทั้งหมดมาฟรีผ่านการสืบทอด
เหลือคำถามเดียว: Post รู้ได้ยังไงว่าต้องไปถามตาราง posts · คำตอบคือกฎตั้งชื่อจากบท 99 —
Rails เอาชื่อ class มาทำเป็นตัวเล็กแล้วเติมพหูพจน์ · Post → posts · ถ้าตั้งชื่อ class ไม่ตามตาราง จะเกิดอะไร?
ลองสร้าง class ชื่อ Article (บทความเหมือนกัน แต่คนละคำ) ในโปรเจกต์ที่มีแค่ตาราง posts
(ผลจริงข้างล่าง ตัดบรรทัด from … ยาว ๆ ท้าย error ออก):
# app/models/article.rb
class Article < ApplicationRecord
end
wongnok-blog(dev):001> Article.table_name
=> "articles"
wongnok-blog(dev):002> Article.count
Article Count (46.2ms) SELECT COUNT(*) FROM "articles" /*application='WongnokBlog'*/
(wongnok-blog):2:in '<main>': PG::UndefinedTable: ERROR: relation "articles" does not exist (ActiveRecord::StatementInvalid)
LINE 1: SELECT COUNT(*) FROM "articles" /*application='WongnokBlog'*...
^
table_name= "ชื่อตารางที่ฉันจะไปหา" · Rails เดาว่าarticlesตามกฎSELECT COUNT(*) FROM "articles"= "นับแฟ้มในลิ้นชัก articles" · แต่ตู้ไม่มีลิ้นชักนี้relation "articles" does not exist= PostgreSQL ตอบว่า "ไม่มีตารางชื่อ articles" (relation = คำที่ PostgreSQL ใช้เรียกตาราง) · สังเกตว่า console เปิดได้ปกติ และtable_nameก็ตอบได้ · พังตอนไปหาตารางจริงครั้งแรก
ถ้าอยากใช้ชื่อ class ที่ไม่ตามกฎจริง ๆ (เช่น ต่อกับตารางเก่าที่ตั้งชื่อไว้ก่อนแล้ว) ให้บอกชื่อตารางเองหนึ่งบรรทัด:
# app/models/article.rb
class Article < ApplicationRecord
self.table_name = "posts"
end
wongnok-blog(dev):001> Article.table_name
=> "posts"
wongnok-blog(dev):002> Article.count
Article Count (37.6ms) SELECT COUNT(*) FROM "posts" /*application='WongnokBlog'*/
=> 2
wongnok-blog(dev):003> Article.first.title
Article Load (3.4ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT 1 /*application='WongnokBlog'*/
=> "ต้มยำกุ้งน้ำข้น"
self.table_name = "posts"—selfในตัว class คือ "class นี้เอง" · หน้าตาเหมือนpost.title = "…"ในบท 97 แต่ตั้งค่าให้ทั้ง class · บรรทัดนี้แปลว่า "Article ใช้ตาราง posts นะ ไม่ต้องเดา"Article.first= "ขอแถวแรก" · SQL ที่ได้:SELECT "posts".*= ขอทุกช่องของแถวจากตาราง posts ·ORDER BY "posts"."id" ASC= เรียงตามช่อง id ของตาราง posts ("posts"."id"อ่านว่า "ช่อง id ใน posts") จากน้อยไปมาก (ASC = น้อยไปมาก) ·LIMIT 1= เอาแค่แถวเดียว · ภาษาคน: "เรียงแฟ้มในลิ้นชัก posts ตามเลขที่ แล้วหยิบแผ่นแรกมา"Article.countได้ 2 เพราะตอนนั้นตาราง posts ในเครื่องตรวจมีบทความ 2 ชิ้น (ตารางในหัวข้อ 3)
เทียบกับ Go: GORM ก็เดาชื่อตารางแบบเดียวกัน (บท 27: User → users) · และถ้าชื่อไม่ตามกฎ ให้เขียน method TableName()
(ผมรันแล้ว ได้แถวจากตาราง posts จริง) · GORM จะเรียก method นี้ถามเองว่า "Article ใช้ตารางไหน":
Go + GORM
type Article struct {
ID uint
Title string
}
func (Article) TableName() string { return "posts" }
Rails
class Article < ApplicationRecord
self.table_name = "posts"
end
ร้านแฟรนไชส์ (Convention over Configuration — บท 90) ตั้งกฎว่า แฟ้มชื่ออะไร ลิ้นชักติดป้ายชื่อนั้นเป็นพหูพจน์ ·
คนดูแลสต็อกของแฟ้ม Post จึงเดินไปลิ้นชัก "posts" ได้เลยโดยไม่ต้องถาม ·
ถ้าร้านเราซื้อตู้เก่ามาที่ติดป้ายไว้แล้ว ก็แปะโน้ตบอกคนดูแลสต็อกทีเดียว (self.table_name)
ที่ไม่เหมือนกัน: คนจริงหาลิ้นชักไม่เจอจะเดินกลับมาถาม · Rails ไม่ตรวจล่วงหน้าเลย — เปิดแอปได้ปกติ แล้วไปพังตอนมีคนใช้ Article ครั้งแรก · และ Rails ไม่ได้รู้ความหมายของคำ — Article กับ Post แปลว่าบทความเหมือนกัน แต่สำหรับ Rails เป็นแค่ตัวอักษรคนละชุด
- table_name · naming convention
- ชื่อตารางที่ model ใช้ · ปกติ Rails เดาจากชื่อ class ตามกฎตั้งชื่อ · เขียน
self.table_name = "…"เมื่อชื่อไม่ตามกฎ · พิมพ์บอก AI ได้ว่า "map this Rails model to an existing legacy table named tbl_article" — legacy = ของเก่าที่มีอยู่ก่อนแล้วแก้ไม่ได้ relation "…" does not exist- PostgreSQL หาตารางชื่อนี้ไม่เจอ · สาเหตุบ่อยสุดคือยังไม่ได้
db:migrateหรือชื่อ class ไม่ตรงกับชื่อตาราง · ถาม senior ได้ว่า "ตาราง articles ควรมีอยู่แล้ว หรือผมต้องรัน migration ก่อน"
9 · ของฟรีที่มากับ model สองบรรทัด
class Post < ApplicationRecord + end ทำให้ได้ method พวกนี้มาโดยไม่ต้องเขียนอะไรเพิ่ม (สไลด์หน้า 8) ·
ไม่ต้องจำ · แค่รู้ว่า "มีของแบบนี้อยู่" เวลาสั่ง AI หรืออ่านโค้ดที่ AI เขียนมา จะได้ไม่งงว่ามาจากไหน · ลงมือจริงในบท 107
| กลุ่ม | method | ภาษาคน |
|---|---|---|
| สร้าง / บันทึก | Post.new · Post.create · post.save! · post.update · post.destroy |
ทำแฟ้มใหม่ (ยังไม่เก็บ) · ทำแล้วเก็บทันที · เก็บ · แก้แล้วเก็บ · ทิ้ง |
| ค้นหา | Post.find(1) · Post.find_by(…) · Post.where(…) · Post.first / Post.last · Post.all |
หยิบแผ่นเลขที่ 1 · หยิบแผ่นแรกที่ตรงเงื่อนไข · หยิบทุกแผ่นที่ตรงเงื่อนไข · แผ่นแรก/สุดท้าย · ทุกแผ่น |
| ถามสถานะของแฟ้ม | post.new_record? · post.persisted? |
ยังไม่เข้าตู้ใช่ไหม · เข้าตู้แล้วใช่ไหม |
| ถามตัวตาราง | Post.count · Post.column_names · Post.table_name · Post.columns_hash |
มีกี่แผ่น · มีช่องอะไร · ลิ้นชักชื่ออะไร · ช่องแต่ละช่องเป็นชนิดไหน |
method ที่ลงท้ายด้วย ? ตอบ true/false เสมอ (กฎเดียวกับ nil? ในบท 94) ·
ส่วนที่ขึ้นต้นด้วย Post. คือถามทั้งลิ้นชัก · ที่ขึ้นต้นด้วย post. คือถามแฟ้มแผ่นเดียว
ยังมีอีกเยอะ (กฎตรวจข้อมูลบท 110 · วิธีค้นแบบละเอียดบท 113) ค่อย ๆ เจอทีละบท
ภารกิจ — ถาม Post ว่ามันรู้อะไรเกี่ยวกับตัวเองบ้าง
🎯 ภารกิจ (~10 นาที)
ทำในโปรเจกต์ wongnok_blog ของคุณจาก Lab 1 (บท 105) · เก็บโปรเจกต์นี้ไว้ บท 107 ใช้ต่อ
- เปิด terminal ในโฟลเดอร์
wongnok_blog· ตั้งDATABASE_URLให้ชี้ PostgreSQL ของคุณแบบเดียวกับบท 105 (export DATABASE_URL=…) - ดูว่ามี model Post หรือยัง:
ls -1F app/models· ถ้าเห็นpost.rbแล้ว ข้ามข้อ 3 - สร้างตาราง posts ด้วยคำสั่งเดียวกับบท 99:
bin/rails generate model Post title:string body:text bin/rails db:migrate - เปิด
bin/rails consoleแล้วพิมพ์ทีละบรรทัด:Post.table_name Post.column_names Post.columns_hash["body"].type Post.count - ตอบในใจ: ช่องไหนใน
column_namesที่คุณไม่ได้สั่งตอน generate · และแต่ละช่องมาจากไหน
ควรเห็น: "posts" · Array 5 ชื่อ ["id", "title", "body", "created_at", "updated_at"] · :text ·
และ 0 (ถ้าเพิ่งสร้างตาราง ยังไม่มีบทความ)
columns_hash คือ Hash (บท 92) ที่ใช้ชื่อช่องเป็นกุญแจ แล้วได้ข้อมูลของช่องนั้นกลับมา · .type = "ช่องนี้เก็บของชนิดไหน" ·
:text = ข้อความยาว ตรงกับที่สั่ง body:text ไว้
ถ้าเจอ error นี้ แปลว่า… (ข้อความจริงจากเครื่องตรวจ · ตัดบรรทัด from … ยาว ๆ ออก)
| เห็นอะไร | แปลว่า |
|---|---|
PG::UndefinedTable: ERROR: relation "posts" does not exist (ActiveRecord::StatementInvalid) |
มีไฟล์ post.rb แล้วแต่ยังไม่มีลิ้นชักในตู้ → ลืม bin/rails db:migrate (ข้อ 3) |
undefined method 'colum_names' for class Post (NoMethodError) ตามด้วย Did you mean? column_names |
พิมพ์ชื่อ method ผิด · Ruby เดาคำที่ถูกให้แล้วในบรรทัด Did you mean? |
uninitialized constant Pots (NameError) ตามด้วย Did you mean? Post |
พิมพ์ชื่อ class ผิด (หรือยังไม่มีไฟล์ app/models/post.rb เลย ถ้าข้อความเป็น constant Post) |
Database not found: … (ActiveRecord::NoDatabaseError) |
ชี้ไปถูกเครื่องแล้ว แต่ยังไม่มี database ชื่อนั้น → bin/rails db:prepare แบบบท 105 |
connection to server on socket "/tmp/.s.PGSQL.5432" failed |
terminal หน้าต่างนี้ยังไม่ได้ export DATABASE_URL=… หรือ PostgreSQL ไม่ได้เปิดอยู่ (error ตัวเดียวกับที่เจอในบท 98) |
ผลจริงจากเครื่องตรวจ (เลขเวลาหน้าชื่อไฟล์ migration · เวลา s/ms ของคุณจะต่าง):
$ ls -1F app/models
application_record.rb
concerns/
$ bin/rails generate model Post title:string body:text
invoke active_record
create db/migrate/20261104114505_create_posts.rb
create app/models/post.rb
invoke test_unit
create test/models/post_test.rb
create test/fixtures/posts.yml
$ bin/rails db:migrate
== 20261104114505 CreatePosts: migrating ======================================
-- create_table(:posts)
-> 0.0034s
== 20261104114505 CreatePosts: migrated (0.0034s) =============================
$ bin/rails console
Loading development environment (Rails 8.1.4)
wongnok-blog(dev):001> Post.table_name
=> "posts"
wongnok-blog(dev):002> Post.column_names
=> ["id", "title", "body", "created_at", "updated_at"]
wongnok-blog(dev):003> Post.columns_hash["body"].type
=> :text
wongnok-blog(dev):004> Post.count
Post Count (3.2ms) SELECT COUNT(*) FROM "posts" /*application='WongnokBlog'*/
=> 0
- ข้อ 5: สั่งไปแค่
titleกับbodyแต่ได้มา 5 ช่อง ·id— Rails เติมให้ทุกตารางเอง (เลขประจำแถว) ·created_at/updated_at— มาจากบรรทัดt.timestampsในไฟล์ migration ที่ generate สร้าง (บท 99) post.rbยังมีแค่สองบรรทัดเหมือนเดิม · ทุกคำตอบใน console มาจากตาราง posts ใน databasePost.countเป็นคำถามเดียวที่ยิง SQL ให้เห็นใน log (SELECT COUNT(*) FROM "posts"= "นับแฟ้มในลิ้นชัก posts") · อีกสามคำถามเป็นเรื่องหน้าตาของตาราง Rails ไปถามไว้ครั้งเดียวแล้วจำไว้ และไม่ได้โชว์คำถามนั้นใน log
ทดสอบความเข้าใจ
1. ช่างเพิ่มช่อง views ลงตาราง posts ด้วย migration (ใบสั่งช่างมาต่อเติมตู้ — บท 108) โดยไม่แตะ app/models/post.rb เลย · เปิด console ใหม่แล้วสั่ง Post.column_names จะเห็นอะไร?
ตัวเลือกที่ 2 ถูก · ผมลองจริง: เพิ่มช่องด้วย migration แล้ว Post.column_names ได้ ["id", "title", "body", "created_at", "updated_at", "views"] ทั้งที่ post.rb ยังมีแค่สองบรรทัด
ตัวเลือกที่ 1 ผิด — นั่นคือวิธีของ Go ที่ต้องเพิ่ม field ใน struct เอง · Rails ไม่มีรายชื่อช่องในไฟล์ model ให้แก้ · ตัวเลือกที่ 3 ผิด — ไม่มีอะไร "ไม่ตรงกัน" เพราะ post.rb ไม่ได้บอกช่องไว้ตั้งแต่แรก · ตัวเลือกที่ 4 ผิด — generate สร้างไฟล์ใหม่ ไม่เกี่ยวกับการอ่านช่องจากตาราง
จุดที่ต้องจำ: ใน Rails ตารางคือที่เดียวที่บอกว่า model มีช่องอะไร · อยากรู้ ให้ถาม Post.column_names ไม่ใช่เปิด post.rb
2. โค้ดแบบไหนตรงกับ Active Record pattern?
ตัวเลือกที่ 4 ถูก · แก่นของ Active Record คือ object หนึ่งตัว = หนึ่งแถว และรู้วิธีบันทึกตัวเอง (แฟ้มเดินเข้าลิ้นชักเอง)
ตัวเลือกที่ 1 ผิด — นั่นคือ Data Mapper (มีพนักงานคลังแยก) · ตัวเลือกที่ 2 ผิด — นั่นคือไม่ใช้ ORM เลย เขียน SQL ด้วยมือ · ตัวเลือกที่ 3 ผิด — ไม่มี database และไม่มี object ที่รู้วิธีเก็บตัวเอง
จุดที่ต้องจำ: แยกสองสายด้วยคำถามเดียว "ใครถือคำสั่ง save" · ตัวข้อมูลเอง = Active Record · คนกลาง = Data Mapper
3. สร้าง app/models/article.rb ที่มีแค่ class Article < ApplicationRecord กับ end · database มีแต่ตาราง posts · สั่ง Article.count จะได้อะไร?
ตัวเลือกที่ 1 ถูก · ผลจริง: SELECT COUNT(*) FROM "articles" แล้วได้ PG::UndefinedTable: ERROR: relation "articles" does not exist · แก้ด้วย self.table_name = "posts"
ตัวเลือกที่ 2 ผิด — Rails ไม่รู้ความหมายของคำ รู้แค่กฎตัดต่อตัวอักษร ·
ตัวเลือกที่ 3 ผิด — Rails ไม่สร้างตารางให้เอง ตารางเกิดจาก migration เท่านั้น ·
ตัวเลือกที่ 4 ผิด — console เปิดได้ปกติ และ Article.table_name ยังตอบ "articles" ได้ · พังตอนไปหาตารางจริงครั้งแรก
จุดที่ต้องจำ: ชื่อ class → ชื่อตาราง ตามกฎ · ชื่อไม่ตามกฎ ต้องบอกด้วย self.table_name (Go ใช้ TableName())
4. ใน Go Post{Title: "x"} สร้างได้แม้ไม่มี database · แต่ Rails Post.new(title: "x") ตอนยังไม่มี database ได้ ActiveRecord::NoDatabaseError · เพราะอะไร?
ตัวเลือกที่ 3 ถูก · ช่องของ Post อยู่ในตาราง ไม่ได้อยู่ในไฟล์ · สร้างแฟ้มเปล่ายังต้องไปดูลิ้นชักก่อน · นี่คือ "ราคา" ข้อ "ทดสอบยาก" ของ Active Record
ตัวเลือกที่ 1 ผิด — ไม่เกี่ยวกับความเร็ว · ตัวเลือกที่ 2 ผิด — ผลจริงในหัวข้อ 4 เห็นว่าหลัง Post.new ยังได้ id: nil และไม่มี SQL INSERT จนกว่าจะ save! ·
ตัวเลือกที่ 4 ผิด — object ทั่วไปอย่าง "abc" หรือ Hash สร้างได้ปกติ · มีแค่ model ที่ต้องรู้จักตาราง
จุดที่ต้องจำ: Active Record แลกความสั้นกับการผูกติด database · object ข้อมูลกับการเก็บข้อมูลแยกกันไม่ออก
ถามกลับได้เลย — ถ้าผลใน console ของคุณไม่ตรงกับในบท ให้ก็อปบรรทัดที่พิมพ์พร้อมผลทั้งก้อน (รวมบรรทัด SQL ถ้ามี) มาถาม · ข้อความ error บรรทัดแรกกับชื่อ error ในวงเล็บท้ายบรรทัด บอกได้เกือบทุกครั้งว่าพังที่ตู้ ที่ลิ้นชัก หรือที่การพิมพ์
ถ้าเปิดสไลด์ดูประกอบ
| หน้า | สไลด์เขียน | ของจริง | ต้องสนใจไหม |
|---|---|---|---|
| 5 | Post.new(title: "Hello", user_id: 1) |
ตาราง posts ตอนนี้ยังไม่มีช่อง user_id → ActiveModel::UnknownAttributeError |
ใช่ ถ้าพิมพ์ตาม — ตัด user_id ออกก่อน |
| 5 | "Post ไปถามตารางเอง … ตอน app เริ่มทำงาน" | ในโหมด development ผมเช็คแล้ว Rails ไปถามตอนใช้ Post ครั้งแรก ไม่ใช่ตอนเปิดแอป · แก่นเหมือนกัน: ถามตอนรัน ไม่ใช่ตอนเขียนโค้ด | ไม่ต้อง |
| 5 | ตัวแปรชื่อ p |
ใช้ได้ แต่ชนกับชื่อคำสั่งพิมพ์ p (บท 93) · บทนี้ใช้ post |
ไม่ต้อง |
| 6 | "Active Record ไม่ใช่ชื่อ gem" | ชื่อมาจาก pattern จริง แต่ Rails ก็มี gem ชื่อ activerecord (activerecord (8.1.4) ใน Gemfile.lock) |
ไม่ต้อง — แค่อย่างงเวลาเห็นชื่อ gem |
| 6 | Martin Fowler · หนังสือปี 2002 · "มาก่อน Rails 2 ปี" | Rails Guides ยืนยันว่า pattern มาจากหนังสือของ Fowler · ปีไม่ได้ตรวจในบทนี้ | ไม่ต้อง |
| 7 | จัด GORM ไว้ฝั่ง Active Record | ในโค้ดหน้า 5 struct ของ GORM ไม่ได้ save ตัวเอง ต้องยื่นให้ db.Create(&p) · การจัดกลุ่มแบบนี้เถียงกันได้ |
ไม่ต้อง — ดูที่ "ใครถือคำสั่ง save" ดีกว่าจำป้าย |
อ้างอิง
- Rails Guides — Active Record Basics — อ่านหัวข้อ 1 What is Active Record? (pattern ของ Fowler · ORM) และหัวข้อ 2.1 Naming Conventions (Post → posts)
- Rails Guides — Overriding the Naming Conventions —
ตัวอย่าง
self.table_name = "…"แบบหัวข้อ 8 - Rails API — ActiveRecord::ModelSchema::ClassMethods —
ดู
column_namesกับtable_nameที่ใช้ในภารกิจ - GORM — Conventions — หัวข้อ Pluralized Table Name กับ TableName (ฝั่ง Go ของหัวข้อ 8)
- GORM — Create —
ตัวอย่าง
db.Create(&user)และการเช็คresult.Errorที่เทียบในหัวข้อ 4 - RubyGems — activerecord — หน้าของ gem ที่ทำงานตาม pattern นี้ใน Rails (ดูเลขรุ่นให้ตรงกับ Gemfile.lock ของคุณ)