บทเรียน 107 · Rails · Day 2

ลองในครัวทดลอง — สร้าง อ่าน แก้ ลบบทความด้วย console

~25 นาที (ครึ่งหลังเป็นการเตรียมข้อมูลของวันที่ 2 ราว 10 นาที) · ควรผ่าน บทเรียน 106 (Active Record คืออะไร) และ บทเรียน 104 (bin/rails console) มาก่อน

บท 106 บอกว่าแฟ้มของ Rails “เดินเข้าลิ้นชักเองได้” · บทนี้เราจะสั่งให้มันเดินจริง
อ่านจบแล้วคุณจะ สร้าง · หยิบมาอ่าน · แก้ · ทิ้ง บทความได้เองจาก bin/rails console และอ่านบรรทัด SQL ที่ console พิมพ์ออกมาได้ว่าแต่ละบรรทัดสั่งตู้ทำอะไร
ท้ายบทคุณจะมีข้อมูลตัวอย่างของวันที่ 2 (ผู้ใช้ 3 คน · บทความ 6 ชิ้น · คอมเมนต์ 7 อัน) อยู่ในเครื่อง พร้อมใช้ในบทที่เหลือทั้งหมด

🔁 ทวนก่อนเริ่ม — คำจากบทก่อน ๆ ที่บทนี้ใช้

🧰 ตรวจด้วย Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 17 (สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — รุ่นแก้บั๊กในสายเดียวกัน ใช้แทนกันได้)

📦 ข้อมูลที่บทนี้ใช้: ครึ่งแรกใช้ตาราง posts ที่มีแค่ title กับ body (แบบเดียวกับบท 99) · ครึ่งหลังเราจะสร้างข้อมูลกลางของวันที่ 2 ที่บท 108–119 ใช้ร่วมกัน

ผลทุกก้อนรันจริงแล้วก็อปมา · เวลา (ms) · วันเวลาในแฟ้ม · เลข 0x… · เลขบรรทัดของ prompt ของคุณจะไม่ตรงกับในบท และจอคุณจะมีสี

CRUD — งานสี่อย่างของคนดูแลสต็อก

ไม่ว่าเว็บไหน งานที่ทำกับข้อมูลมีแค่สี่แบบ: เก็บของใหม่ · หยิบมาดู · แก้ · ทิ้ง
คนในวงการเรียกรวมว่า CRUD (อ่านว่า “ครัด”) ย่อมาจาก Create · Read · Update · Delete
บล็อกของเราก็คือ CRUD ของบทความ: เขียนบทความใหม่ · เปิดอ่าน · แก้คำผิด · ลบทิ้ง

ตัวอักษรภาษาคนคำสั่ง Rails ที่จะได้ลองคำ SQL ที่ console จะโชว์
Createเก็บแฟ้มใหม่ลงลิ้นชักPost.new + save · Post.createINSERT
Readหยิบแฟ้มมาอ่านfind · find_by · where · first · last · all · countSELECT
Updateแก้แฟ้มที่อยู่ในลิ้นชักupdateUPDATE
Deleteทิ้งแฟ้มออกจากลิ้นชักdestroyDELETE

เทียบกับ Go: ในบท 27 คุณเขียน repository (ไฟล์ที่รวมคำสั่งคุยกับฐานข้อมูลไว้ที่เดียว) ที่มี Create · FindByID · FindAll ด้วย GORM — นั่นคือตัว C กับ R ของ CRUD · บทนี้คือของชุดเดียวกันฝั่ง Rails แต่ไม่ต้องเขียน repository เอง เพราะ model ทำให้หมดแล้ว

🗄️ เทียบให้เห็นภาพ — คนดูแลสต็อกมีงานแค่สี่อย่าง

ห้องเก็บของของร้าน มีตู้เก็บแฟ้ม (database) และลิ้นชัก posts · คนดูแลสต็อก (model Post) ทั้งวันทำแค่สี่อย่าง: รับแฟ้มใหม่มาเก็บ · หยิบแฟ้มให้คนที่ขอ · แก้ข้อความบนแฟ้ม · ทิ้งแฟ้มที่ไม่ใช้
ทุกครั้งที่ทำ เขาสั่งตู้ด้วยภาษาเฉพาะของตู้ชื่อ SQL · เราพูด Ruby กับเขา เขาแปลเป็น SQL ให้เอง

ที่ไม่เหมือนกัน: คนจริงทิ้งแฟ้มลงถังขยะ ยังคุ้ยคืนได้ · destroy ใน Rails ลบออกจากตู้จริง ไม่มีถังขยะให้คุ้ย (ยกเว้นเปิด console แบบ --sandbox จากบท 104 ที่ยกเลิกทุกอย่างตอนออก)

🗣 ศัพท์ที่เพิ่งเรียนไป
CRUD
Create · Read · Update · Delete = งานพื้นฐานสี่อย่างกับข้อมูล · หน้าเว็บส่วนใหญ่คือ CRUD ของอะไรสักอย่าง · พิมพ์บอก AI ได้ว่า “build basic CRUD for posts in Rails” แล้วจะได้ของครบสี่อย่าง (บท 118 จะสอนอ่านสิ่งที่ได้มา)
SQL
ภาษาที่ใช้สั่งฐานข้อมูล เช่น PostgreSQL · Active Record แปลคำสั่ง Ruby เป็น SQL ให้ · เราไม่ต้องเขียนเอง แต่ต้องอ่านออก · ถาม senior ได้ว่า “บรรทัดนี้ Active Record ยิง SQL อะไรออกไป”

เปิดครัวทดลอง แล้วถาม Post ว่ารู้อะไรบ้าง

ทำในโปรเจกต์ wongnok_blog เดิมจากบท 106 · เปิด terminal หน้าต่างใหม่ต้อง export DATABASE_URL=… ก่อนทุกครั้ง (แบบบท 105)
ต้องมี model Post กับตาราง posts ก่อน · เช็คด้วย ls app/models ในโฟลเดอร์โปรเจกต์
ถ้ายังไม่มีไฟล์ post.rb ให้สร้างแบบเดียวกับบท 99 (ถ้ามีแล้วจากบท 106 ข้ามได้เลย):

$ bin/rails generate model Post title:string body:text
$ bin/rails db:migrate

เปิด console แล้วถามว่า “ตอนนี้ลิ้นชักมีกี่แผ่น” (ผลจริง):

$ bin/rails console
Loading development environment (Rails 8.1.4)
wongnok-blog(dev):001> Post.count
  Post Count (54.6ms)  SELECT COUNT(*) FROM "posts" /*application='WongnokBlog'*/
=> 0

บรรทัดกลางคือของใหม่ที่ Day 1 ยังไม่เคยเห็น (บท 106 แอบเห็นมาแล้วหนึ่งครั้ง) — console บอกเราว่ามันเพิ่งไปคุยกับตู้มา · แยกทีละส่วน:

