🔁 ทวนก่อนเริ่ม — คำพวกนี้จะโผล่ในบทนี้
db/seeds.rb·bin/rails db:seed(บท 107) — ไฟล์ข้อมูลตัวอย่าง กับคำสั่งที่รันไฟล์นั้น · ของ Day 2 ใช้create!สร้างสมชาย สมหญิง มานะbin/rails db:prepare(บท 108) — ตู้ยังไม่มี → สร้างตู้ + โหลดschema.rb+ ใส่ seeds · ตู้มีแล้ว → รันแค่ migration ที่ขาด- scaffold (บท 118) — ชุดเปิดแผนกสำเร็จรูปจากแฟรนไชส์ ได้หน้าเว็บ 7 action ทันที แต่หน้าตาดิบ
git status·git add·git commit(บท 119) — ดูว่าไฟล์ไหนเปลี่ยน · เลือกไฟล์ · ถ่ายรูปเก็บสถานะ- N+1 (บท 114) — เดินไปตู้ทีละแผ่นหลายสิบรอบ แทนที่จะเบิกทีเดียวทั้งตะกร้า
🧰 เวอร์ชันที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 18 (สไลด์กับ repo ของอาจารย์เขียน Ruby 4.0.5–4.0.6 · Rails 8.1.3 — เรื่องในบทนี้ไม่ต่างกัน)
📍 โค้ดเริ่มจากไหน: บทนี้เริ่มจากยังไม่มีโปรเจกต์ Day 3 เลย ·
จบบทคุณจะมีโปรเจกต์ของอาจารย์ (สาย day3-start) + ไฟล์ข้อมูลตัวอย่างของบทนี้ ที่ commit ไว้ในสายของเราเองชื่อ my-day3 (สาย = branch · หัวข้อ 4 อธิบาย) ·
บท 121–125 ทดลองบนสถานะนี้ · ผลรันทุกกล่องมาจากเครื่องจริง · เวลา ms · เลข commit · วันเวลาใน log ของคุณจะไม่ตรงกับของผม
1 · วันนี้ไม่ได้ "เรียน Rails" — วันนี้สร้างระบบหนึ่งระบบ
สไลด์หน้าแรกของ Day 3 เปิดด้วยประโยคว่า วันนี้เราไม่ได้มานั่งเรียนเรื่อง Rails ทีละหัวข้อ แต่สร้างระบบหนึ่งระบบให้เสร็จ แล้วได้ Rails ติดมือไปเอง · ทุกหัวข้อจบด้วยการทำของจริงต่อท้าย
Day 1–2 เราอยู่หลังร้านเป็นส่วนใหญ่: พนักงานต้อนรับ (routes) · หัวหน้าเชฟ (controller) · คนดูแลสต็อกกับตู้เก็บแฟ้ม (model · database) · Day 3 ย้ายมาหน้าร้านกับการจัดจาน — หน้าเว็บที่คนอ่านรู้เรื่อง · หน้าตาสวย · และกดแล้วขยับได้โดยไม่ต้องโหลดใหม่ทั้งหน้า
สไลด์ Day 3 จะพูดคำว่า "Vibe" บ่อย (Lab Vibe 1 · 2 · 3) = เขียน prompt (ข้อความสั่งงาน — บท 89) ให้ AI ทำ แล้วเราตรวจ · บทเรียนของเว็บนี้จะพาทำเองทีละขั้นก่อน แล้วค่อยให้ prompt กับรายการตรวจไว้ใช้กับ AI ทีหลัง — เพราะคนที่ไม่เคยทำเองจะตรวจงานของ AI ไม่ออก
2 · แผนที่ของวัน และ 3 ประโยคที่ต้องจำ
สไลด์แบ่ง Day 3 เป็น 6 บล็อก A–F · ชุดที่มาถึงตอนนี้คือ ครึ่งเช้า (Part 1) = บล็อก A B C · บล็อก D E F (Part 2) อยู่ใน Rails Day 4 · Part 1 ของเว็บนี้ (D E = บท 136–145 · F ยังไม่ได้ทำ):
| บท | บล็อก | ทำอะไร (ภาษาคน) | ปิดท้ายด้วย |
|---|---|---|---|
| 120–121 | เตรียมตัว | เปิดโปรเจกต์กลาง · ใส่ข้อมูลตัวอย่าง · อ่านคู่มือที่แนบให้ AI ทุกครั้ง | โปรเจกต์ที่รันได้ |
| 122–126 | A · View | คนจัดจาน: ERB · แม่พิมพ์ใช้ซ้ำ (partial) · เครื่องมือจัดจาน (helper) · ฟอร์ม · ช่องโหว่ใน view | Lab Vibe 1 — หน้ารายการ/หน้าบทความที่อ่านรู้เรื่อง |
| 127–129 | B · CSS + asset | ของตกแต่งร้าน: ไฟล์ CSS/JS เดินทางยังไง · Tailwind · ชุดจานชามสำเร็จรูป (daisyUI) | Lab Vibe 2 — แต่งให้เป็นเว็บ + สลับธีมสว่าง/มืด |
| 130–135 | C · Hotwire | เปลี่ยนเฉพาะจานบนโต๊ะเดิม ไม่ต้องรื้อโต๊ะ: แก้ในหน้าเดิม · คอมเมนต์เด้งทุกแท็บ · ตัวนับอักษร | Lab Vibe 3 — ทำให้มันขยับ |
| 136–145 | D E F (Part 2 · อยู่ใน Rails Day 4 · Part 1) | เนื้อหาแบบมีรูปแบบ/ไฟล์แนบ · งานเบื้องหลัง · ทำให้เร็ว (cache) | Lab Vibe 4 · 5 (บล็อก F ยังไม่ได้ทำ) |
สไลด์หน้า 8 บอกว่า "ถ้าวันนี้จำได้แค่ 3 อย่าง" · ตอนนี้ยังไม่ต้องเข้าใจทั้งหมด จำไว้ก่อน เดี๋ยวเจอ:
| # | ประโยค | ภาษาคน (ภาพร้านอาหาร) | เจอเมื่อไหร่ |
|---|---|---|---|
| ① | เว็บ interactive ไม่จำเป็นต้องมี state สองชุด | state = ข้อมูลที่หน้าเว็บจำไว้ (แบบ useState บท 73) · ให้ครัวถือสมุดสต็อกเล่มเดียว · ถ้าโต๊ะลูกค้าจดสมุดเล่มที่สองเอง สักวันสองเล่มจะไม่ตรงกัน (บั๊ก sync) | บท 122 · 130 |
| ② | exactly-once ไม่มีอยู่จริง ออกแบบให้ทำซ้ำได้แทน | ไม่มีทางรับประกันว่างานจะถูกทำ "ครั้งเดียวพอดี" · ให้ทำซ้ำแล้วผลไม่เพี้ยนแทน | บทนี้ (seeds) · เต็ม ๆ ใน Part 2 |
| ③ | อย่า invalidate cache ให้ key เปลี่ยนแทน | cache = สำเนาที่เก็บไว้ใช้ซ้ำ จะได้ไม่ต้องทำใหม่ทุกรอบ (บท 127 อธิบาย) · ลูกค้าประจำจำเมนูเล่มเก่าไว้ · อย่าไล่เก็บเมนูเล่มเก่าคืน แต่พิมพ์เลขรุ่นใหม่บนปก ทุกคนรู้เองว่าต้องหยิบเล่มใหม่ | บท 127 · เต็ม ๆ ใน Part 2 |
3 · ทำไม Day 3 ไม่ใช้แอปของ Day 2 ต่อ
สไลด์หน้า 3 ให้เหตุผลว่า งานจริงไม่เคยเริ่มจากศูนย์ — เข้าบริษัทวันแรก คุณจะได้โค้ดที่คนอื่นเขียนไว้แล้ว ไม่ได้ rails new เอง ·
และบอกด้วยว่า "ถ้า Lab 2 ยังไม่เสร็จ ไม่เป็นไร" = ทุกคนในห้องเริ่มที่จุดเดียวกัน ใครทำ Lab 2 ไม่เสร็จก็ไม่ตกขบวน
โปรเจกต์ของอาจารย์ชื่อ wongnok-blog-all-in-one · หน้าตาคล้ายของเรา แต่ไม่เหมือน:
แอป Day 2 ของเรา (wongnok_blog) | โปรเจกต์ Day 3 (day3-start) | |
|---|---|---|
| ใครสร้าง | เราเอง ตั้งแต่ rails new บท 98 | อาจารย์ · ทั้งห้องได้ชุดเดียวกัน |
| ผู้ใช้ (users) | มี (สมชาย สมหญิง มานะ) | ไม่มี · ระบบล็อกอินรอ Day 4 |
| เนื้อหาบทความ | ช่อง body (ว่างได้) + published · views | ช่อง description ห้ามว่าง · ไม่มี published/views |
| หมวด | เพิ่มใน Lab บท 119 · บทความไม่มีหมวดได้ | ทุกบทความต้องมีหมวด (category_id ห้ามว่าง) |
| คอมเมนต์ | มี (ผูกกับผู้ใช้) | ยังไม่มี · เราสร้างเองในบท 132 |
| หน้าเว็บ | ทำไปบางส่วนระหว่าง Day 1–2 (เช่น scaffold ของ Category บท 118–119) | scaffold ดิบ ๆ ของทั้ง Post และ Category · ติดตั้ง Tailwind + daisyUI ไว้แล้ว |
| ข้อมูลตัวอย่าง | 6 บทความ | ว่าง → เราใส่ 30 บทความในบทนี้ |
✅ แอป Day 2 เก็บไว้ได้ ไม่ต้องลบ — มันอยู่คนละโฟลเดอร์ และใช้ฐานข้อมูลคนละชื่อ (wongnok_blog_development กับ wongnok_blog_all_in_one_development) · อยากเปิดดูเทียบเมื่อไหร่ก็ได้
4 · ขั้น 1 — ก็อปโปรเจกต์ด้วย git clone แล้วสลับไป day3-start
โค้ดของอาจารย์เก็บอยู่บน GitHub เป็น repository (เรียกสั้น ๆ ว่า repo = โฟลเดอร์โปรเจกต์ที่มี git คอยจดประวัติ) ·
git clone คือก็อป repo ทั้งก้อนลงเครื่องเรา พร้อมประวัติการแก้ทุกครั้ง ไม่ใช่แค่ไฟล์ล่าสุด
ในประวัตินั้นมีหลายสาย แต่ละสายเรียกว่า branch — อาจารย์ทำสาย day3-start (จุดเริ่มเช้าวันนี้) · day3-a-done (ทำบล็อก A เสร็จ) ฯลฯ ·
git checkout ชื่อสาย คือสลับไฟล์ทั้งโฟลเดอร์ให้เป็นหน้าตาของสายนั้น
git clone คือแฟรนไชส์ยกร้านต้นแบบมาทั้งร้าน พร้อมสมุดบันทึกการปรับปรุงร้านทุกหน้าตั้งแต่วันเปิด ·
branch คือสมุดแยกเล่ม: เล่ม "เช้าวันนี้" · เล่ม "หลังจัดหน้าร้านเสร็จ" ·
checkout คือจัดร้านให้ตรงกับเล่มที่เลือก — โต๊ะ เก้าอี้ ป้ายเมนู ย้ายตามเล่มนั้นทันที
ที่ไม่เหมือนกัน: git จัดได้แค่ไฟล์ในโฟลเดอร์ · ของในตู้เก็บแฟ้ม (ข้อมูลใน PostgreSQL) ไม่ได้อยู่ใน git ·
สลับ branch แล้วข้อมูลในฐานข้อมูลยังอยู่ครบเหมือนเดิม (หลังใส่บทความ 30 ชิ้นในหัวข้อ 7 ผมลองสลับสายแล้ว ยังนับได้ 30) ·
และร้านจริงย้ายโต๊ะต้องใช้แรง แต่ git สลับทั้งโฟลเดอร์ได้ในเสี้ยววินาที
ไปที่โฟลเดอร์ที่เก็บโปรเจกต์ (โฟลเดอร์เดียวกับที่มี wongnok_blog ของ Day 1–2 · อย่า clone ซ้อนเข้าไปข้างในโปรเจกต์เดิม) แล้วพิมพ์ทีละบรรทัด:
$ git clone https://github.com/chonlawit-odds/wongnok-blog-all-in-one.git
Cloning into 'wongnok-blog-all-in-one'...
remote: Enumerating objects: 660, done.
remote: Counting objects: 100% (660/660), done.
remote: Compressing objects: 100% (453/453), done.
remote: Total 660 (delta 291), reused 537 (delta 178), pack-reused 0 (from 0)
Receiving objects: 100% (660/660), 291.02 KiB | 2.29 MiB/s, done.
Resolving deltas: 100% (291/291), done.
$ cd wongnok-blog-all-in-one
$ git checkout day3-start
branch 'day3-start' set up to track 'origin/day3-start'.
Switched to a new branch 'day3-start'
Cloning into 'wongnok-blog-all-in-one'...= ได้โฟลเดอร์ใหม่ชื่อเดียวกับ repo · บรรทัดremote:·Receiving= นับของที่ดาวน์โหลด (ตัวเลขของคุณจะต่าง ถ้าอาจารย์เพิ่มงานขึ้น GitHub ทีหลัง)cd= เดินเข้าโฟลเดอร์นั้น · คำสั่งหลังจากนี้ทั้งบทต้องพิมพ์ในโฟลเดอร์นี้origin= ชื่อเล่นที่ git ตั้งให้ "ต้นทางที่เรา clone มา" (GitHub ของอาจารย์)set up to track 'origin/day3-start'= ในเครื่องยังไม่มีสายนี้ git จึงสร้างสายในเครื่องให้ แล้วผูกไว้กับสายเดียวกันบน GitHubSwitched to a new branch 'day3-start'= ตอนนี้ไฟล์ทั้งโฟลเดอร์เป็นหน้าตาของday3-startแล้ว
อยากดูว่ามีสายอะไรบ้าง พิมพ์ git branch -a (-a = all รวมสายบน GitHub) · ผลจริง (ตัดสาย dependabot/… ของบอทอัปเดต gem ออก):
$ git branch -a
* day3-start
main
remotes/origin/HEAD -> origin/main
remotes/origin/day3-a-done
remotes/origin/day3-b-done
remotes/origin/day3-c-done
remotes/origin/day3-d-done
remotes/origin/day3-e-done
remotes/origin/day3-f-done
remotes/origin/day3-start
remotes/origin/day4-final
remotes/origin/day4-g-done
remotes/origin/day4-j-done
…
remotes/origin/main
* = สายที่เราอยู่ตอนนี้ · สองบรรทัดแรกคือสายที่มีในเครื่องเรา · remotes/origin/… คือสายที่มีบน GitHub (git จำรายชื่อไว้ตอน clone) ·
HEAD -> origin/main = "สายหลักบน GitHub คือ main" ไม่ต้องสนใจ
สาย day3-a-done ฯลฯ เอาไว้ "ตามทัน" — หัวข้อ 9 จะเตือนเรื่องนี้ · สาย day3-d/e/f กับ day4-… เป็นของ Part 2 และ Day 4 ที่ยังไม่ได้เรียน
เทียบกับที่เคยทำ: ตอนเริ่ม Next.js (บท 70) เราใช้คำสั่งสร้างโปรเจกต์ใหม่ ·
ตอน Rails Day 1 เราใช้ rails new · ทั้งสองคือ "สร้างร้านใหม่เปล่า ๆ" · git clone คือ "รับร้านที่มีคนเปิดไว้แล้ว" — งานจริงส่วนใหญ่เริ่มแบบหลัง
- repository (repo) ·
git clone - repo = โฟลเดอร์โปรเจกต์ที่ git จดประวัติไว้ · clone = ก็อป repo พร้อมประวัติทั้งหมดลงเครื่อง · ถาม senior วันแรกได้ว่า "ขอ URL ของ repo ที่ต้อง clone และ branch ที่ควรเริ่มทำงานหน่อยครับ"
- branch ·
checkout·origin - branch = สายประวัติแยกเส้น · checkout = สลับไฟล์ทั้งโฟลเดอร์ไปเป็นของสายนั้น (git รุ่นใหม่มีคำสั่ง
git switchที่ทำงานเดียวกันแต่ชื่อชัดกว่า — ใช้ในหัวข้อ 7) · origin = ต้นทางที่ clone มา · บอก AI ได้ว่า "I'm on branch day3-start of a cloned repo; don't touch other branches"
5 · ขั้น 2 — ติดตั้ง gem แล้วเปิดฐานข้อมูลด้วย docker compose
repo มาแค่โค้ด · gem (library ของ Ruby) ที่โค้ดต้องใช้ยังไม่ได้ลงเครื่อง ·
bundle install อ่านรายการใน Gemfile + เลขรุ่นที่ล็อกไว้ใน Gemfile.lock แล้วติดตั้งให้ครบ — แบบเดียวกับ npm install ที่อ่าน package.json (บท 70)
$ bundle install
…
Bundle complete! 27 Gemfile dependencies, 128 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
1 installed gem you directly depend on is looking for funding.
Run `bundle fund` for details
เครื่องผมมี gem ครบอยู่แล้วจึงได้แค่บรรทัดสรุป (สองบรรทัดท้ายเรื่อง funding = gem ตัวหนึ่งรับเงินสนับสนุน ไม่ต้องสนใจ) · ของคุณครั้งแรกจะมีบรรทัด Installing … เป็นสิบ ๆ บรรทัดก่อน (repo นี้มี gem ที่แอป Day 2 ไม่มี เช่น bullet · rack-mini-profiler · strong_migrations) ·
ลงท้ายด้วย Bundle complete! = ผ่าน
ต่อไปคือตู้เก็บแฟ้ม · repo นี้มีไฟล์ compose.yaml ที่เขียนไว้ว่าต้องเปิดโปรแกรมอะไรบ้าง ·
docker compose up -d อ่านไฟล์นั้นแล้วเปิดให้ทั้งชุด ในกล่องแยก (container) ที่ไม่ปนกับโปรแกรมในเครื่อง ·
คุณเคยใช้ไฟล์แบบนี้เปิด PostgreSQL มาแล้วในฝั่ง Go (บท 27)
# compose.yaml (ตัดมาเฉพาะส่วนที่ต้องรู้)
services:
postgres:
image: postgres:18
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql
minio:
…
ports: ["9000:9000", "9001:9001"]
postgres:= ตู้เก็บแฟ้ม PostgreSQL รุ่น 18 · ผู้ใช้postgresรหัสpostgresports: "5432:5432"= เปิดประตูหมายเลข 5432 ของเครื่องเรา ให้ทะลุเข้าไปที่ตู้ในกล่อง (5432 คือหมายเลขประตูมาตรฐานของ PostgreSQL)volumes: pgdata:…= เก็บข้อมูลไว้นอกกล่อง · ปิดกล่องแล้วเปิดใหม่ บทความไม่หายminio:= โกดังเก็บไฟล์ (ใช้เก็บรูป/ไฟล์แนบ) · วันนี้ยังไม่ใช้ — ได้ใช้ใน บท 138
เปิด Docker Desktop ให้ทำงานก่อน แล้วพิมพ์ docker compose up -d ในโฟลเดอร์โปรเจกต์ ·
-d = เปิดทิ้งไว้เบื้องหลัง คืนเทอร์มินัลให้พิมพ์ต่อได้ · ครั้งแรกจะดาวน์โหลดตัวโปรแกรมนานหน่อย ·
(เครื่องตรวจของผมไม่ได้รันคำสั่งนี้ เพราะใช้ PostgreSQL ที่เปิดไว้แล้ว — จึงไม่มีผลลัพธ์ให้ดู)
compose.yaml คือใบสั่งอุปกรณ์ของร้านต้นแบบ: "ตู้เก็บแฟ้มรุ่น 18 หนึ่งตู้ กุญแจชื่อ postgres · โกดังเก็บของหนึ่งหลัง" ·
docker compose up -d คือยกของตามใบสั่งมาวางแล้วเสียบปลั๊ก · แต่ละอย่างต้องตั้งตรงประตูหมายเลขของมัน (ตู้แฟ้มอยู่ประตู 5432)
ที่ไม่เหมือนกัน: ร้านจริงวางตู้สองใบหน้าประตูเดียวกันได้ · ในเครื่องคอมพิวเตอร์หนึ่งหมายเลขประตูมีเจ้าของได้แค่โปรแกรมเดียว · ถ้ามี PostgreSQL ตัวเก่าตั้งอยู่ที่ 5432 แล้ว ตู้ใหม่จะเข้าที่ไม่ได้ (ดูกล่องเตือนข้างล่าง) · และกล่องของ docker ไม่ใช่ตู้ที่จับต้องได้ — มันคือโปรแกรมที่ถูกกั้นให้มองไม่เห็นของอื่นในเครื่อง
⚠️ สองจุดที่พิมพ์ตามแล้วพังบ่อยที่สุด
(1) ประตู 5432 มีเจ้าของอยู่แล้ว — docker compose up -d ฟ้อง error ที่พูดถึง 5432 = มี PostgreSQL ตัวเก่าเปิดอยู่
เช่น container devpool-postgres ของฝั่ง Go บท 27 (ตั้ง restart: always ไว้ = เปิดเครื่องแล้วมันขึ้นเอง) หรือตัวที่ใช้กับแอป Day 2
docker ps= ดูรายการกล่องที่เปิดอยู่ · มองคอลัมน์ PORTS ว่าตัวไหนมี 5432docker stop ชื่อกล่อง= ปิดกล่องนั้น · ข้อมูลไม่หาย อยากใช้อีกก็docker start ชื่อกล่อง- ถ้าลง PostgreSQL ผ่าน Homebrew:
brew services listดูชื่อ แล้วbrew services stop ชื่อที่เห็น
(2) ค่า DATABASE_URL ค้างจาก Day 1–2 — ถ้าเคย export DATABASE_URL=… ไว้ (บท 98) Rails จะเชื่อค่านั้นก่อนไฟล์ตั้งค่า แล้วไปใช้ฐานข้อมูลของแอปเก่า ·
เช็คด้วย echo $DATABASE_URL ต้องได้บรรทัดว่าง · ถ้าไม่ว่างให้ unset DATABASE_URL (ลบตัวแปรนี้ในเทอร์มินัลนี้)
ทำไมไม่ต้องตั้งรหัสฐานข้อมูลเอง? เพราะ config/database.yml ของ repo ตั้งค่าเริ่มต้นไว้ตรงกับ compose.yaml แล้ว ·
ไฟล์นี้ใช้ ERB (<%= %> ที่เรียนบท 103 — ใช้ได้ในไฟล์ตั้งค่าด้วย):
# config/database.yml (ตัดมาบางส่วน)
host: <%= ENV.fetch("DATABASE_HOST", "localhost") %>
port: <%= ENV.fetch("DATABASE_PORT", 5432) %>
username: <%= ENV.fetch("DATABASE_USER", "postgres") %>
password: <%= ENV.fetch("DATABASE_PASSWORD", "postgres") %>
ENV.fetch("DATABASE_HOST", "localhost") = "อ่านตัวแปรสภาพแวดล้อม (ค่าที่ตั้งไว้ในเทอร์มินัล) ชื่อ DATABASE_HOST · ถ้าไม่มี ใช้ค่าหลังลูกน้ำ" ·
ของคุณไม่ได้ตั้งอะไร จึงได้ localhost · 5432 · postgres · postgres = ตรงกับตู้ใน compose.yaml พอดี ·
ของคุณไม่ต้องตั้งตัวแปรพวกนี้ (เครื่องตรวจของผมตั้งไว้ให้ชี้ไปตู้อื่น เพราะไม่ได้ใช้ docker)
bundle install·Gemfile.lock- ติดตั้ง gem ตามรายการ · ไฟล์
.lockล็อกเลขรุ่นให้ทุกเครื่องได้รุ่นเดียวกัน (≈package-lock.json·go.sum) · เจอ error ว่าหา gem ไม่เจอ ถาม senior ได้ว่า "ดึงโค้ดใหม่ (git pull) แล้วต้อง bundle install ใหม่ไหม" — ส่วนใหญ่ใช่ - docker compose · container · port
- compose = เปิดหลายโปรแกรมตามไฟล์
compose.yamlทีเดียว · container = กล่องแยกที่โปรแกรมรันอยู่ · port = หมายเลขประตูที่โปรแกรมรอรับการเชื่อมต่อ (หนึ่งหมายเลข หนึ่งเจ้าของ) · บอก AI ได้ว่า "docker compose up fails because port 5432 is already in use — how do I find which container owns it?"
6 · ขั้น 3 — db:prepare แล้วเดินดูว่าได้อะไรมาบ้าง
ตู้เปิดแล้ว · สั่งช่างตั้งลิ้นชักตามแบบ (บท 108):
$ bin/rails db:prepare
Created database 'wongnok_blog_all_in_one_development'
Created database 'wongnok_blog_all_in_one_test'
- ได้สองตู้:
…_development= ตู้ที่ใช้ตอนพัฒนา ·…_test= ตู้สำรองไว้รันเทส (Day 4) - ไม่มีบรรทัด
== CreatePosts: migratingเพราะบนตู้ใหม่db:prepareโหลดแบบจากdb/schema.rbทีเดียว ไม่ได้ไล่ทำใบสั่งช่างทีละใบ (บท 108) - มันรัน
db/seeds.rbให้ด้วย · แต่ seeds ของday3-startมีแค่บรรทัด comment ตัวอย่างของ Rails → ลิ้นชักยังว่าง (ผมนับใน console ได้Post.count= 0 ·Category.count= 0)
ก่อนใส่ข้อมูล เดินดูไฟล์สำคัญสั้น ๆ (เปิดโฟลเดอร์ใน editor) — ไม่ต้องจำ แค่รู้ว่าอะไรอยู่ตรงไหน ทุกไฟล์มีบทของมันรออยู่:
| ไฟล์ | คืออะไร (ภาษาคน) | ลงลึกบท |
|---|---|---|
db/schema.rb | แบบลิ้นชักล่าสุด: categories (name · slug ห้ามซ้ำ) · posts (title · description · category_id ห้ามว่างทั้งสามช่อง) | 108 (ทวน) |
app/models/post.rb · category.rb | คนดูแลสต็อก: Post ต้องมีหมวด · title ≤ 120 ตัว · Category สร้าง slug จากชื่อให้เอง | 110 · 115 (ทวน) |
app/controllers/posts_controller.rb | หัวหน้าเชฟจาก scaffold · index หยิบ Post.all ทั้งลิ้นชัก | 118 (ทวน) · 126 |
app/views/posts/ | คนจัดจาน: index · show · _post · _form … หน้าตาดิบจาก scaffold | 122–126 |
prompts/00-context.md | คู่มือพนักงานใหม่ที่แนบให้ AI: stack ของโปรเจกต์ + กฎ 8 ข้อจาก Day 2 | 121 |
AGENTS.md · CLAUDE.md | ป้ายหน้าห้องพนักงานที่ AI อ่านก่อนเข้างาน · CLAUDE.md ไม่ใช่ไฟล์จริง เป็นป้ายชี้ (symlink) ไปที่ AGENTS.md — เปิดชื่อไหนก็เจอของเดียวกัน | 121 |
app/assets/tailwind/application.css | จุดตั้งค่า Tailwind + daisyUI (ชุดจานชามสำเร็จรูป) | 128 |
config/environments/development.rb | ท้ายไฟล์เปิด bullet ตัวจับ N+1 (บท 114) ไว้แล้ว | 125 |
Procfile.dev | รายการโปรแกรมที่ bin/dev เปิดพร้อมกัน | หัวข้อ 8 · 128 |
ไฟล์เดียวในตารางที่ขอดูโค้ดตอนนี้ คือท้าย config/environments/development.rb — เพราะมันจะมีผลกับหน้าเว็บที่เราเปิดในหัวข้อ 8:
config.after_initialize do
Bullet.enable = true
Bullet.rails_logger = true
Bullet.add_footer = true
end
after_initialize = "ตั้งค่าเสร็จแล้วค่อยทำในบล็อกนี้" · สามบรรทัด = เปิด bullet (gem ตัวจับ N+1) · ให้มันเขียนเตือนลง log · และแปะแถบเตือนท้ายหน้าเว็บเวลาเจอ N+1 ·
ไฟล์นี้ใช้เฉพาะตอน development (ตอนเขียนโค้ดในเครื่อง) → ผู้ใช้จริงไม่เห็น
- development environment
- โหมดที่ใช้ตอนเขียนโค้ดในเครื่อง (คู่กับ test และ production) · ตั้งค่าแยกกันใน
config/environments/· ถาม senior ได้ว่า "gem ตัวนี้เปิดแค่ใน development หรือขึ้น production ด้วย"
7 · ขั้น 4 — ข้อมูลตัวอย่าง 30 บทความ ที่รันซ้ำกี่รอบก็ไม่เบิ้ล
ทำไมต้อง 30? สไลด์หน้า 20 ให้เหตุผลว่า ปัญหาที่เราจะเจอทั้งวัน — N+1 · ความช้า · การแบ่งหน้า (pagination = แสดงทีละหน้า หน้าละ 10 ชิ้น) — ไม่โผล่ตอนมีข้อมูล 3 แถว · ข้อมูลน้อยเกินไปคือเหตุผลอันดับหนึ่งที่บั๊กพวกนี้หลุดไปถึงผู้ใช้จริง
ซ้อมเปิดร้านโดยมีลูกค้าแค่ 3 โต๊ะ ทุกอย่างดูดี: คนเสิร์ฟเดินไปตู้ทีละใบก็ทัน · เมนูหน้าเดียวพอ · พอวันเปิดจริงลูกค้ามา 30 โต๊ะ คนเสิร์ฟเดินจนขาลาก (N+1) · รายการยาวจนต้องแบ่งหน้า · ปัญหามีอยู่ตั้งแต่วันซ้อม แค่ยังไม่มีใครเห็น
ที่ไม่เหมือนกัน: 30 แถวในเครื่องเรายังเร็วมาก — ตัวเลข ms ใน log แทบไม่ขยับ · สิ่งที่ 30 แถวทำให้เห็นคือจำนวนครั้งที่เดินไปตู้ (นับบรรทัดใน log ได้) และหน้าที่ยาวเกินไป ไม่ใช่ความช้าที่ผู้ใช้รู้สึก · ร้านจริงที่มีหลายหมื่นแถว ความช้าถึงจะโผล่ให้เห็นด้วยตา
ใส่ข้อมูลตัวอย่าง: เปิด db/seeds.rb · ลบของเดิมทิ้งทั้งไฟล์ (มีแค่ comment) แล้ววางอันนี้แทนทั้งไฟล์ ·
ไฟล์นี้เป็นชุดเดียวกับที่บท 121–135 ใช้ ผลรันของคุณจะได้ตรงกับบทเรียน:
# ข้อมูลตัวอย่างของ Rails Day 3 — หมวด 5 หมวด · บทความ 30 ชิ้น
# รันซ้ำกี่รอบก็ได้ ข้อมูลไม่เบิ้ล เพราะใช้ find_or_create_by! (มีอยู่แล้ว = ใช้ของเดิม)
CATEGORIES = {
"thai-food" => "อาหารไทย",
"noodles" => "ก๋วยเตี๋ยว",
"desserts" => "ของหวาน",
"street-food" => "อาหารริมทาง",
"drinks" => "เครื่องดื่ม"
}
POSTS = {
"thai-food" => [
[ "ต้มยำกุ้งน้ำข้น", "ต้มยำกุ้งแบบน้ำข้นใส่นมข้นจืด รสเปรี้ยวเผ็ดกลมกล่อม กุ้งแม่น้ำตัวใหญ่ หอมตะไคร้ ข่า ใบมะกรูด กินกับข้าวสวยร้อน ๆ ได้ทั้งถ้วย ร้านนี้เปิดมาสามสิบปีแล้ว คิวยาวทุกวันเสาร์อาทิตย์" ],
[ "แกงเขียวหวานไก่", "แกงเขียวหวานกะทิสด ใส่มะเขือเปราะกับใบโหระพา รสหวานนำ เผ็ดตามหลัง" ],
[ "ผัดกะเพราหมูสับไข่ดาว", "เมนูสิ้นคิดที่ไม่เคยทำให้ผิดหวัง หมูสับผัดพริกกระเทียมไฟแรง ใบกะเพราเยอะ ๆ ไข่ดาวขอบกรอบ ราดข้าวแล้วตักพริกน้ำปลาเพิ่มอีกนิด" ],
[ "ส้มตำปูปลาร้า", "ตำแบบอีสานแท้ ปลาร้าต้มสุกหอมกลิ่น ปูนาเค็มตัวเล็ก เผ็ดจัดจ้านจนต้องสั่งข้าวเหนียวเพิ่ม" ],
[ "ข้าวซอยไก่", "ข้าวซอยเชียงใหม่ น้ำแกงเข้มข้นหอมเครื่องเทศ เส้นทอดกรอบด้านบน เสิร์ฟกับหอมแดงและผักกาดดอง" ],
[ "มัสมั่นเนื้อ", "เนื้อตุ๋นจนนุ่มละลายในปาก มันฝรั่งกับถั่วลิสงคั่ว น้ำแกงหวานมันหอมอบเชย กินกับโรตีก็เข้ากัน" ]
],
"noodles" => [
[ "ก๋วยเตี๋ยวเรืออยุธยา", "น้ำซุปเข้มข้นใส่เลือดหมู ชามเล็กกินได้หลายชาม เส้นหมี่กับเส้นเล็กอร่อยพอกัน" ],
[ "บะหมี่เกี๊ยวหมูแดง", "เส้นบะหมี่ไข่เหนียวนุ่ม เกี๊ยวกุ้งห่อแน่น หมูแดงหวานกำลังดี ขอแห้งใส่น้ำตาลพริกป่นตามชอบ" ],
[ "ผัดไทยกุ้งสด", "เส้นจันท์ผัดกับซอสมะขามเปียกสูตรร้าน ใส่กุ้งสด ไข่ เต้าหู้ ถั่วงอก โรยถั่วป่น บีบมะนาวก่อนกิน" ],
[ "เย็นตาโฟทรงเครื่อง", "น้ำซอสเย็นตาโฟสีชมพูหวานอมเปรี้ยว ลูกชิ้นปลาเหนียว เต้าหู้ทอด ผักบุ้งลวก และปลาหมึกกรอบ" ],
[ "ก๋วยจั๊บน้ำข้น", "เส้นก๋วยจั๊บม้วนในน้ำซุปพริกไทยร้อน ๆ หมูกรอบ ไข่ต้มยางมะตูม กินตอนเช้าอุ่นท้องทั้งวัน" ],
[ "ราดหน้าหมูหมัก", "เส้นใหญ่ผัดซีอิ๊วจนหอมกระทะ ราดน้ำราดหน้าข้นกำลังดี หมูหมักนุ่ม คะน้ากรอบ" ]
],
"desserts" => [
[ "ข้าวเหนียวมะม่วง", "มะม่วงน้ำดอกไม้สุกหวานหอม ข้าวเหนียวมูนกะทิเค็มนิด ๆ โรยถั่วทองกรอบ ของหวานที่นักท่องเที่ยวต้องลอง" ],
[ "บัวลอยไข่หวาน", "บัวลอยแป้งนุ่มหลากสีในน้ำกะทิอุ่น ใส่ไข่หวานลูกกลม ๆ ทานตอนหน้าฝนดีที่สุด" ],
[ "ทับทิมกรอบ", "แห้วหุ้มแป้งมันสีแดงกรอบเด้ง ในน้ำกะทิหอมใบเตย ใส่น้ำแข็งไสเย็นชื่นใจ" ],
[ "ขนมครก", "ขนมครกหน้ากะทิหวานมัน ตัวขนมกรอบนอกนุ่มใน มีหน้าข้าวโพดกับต้นหอมให้เลือก" ],
[ "ลอดช่องน้ำกะทิ", "ลอดช่องสีเขียวจากใบเตย น้ำกะทิหวานหอมน้ำตาลโตนด ใส่น้ำแข็งเกล็ด" ],
[ "กล้วยบวชชี", "กล้วยน้ำว้าสุกต้มในกะทิ หวานมันเค็มนิด ๆ ของหวานบ้าน ๆ ที่ทำง่ายที่สุด" ]
],
"street-food" => [
[ "หมูปิ้งข้าวเหนียว", "หมูปิ้งหมักนมสด ปิ้งเตาถ่านจนขอบไหม้นิด ๆ กินกับข้าวเหนียวร้อน ๆ เป็นอาหารเช้าที่หาได้ทุกซอย" ],
[ "ไก่ทอดหาดใหญ่", "ไก่ทอดหนังกรอบหอมเครื่องเทศ โรยหอมเจียว กินคู่ข้าวเหนียวและน้ำจิ้มหวาน" ],
[ "ลูกชิ้นปิ้งน้ำจิ้มแจ่ว", "ลูกชิ้นหมูเด้งปิ้งบนเตาถ่าน ราดน้ำจิ้มเผ็ดหวาน ไม้ละห้าบาทหน้าโรงเรียน" ],
[ "ข้าวมันไก่ประตูน้ำ", "ข้าวมันหอมไขมันไก่ ไก่ต้มนุ่ม น้ำจิ้มเต้าเจี้ยวขิงสับรสจัด ซุปฟักร้อน ๆ ฟรีทุกจาน" ],
[ "โรตีกล้วยไข่", "แป้งโรตีบางกรอบ ห่อกล้วยหอมกับไข่ ราดนมข้นหวาน ร้านรถเข็นตอนหัวค่ำ" ],
[ "หอยทอดกรอบ", "แป้งทอดกรอบ ใส่หอยแมลงภู่ตัวโต ไข่ ถั่วงอกผัด กินกับซอสพริกศรีราชา" ]
],
"drinks" => [
[ "ชาไทยเย็น", "ชาไทยสีส้มเข้ม ชงหอม ใส่นมข้นจืดกับนมข้นหวาน เย็นชื่นใจในวันที่อากาศร้อนสี่สิบองศา" ],
[ "กาแฟโบราณ", "กาแฟชงถุงผ้าแบบร้านกาแฟสมัยก่อน เข้ม หวานมัน ทานคู่ปาท่องโก๋" ],
[ "น้ำมะพร้าวปั่น", "มะพร้าวน้ำหอมปั่นกับน้ำแข็ง เนื้อมะพร้าวอ่อนเป็นชิ้น ๆ หวานธรรมชาติ" ],
[ "ชามะนาว", "ชาดำเย็นบีบมะนาวสด เปรี้ยวหวานสดชื่น ดื่มคู่อาหารรสจัดเข้ากันมาก" ],
[ "โอเลี้ยง", "กาแฟดำเย็นสูตรโบราณ หวานเข้ม ใส่น้ำแข็งเต็มแก้ว ราคาย่อมเยาที่สุดในร้าน" ],
[ "นมเย็น", "นมสีชมพูจากน้ำแดง หวานหอม เด็ก ๆ ชอบที่สุด เติมไข่มุกได้" ]
]
}
categories = CATEGORIES.to_h do |slug, name|
[ slug, Category.find_or_create_by!(slug: slug) { |c| c.name = name } ]
end
# สลับหมวดกันทีละชิ้น หน้าแรกจะได้มีหลายหมวดปนกัน · ชิ้นที่สร้างทีหลังใหม่กว่า
position = 0
6.times do |round|
CATEGORIES.each_key do |slug|
title, description = POSTS[slug][round]
Post.find_or_create_by!(title: title) do |post|
post.description = description
post.category = categories[slug]
post.created_at = (30 - position).days.ago
end
position += 1
end
end
puts "categories: #{Category.count} · posts: #{Post.count}"
ไม่ต้องอ่านข้อความไทยทุกบรรทัด · ดูแค่โครงหลัก:
| ท่อน | แปลเป็นภาษาคน |
|---|---|
CATEGORIES = { "thai-food" => "อาหารไทย", … } |
hash (บท 92) ของหมวด 5 หมวด: slug (ชื่อสั้นภาษาอังกฤษไว้ใส่ใน URL) => ชื่อไทย · ตัวแปรที่ขึ้นต้นด้วยตัวใหญ่ทั้งคำ = ค่าคงที่ (constant) ไม่ตั้งใจให้เปลี่ยน |
POSTS = { "thai-food" => [ [ "ต้มยำ…", "…" ], … ] } |
หมวดละ 6 บทความ · แต่ละบทความเป็นคู่ [ ชื่อเรื่อง, คำอธิบาย ] |
categories = CATEGORIES.to_h do |slug, name| … end |
วนทีละหมวด สร้าง (หรือหาของเดิม) แล้วเก็บเป็น hash ใหม่ slug => หมวดที่อยู่ในตู้ ไว้ใช้ท่อนถัดไป · to_h = "แปลงผลของการวนเป็น hash" (แต่ละรอบคืนคู่ [ slug, หมวด ]) |
Category.find_or_create_by!(slug: slug) { |c| c.name = name } |
"หาหมวดที่ slug นี้ · ไม่เจอค่อยสร้าง แล้วกรอกชื่อไทยให้" · { |c| … } คือบล็อกแบบบรรทัดเดียว เหมือน do |c| … end (บท 93) · คำว่า find_or_create_by! อธิบายละเอียดใต้ผลรัน |
6.times do |round| + CATEGORIES.each_key do |slug| |
วน 6 รอบ · ในแต่ละรอบหยิบหมวดละหนึ่งชิ้น → ได้ลำดับ อาหารไทย ก๋วยเตี๋ยว ของหวาน ริมทาง เครื่องดื่ม แล้ววนใหม่ · หน้าแรกจะมีหลายหมวดปนกัน |
title, description = POSTS[slug][round] |
หยิบคู่ [ ชื่อเรื่อง, คำอธิบาย ] ออกมา แล้วแตกใส่สองตัวแปรในบรรทัดเดียว (ตัวแรกไป title ตัวที่สองไป description) |
post.created_at = (30 - position).days.ago |
ตั้งวันที่เขียน: ชิ้นแรก 30 วันก่อน · ชิ้นสุดท้าย 1 วันก่อน · days.ago = "นับถอยหลังจากตอนที่รัน" → วันที่ในเครื่องคุณจะต่างจากของผม ·
position += 1 = นับเพิ่มทีละหนึ่งหลังสร้างแต่ละชิ้น |
post.category = categories[slug] |
ผูกบทความเข้ากับหมวด ด้วยหมวดที่เก็บไว้ในท่อนแรก (belongs_to บท 115 — Rails เติม category_id ให้เอง) |
puts "categories: #{…} · posts: #{…}" |
พิมพ์สรุปจำนวนหลังรันเสร็จ (#{ } = แทรกค่าลงในข้อความ บท 91) |
รันสองรอบติดกัน:
$ bin/rails db:seed
categories: 5 · posts: 30
$ bin/rails db:seed
# ↑ รอบที่สอง · ไฟล์เดิม ไม่ได้แก้อะไร
ตอนนี้ตู้มี 30 บทความแล้ว · รันซ้ำอีกรอบจะพิมพ์ว่า posts เท่าไหร่?
$ bin/rails db:seed
categories: 5 · posts: 30
ยัง 30 · รอบสองไม่ได้สร้างอะไรเพิ่มเลย · ผมล้าง log แล้วรันอีกรอบ เปิด log/development.log นับได้ว่ามีแต่การค้นหา (SELECT) 38 บรรทัด · การเพิ่มแถว (INSERT) 0 บรรทัด ·
บรรทัดแรก ๆ หน้าตาแบบนี้:
Category Load (0.4ms) SELECT "categories".* FROM "categories" WHERE "categories"."slug" = 'thai-food' LIMIT 1 /*application='WongnokBlogAllInOne'*/
Category Load (0.3ms) SELECT "categories".* FROM "categories" WHERE "categories"."slug" = 'noodles' LIMIT 1 /*application='WongnokBlogAllInOne'*/
= "ขอดูหมวดที่ slug เป็น thai-food หนึ่งแผ่น" · เจอ → ใช้ของเดิม ไม่สร้าง · ทำแบบนี้ทั้ง 5 หมวด 30 บทความ ·
ท้ายบรรทัด /*application='…'*/ = ป้ายที่ Rails แปะใน SQL บอกว่ามาจากแอปไหน ไม่ต้องสนใจ
ตัวที่ทำให้ไม่เบิ้ลคือ find_or_create_by! = "มีแล้วใช้ของเดิม ไม่มีค่อยสร้าง" ·
Post.find_or_create_by!(title: title) do |post| … end แปลว่า "หาบทความที่ title ตรงนี้ · เจอก็เอาอันนั้น · ไม่เจอค่อยสร้างใหม่ โดยกรอกช่องอื่นตามบล็อก" ·
! ท้ายชื่อ = ถ้าสร้างไม่ผ่าน validation ให้ระเบิด ไม่เงียบ (บท 110)
seeds ของ Day 2 ใช้ create! = ใบสั่งคนดูแลสต็อกว่า "ใส่แฟ้มตัวอย่าง 30 แผ่นลงลิ้นชัก" · ส่งใบนี้สองรอบ ได้ 60 แผ่น ทุกเรื่องซ้ำสองแผ่น ·
find_or_create_by! = ใบสั่งว่า "เปิดลิ้นชักดูหัวแฟ้มก่อน ชื่อเรื่องนี้มีแล้วข้าม ไม่มีค่อยใส่" · ส่งกี่รอบก็ได้ 30 แผ่นเท่าเดิม
ที่ไม่เหมือนกัน: คนจริงอาจอ่านทั้งแฟ้มเทียบว่า "แผ่นเดียวกันไหม" · Rails ดูแค่ช่องที่เราบอก (ที่นี่คือ title) ·
ถ้าคุณแก้คำอธิบายใน seeds แล้วรันซ้ำ บทความเดิมไม่ถูกแก้ตาม — บล็อก do |post| … end ทำงานเฉพาะตอนสร้างใหม่ ·
ผมลองเปลี่ยนคำอธิบายของ "นมเย็น" แล้วรันซ้ำ ในตู้ยังเป็น นมสีชมพูจากน้ำแดง หวานหอม… ข้อความเดิม · และถ้าแก้ชื่อเรื่อง จะได้บทความใหม่เพิ่มอีกชิ้น
เทียบกับ Go: GORM มี FirstOrCreate ทำงานแบบเดียวกัน ("หาก่อน ไม่เจอค่อยสร้าง") ที่เจอในบท 53 ·
นี่คือร่างเล็ก ๆ ของประโยคที่ ② ของวัน — ไม่มีใครรับประกันได้ว่าคำสั่งจะถูกรันครั้งเดียว (เพื่อนกดซ้ำ · สคริปต์ติดตั้งรันใหม่) ·
ทางที่ปลอดภัยคือเขียนให้รันซ้ำแล้วผลเท่าเดิม
# ลองแก้ใน db/seeds.rb บรรทัดเดียว (ตอนนี้ตู้มี 30 บทความแล้ว)
Post.find_or_create_by!(title: title) do |post| # ก่อน
Post.create!(title: title) do |post| # หลัง — แบบ seeds ของ Day 2
$ bin/rails db:seed
จะพิมพ์ว่า posts เท่าไหร่? แล้วถ้าแก้บรรทัดของ Category เป็น create! ด้วย จะเกิดอะไร?
$ bin/rails db:seed
categories: 5 · posts: 60
ได้ 60 — บทความชื่อซ้ำกันเข้าตู้ได้หน้าตาเฉย เพราะ Post ไม่มีกฎห้ามชื่อเรื่องซ้ำ ทั้งใน model และใน DB · หน้า /posts จะมีทุกเรื่องสองชิ้น
ส่วนหมวดถ้าเปลี่ยนเป็น Category.create!(slug: slug, name: name) แล้วรันซ้ำ (ผลจริง ตัดท้าย):
$ bin/rails db:seed
bin/rails aborted!
ActiveRecord::RecordInvalid: Validation failed: Slug has already been taken (ActiveRecord::RecordInvalid)
…/db/seeds.rb:56:in 'block in <main>'
…
หมวดมีกฎ uniqueness ที่ slug (บท 111) จึงระเบิดตั้งแต่หมวดแรก · ทั้งสองแบบคือ "รันซ้ำแล้วพัง" — แบบแรกพังเงียบ (ข้อมูลเบิ้ล) แบบหลังพังดัง ·
แก้กลับเป็น find_or_create_by! ก่อนไปต่อ (ถ้าตู้เบิ้ลไปแล้ว ดูวิธีล้างในภารกิจท้ายบท)
- seed data ·
find_or_create_by! - seed data = ข้อมูลตัวอย่าง/ข้อมูลตั้งต้นที่ใส่ด้วย
db/seeds.rb·find_or_create_by!= หาตามช่องที่บอก เจอใช้ของเดิม ไม่เจอสร้าง · บอก AI ได้ว่า "make db/seeds.rb idempotent with find_or_create_by! keyed on title" - idempotent
- (อ่านว่า ไอเดมโพเทนต์) = ทำซ้ำกี่รอบผลก็เท่ากับทำรอบเดียว · คำนี้จะกลับมาเต็ม ๆ ใน Part 2 · ถาม senior ได้ว่า "สคริปต์นี้ idempotent ไหม รันซ้ำแล้วข้อมูลเบิ้ลหรือเปล่า" — คำถามที่ช่วยกันบั๊กได้มากกว่าที่คิด
เก็บ seeds ไว้ด้วย commit — บท 122–125 เราจะทดลองแก้ไฟล์ แล้วสั่ง git restore (คืนไฟล์ที่แก้แต่ยังไม่ commit กลับเป็นสภาพตอน commit ล่าสุด) ·
ถ้า seeds ยังไม่ได้ commit มันจะถูกคืนเป็นไฟล์ว่างไปด้วย ·
และเราจะไม่ commit ทับสาย day3-start ของอาจารย์ แต่แตกสายของเราเองชื่อ my-day3:
$ git status
On branch day3-start
Your branch is up to date with 'origin/day3-start'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: db/seeds.rb
no changes added to commit (use "git add" and/or "git commit -a")
$ git switch -c my-day3
Switched to a new branch 'my-day3'
$ git add db/seeds.rb
$ git commit -m "seeds: 5 หมวด 30 บทความ"
[my-day3 0e10665] seeds: 5 หมวด 30 บทความ
1 file changed, 73 insertions(+), 9 deletions(-)
modified: db/seeds.rb= ไฟล์เดียวที่เราแก้ · ถูกต้องgit switch -c my-day3= สร้างสายใหม่ชื่อ my-day3 จากจุดที่ยืนอยู่ แล้วย้ายไปอยู่สายนั้น (-c= create) · ของที่แก้ค้างไว้ติดตามมาด้วย[my-day3 0e10665]= commit ลงสาย my-day3 ·0e10665คือเลขประจำ commit (ของคุณจะต่าง)73 insertions(+), 9 deletions(-)= เพิ่ม 73 บรรทัด (seeds ของเรา) · ลบ 9 บรรทัด (comment เดิมของ Rails)
ผมเทียบไฟล์ทั้งโปรเจกต์ของสาย my-day3 กับโค้ดที่บทถัด ๆ ไปใช้ตรวจแล้ว ตรงกันทุกไฟล์ ·
และวันหลังถ้าคุณตั้งตู้ใหม่ (เช่น db:drop แล้ว db:prepare) ข้อมูล 30 ชิ้นจะเข้าตู้ให้เองเลย เพราะ seeds อยู่ในสายแล้ว (ผมลองบนตู้ใหม่ได้ categories: 5 · posts: 30) ·
แต่ถ้าตู้มีอยู่แล้ว db:prepare จะไม่รัน seeds ซ้ำ — ต้องสั่ง db:seed เอง
8 · ขั้น 5 — bin/dev แล้วเปิด /posts
Day 1–2 เราเปิดร้านด้วย bin/rails server · Day 3 มีงานที่สองที่ต้องวิ่งคู่กัน: คอยสร้างไฟล์ CSS ของ Tailwind ใหม่ทุกครั้งที่เราแก้ view ·
bin/dev เปิดทั้งสองงานพร้อมกันตามรายการใน Procfile.dev — ทำนองเดียวกับ npm run dev ของ Next.js (บท 72)
# Procfile.dev
web: bin/rails server
css: bin/rails tailwindcss:watch
web: = ร้าน Rails ตัวเดิม · css: = คนคอยเฝ้าไฟล์ แก้เมื่อไหร่สร้าง CSS ใหม่ (ละเอียดบท 128) ·
ครั้งแรกถ้าเครื่องยังไม่มี gem ชื่อ foreman (ตัวที่อ่าน Procfile แล้วเปิดหลายงานพร้อมกัน) bin/dev จะติดตั้งให้เองก่อน — ดูได้จากบรรทัดต้น ๆ ของไฟล์ bin/dev
$ bin/dev
11:27:37 web.1 | started with pid 40007
11:27:37 css.1 | started with pid 40008
11:27:37 web.1 | => Booting Puma
11:27:37 web.1 | => Rails 8.1.4 application starting in development
…
11:27:37 web.1 | * Listening on http://127.0.0.1:3000
11:27:37 web.1 | * Listening on http://[::1]:3000
11:27:37 web.1 | Use Ctrl-C to stop
11:27:38 css.1 | ≈ tailwindcss v4.3.3
11:27:38 css.1 |
11:27:38 css.1 | /*! 🌼 daisyUI 5.7.50 */
11:27:38 css.1 | Done in 89ms
…
- ทุกบรรทัดขึ้นต้นด้วยเวลา + ชื่องาน:
web.1= ร้าน Rails ·css.1= Tailwind · ข้อความของสองงานสลับกันในหน้าจอเดียว Listening on http://127.0.0.1:3000= ร้านเปิดที่ประตู 3000 แล้วtailwindcss v4.3.3·daisyUI 5.7.50·Done in 89ms= สร้างไฟล์ CSS รอบแรกเสร็จ- หลังจากนั้นจะมีบรรทัด
css.1 | Done in …msโผล่เป็นระยะ แม้เราไม่ได้แก้อะไร (ของผมขึ้นถี่ตอนเปิดหน้าเว็บ) = Tailwind สร้าง CSS ใหม่อีกรอบ · ปกติ ไม่ต้องสนใจ - เลิกใช้: กด Ctrl-C ครั้งเดียว ปิดทั้งสองงาน · เวลาและเลข pid ของคุณจะต่าง
เปิดเบราว์เซอร์ไปที่ http://localhost:3000/posts · log ในหน้าต่างเดียวกับ bin/dev ขึ้นแบบนี้ (ผมตัดบรรทัด Rendered posts/_post.html.erb 30 บรรทัดที่ซ้ำกันออก) ·
ก้อนนี้ผมดึงหน้าด้วย curl บรรทัดที่สองจึงลงท้าย as */* · ถ้าเปิดด้วยเบราว์เซอร์ จะเป็น as HTML (ผมเปิดด้วย Chrome แล้วได้แบบนั้น)
11:27:40 web.1 | Started GET "/posts" for ::1 at 2026-11-07 11:27:40 +0700
11:27:40 web.1 | Processing by PostsController#index as */*
11:27:40 web.1 | Rendering layout layouts/application.html.erb
11:27:40 web.1 | Rendering posts/index.html.erb within layouts/application
11:27:40 web.1 | Post Exists? (1.9ms) SELECT 1 AS one FROM "posts" LIMIT 1 /*action='index',…*/
11:27:40 web.1 | ↳ app/views/posts/index.html.erb:15
11:27:40 web.1 | Post Load (0.7ms) SELECT "posts".* FROM "posts" /*action='index',…*/
11:27:40 web.1 | ↳ app/views/posts/index.html.erb:16
…
11:27:40 web.1 | Completed 200 OK in 65ms (Views: 28.5ms | ActiveRecord: 6.8ms (2 queries, 0 cached) | GC: 16.6ms)
Completed 200 OK … (2 queries, 0 cached)= ตอบสำเร็จ · เดินไปตู้ 2 ครั้ง · ตัวเลข ms ที่เหลือเป็นเวลา ไม่ต้องสนใจRendering layout …·Rendering posts/index.html.erb within …= เริ่มจัดจาน: วางจานรอง (layout บท 103) แล้วเติมหน้า index ลงตรงกลางPost Exists?= "มีบทความสักชิ้นไหม" · มาจาก<% if @posts.any? %>ในapp/views/posts/index.html.erbPost Load= หยิบทั้งลิ้นชัก ไม่มี LIMIT · มาจาก<% @posts.each do |post| %>ในไฟล์เดียวกัน↳ app/views/posts/index.html.erb:15= "บรรทัดไหนในไฟล์ไหนสั่งให้เดินไปตู้" (เลขอาจเลื่อนจากในไฟล์ไป 1 เพราะป้าย comment ที่ Rails แปะหัวไฟล์ — ไม่ต้องสนใจ)
30 ชิ้นยังไหว · ถ้ามีหมื่นชิ้นก็หยิบหมื่นชิ้น — กฎข้อ 6 ใน prompts/00-context.md ("index action ต้องมี limit หรือ pagination") ยังไม่ผ่าน · เราจะแก้ใน Lab บท 126
HTML ที่เบราว์เซอร์ได้รับจริง (ผมดึงด้วย curl -s localhost:3000/posts ตัดมาบทความแรก) · ใน development Rails แปะ comment บอกว่าท่อนไหนมาจากไฟล์ไหนให้ด้วย
(ตั้งไว้ใน development.rb: annotate_rendered_view_with_filenames = true):
โค้ด — app/views/posts/_post.html.erb
<div id="<%= dom_id post %>" class="…">
<div>
<strong …>Title:</strong>
<%= post.title %>
</div>
<div>
<strong …>Description:</strong>
<%= post.description %>
</div>
<div>
<strong …>Category:</strong>
<%= post.category_id %>
</div>
</div>
HTML ที่ได้ (ของจริง)
<!-- BEGIN app/views/posts/_post.html.erb
--><div id="post_1" class="w-full sm:w-auto my-5 space-y-5">
<div>
<strong class="block font-medium mb-1">Title:</strong>
ต้มยำกุ้งน้ำข้น
</div>
<div>
<strong class="block font-medium mb-1">Description:</strong>
ต้มยำกุ้งแบบน้ำข้นใส่นมข้นจืด รสเปรี้ยวเผ็ดกลมกล่อม …
</div>
<div>
<strong class="block font-medium mb-1">Category:</strong>
1
</div>
</div>
<!-- END app/views/posts/_post.html.erb -->
<!-- BEGIN … -->/<!-- END … -->= ป้ายที่ Rails แปะให้ตอน development · เบราว์เซอร์ไม่แสดงบนจอ<%= dom_id post %>→post_1= helper ที่ตั้ง id ให้ตามชื่อ model + เลขที่แฟ้ม (บท 131 จะพึ่งมันมาก)class="w-full sm:w-auto my-5 space-y-5"= คลาสของ Tailwind (บท 72) ที่ scaffold ใส่มาให้ · ในโค้ดผมย่อเป็น…· เรื่อง CSS รอบล็อก B (บท 127–129)<%= post.title %>→ต้มยำกุ้งน้ำข้น= ERB เติมค่าลงช่อง (บท 103)<%= post.category_id %>→1= scaffold พิมพ์เลขที่แฟ้มของหมวดออกมาตรง ๆ ไม่ใช่ชื่อ "อาหารไทย" · ทั้งหน้ามีแต่เลข 1–5
บนจอเบราว์เซอร์ (ผมเปิดด้วย Chrome จริง) จะเห็นรายการยาว 30 ชิ้นต่อกันในหน้าเดียว · แต่ละชิ้นมีหัวข้อ Title: Description: Category: กับปุ่ม Show · Edit · Destroy ·
และป้ายเล็กมุมซ้ายบนหน้าตาประมาณ 699.6 ms ×17 = rack-mini-profiler (gem ตัวจับเวลาว่าหน้านี้ใช้เวลาเท่าไหร่ · ตัวเลขของคุณจะต่าง · เลขหลัง × ไม่ต้องสนใจตอนนี้ · หน้าแรกหลังเปิดร้านมักช้ากว่าหน้าถัด ๆ ไป) ·
ส่วนแถบเตือนของ bullet ยังไม่ขึ้น เพราะหน้านี้ยังไม่มี N+1 (มันพิมพ์แค่เลข category_id ไม่ได้เดินไปเปิดลิ้นชัก categories) — เราจะเห็นมันโผล่ในบท 125
✅ นี่คือจุดเริ่มของวัน: หน้ารายการที่หยิบทั้งลิ้นชัก · หมวดเป็นตัวเลข · ไม่มีแบ่งหน้า · หน้าตาเป็นฟอร์มราชการ · ทั้งวันเราจะเปลี่ยนหน้านี้ให้เป็นเว็บจริง — Lab 126 อ่านรู้เรื่อง · Lab 129 สวย · บท 131–135 ขยับได้
bin/dev·Procfile.dev· foreman- bin/dev = สคริปต์เปิดร้านแบบ Day 3 · Procfile.dev = รายการงาน "ชื่อ: คำสั่ง" · foreman = ตัวเปิดทุกงานในรายการพร้อมกันแล้วรวม log ไว้หน้าจอเดียว · ถาม senior ได้ว่า "โปรเจกต์นี้เปิดด้วย bin/dev หรือ bin/rails server — CSS จะได้ build ด้วย"
- rack-mini-profiler
- gem ที่แปะป้ายเวลาโหลดหน้าไว้มุมจอตอน development (ผู้ใช้จริงไม่เห็น) · บอก AI ได้ว่า "mini-profiler shows this page takes 700ms — help me find which query is slow"
9 · ตามไม่ทัน — branch สำเร็จรูปของอาจารย์
สไลด์บอกว่าทุกบล็อกมี branch "ทำเสร็จแล้ว" ให้ checkout ตามทันได้ (day3-a-done · day3-b-done · day3-c-done) · มีจริงตามที่เห็นใน git branch -a
⚠️ branch สำเร็จรูปของอาจารย์ไม่ตรงกับโค้ดในบทเรียนของเว็บนี้ — ผู้เตรียมแอปของเว็บนี้เปิดดูแล้ว พบว่าใช้ระบบแปลภาษา (t(".title") + gem rails-i18n) และ seeds คนละชุด ·
ถ้า checkout ไป ไฟล์จะหน้าตาต่างจากที่บทเรียนพาพิมพ์ และผลรันจะไม่ตรง ·
ทางที่ดีกว่า: ทุกบท Lab บอกไว้ต้นบทว่า "เริ่มจากโค้ดสถานะไหน" — ตามไม่ทันให้กลับไปทำบท Lab ก่อนหน้า (126 · 129 · 131–134) ตามลำดับ
🎯 ภารกิจ (~10 นาที) — ให้เห็น 30 บทความ แล้วนับใน console
- ทำขั้น 1–5 ให้ครบ: clone ·
git checkout day3-start·bundle install·docker compose up -d·bin/rails db:prepare· วาง seeds ·db:seedสองรอบ ·git switch -c my-day3+ commit bin/devแล้วเปิดhttp://localhost:3000/posts· เลื่อนลงล่างสุด บทความสุดท้ายควรเป็น นมเย็น- เปิดอีกแท็บเทอร์มินัล (ปล่อย
bin/devไว้) ·cdเข้าโฟลเดอร์โปรเจกต์ ·bin/rails console - ถามตู้ 4 คำถาม: มีบทความกี่ชิ้น (
.count) · มีกี่หมวด · บทความใหม่สุดชื่ออะไร (ใช้ scoperecentที่มีในpost.rbแล้วเอาชิ้นแรก) · หมวดของหวาน (slugdesserts) มีกี่ชิ้น (หาหมวดด้วยfind_byแล้วต่อ.posts· บท 115) - พิมพ์
exitออกจาก console · รันbin/rails db:seedอีกรอบ · เข้า console นับใหม่
ควรเห็น: 30 · 5 · "นมเย็น" · 6 · และหลัง seed รอบที่สามยังเป็น 30
ถ้าเจออาการนี้ แปลว่า…
ActiveRecord::ConnectionNotEstablished: connection to server at "::1", port 5432 failed: Connection refused→ ตู้ยังไม่เปิด · Docker Desktop ยังไม่ทำงาน หรือยังไม่ได้docker compose up -d(บรรทัดแรกของ error จริงจากdb:prepareตอนไม่มีตู้เปิดอยู่ที่ประตูนั้น · ผมลองกับประตูอื่นที่ว่าง ข้อความเหมือนกันต่างแค่เลขประตู)Post.countได้0→ วาง seeds หลังdb:prepareแล้วลืมสั่งbin/rails db:seed(ตู้มีแล้วdb:prepareไม่รัน seeds ให้อีก)Post.countได้60→ seeds ไม่ใช่ไฟล์ของบทนี้ (มีcreate!) · แก้ไฟล์ให้ตรง แล้วล้างตู้ตั้งใหม่ด้วยbin/rails db:reset- เปิด
/postsได้ แต่ไม่มีบทความ ทั้งที่ console นับได้ 30 → เช็คecho $DATABASE_URLในหน้าต่างที่รันbin/dev· ถ้าไม่ว่าง ร้านกำลังเปิดตู้ของแอปเก่าอยู่
ข้อ 3–4 ผลจริง (เวลาในวงเล็บ · เลข id ของหมวดของคุณอาจต่าง):
$ bin/rails console
Loading development environment (Rails 8.1.4)
wongnok-blog-all-in-one(dev):001> Post.count
Post Count (39.7ms) SELECT COUNT(*) FROM "posts" /*application='WongnokBlogAllInOne'*/
=> 30
wongnok-blog-all-in-one(dev):002> Category.count
Category Count (1.0ms) SELECT COUNT(*) FROM "categories" /*application='WongnokBlogAllInOne'*/
=> 5
wongnok-blog-all-in-one(dev):003> Post.recent.first.title
Post Load (4.0ms) SELECT "posts".* FROM "posts" ORDER BY "posts"."created_at" DESC LIMIT 1 /*application='WongnokBlogAllInOne'*/
=> "นมเย็น"
wongnok-blog-all-in-one(dev):004> Category.find_by(slug: "desserts").posts.count
Category Load (0.4ms) SELECT "categories".* FROM "categories" WHERE "categories"."slug" = 'desserts' LIMIT 1 /*application='WongnokBlogAllInOne'*/
Post Count (0.7ms) SELECT COUNT(*) FROM "posts" WHERE "posts"."category_id" = 3 /*application='WongnokBlogAllInOne'*/
=> 6
wongnok-blog-all-in-one(dev)= ชื่อโปรเจกต์ + โหมด development · ต่างจากwongnok-blog(dev)ของ Day 2 → ใช้เช็คได้ว่าเปิดถูกโปรเจกต์Post.recent.first→ORDER BY "posts"."created_at" DESC LIMIT 1= "เรียงจากวันที่ใหม่ไปเก่า เอาแผ่นแรก" · ได้นมเย็นเพราะ seeds ให้มันเป็นชิ้นสุดท้าย (1 วันก่อน)- คำถามที่ 4 เดินไปตู้สองครั้ง: หาหมวดก่อน แล้วนับบทความที่
category_idตรงกับหมวดนั้น (has_many :posts· บท 115)
ข้อ 5: db:seed พิมพ์ categories: 5 · posts: 30 และนับใหม่ได้ 30 เท่าเดิม · ถ้าต้องใช้ db:reset ผลจริงหน้าตาแบบนี้ (ทิ้งตู้ ตั้งใหม่จาก schema.rb แล้วรัน seeds · เลขที่แฟ้มเริ่มจาก 1 ใหม่):
$ bin/rails db:reset
Dropped database 'wongnok_blog_all_in_one_development'
Dropped database 'wongnok_blog_all_in_one_test'
Created database 'wongnok_blog_all_in_one_development'
Created database 'wongnok_blog_all_in_one_test'
categories: 5 · posts: 30
db:reset ลบข้อมูลทั้งหมดจริง · ใช้กับตู้ฝึกในเครื่องเท่านั้น
ทดสอบความเข้าใจ
ข้อ 1 · ตู้ว่าง แล้วรัน bin/rails db:seed ด้วย seeds ของบทนี้ 3 รอบติดกัน · สุดท้าย Post.count ได้เท่าไหร่?
ตัวเลือกที่ 2 ถูก · ผลจริงในหัวข้อ 7: รอบสองพิมพ์ posts: 30 และใน log มีแต่ SELECT ไม่มี INSERT สักบรรทัด
ตัวเลือกที่ 1 คือพฤติกรรมของ create! (seeds แบบ Day 2 — ในหัวข้อ 7 ผมเปลี่ยนเป็น create! แล้วรันรอบเดียวบนตู้ที่มี 30 ก็ได้ 60) ·
ตัวเลือกที่ 3 ผิดเพราะ find_or_create_by! หาก่อน · เจอแล้วไม่สร้าง จึงไม่มีอะไรให้ชน (และ Post ไม่มีกฎห้ามชื่อซ้ำด้วย) ·
ตัวเลือกที่ 4 ได้เลขถูกแต่เหตุผลผิด — ไม่มีบรรทัดไหนในไฟล์ลบของ และของเดิมยังเป็นแผ่นเดิม เลขที่แฟ้มไม่เปลี่ยน
จุดที่ต้องจำ: ไฟล์ seeds ที่ดี "รันซ้ำแล้วผลเท่าเดิม" — ร่างเล็กของประโยค ② ของวัน
ข้อ 2 · ทำไม Day 3 ให้ clone โปรเจกต์ของอาจารย์ แทนที่จะใช้แอป Day 2 ที่เราทำเองต่อ?
ตัวเลือกที่ 4 ถูก · สองเรื่องจากสไลด์หน้า 3: ใครทำ Lab 2 ไม่เสร็จก็ไม่ตกขบวน · และเข้าทำงานจริงแทบไม่มีใครได้เริ่มจาก rails new
ตัวเลือกที่ 1 ผิด — เครื่องตรวจใช้ Rails 8.1.4 ทั้งสองแอป ·
ตัวเลือกที่ 2 ผิด — ชื่อฐานข้อมูลต่างกัน (wongnok_blog_… กับ wongnok_blog_all_in_one_…) แอปเก่าเก็บไว้ได้ ·
ตัวเลือกที่ 3 ผิด — day3-start เป็น scaffold ดิบ ๆ หมวดยังโชว์เป็นตัวเลข · งานทั้งวันคือทำ view ให้เป็นเว็บจริง
จุดที่ต้องจำ: ทักษะที่วันนี้ฝึกคือ "เข้าไปต่อของที่มีอยู่" — อ่านโค้ดคนอื่นก่อน แล้วค่อยแก้
ข้อ 3 · อยู่สาย my-day3 ที่ตู้มี 30 บทความ แล้วสั่ง git switch day3-start (สายที่ seeds ยังว่าง) · ถาม Post.count ได้เท่าไหร่?
ตัวเลือกที่ 1 ถูก · ผมลองแล้ว: สลับไป day3-start ไฟล์ db/seeds.rb กลับเป็น comment ของ Rails แต่ Post.count ยังได้ 30
ตัวเลือกที่ 2 คิดว่าตู้เก็บแฟ้มเป็นส่วนหนึ่งของร้านที่ git จัดได้ — ไม่ใช่ ข้อมูลอยู่ใน PostgreSQL นอกโฟลเดอร์ · ตัวเลือกที่ 3 ผิด — ตู้ยังเป็นตู้เดิม ไม่มีอะไรต้องเตรียมใหม่ (ยกเว้นสายใหม่มี migration เพิ่ม ซึ่ง day3-start ไม่มี) · ตัวเลือกที่ 4 ผิด — git ไม่ได้แตะข้อมูลในตู้เลย
จุดที่ต้องจำ: git ดูแลไฟล์ · ข้อมูลในฐานข้อมูลต้องจัดการเอง (seeds · migration)
ข้อ 4 · หน้า /posts ช่อง Category โชว์เลข 1 2 3 แทนชื่อหมวด · เพราะอะไร?
ตัวเลือกที่ 3 ถูก · HTML จริงในหัวข้อ 8: <%= post.category_id %> ใน _post.html.erb → 1 · scaffold ไม่รู้ว่าเราอยากเห็นชื่อ มันพิมพ์ช่องที่มีในตารางตรง ๆ
ตัวเลือกที่ 1 ผิด — ใน console Category.count ได้ 5 และทุกหมวดมีชื่อ ·
ตัวเลือกที่ 2 ผิด — bullet แค่เตือน ไม่เคยแก้หน้าเว็บ · และหน้านี้ยังไม่มี N+1 เพราะไม่ได้เปิดลิ้นชัก categories เลย ·
ตัวเลือกที่ 4 ผิด — db:prepare โหลด schema ครบทั้งสองตารางแล้ว
จุดที่ต้องจำ: โชว์ชื่อหมวด (post.category.name) จะทำให้ต้องเดินไปลิ้นชัก categories ทีละบทความ = N+1 · Lab บท 126 จะแก้ทั้งสองเรื่องพร้อมกัน
ถ้าเปิดสไลด์ดูประกอบ
| หน้า | สไลด์เขียน | ของจริง | ต้องสนใจไหม |
|---|---|---|---|
| 3 | กล่องตามทัน: git clone → git checkout day3-start → bin/rails db:prepare db:seed |
ขาด bundle install กับ docker compose up -d ก่อน db:prepare · และ seeds ของ day3-start ว่าง db:seed จึงไม่ได้อะไร | ✅ ใช่ — ทำตามขั้น 1–4 ของบทนี้ |
| 3 | posts.description เป็น text (ไม่มี NOT NULL) | schema.rb เป็น null: false + model มี validates :description, presence: true | นิดเดียว — บทความต้องมีคำอธิบายเสมอ |
| 3 | "โฟลเดอร์ prompts/ ที่ยังว่างอยู่" | มี prompts/00-context.md (stack + กฎ 8 ข้อจาก Day 2) แล้ว | ไม่ — บท 121 อ่านไฟล์นี้ |
| 3 | "ทุกบล็อคจะมี branch แบบนี้" ไว้ตามทัน | มีจริง แต่ใช้ i18n + seeds คนละชุด ไม่ตรงกับบทเรียนของเว็บนี้ | ใช่ ถ้าจะใช้ตามทัน — อ่านหัวข้อ 9 |
| 5 | Stack ใน 00-context.md: Ruby 4.0.6 / Rails 8.1.3 / PostgreSQL 17 | ไฟล์จริงเขียน Ruby 4.0.5 / Rails 8.1.3 / PostgreSQL 18 · Gemfile ของ day3-start ขอ Rails ~> 8.1.4 · compose.yaml เปิด PostgreSQL 18 · เครื่องตรวจใช้ Ruby 4.0.9 | ไม่ |
| 20 | "seed 30 โพสต์ 5 หมวด" เป็นงานใน Lab Vibe 1 | เว็บนี้ทำไว้ตั้งแต่บทนี้ เพื่อให้บท 121–125 มีข้อมูลทดลอง | ไม่ — Lab 126 ข้ามข้อนี้ได้ |
อ้างอิง
- GitHub — chonlawit-odds/wongnok-blog-all-in-one —
repo ของอาจารย์ที่ทั้งห้องใช้ · กดปุ่มเลือก branch (มุมซ้ายบน เหนือรายการไฟล์) แล้วเลือก
day3-startจะเห็นไฟล์ชุดเดียวกับที่ clone มา - Rails Guides — Active Record Migrations —
อ่านหัวข้อ 4.3 Preparing the Database ย่อหน้าที่ขึ้นต้นว่า "Once the database and tables exist" ซึ่งบอกว่าเมื่อตู้มีอยู่แล้ว
db:prepareจะไม่โหลด seeds ซ้ำ · แล้วต่อด้วย หัวข้อ 4.4 Resetting the Database (คำสั่งdb:resetในภารกิจ) - Rails Guides — Active Record Query Interface — อ่านหัวข้อ 4.1 find_or_create_by และ 4.2 find_or_create_by! ดู SQL ที่มันสร้าง (SELECT ก่อน แล้ว INSERT เมื่อหาไม่เจอ) · ท้าย 4.1 มีประโยค "The block will only be executed if the customer is being created" = บล็อกทำงานเฉพาะตอนสร้าง ตรงกับที่ผมลองในหัวข้อ 7
- Rails API — ActiveRecord::Relation#find_or_create_by —
ประโยค "This method accepts a block, which is passed down to
create" = บล็อกถูกส่งต่อให้ตอนสร้างเท่านั้น (เหตุผลที่แก้คำอธิบายใน seeds แล้วของเดิมไม่เปลี่ยน) · สองย่อหน้าท้ายพูดถึง race condition (สองคนสั่งพร้อมกัน) ไว้อ่านเมื่อถึง Part 2 - Git — git-switch —
คู่มือทางการของ git · อ่านตัวอย่างในหัวข้อ EXAMPLES ที่ใช้
-cสร้างสายใหม่แล้วย้ายไปทันที - Docker Docs — docker compose up —
อ่านตาราง Options บรรทัด
-d, --detach(เปิดทิ้งไว้เบื้องหลัง)