📒 เทียบให้เห็นภาพ — สมุดบันทึกการเบิกของ

ทุกครั้งที่คนดูแลสต็อกเดินไปที่ตู้ เขาจดลงสมุดหนึ่งบรรทัด: ใครไป · ไปทำอะไร · ใช้เวลาเท่าไหร่ · พูดกับตู้ว่าอะไร · บรรทัดที่ขึ้นต้นด้วย Post Count (…ms) คือหน้าสมุดเล่มนั้น
อยากรู้ว่าคำสั่ง Ruby หนึ่งบรรทัดทำให้เดินไปตู้กี่รอบ → นับบรรทัดในสมุด

ที่ไม่เหมือนกัน: สมุดจริงต้องมีคนนั่งจด · ที่นี่ Rails จดให้เองทุกครั้งโดยไม่ต้องสั่ง · และจดเป็นภาษา SQL ไม่ใช่ภาษาคน — งานของเราคืออ่านให้ออก (บทนี้จะแปลให้ทุกบรรทัด)

คำถามเรื่องโครงของลิ้นชัก (Post.table_name · Post.column_names · Post.columns_hash) คุณลองไปแล้วในภารกิจบท 106 · บทนี้จึงข้ามไปที่ของใหม่เลย: สั่งให้แฟ้มเข้าออกลิ้นชักจริง แล้วอ่านสมุดบันทึกทุกบรรทัด

🗣 ศัพท์ที่เพิ่งเรียนไป
SQL log (log ของ query)
บรรทัดที่ Rails พิมพ์บอกทุกครั้งที่คุยกับฐานข้อมูล (ใครทำ · ใช้เวลาเท่าไหร่ · SQL ที่ส่งไป) · ในบทนี้เห็นใน console · ตอนรันเว็บจะเห็นในหน้าต่าง server · พิมพ์บอก AI ได้ว่า “here is the SQL log from rails console” แล้วแปะทั้งก้อน — ได้คำตอบตรงกว่าเล่าเป็นคำพูดมาก
COUNT(*)
คำ SQL “นับจำนวนแถว” · ฝั่ง Ruby คือ Post.count · ใช้เช็คเร็ว ๆ ว่ามีข้อมูลอยู่กี่แถวโดยไม่ต้องหยิบออกมาทั้งหมด · ถาม senior ได้ว่า “ตารางนี้ตอนนี้มีกี่ row ใน dev กับใน production”

C — สร้าง: new ยังอยู่ในมือ · save ถึงลงลิ้นชัก

สร้างบทความมีสองจังหวะ: กรอกแฟ้ม กับ เอาแฟ้มเข้าลิ้นชัก
Post.new(...) ทำแค่จังหวะแรก · ลองดู (ผลจริง):

wongnok-blog(dev):002> post = Post.new(title: "ต้มยำกุ้งน้ำข้น", body: "สูตรต้มยำกุ้งแบบร้านดัง")
=>
#<Post:0x0000000127dc4650
...
wongnok-blog(dev):003> post
=>
#<Post:0x0000000127dc4650
 id: nil,
 title: "ต้มยำกุ้งน้ำข้น",
 body: "สูตรต้มยำกุ้งแบบร้านดัง",
 created_at: nil,
 updated_at: nil>
wongnok-blog(dev):004> post.new_record?
=> true
wongnok-blog(dev):005> post.persisted?
=> false
wongnok-blog(dev):006> post.id
=> nil
wongnok-blog(dev):007> Post.count

ก่อนหน้านี้ Post.count ได้ 0 · หลังจาก Post.new(...) ไปหนึ่งชิ้น ตอนนี้จะได้เท่าไหร่?

ผลจริง — ยังเป็น 0:

  Post Count (1.6ms)  SELECT COUNT(*) FROM "posts" /*application='WongnokBlog'*/
=> 0

count ไปนับที่ลิ้นชักจริง · แฟ้มที่ new ไว้ยังอยู่ในมือเรา (ในหน่วยความจำของ console) ไม่ได้อยู่ในลิ้นชัก · ถ้าปิด console ตอนนี้ แฟ้มนี้หายไปเลย ไม่มีใครรู้ว่าเคยมี

จังหวะที่สอง — save คือ “เดินเอาแฟ้มเข้าลิ้นชัก” (ผลจริง):

wongnok-blog(dev):008> post.save
  TRANSACTION (1.4ms)  BEGIN /*application='WongnokBlog'*/
  Post Create (3.1ms)  INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES ('ต้มยำกุ้งน้ำข้น', 'สูตรต้มยำกุ้งแบบร้านดัง', '2026-11-04 12:17:23.004987', '2026-11-04 12:17:23.004987') RETURNING "id" /*application='WongnokBlog'*/
  TRANSACTION (1.7ms)  COMMIT /*application='WongnokBlog'*/
=> true
wongnok-blog(dev):009> post.id
=> 1
wongnok-blog(dev):010> post.new_record?
=> false
wongnok-blog(dev):011> post.persisted?
=> true

สมุดบันทึกมีสามบรรทัด · แปลทีละบรรทัด:

กรอบ BEGIN … COMMIT ชื่อทางการคือ transaction = “ทำทั้งหมด หรือไม่ทำเลย” · เทียบกับ Go: คือ tx, _ := db.Begin() … tx.Commit() ในบท 8 (คู่กับ defer tx.Rollback()) — Rails ครอบให้ทุกครั้งที่ save โดยไม่ต้องเขียนเอง · ทำไมต้องครอบแม้มีคำสั่งเดียว และใช้เองยังไง อยู่บท 111

📝 เทียบให้เห็นภาพ — กรอกแฟ้มในมือ กับ เดินเอาเข้าลิ้นชัก

Post.new = หยิบแฟ้มเปล่ามากรอกข้อความ แล้วถือไว้ในมือ · ยังไม่มีเลขที่แฟ้ม ลิ้นชักไม่รู้ว่ามีแฟ้มนี้ · save = แฟ้มเดินเข้าลิ้นชักเอง (บท 106) ตู้ประทับเลขที่ให้ แล้วแฟ้มก็จำเลขนั้นไว้
new_record? คือถามว่า “ยังถืออยู่ในมือใช่ไหม” · persisted? คือถามว่า “เข้าลิ้นชักแล้วใช่ไหม”

ที่ไม่เหมือนกัน: แฟ้มกระดาษในมือ วางลืมไว้ก็ยังอยู่บนโต๊ะ · แฟ้มที่ new แล้วไม่ได้ save อยู่แค่ในหน่วยความจำ — ปิด console หรือ request จบ ก็หายไปทันทีโดยไม่มีอะไรเตือน

ถ้าไม่ต้องการถือแฟ้มไว้ก่อน มีคำสั่งที่ทำสองจังหวะในคำเดียว — Post.create = new แล้ว save ทันที (ผลจริง):

wongnok-blog(dev):012> Post.create(title: "ส้มตำปูปลาร้า", body: "ตำให้นัวต้องใส่อะไร")
  TRANSACTION (2.4ms)  BEGIN /*application='WongnokBlog'*/
  Post Create (4.8ms)  INSERT INTO "posts" ("title", "body", "created_at", "updated_at") VALUES ('ส้มตำปูปลาร้า', 'ตำให้นัวต้องใส่อะไร', '2026-11-04 12:17:24.245476', '2026-11-04 12:17:24.245476') RETURNING "id" /*application='WongnokBlog'*/
  TRANSACTION (2.4ms)  COMMIT /*application='WongnokBlog'*/
=>
#<Post:0x0000000124efb7d8
 id: 2,
 title: "ส้มตำปูปลาร้า",
 body: "ตำให้นัวต้องใส่อะไร",
 created_at: "2026-11-04 12:17:24.245476000 +0000",
 updated_at: "2026-11-04 12:17:24.245476000 +0000">
wongnok-blog(dev):013> Post.count
  Post Count (2.1ms)  SELECT COUNT(*) FROM "posts" /*application='WongnokBlog'*/
=> 2

⚠️ บทนี้ใช้ bin/rails console ธรรมดา ไม่ใช่ --sandbox เพราะอยากให้ข้อมูลลงตู้จริง · ถ้าเปิดแบบ --sandbox (บท 104) คุณจะเห็นคำว่า SAVEPOINT กับ RELEASE SAVEPOINT แทน COMMIT (ผมลองแล้ว) — ไม่ได้พัง แค่หน้าตาไม่ตรงกับในบท เพราะ sandbox ครอบกรอบงานใหญ่ไว้อีกชั้นเพื่อยกเลิกทุกอย่างตอนออก

🗣 ศัพท์ที่เพิ่งเรียนไป
INSERT · RETURNING
คำ SQL สำหรับ “ใส่แถวใหม่” · RETURNING "id" = ขอเลขที่ได้กลับมา (ของ PostgreSQL) · ถาม senior ได้ว่า “save หนึ่งครั้งยิง INSERT กี่ครั้ง ดูจาก log ตรงไหน”
transaction (BEGIN / COMMIT)
กรอบงาน “ทำทั้งหมดหรือไม่ทำเลย” · BEGIN = เริ่ม · COMMIT = ยืนยัน · ROLLBACK = ยกเลิกทั้งกรอบ · พิมพ์บอก AI ได้ว่า “wrap these two saves in a transaction” (เรื่องเต็มบท 111)
new_record? · persisted?
ถามสถานะของแฟ้ม: ยังอยู่ในมือ / อยู่ในลิ้นชักแล้ว · ใช้ตอนงงว่า “ตกลง save ไปหรือยัง” · ถาม senior ได้ว่า “ตรงนี้ object persisted แล้วหรือยัง” — คนทำ Rails ใช้คำนี้คุยกันจริง

R — อ่าน: find · find_by · where · first · last · all

ตอนนี้ลิ้นชักมีสองแผ่น · มีหลายวิธีขอแฟ้มจากคนดูแลสต็อก ต่างกันที่บอกเงื่อนไขยังไง และได้กลับมากี่แผ่น
เริ่มจากขอตามเลขที่แฟ้ม (ผลจริง):

wongnok-blog(dev):014> Post.find(1)
  Post Load (2.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = 1 LIMIT 1 /*application='WongnokBlog'*/
=>
#<Post:0x0000000124c90a20
 id: 1,
 title: "ต้มยำกุ้งน้ำข้น",
 body: "สูตรต้มยำกุ้งแบบร้านดัง",
 created_at: "2026-11-04 12:17:23.004987000 +0000",
 updated_at: "2026-11-04 12:17:23.004987000 +0000">

SQL บรรทัดนี้คือแม่แบบของการอ่านเกือบทั้งหมด · อ่านทีละท่อน:

→ รวมกัน: “ขอแฟ้มทุกช่องจากลิ้นชัก posts เฉพาะแผ่นเลขที่ 1 เอามาแผ่นเดียว”

ขอตามช่องอื่น และขอแบบรายการ (ผลจริง):

wongnok-blog(dev):015> Post.find_by(title: "ส้มตำปูปลาร้า")
  Post Load (2.4ms)  SELECT "posts".* FROM "posts" WHERE "posts"."title" = 'ส้มตำปูปลาร้า' LIMIT 1 /*application='WongnokBlog'*/
=>
#<Post:0x0000000124808fc0
 id: 2,
 title: "ส้มตำปูปลาร้า",
 …
wongnok-blog(dev):016> Post.where(title: "ส้มตำปูปลาร้า")
  Post Load (2.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."title" = 'ส้มตำปูปลาร้า' /* loading for pp */ LIMIT 11 /*application='WongnokBlog'*/
=>
[#<Post:0x000000012454f3d8
  id: 2,
  title: "ส้มตำปูปลาร้า",
  …

อีกสามคำที่ใช้บ่อย (ผลจริง ตัดเหลือบรรทัดที่สำคัญ):

wongnok-blog(dev):017> Post.first
  Post Load (3.2ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" ASC LIMIT 1 /*application='WongnokBlog'*/
=>
#<Post:0x00000001051b52a0
 id: 1,
 …
wongnok-blog(dev):018> Post.last
  Post Load (2.6ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" DESC LIMIT 1 /*application='WongnokBlog'*/
=>
#<Post:0x0000000127caed88
 id: 2,
 …
wongnok-blog(dev):019> Post.all
  Post Load (2.2ms)  SELECT "posts".* FROM "posts" /* loading for pp */ LIMIT 11 /*application='WongnokBlog'*/
=>
[#<Post:0x0000000127c0b408
  id: 1,
  …
 #<Post:0x0000000127c0b2c8
  id: 2,
  …

ถ้าหาไม่เจอ — สามคำตอบที่ต่างกัน

ลองขอของที่ไม่มีในลิ้นชัก ด้วยสามวิธี (ผลจริง):

wongnok-blog(dev):020> Post.find(99)
  Post Load (2.7ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = 99 LIMIT 1 /*application='WongnokBlog'*/
(wongnok-blog):20:in '<main>': Couldn't find Post with 'id'=99 (ActiveRecord::RecordNotFound)
wongnok-blog(dev):021> Post.find_by(title: "ไม่มีเรื่องนี้")
  Post Load (2.5ms)  SELECT "posts".* FROM "posts" WHERE "posts"."title" = 'ไม่มีเรื่องนี้' LIMIT 1 /*application='WongnokBlog'*/
=> nil
wongnok-blog(dev):022> Post.where(title: "ไม่มีเรื่องนี้")
  Post Load (1.8ms)  SELECT "posts".* FROM "posts" WHERE "posts"."title" = 'ไม่มีเรื่องนี้' /* loading for pp */ LIMIT 11 /*application='WongnokBlog'*/
=> []
🙋 เทียบให้เห็นภาพ — ถามคนดูแลสต็อกสามแบบ

find(99) = “เอาแฟ้มเลขที่ 99 มาให้ที” · คนดูแลสต็อกถือว่าคุณมั่นใจว่ามี — ไม่มีก็โวยวายเสียงดัง ให้รู้ว่ามีอะไรผิดปกติ
find_by(…) = “มีแฟ้มชื่อนี้ไหม ถ้ามีเอาแผ่นแรกมา” · ไม่มีก็ยักไหล่ตอบ “ไม่มีครับ” (nil)
where(…) = “เอาทุกแผ่นที่ตรงเงื่อนไขใส่ตะกร้ามา” · ไม่มีก็ยื่นตะกร้าเปล่ามาให้ ([])

ที่ไม่เหมือนกัน: คนจริงตอบ “ไม่มีครับ” แล้วเราก็รู้ทันที · แต่ nil ไม่ร้องเตือน ถ้าเราไม่เช็คแล้วเอาไปใช้ต่อ เช่น .title จะพังทีหลังด้วย NoMethodError ในบรรทัดที่ห่างจากต้นเหตุ (เจอจริงในภารกิจท้ายบท)

เทียบกับ Go: บท 27 เจอเรื่องเดียวกันใน GORM — db.First(&missing, 9999) คืน gorm.ErrRecordNotFound (ตรงกับ find) · ส่วน db.Where(…).Find(&list) ไม่ถือว่าเป็น error แค่ได้ slice ว่าง (ตรงกับ where) · ต่างกันที่ Go คืน error มาให้เช็ค ส่วน Ruby โยน error ออกมา (rescue บท 96)

🗣 ศัพท์ที่เพิ่งเรียนไป
SELECT · WHERE · ORDER BY · LIMIT
สี่คำที่จะเจอในทุก query อ่าน: ขอช่องไหน · เฉพาะแผ่นที่… · เรียงตาม… · เอามากี่แผ่น · ถาม senior ได้ว่า “query นี้มี WHERE ด้วยช่องไหน ช่องนั้นมี index ไหม” (index คือสารบัญของลิ้นชัก บท 109)
ActiveRecord::RecordNotFound
error ของ find เมื่อหาไม่เจอ · ใน controller Rails แปลงเป็นหน้า 404 ให้ในเว็บจริง · พิมพ์บอก AI ได้ว่า “use find_by and handle nil instead of find here” ถ้าการไม่เจอเป็นเรื่องปกติของหน้านั้น

หน้าค้นหาบทความ: ผู้ใช้พิมพ์ชื่อเรื่องมา ซึ่งอาจไม่มีอยู่จริง · ถ้าไม่เจอ อยากโชว์ข้อความ “ไม่พบบทความ” แทนที่หน้าเว็บจะพัง ควรใช้คำสั่งไหน?

ตัวเลือกที่ 2 ถูก — find_by ค้นตามช่องไหนก็ได้ ไม่เจอก็ตอบ nil เงียบ ๆ (ผลจริง: Post.find_by(title: "ไม่มีเรื่องนี้") → => nil) · เราเช็ค nil แล้วโชว์ข้อความเองได้

ตัวเลือกที่ 1 ผิด เพราะ find รับเลขที่แฟ้ม ไม่ใช่ชื่อเรื่อง · ไม่เจอมันระเบิดเป็น RecordNotFound ไม่เคยคืน nil (ผมลองส่งชื่อเรื่องเข้าไป ได้ RecordNotFound จริง) · ตัวเลือกที่ 3 ผิด เพราะ first รับได้แค่ “จำนวนแผ่น” เช่น first(3) ไม่รับเงื่อนไข · ลองแล้วได้ TypeError · ตัวเลือกที่ 4 ผิด เพราะ count คิดว่าคำที่ส่งไปเป็นชื่อช่อง · ลองแล้วพังเป็น PG::UndefinedColumn และต่อให้ไม่พังก็ได้แค่ตัวเลข ไม่ได้ตัวบทความไปโชว์

จุดที่ต้องจำ: มั่นใจว่ามีแน่ (เลขที่จากลิงก์) → find · อาจไม่มีก็ได้ → find_by แล้วเช็ค nil · อยากได้หลายแผ่น → where

U กับ D — แก้ด้วย update · ทิ้งด้วย destroy

จะแก้หรือทิ้ง ต้องหยิบแฟ้มแผ่นนั้นออกมาก่อน แล้วสั่งที่ตัวแฟ้ม (ผลจริง):

wongnok-blog(dev):023> post = Post.find(1)
  Post Load (2.7ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = 1 LIMIT 1 /*application='WongnokBlog'*/
=>
#<Post:0x0000000127c05008
...
wongnok-blog(dev):024> post.update(title: "ต้มยำกุ้งน้ำใส")
  TRANSACTION (2.0ms)  BEGIN /*application='WongnokBlog'*/
  Post Update (3.9ms)  UPDATE "posts" SET "title" = 'ต้มยำกุ้งน้ำใส', "updated_at" = '2026-11-04 12:17:28.032069' WHERE "posts"."id" = 1 /*application='WongnokBlog'*/
  TRANSACTION (3.6ms)  COMMIT /*application='WongnokBlog'*/
=> true
wongnok-blog(dev):025> post.title
=> "ต้มยำกุ้งน้ำใส"

ทิ้งแฟ้มแผ่นเดิม (ผลจริง):

wongnok-blog(dev):026> post.destroy
  TRANSACTION (1.4ms)  BEGIN /*application='WongnokBlog'*/
  Post Destroy (2.6ms)  DELETE FROM "posts" WHERE "posts"."id" = 1 /*application='WongnokBlog'*/
  TRANSACTION (1.4ms)  COMMIT /*application='WongnokBlog'*/
=>
#<Post:0x0000000127c05008
 id: 1,
 title: "ต้มยำกุ้งน้ำใส",
 …
wongnok-blog(dev):027> Post.count
  Post Count (1.5ms)  SELECT COUNT(*) FROM "posts" /*application='WongnokBlog'*/
=> 1
wongnok-blog(dev):028> post.persisted?
wongnok-blog(dev):029> post.title
wongnok-blog(dev):030> Post.find(1)

แฟ้มเลขที่ 1 เพิ่งถูกทิ้งไป · ตัวแปร post ยังอยู่ใน console · สามบรรทัดนี้จะได้อะไร?

ผลจริง:

wongnok-blog(dev):028> post.persisted?
=> false
wongnok-blog(dev):029> post.title
=> "ต้มยำกุ้งน้ำใส"
wongnok-blog(dev):030> Post.find(1)
  Post Load (2.5ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = 1 LIMIT 1 /*application='WongnokBlog'*/
(wongnok-blog):30:in '<main>': Couldn't find Post with 'id'=1 (ActiveRecord::RecordNotFound)

ตัวแปร post เหมือนสำเนาของแฟ้มที่ถือค้างไว้ในมือ · ทิ้งแฟ้มตัวจริงในลิ้นชักไปแล้ว สำเนาในมือก็ยังอ่านได้ (title ยังอยู่) · แต่มันรู้ตัวว่าไม่ได้อยู่ในลิ้นชักแล้ว (persisted? เป็น false) · ถ้าไปขอจากลิ้นชักใหม่ด้วย find(1) จึงไม่เจอ

เทียบกับ Go: GORM ใช้ db.Model(&p).Update("Title", "ต้มยำกุ้งน้ำใส") และ db.Delete(&p) (บท 27 เคยเห็น Update/Delete แบบนี้) — ต้องเรียกผ่าน db แล้วส่ง pointer ของ struct (&p) เข้าไปทุกครั้ง · Rails สั่งที่ตัวแฟ้มตรง ๆ post.update(…) · post.destroy เพราะแฟ้มรู้ทางไปลิ้นชักเอง (บท 106)

🗣 ศัพท์ที่เพิ่งเรียนไป
UPDATE … SET … WHERE · DELETE FROM … WHERE
คำ SQL สำหรับแก้และลบ · WHERE ตัวท้ายคือตัวกำหนดว่าโดนกี่แผ่น — ถ้าไม่มี WHERE = โดนทั้งลิ้นชัก · ถาม senior ได้ว่า “ก่อนรัน UPDATE/DELETE เองใน production ทีมนี้ตรวจกันยังไง”
dirty tracking (Rails จำว่าช่องไหนเปลี่ยน)
เหตุผลที่ UPDATE มีแค่ช่องที่เปลี่ยน · Rails จำค่าเดิมของแต่ละช่องไว้ · สไลด์หน้า 8 มี post.changed? ไว้ถามว่ามีช่องไหนเปลี่ยนแต่ยังไม่ save · พิมพ์บอก AI ได้ว่า “only update the title column, don't touch body” — แล้วตรวจจาก SQL log ว่าจริงไหม

เทียบกับ GORM ทั้งชุด

สไลด์หน้า 10 ให้จับคู่ GORM กับ Active Record · รวมของทั้งบทไว้ตารางเดียว (ฝั่ง Go มาจากบท 27):

อยากทำGo · GORMRuby · Active Record
สร้างแล้วเก็บdb.Create(&p)post.save · Post.create(…)
หยิบตามเลขที่db.First(&p, 1)Post.find(1)
หยิบตามเงื่อนไขdb.Where("title = ?", x).Find(&list)Post.where(title: x)
หยิบทั้งหมดdb.Find(&list)Post.all
แก้db.Model(&p).Update("Title", x)post.update(title: x)
ลบdb.Delete(&p)post.destroy
หาตามเลขที่ไม่เจอFirst คืน gorm.ErrRecordNotFoundfind โยน ActiveRecord::RecordNotFound
ชื่อตารางstruct Post → ตาราง posts เอง · ชื่อไม่ตามกติกาค่อยเขียน method TableName()class Post → ลิ้นชัก posts เอง · ชื่อไม่ตามกติกาค่อยเขียน self.table_name = "…"
ช่องในตารางเขียนเองใน struct (หรือ embed gorm.Model ได้ ID + เวลา)ไม่ต้องเขียนใน class · Rails อ่านจากตารางจริงตอนรัน

ใน GORM ถ้าอยากใช้ชื่อตารางแปลก ๆ ต้องเขียน method TableName() · ใน Rails ถ้า class ชื่อ Post และตารางชื่อ posts (ตามกติกา) ต้องเขียนอะไรเพิ่มใน post.rb?

ตัวเลือกที่ 4 ถูก — ผลจริง: post.rb มีแค่ class Post < ApplicationRecord กับ end แต่ Post.table_name ได้ => "posts" · นี่คือ Convention over Configuration (ร้านแฟรนไชส์ ทุกสาขาวางของที่เดิม)

ตัวเลือกที่ 1 ผิด เพราะเขียนได้แต่ไม่จำเป็น — ใช้เฉพาะตอนชื่อตารางไม่ตามกติกา เช่นต่อกับฐานข้อมูลเก่าที่ตั้งชื่อไว้แล้ว · ตัวเลือกที่ 2 ผิด เพราะชื่อตารางเป็นเรื่องของทั้ง class ไม่ใช่ของแฟ้มแต่ละแผ่น · method แบบนี้จึงไม่ได้ช่วยให้ Post.table_name เปลี่ยน · ตัวเลือกที่ 3 ผิด เพราะบรรทัดนั้นบอกแค่ว่าสืบทอดจากใคร Ruby ไม่ได้มีที่ให้ใส่ชื่อตาราง

จุดที่ต้องจำ: ตั้งชื่อตามกติกา = ไม่ต้องเขียนอะไร · self.table_name มีไว้สำหรับกรณีพิเศษเท่านั้น (ทั้งสองภาษาคิดแบบเดียวกัน)

เพื่อนแปะ log มาให้ดูหนึ่งบรรทัด (ตารางของเพื่อนมีช่อง views = ยอดวิว ซึ่งเราจะเพิ่มกันในหัวข้อถัดไป): UPDATE "posts" SET "views" = 121, "updated_at" = '…' WHERE "posts"."id" = 1 — คำสั่ง Ruby ที่ทำให้เกิดบรรทัดนี้น่าจะเป็นอันไหน?

ตัวเลือกที่ 3 ถูก — UPDATE … SET "views" = 121 … WHERE "posts"."id" = 1 แปลว่า “แก้แฟ้มแผ่นเลขที่ 1 ให้ช่อง views เป็น 121” · ตรงกับ update บนแฟ้มเลขที่ 1

ตัวเลือกที่ 1 ผิด เพราะสร้างใหม่จะเห็น INSERT INTO ไม่ใช่ UPDATE · ตัวเลือกที่ 2 ผิด เพราะการหาจะเห็น SELECT … WHERE "posts"."views" = 121 · ไม่มีอะไรถูกแก้ · ตัวเลือกที่ 4 ผิด เพราะการทิ้งจะเห็น DELETE FROM และเลข 121 จะอยู่หลัง "id" =

จุดที่ต้องจำ: อ่านคำแรกของ SQL ก่อน — INSERT สร้าง · SELECT อ่าน · UPDATE แก้ · DELETE ลบ · แล้วดู WHERE ว่าโดนแผ่นไหน

เตรียมตู้ของวันที่ 2 — ข้อมูลตัวอย่างที่ทุกบทหลังจากนี้ใช้

ตั้งแต่บท 108 ถึง 119 ทุกบทใช้ข้อมูลชุดเดียวกัน: ผู้ใช้ 3 คน (สมชาย · สมหญิง · มานะ) · บทความ 6 ชิ้น · คอมเมนต์ 7 อัน · ถ้าคุณมีชุดเดียวกันในเครื่อง ผลที่พิมพ์ตามบทจะตรงกับในบท
ตอนนี้ทำตามไปก่อน · ทุกคำสั่งในหัวข้อนี้มีคำอธิบายเต็มในบท 108 (migration) และบท 115 (ความสัมพันธ์ระหว่างตาราง)

⚠️ ขั้นที่ 6 ล้างข้อมูลทั้งหมดในฐานข้อมูลของแอปนี้ (ในเครื่องคุณ) แล้วสร้างใหม่ · ถ้าในแอปนี้มีข้อมูลที่อยากเก็บ ให้ทำในแอปใหม่ที่สร้างด้วย rails new แทน

ขั้นที่ 1 · ปิด console ก่อน — พิมพ์ exit · ถ้าเปิดค้างไว้ ขั้นที่ 6 จะพัง (ดู error ในภารกิจ)

ขั้นที่ 2 · สั่งสร้างใบสั่งช่างอีก 4 ใบ พิมพ์ทีละบรรทัดในโฟลเดอร์โปรเจกต์ (ผลจริง · เลขเวลาหน้าชื่อไฟล์ของคุณจะต่าง):

$ bin/rails generate migration AddPublishedAndViewsToPosts published:boolean views:integer
      invoke  active_record
      create    db/migrate/20261104114643_add_published_and_views_to_posts.rb
$ bin/rails generate model User name:string email:string
      invoke  active_record
      create    db/migrate/20261104114645_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
$ bin/rails generate migration AddUserToPosts user:references
      invoke  active_record
      create    db/migrate/20261104114646_add_user_to_posts.rb
$ bin/rails generate model Comment body:text post:references user:references
      invoke  active_record
      create    db/migrate/20261104114648_create_comments.rb
      create    app/models/comment.rb
      invoke    test_unit
      create      test/models/comment_test.rb
      create      test/fixtures/comments.yml

ขั้นที่ 3 · แก้ใบสั่งใบแรกให้มีค่าเริ่มต้น — เปิดไฟล์ db/migrate/…_add_published_and_views_to_posts.rb แล้วเติมท้ายสองบรรทัดให้เป็นแบบนี้:

# db/migrate/…_add_published_and_views_to_posts.rb
class AddPublishedAndViewsToPosts < ActiveRecord::Migration[8.1]
  def change
    add_column :posts, :published, :boolean, default: false, null: false
    add_column :posts, :views, :integer, default: 0, null: false
  end
end

ขั้นที่ 4 · บอก model ว่าใครเป็นของใคร — comment.rb generator เขียนให้ครบแล้ว · อีกสองไฟล์แก้ให้เป็นแบบนี้:

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :comments
end
# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
  has_many :comments
end
# app/models/comment.rb — generator เขียนให้แล้ว แค่เปิดดู
class Comment < ApplicationRecord
  belongs_to :post
  belongs_to :user
end

ขั้นที่ 5 · ใส่ข้อมูลตัวอย่าง — เปิดไฟล์ db/seeds.rb (มีอยู่แล้วตั้งแต่ rails new) ลบของเดิมทิ้งทั้งไฟล์ แล้ววางอันนี้แทน:

# ข้อมูลตัวอย่างของ wongnok_blog (Day 2)
somchai = User.create!(name: "สมชาย", email: "somchai@example.com")
somying = User.create!(name: "สมหญิง", email: "somying@example.com")
mana    = User.create!(name: "มานะ",   email: "mana@example.com")

p1 = somchai.posts.create!(title: "ต้มยำกุ้งน้ำข้น", body: "สูตรต้มยำกุ้งแบบร้านดัง", published: true,  views: 120)
p2 = somchai.posts.create!(title: "ส้มตำปูปลาร้า",   body: "ตำให้นัวต้องใส่อะไร",     published: true,  views: 340)
p3 = somying.posts.create!(title: "ข้าวมันไก่ประตูน้ำ", body: "ร้านเด็ดย่านประตูน้ำ",  published: true,  views: 85)
p4 = somying.posts.create!(title: "ก๋วยเตี๋ยวเรืออยุธยา", body: "ยังเขียนไม่เสร็จ",     published: false, views: 0)
p5 = mana.posts.create!(title: "ผัดกะเพราหมูสับ",    body: "ไข่ดาวต้องกรอบ",          published: true,  views: 510)
p6 = mana.posts.create!(title: "แกงเขียวหวานไก่",    body: "ร่างไว้ก่อน",              published: false, views: 0)

p1.comments.create!(user: somying, body: "น่ากินมาก")
p1.comments.create!(user: mana,    body: "ขอสูตรน้ำพริกเผาด้วย")
p2.comments.create!(user: mana,    body: "เผ็ดแค่ไหนดี")
p3.comments.create!(user: somchai, body: "ร้านนี้คิวยาวไหม")
p5.comments.create!(user: somchai, body: "ไข่ดาวกรอบคือที่สุด")
p5.comments.create!(user: somying, body: "ใส่ถั่วฝักยาวด้วยไหม")
p5.comments.create!(user: mana,    body: "ขอบคุณทุกคนที่อ่าน")

ขั้นที่ 6 · ให้ช่างสร้างตู้ใหม่ทั้งตู้ แล้วใส่ข้อมูลตัวอย่าง (ผลจริง ตัดกลางเป็น …):

$ bin/rails db:migrate:reset
Dropped database 'a107'
Created database 'a107'
== 20261104123046 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0019s
== 20261104123046 CreatePosts: migrated (0.0019s) =============================
…
== 20261104123156 CreateComments: migrating ===================================
-- create_table(:comments)
   -> 0.0039s
== 20261104123156 CreateComments: migrated (0.0039s) ==========================

$ bin/rails db:seed
$ bin/rails runner 'p [User.count, Post.count, Comment.count]'
[3, 6, 7]
🔨 เทียบให้เห็นภาพ — รื้อตู้ทั้งตู้แล้วต่อใหม่ตามแฟ้มใบสั่งช่าง

ร้านเก็บใบสั่งช่างทุกใบไว้ในแฟ้ม (db/migrate/) ตั้งแต่วันเปิดร้าน · db:migrate:reset คือยกตู้เก่าทิ้ง ยกตู้เปล่ามาตั้ง แล้วยื่นแฟ้มใบสั่งทั้งหมดให้ช่างทำตามทีละใบ ได้ตู้หน้าตาเดียวกับตู้ล่าสุดเป๊ะ · db:seed คือเอาของตัวอย่างใส่ตู้ ให้มีอะไรหยิบลองเล่น

ที่ไม่เหมือนกัน: ช่างจริงรื้อตู้แล้ว แฟ้มเก่ายังกองอยู่ในกล่องให้ย้ายกลับ · db:migrate:reset ทิ้งข้อมูลทั้งหมดจริง ไม่มีกล่องพักของ — ทำได้สบายใจกับแอปฝึกในเครื่อง แต่ห้ามสั่งกับฐานข้อมูลที่มีข้อมูลจริง

ลองของใหม่ที่ได้มา · เปิด bin/rails console ใหม่ แล้วลองสร้างบทความแบบที่ทำได้ในครึ่งแรก (ผลจริง):

wongnok-blog(dev):001> post = Post.new(title: "ข้าวซอยไก่", body: "ของดีเชียงใหม่")
=>
#<Post:0x0000000122946d58
...
wongnok-blog(dev):002> post.save
  TRANSACTION (2.0ms)  BEGIN /*application='WongnokBlog'*/
  TRANSACTION (0.8ms)  ROLLBACK /*application='WongnokBlog'*/
=> false
wongnok-blog(dev):003> post.errors.full_messages
=> ["User must exist"]
wongnok-blog(dev):004> post.id
=> nil

บอกว่าใครเป็นคนเขียน แล้ว save ใหม่ (ผลจริง):

wongnok-blog(dev):005> somchai = User.find_by(name: "สมชาย")
  User Load (1.6ms)  SELECT "users".* FROM "users" WHERE "users"."name" = 'สมชาย' LIMIT 1 /*application='WongnokBlog'*/
=>
#<User:0x0000000122fa5910
...
wongnok-blog(dev):006> post.user = somchai
=>
#<User:0x0000000122fa5910
...
wongnok-blog(dev):007> post.save
  TRANSACTION (1.3ms)  BEGIN /*application='WongnokBlog'*/
  Post Create (3.2ms)  INSERT INTO "posts" ("title", "body", "created_at", "updated_at", "published", "views", "user_id") VALUES ('ข้าวซอยไก่', 'ของดีเชียงใหม่', '2026-11-04 12:17:34.499959', '2026-11-04 12:17:34.499959', FALSE, 0, 1) RETURNING "id" /*application='WongnokBlog'*/
  TRANSACTION (1.5ms)  COMMIT /*application='WongnokBlog'*/
=> true
wongnok-blog(dev):008> post.id
=> 7
wongnok-blog(dev):009> post.user.name
=> "สมชาย"
🗣 ศัพท์ที่เพิ่งเรียนไป
seeds · db:seed
ข้อมูลตัวอย่างที่เขียนเป็นโค้ดไว้ใน db/seeds.rb ให้ทุกคนในทีมได้ข้อมูลชุดเดียวกันด้วยคำสั่งเดียว · ถาม senior ได้ว่า “seeds ของโปรเจกต์นี้รันซ้ำได้ไหม หรือต้อง reset ก่อน”
db:migrate:reset
ลบฐานข้อมูลทิ้ง สร้างใหม่ แล้วรัน migration ทุกไฟล์ตั้งแต่ไฟล์แรก · ใช้กับเครื่องตัวเองเท่านั้น (คำสั่งตระกูล reset อื่น ๆ บท 108) · พิมพ์บอก AI ได้ว่า “my dev database is messed up, how do I rebuild it from migrations and seeds”
belongs_to · foreign key
belongs_to :user = แฟ้มนี้เป็นของผู้ใช้หนึ่งคน · ช่อง user_id ที่ชี้ไปหาแฟ้มในลิ้นชักอื่นเรียกว่า foreign key (บท 115) · ถาม senior ได้ว่า “belongs_to ตัวนี้ควรเป็น optional ไหม” (optional = ยอมให้ไม่มีเจ้าของได้) — เจอ User must exist เมื่อไหร่ ให้นึกถึงคำนี้

หลังเพิ่ม belongs_to :user ใน Post แล้ว · post.save ตอบ => false และ full_messages ได้ ["User must exist"] · ข้อไหนถูก?

ตัวเลือกที่ 1 ถูก — ผลจริงหลัง post.save ไม่มีบรรทัด INSERT เลย · คนดูแลสต็อกตรวจแล้วไม่รับตั้งแต่ก่อนใส่ลิ้นชัก · post.id จึงยังเป็น nil

ตัวเลือกที่ 2 ผิด เพราะ false แปลว่าไม่ได้เก็บ และช่อง user_id ถูกตั้งห้ามว่างไว้ด้วย · ตัวเลือกที่ 3 ผิด เพราะไม่มี INSERT ถูกส่งไปให้ตู้ปฏิเสธเลย — ROLLBACK ที่อาจเห็นคือ Rails ยกเลิกกรอบงานเองหลังตรวจแฟ้มไม่ผ่าน ไม่ใช่ตู้ปฏิเสธ · ตัวเลือกที่ 4 ผิด เพราะ console ยังใช้ได้ปกติ ใส่ post.user = … แล้ว save ใหม่ก็ได้ true

จุดที่ต้องจำ: save ตอบ false = ไม่ได้เก็บ · ถามเหตุผลด้วย errors.full_messages · ดู log ว่ามี INSERT หรือเปล่า

🎯 ภารกิจ — CRUD ครบวงบนข้อมูลของวันที่ 2 (~10 นาที)

ทำหลังเตรียมตู้ครบ 6 ขั้นแล้ว · ข้อ 1 ห้ามข้าม เพราะบทความ “ข้าวซอยไก่” ที่เพิ่งลองสร้างกินเลขที่ 7 ไปแล้ว:

  1. ล้างตู้ก่อน: exit ออกจาก console · สั่งขั้นที่ 6 ซ้ำทั้งสองบรรทัด (bin/rails db:migrate:reset แล้ว bin/rails db:seed) · เปิด bin/rails console ใหม่ แล้วพิมพ์ข้อถัดไปทีละบรรทัด
  2. Post.count และ User.count — เช็คว่าข้อมูลตัวอย่างครบ
  3. Post.find_by(title: "ผัดกะเพราหมูสับ").views — บทความนี้มีคนอ่านกี่ครั้ง
  4. Post.where(published: false).count — มีบทความที่ยังไม่เผยแพร่กี่ชิ้น
  5. mana = User.find_by(name: "มานะ") แล้ว post = Post.create(title: "ข้าวผัดปู", body: "ใส่ปูเยอะ ๆ", user: mana) แล้ว post.id
  6. post.update(title: "ข้าวผัดปูก้อน") — ดู SQL ว่ามีช่องไหนบ้าง
  7. post.destroy · Post.count · แล้ว Post.find(7)

ก่อนกดเฉลย: ข้อ 4 ได้เท่าไหร่ (ย้อนดูข้อมูลตัวอย่าง) · ข้อ 5 post.id เป็นเลขอะไร · และข้อ 7 บรรทัดสุดท้ายได้อะไร?

สิ่งที่ควรเห็น (ผลจริง ตัดผลยาวเป็น …):

wongnok-blog(dev):001> Post.count
  Post Count (41.1ms)  SELECT COUNT(*) FROM "posts" /*application='WongnokBlog'*/
=> 6
wongnok-blog(dev):002> User.count
  User Count (2.6ms)  SELECT COUNT(*) FROM "users" /*application='WongnokBlog'*/
=> 3
wongnok-blog(dev):003> Post.find_by(title: "ผัดกะเพราหมูสับ").views
  Post Load (1.0ms)  SELECT "posts".* FROM "posts" WHERE "posts"."title" = 'ผัดกะเพราหมูสับ' LIMIT 1 /*application='WongnokBlog'*/
=> 510
wongnok-blog(dev):004> Post.where(published: false).count
  Post Count (1.8ms)  SELECT COUNT(*) FROM "posts" WHERE "posts"."published" = FALSE /*application='WongnokBlog'*/
=> 2
…
wongnok-blog(dev):007> post.id
=> 7
wongnok-blog(dev):008> post.update(title: "ข้าวผัดปูก้อน")
  TRANSACTION (1.4ms)  BEGIN /*application='WongnokBlog'*/
  Post Update (3.0ms)  UPDATE "posts" SET "title" = 'ข้าวผัดปูก้อน', "updated_at" = '2026-11-04 11:50:49.801073' WHERE "posts"."id" = 7 /*application='WongnokBlog'*/
  TRANSACTION (1.7ms)  COMMIT /*application='WongnokBlog'*/
=> true
…
wongnok-blog(dev):010> Post.count
  Post Count (1.4ms)  SELECT COUNT(*) FROM "posts" /*application='WongnokBlog'*/
=> 6
wongnok-blog(dev):011> Post.find(7)
  Post Load (1.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = 7 LIMIT 1 /*application='WongnokBlog'*/
(wongnok-blog):11:in '<main>': Couldn't find Post with 'id'=7 (ActiveRecord::RecordNotFound)
  • ข้อ 2 — บทความ 6 · ผู้ใช้ 3 · ข้อ 3 — ยอดวิวผัดกะเพรา 510
  • ข้อ 4 — 2 (ก๋วยเตี๋ยวเรือ กับ แกงเขียวหวาน) · SQL มี WHERE "posts"."published" = FALSE = “นับเฉพาะแผ่นที่ช่อง published เป็นเท็จ”
  • ข้อ 5 — 7 · ถ้าข้ามข้อ 1 จะได้ 8 หรือมากกว่า · ตู้ไม่เอาเลขที่เคยแจกไปแล้วกลับมาแจกซ้ำ แม้แฟ้มนั้นจะถูกทิ้งไปแล้ว (ผมลองทิ้งเลข 7 แล้วสร้างใหม่ ได้ 8)
  • ข้อ 6 — SQL มีแค่ title กับ updated_at ไม่มี body
  • ข้อ 7 — RecordNotFound เพราะทิ้งไปแล้ว · Post.count กลับมาเป็น 6 เท่าเดิม ✅ ข้อมูลพร้อมสำหรับบท 108

ถ้าเจอ error นี้ แปลว่า…

  • PG::NotNullViolation: ERROR: column "user_id" of relation "posts" contains null values ตอนเตรียมตู้ — คุณสั่ง bin/rails db:migrate แทน db:migrate:reset · บทความจากครึ่งแรกไม่มีเจ้าของ ใบสั่งที่ 3 ห้ามช่องนี้ว่างเลยพัง (ผมลองแล้ว ได้ข้อความนี้จริง) → สั่งขั้นที่ 6 ตามที่เขียน
  • PG::ObjectInUse: ERROR: database "…" is being accessed by other users — ยังมี console (หรือ bin/rails server) เปิดค้างอยู่ จึงทิ้งตู้ไม่ได้ → exit ทุกหน้าต่างแล้วสั่งใหม่
  • Post.count ได้ 12 และ User.count ได้ 6 — สั่ง db:seed ไปสองรอบ ข้อมูลเลยเบิ้ล (ผมลองแล้ว ได้ [6, 12, 14]) → สั่งขั้นที่ 6 ใหม่ทั้งสองบรรทัด
  • ข้อ 5 ได้ post.id เป็น nil — พิมพ์ชื่อผิด เช่น "มานา" · find_by ตอบ nil เงียบ ๆ แล้ว create ไม่ได้เก็บเพราะไม่มีผู้เขียน · ลอง post.errors.full_messages จะได้ ["User must exist"] (ผลจริง) · create ไม่ระเบิด มันแค่คืนแฟ้มที่ยังไม่ได้เก็บมาให้
  • undefined method 'name' for nil (NoMethodError) — ตัวแปรที่ได้จาก find_by เป็น nil แล้วคุณเรียก .name ต่อ (ผลจริงตอนผมลองพิมพ์ชื่อผิดแล้วเรียก .name ต่อ) · ย้อนไปเช็คบรรทัดที่ find_by
  • เห็น email: "[FILTERED]" ตอนโชว์ผู้ใช้ — ไม่ใช่ error · Rails ซ่อนอีเมลเวลาพิมพ์ทั้งแฟ้มออกจอ (กันข้อมูลส่วนตัวหลุดลง log) · User.find(3).email ยังได้ "mana@example.com" ตามจริง

อยากได้ข้อมูลกลับมาสะอาดเหมือนเดิมเมื่อไหร่ ก็สั่งขั้นที่ 6 ซ้ำได้เสมอ · ติดตรงไหน หรือผลของคุณไม่ตรงกับที่เขียนไว้ ถามกลับมาได้เลย พร้อมแปะสิ่งที่จอขึ้นมาทั้งก้อน

ถ้าเปิดสไลด์ดูประกอบ

หน้าสไลด์เขียนของจริงต้องสนใจไหม
9“พังไม่ได้ เพราะไม่มีการเขียนอะไรลง DB”จริงสำหรับคำสั่งในหน้านั้น (ถามโครงตาราง · Post.new) · แต่ save · update · destroy ในบทนี้เขียนลงตู้จริงนิดหน่อย
9Post.where(published: true).to_sqlต้องมีช่อง published ก่อน (ได้หลังเตรียมตู้ของวันที่ 2) · to_sql อยู่บท 113ไม่ต้อง — ลองในบท 113
9คอมเมนต์ “ลบ model ทิ้งยังถามได้” ข้าง Post.ancestorsancestors คือรายชื่อ class แม่และ module ที่ Post สืบทอดมา · บทนี้ข้ามไปเพราะไม่ต้องใช้ทำ CRUDไม่ต้อง
10จับคู่ gorm.Model embed ↔ < ApplicationRecordgorm.Model ให้ช่อง (ID · เวลา) · < ApplicationRecord ให้ความสามารถ (save · find) ส่วนช่องมาจากตารางนิดหน่อย — คู่กันแบบหลวม ๆ
10GORM “struct tag: table name”GORM เปลี่ยนชื่อตารางด้วย method TableName() ไม่ใช่ struct tag (struct tag ใช้กับชื่อคอลัมน์)ไม่ต้อง — คำตอบยังคู่กับ self.table_name เหมือนเดิม
8save! · valid? · changed? · reload · pluck · exists?บทนี้ยังไม่ใช้ · save! valid? บท 110 · pluck exists? บท 113ไม่ต้อง — สไลด์บอกเองว่าไม่ต้องจำทุกคำ

อ้างอิง