Cheat sheet · Rails · Day 1

Rails Day 1 สำหรับมือใหม่ — Ruby · โครงสร้าง · routing · controller · view แผ่นเดียวจบ

คู่กับ บทเรียน 90–105 · ทุกผลรันในหน้านี้ก็อปมาจากบทเรียน ซึ่งรันจริงบนเครื่องตรวจ ไม่ได้พิมพ์จากความจำ

หน้านี้ไม่ได้ให้อ่านรวดเดียว — เปิดไว้ข้างจอตอนลงมือทำ แล้วกวาดตาหาแถวที่ต้องการ
งงว่า “ส่วนนี้ของ Rails คือใคร” → หัวข้อ 1 (ภาพร้านอาหาร) · งงว่า “Post กับ posts ต่างกันยังไง” → หัวข้อ 2
เห็นโค้ด Ruby แล้วอ่านไม่ออก → หัวข้อ 3 (เทียบกับ Go ทีละบรรทัด) · หน้าเว็บขึ้นสีแดง → ข้ามไปหัวข้อ 12 (ตาราง error) ได้เลย

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

แอปตัวอย่างทั้งคอร์สคือ บล็อก ชื่อ wongnok_blog · ของหลักคือ บทความ ภาษาอังกฤษเรียกว่า post · Day 1 ยังไม่มี database จริง — บทความในตัวอย่างหลายตัวจำลองด้วย Hash (เขียนกำกับไว้ทุกครั้ง)

ของในเครื่องคุณ เช่น เวลา · PID (เลขประจำโปรแกรมที่กำลังรัน) · เลข commit · เลข 0x… จะไม่ตรงกับในหน้านี้ — เป็นเรื่องปกติ

สารบัญ

  1. ภาพร้านอาหาร — Rails ทั้งร้านในตารางเดียว
  2. Post กับ posts — กฎตั้งชื่อ
  3. Ruby เทียบ Go ทีละเรื่อง
  4. map · select · find — ผลรันจริง
  5. 5 ไวยากรณ์ที่เจอในโค้ด Rails
  6. rails new · โฟลเดอร์ · Gemfile
  7. คำสั่งที่ใช้บ่อยใน Day 1
  8. routes · resources 8 เส้น · _path
  9. controller · params · render กับ redirect_to
  10. view · ERB · layout · link_to · ความปลอดภัย
  11. debugger · console
  12. ตาราง error → แปลว่า → แก้ยังไง
  13. ศัพท์ภาษาคน
  14. ถ้าเปิดสไลด์ดูประกอบ
  15. อ้างอิง

1 · ภาพร้านอาหาร — Rails ทั้งร้านในตารางเดียว

ทั้งคอร์สใช้ภาพเดียว: แอป Rails คือร้านอาหาร · Go ที่เคยเรียน = เปิดร้านเองตั้งแต่ศูนย์ (จ้างพนักงานต้อนรับเองด้วย Gin router · เขียนใบงานเชฟเองด้วย handler · ซื้อตู้เองด้วย GORM) · Rails = ร้านแฟรนไชส์ที่ได้ครัวครบชุด ขอแค่วางของตามตำแหน่งที่แฟรนไชส์กำหนด

ส่วนของเว็บ / Railsในร้านอาหารอยู่ที่ไหน · หน้าตาใน Go คือ
browserลูกค้าChrome · Safari · หรือคำสั่ง curl ใน terminalclient ตัวเดียวกัน
requestใบสั่งอาหารใน log: Started GET "/posts"คำขอที่ handler รับผ่าน c *gin.Context
responseจานอาหารที่ยกมาเสิร์ฟHTML + เลขสถานะ เช่น Completed 200 OKc.String(…) · c.Data(…)
serverการเปิดร้านbin/rails server → Puma (โปรแกรม web server ที่มากับ Rails) รอที่พอร์ต 3000r.Run()
middlewareด่านหน้าครัว ที่ใบสั่งทุกใบต้องผ่านดูรายชื่อด้วย bin/rails middleware (แอปใหม่มี 28 ด่าน)r.Use(…) · gin.Default() ใส่ให้ 2 ตัว
routesพนักงานต้อนรับ ดูใบสั่งแล้วชี้ว่าแผนกไหนรับconfig/routes.rb ไฟล์เดียวทั้งแอปr.GET("/posts", h.List)
controllerหัวหน้าเชฟของแผนก (เช่นแผนกบทความ)app/controllers/posts_controller.rbกลุ่ม handler ของเรื่องเดียวกัน
actionเมนูแต่ละอย่างของแผนกdef index · def show ใน controllerhandler หนึ่งตัว
paramsรายละเอียดในใบสั่ง (“จานที่ 42”)params[:id] · params[:name]c.Param("id") · c.Query("name")
@ตัวแปร ใน controllerถาดหน้าครัว ที่เชฟวางของให้คนจัดจาน@posts = …gin.H{"posts": posts} ที่ส่งให้ template
modelห้องเก็บวัตถุดิบ + คนดูแลสต็อกapp/models/post.rb (ใช้จริง Day 2)struct + GORM
database · ตาราง · แถวตู้เก็บแฟ้ม · ลิ้นชักบทความ · แฟ้มหนึ่งแผ่นตาราง posts ใน PostgreSQLตารางที่ GORM ใช้
viewการจัดจานก่อนเสิร์ฟapp/views/posts/index.html.erbแม่แบบ html/template
layoutจานรองแบบเดียวกันทุกจานapp/views/layouts/application.html.erbไม่มีให้ — ต้องจัดเอง
helperเครื่องพิมพ์ป้ายบอกทางlink_to · post_path(42)ต่อข้อความเอง fmt.Sprintf("/posts/%d", id)
gem · Gemfile · Gemfile.lockอุปกรณ์สำเร็จรูป · รายการซื้อของ · ใบเสร็จไฟล์บนสุดของโปรเจกต์module · go.mod · go.sum
Convention over Configurationแฟรนไชส์ที่ทุกสาขาวางของที่เดิมรู้คำว่า posts คำเดียว ก็เดาได้ทั้งสาย (หัวข้อ 2)ไม่มี — ต้องบอกทุกอย่างเอง
server logสมุดจดใบสั่งหน้าครัวหน้าต่าง terminal ที่รัน server · log/development.logบรรทัด [GIN] …
bin/rails consoleครัวทดลองหลังร้านพิมพ์ Ruby ทีละบรรทัด โดยโหลดแอปไว้แล้วไม่มี — ต้องเขียน main.go ใหม่
debuggerกดหยุดเวลาในครัว แล้วเดินไปเปิดหม้อดูเขียนคำว่า debugger ในบรรทัดที่อยากให้หยุดDelve (dlv) ต้องติดตั้งเอง
migration (Day 2)ใบสั่งช่างมาต่อเติมตู้เก็บแฟ้มdb/migrate/…goose

เส้นทางของใบสั่งหนึ่งใบ (บท 90 · 100) — ผิดตรงไหน ไล่ตามเส้นนี้:

ลูกค้าพิมพ์ localhost:3000/posts
   │  ใบสั่ง: GET /posts
   ▼
ประตูร้าน (Puma) ──▶ ด่านหน้าครัว (middleware) ── จดสมุด: Started GET "/posts"
   ▼
พนักงานต้อนรับ (config/routes.rb)     "GET /posts → แผนก posts เมนู index"
   ▼
หัวหน้าเชฟ (PostsController#index)    วางของบนถาด: @posts = [...]
   │   ╲ (Day 2) ห้องเก็บวัตถุดิบ = model Post + database
   ▼
จัดจาน (app/views/posts/index.html.erb) วางบนจานรอง (layout)
   ▼
ยกจานเสิร์ฟ = response: HTML + 200 OK
🍜 ทำไมใช้ภาพร้านอาหาร

แต่ละคนในร้านทำงานอย่างเดียวแล้วส่งต่อ — พนักงานต้อนรับไม่ทำอาหาร · หัวหน้าเชฟไม่ค้นตู้เอง · คนดูแลสต็อกไม่จัดจาน · หน้าเว็บผิดจึงรู้ทันทีว่าต้องไปหาใคร: URL ไปผิดทาง → routes · ข้อมูลผิด → model · หน้าตาผิด → view

ที่ไม่เหมือนกัน: ในร้านจริงมีคนเดินไปมา · ใน Rails ทุก “คน” คือโค้ดในโปรแกรมเดียวที่เรียกต่อกัน เสร็จในไม่กี่มิลลิวินาที · และหัวหน้าเชฟ (controller) เป็นคนใหม่ทุกใบสั่ง — จำอะไรจากใบสั่งก่อนไม่ได้เลย

2 · Post กับ posts — กฎตั้งชื่อ

กฎจำง่าย: ตัวใหญ่เอกพจน์ (Post) = แม่พิมพ์ของบทความหนึ่งชิ้น · ตัวเล็กพหูพจน์ (posts) = ที่เก็บหลายชิ้น หรือ URL · ชื่อเดียวแตกไปอยู่ได้ทุกที่ข้างล่าง — Rails จับคู่ให้เองจากชื่อ ผิดตัวเดียว (เช่นขาด s) ก็หาไม่เจอ

ของชื่อหนึ่ง / หลายชิ้นภาษาคนใน Go คือ
model (class)Postหนึ่งแม่พิมพ์: บทความหนึ่งชิ้นมีอะไรบ้างtype Post struct
ไฟล์ modelapp/models/post.rbหนึ่งชื่อไฟล์ = ชื่อ class ตัวเล็กไฟล์ .go ตั้งชื่ออะไรก็ได้
ตารางใน databasepostsหลายลิ้นชักที่เก็บบทความทุกชิ้นตาราง posts ที่ GORM เดาให้
URL/posts · /posts/42หลายหน้ารวม · บทความเบอร์ 42 ในกลุ่มบทความr.GET("/posts/:id", …)
controller (class)PostsControllerหลายแผนกที่ดูแลบทความทั้งกลุ่มกลุ่ม handler
ไฟล์ controllerapp/controllers/posts_controller.rbหลายตัวเล็ก คั่นคำด้วย _—
โฟลเดอร์ viewapp/views/posts/หลายเก็บหน้าตาทุกหน้าของแผนกบทความ—
ไฟล์ view ของเมนู indexapp/views/posts/index.html.erb—ชื่อแผนก + ชื่อเมนู = ที่อยู่ไฟล์ต้องบอกชื่อ template เองทุกครั้ง
ชื่อใน route"posts#index"หลายซ้าย # = แผนก · ขวา = เมนูส่งตัวฟังก์ชัน h.List ตรง ๆ

สองรูปแบบการเขียน: ชื่อ class ตัวใหญ่ต้นคำเขียนติดกัน (PostsController · เรียก CamelCase) · ชื่อไฟล์และตารางตัวเล็กหมดคั่นด้วย _ (posts_controller.rb · เรียก snake_case) · class BlogPost ต้องอยู่ไฟล์ blog_post.rb

ให้ Rails แปลงชื่อให้ดู — พิมพ์ใน bin/rails console (ผลจริง บท 99)

พิมพ์ได้ภาษาคน
"Post".tableize"posts"ชื่อ model → ชื่อตาราง
"posts".classify"Post"ชื่อตาราง → ชื่อ class
"post".pluralize"posts"ทำเป็นพหูพจน์
"people".singularize"person"ทำเป็นเอกพจน์
"Category".tableize"categories"y → ies
"Person".tableize · "Mouse".tableize · "Sheep".tableize"people" · "mice" · "sheep"รู้จักคำพิเศษ ไม่ได้แค่เติม s
"human".pluralize"humen" ⚠️ผิด! Rails มีแค่กฎตัดท้ายคำ · ชื่อไม่คุ้นให้ลองที่นี่ก่อนสร้าง model

❓ “ใน Go ต้องประกาศ struct ชัด ๆ แล้ว Post โผล่มาเฉย ๆ ได้ไง?” — คำตอบมีสองครึ่ง (บท 90 · 95 · 99)

  1. มีไฟล์ประกาศจริง คือ app/models/post.rb (สร้างด้วย bin/rails generate model Post title:string body:text) · ทั้งไฟล์มีแค่ class Post < ApplicationRecord / end · และไม่ต้อง import — พอโค้ดเรียก Post Rails แปลงเป็นชื่อไฟล์ post.rb แล้วไปเปิดให้เอง (เรียกว่า autoload)
  2. ช่องข้อมูลมาจากตาราง ไม่ได้มาจากไฟล์ · ตอนรัน Rails เปิดตาราง posts ดูว่ามีคอลัมน์อะไร แล้วสร้าง method ให้ · bin/rails runner 'p Post.column_names' ได้ ["id", "title", "body", "created_at", "updated_at"] · Go กลับกัน: เขียน struct ก่อน แล้ว GORM ทำตารางตาม

3 · Ruby เทียบ Go ทีละเรื่อง

อ่านแถวละเรื่อง · คอลัมน์ขวาสุดคือสิ่งที่ต่างจาก Go จนพลาดบ่อย · ตัวเลขในวงเล็บคือบทที่สอน

พื้นฐาน — ตัวแปร · string · object (91)

เรื่องGoRubyภาษาคน / จุดที่ต่าง
comment// …# …Ruby ข้ามไม่รัน · # ใน "#{}" ไม่ใช่ comment
ลองโค้ดทีละบรรทัดต้องมี package main + func main()พิมพ์ irb ใน terminalกระดาษทดเลข · พิมพ์ exit แล้วหายหมด
ตัวแปรtitle := "Hi"title = "Hi"ไม่ต้องบอกชนิด · พิมพ์ชื่อผิดตอนให้ค่า Ruby คิดว่าเป็นตัวใหม่ ไม่เตือน
ค่าคงที่const BlogName = "x"BLOG_NAME = "x"แค่ขึ้นต้นตัวใหญ่ · เปลี่ยนได้แต่โดน warning: already initialized constant
ตัวใหญ่ต้นชื่อแปลว่าexport ให้ package อื่นใช้เป็นค่าคงที่ (ชื่อ class ก็ด้วย)ความหมายคนละเรื่องกัน
แทรกค่าในข้อความfmt.Sprintf("บทความ: %s", t)"บทความ: #{t}"ต้องใช้ "…" คู่ · '…' เดี่ยวพิมพ์ #{t} ออกมาตรงตัว
ข้อความ + ตัวเลข"อ่าน " + views → build ไม่ผ่าน"อ่าน " + views → TypeError ตอนรันใช้ "อ่าน #{views}" (แปลงให้เอง) หรือ views.to_s
สั่งงานค่าlen(t) · strings.ToUpper(t)t.length · t.upcase · 42.even?ทุกค่าเป็น object มี method ติดตัว เรียกด้วยจุด
ต่อหลายขั้นlen(strings.ToUpper(strings.TrimSpace(s))) อ่านในออกนอกs.strip.upcase.length อ่านซ้ายไปขวาเรียกว่า method chaining · เจอใน Rails บ่อยมาก
ถามชนิดรู้จาก type ที่ประกาศ42.class → Integer · nil.class → NilClassถามได้ทุกค่า เพราะทุกค่าเป็น object
ไม่มีค่าnil (pointer · slice)nilRuby เจอบ่อยกว่ามาก: หาช่องใน Hash ไม่เจอ · ขอช่อง Array ที่ไม่มี · method ที่ไม่ได้คืนอะไร

ของหลายชิ้น — Array · Hash · Symbol (92)

เรื่องGoRubyภาษาคน / จุดที่ต่าง
รายการ[]int{10, 20, 30}[10, 20, 30]Array = slice · ช่องแรกคือ 0
เพิ่มท้ายs = append(s, 40)s << 40<< อ่านว่า “ยัดต่อท้าย” (shovel)
ความยาวlen(s)s.lengthถามจาก Array เองด้วยจุด
ชิ้นสุดท้ายไม่มีเลขติดลบให้ใช้s[-1]เลขติดลบ = นับจากท้าย
ขอช่องที่ไม่มีpanic: runtime error: index out of ranges[10] → nil เงียบ ๆเห็น nil งง ๆ ให้สงสัยเลขช่องก่อน
คู่ชื่อ–ค่าmap[string]int{"a": 1}{ a: 1 }Hash = map · ในตัวอย่าง Day 1 ใช้จำลองบทความหนึ่งชิ้น
หาไม่เจอv, ok := m["b"] → 0 falsem[:b] → nil · m.fetch(:b, 0) → 0Ruby ไม่มี ok · อยากให้พังชัด ๆ ใช้ fetch(:b) → KeyError
ป้ายชื่อใกล้สุดคือ const StatusActive = "active":title (symbol)ชื่อเรียก ไม่ใช่ข้อความโชว์ผู้ใช้ · title: ตอนสร้าง กับ [:title] ตอนหยิบ = ตัวเดียวกัน
key คนละชนิดkey เป็น string เสมอpost[:title] ≠ post["title"]Hash ที่เก็บด้วย :title หยิบด้วย "title" ได้ nil · ยกเว้น params ของ Rails ที่หยิบได้ทั้งสองแบบ

method และ block (93)

เรื่องGoRubyภาษาคน / จุดที่ต่าง
ประกาศfunc Greet(name string) string { … }def greet(name) … endไม่บอกชนิด · ปิดด้วย end ไม่ใช่ปีกกา · ชื่อใช้ snake_case
ส่งค่ากลับreturn x บังคับบรรทัดสุดท้ายที่รัน = ค่าที่ส่งกลับเผลอแปะ puts ไว้ท้าย method → ได้ nil (puts คืน nil เสมอ)
method บรรทัดเดียว—def add(a, b) = a + bมี = ไม่มี end
ส่งของโดยบอกชื่อช่องส่ง struct · ลืม field ได้ค่าศูนย์เงียบ ๆdef make_post(title:, draft: false)title: ไม่มีค่าตาม = บังคับ · ลืมส่ง → missing keyword: :title
ถามใช่/ไม่ใช่func IsDraft(p Post) booldraft?(post) · [].empty?? เป็นตัวอักษรในชื่อ · ข้อตกลง = ตอบ true/false
แก้ของเดิม—name.upcase!! = “อันตรายกว่า” · ไม่มีอะไรต้องเปลี่ยน → คืน nil อย่าเอาไปต่อ
ส่งก้อนโค้ดส่ง func(n int) { … } เป็น argument{ |n| … } หรือ do |n| … endเรียกว่า block · |n| = ชื่อของชิ้นที่ไหลมาถึง · บรรทัดเดียวใช้ { } หลายบรรทัดใช้ do … end
วนทีละชิ้นfor _, n := range nums { … }nums.each { |n| puts n }คน Ruby แทบไม่เขียน for
ทำซ้ำ N ครั้งfor i := 0; i < 3; i++ { … }3.times { |i| … }นับจาก 0 · ตัวเลขก็มี method
เรียกก้อนที่ฝากมาเรียก f(n)yieldไม่มีใครฝาก block มา → no block given (yield)
พิมพ์ดูค่าfmt.Printlnputs (ให้คนอ่าน · คืน nil) · p (ให้โปรแกรมเมอร์ · คืนของเดิม)debug ใช้ p — เห็น "1" ต่างจาก 1 และเห็นคำว่า nil

จริง/เท็จ และ nil (94)

อยากรู้ว่า…GoRubyอย่าเขียน / กับดัก
กฎของ ifต้องเป็น bool · if views → non-boolean conditionไม่ผ่านแค่ nil กับ false0 · "" · " " · [] · "false" ผ่านหมด
ตัวเลขมากกว่า 0 ไหมif views > 0views.positive? · เท่ากับ 0: views.zero?if views — 0 ก็ผ่าน
เป็น nil ไหมx != nilx.nil?if !x แยก nil กับ false ไม่ออก
เรียก method ถ้ามีค่าif p.Author != nil { … }post[:author]&.upcaseไม่มีค่าได้ nil กลับมา — เลื่อนปัญหาไปข้างหน้า ไม่ได้แก้
ใส่ค่าเริ่มต้นถ้ายังว่างif tags == nil { tags = … }tags ||= ["general"]ค่า false ที่ตั้งใจใส่ โดนทับเป็นค่าใหม่ · ใช้ x = true if x.nil? แทน
ช่องฟอร์มที่ผู้ใช้ปล่อยว่างstrings.TrimSpace(q) == ""q.blank? · q.presence || "all"q || "all" — "" กับ " " ผ่าน || ไปเลย
Active Support (ของที่ Rails เติมให้ Ruby)ผลจริงภาษาคน
"".blank? · " ".blank? · nil.blank? · [].blank? · false.blank?true ทั้งหมด“ว่าง” แบบที่คนทั่วไปมอง
0.blank?falseตัวเลขไม่เคยว่าง
"ramen".present?trueตรงข้ามกับ blank? เป๊ะ
"ramen".presence · " ".presence · nil.presence"ramen" · nil · nilมีของจริงส่งตัวมันมา · ว่างส่ง nil — ใช้คู่กับ ||
3.days.agoวันเวลาเมื่อ 3 วันก่อน (ใน console ของแอปใหม่แสดงเป็นโซน UTC)Ruby เปล่าพัง NoMethodError · ในแอป Rails ใช้ได้เลย

นอกแอป Rails ต้องสั่งโหลด Active Support เอง: ruby -ractive_support/all ไฟล์.rb (-r = โหลดสิ่งนี้ก่อนรัน)

class และ module (95)

เรื่องGoRubyภาษาคน / จุดที่ต่าง
แม่พิมพ์type Post struct { Title string }class Post … endแม่พิมพ์ขนมไข่ · ของแต่ละชิ้นที่ได้คือ object
สร้างของหนึ่งชิ้นNewPost("Hi") · Post{Title: "Hi"}Post.new("Hi")new ไปเรียก def initialize(title) ให้เอง
ช่องข้อมูลfield Title@title = title@ = ติดอยู่กับ object ตัวนี้ ไม่หายตอน method จบ
อ่านจากข้างนอกpost.Title ได้เลยถ้าตัวใหญ่ต้องมี method · attr_accessor :title (อ่าน+แก้) · attr_reader :title (อ่านอย่างเดียว)ไม่มี → undefined method 'title' for an instance of Post
method ของแม่พิมพ์func (p Post) Greeting() stringdef greeting ข้างใน classไม่ต้องเขียน receiver · @title ≈ p.Title
สืบทอดไม่มี (ใช้ฝัง struct)class Recipe < Post · super(title)< ตรงนี้ไม่ใช่น้อยกว่า · super ไม่ใส่วงเล็บ = ส่ง argument ทุกตัวต่อ
แชร์ method ข้าม classฝัง struct (embedding)module Sluggable + include Sluggableกล่องเครื่องมือกลาง · module สร้าง object เองไม่ได้ · ใน Rails เก็บที่ concerns/
@ตัวแปร สะกดผิดfield ผิด → compile ไม่ผ่าน@titel → nil เงียบ ๆผลว่างแปลก ๆ ให้สงสัยตัวสะกดของ @… ก่อน

error และ duck typing (96)

เรื่องGoRubyภาษาคน / จุดที่ต่าง
error คืออะไรค่าที่ return มาคู่ผลลัพธ์ (int, error)exception ที่ถูก “โยน” ขึ้นไปจนกว่าจะมีคนรับกล่องจดหมาย vs กริ่งไฟไหม้ · ไม่มีใครรับ = โปรแกรมหยุด
รับ errorif err != nil { … } ทุกจุดbegin … rescue … endครอบทีเดียวได้หลายบรรทัด · ≈ try/catch ของ TS
รับเฉพาะชนิดเช็คค่า err เองrescue ZeroDivisionError · rescue ArgumentError => eเข้าช่องแรกที่ชนิดตรง · => e เก็บ error ไว้ดู e.message
สร้าง error เองerrors.New("…")class OutOfStockError < StandardError; end · raise OutOfStockError, "…"ต้อง < StandardError · ถ้า < Exception แล้ว rescue => e ไม่รับ
ทำเสมอdeferensureป้าย “ออกจากครัวต้องปิดแก๊ส” · พังหรือไม่ก็ทำ
rescue ทั้ง method—เขียน rescue ก่อน end ของ def ไม่ต้องมี beginRails ใช้ท่านี้บ่อย
รับ “อะไรก็ได้ที่มี title”ประกาศ interface · compiler ตรวจตอน buildไม่ประกาศอะไร เรียก x.title เลยduck typing · ส่งผิดพังตอนรัน (NoMethodError) · ถามก่อนได้ด้วย respond_to?(:title)
ไม่มีใครรับใน Rails—Rails รับชั้นบนสุดให้ ตอบ 500พังแค่ใบสั่งนั้น ร้านยังเปิด · รายละเอียดอยู่ใน server log

4 · map · select · find — ผลรันจริง (บท 92)

สายพานในครัว: ของไหลมาทีละชิ้นผ่านสถานีต่าง ๆ · ทุกตัวไม่แตะ Array เดิม สร้างของใหม่ให้ · ตัวอย่างใช้ nums = [3, 1, 4, 1, 5]

โค้ดภาษาคนผลจริงใน Go คือ
nums.map { |n| n * 2 }ทำอย่างเดียวกันกับทุกชิ้น[6, 2, 8, 2, 10]for + append ลงถังใหม่
nums.select { |n| n > 2 }เก็บเฉพาะชิ้นที่ผ่าน (= filter ของ TS)[3, 4, 5]for + if + append
nums.reject { |n| n > 2 }ทิ้งชิ้นที่ผ่าน[1, 1]เหมือนข้างบน กลับเงื่อนไข
nums.find { |n| n > 2 }ชิ้นแรกที่ใช่ แล้วหยุด3for + if + break
nums.find { |n| n > 100 }หาไม่เจอnil—
nums.select { |n| n > 100 }คัดแล้วไม่เหลือ[] (ไม่ใช่ nil)—
nums.sum · nums.sortรวม · เรียงน้อยไปมาก14 · [1, 1, 3, 4, 5]วน for เอง
nums.min · nums.maxน้อยสุด · มากสุด1 · 5วนเทียบเอง
p nums หลังทำทั้งหมดของเดิมยังอยู่ครบ[3, 1, 4, 1, 5]—

กับบทความหลายชิ้น (จำลองด้วย Array ของ Hash · 3 ชิ้น ยอดอ่าน 120 · 40 · 300):

โค้ดภาษาคนผลจริง
posts.select { |post| post[:published] }.map { |post| post[:title] }เลือกที่เผยแพร่แล้ว → เอาแต่ชื่อ["ต้มยำกุ้ง", "เที่ยวน่าน"]
posts.sum { |post| post[:views] }รวมยอดอ่าน460
posts.sort_by { |post| post[:views] }.map { |post| post[:title] }เรียงตามยอดอ่าน แล้วดูแค่ชื่อ["ส้มตำ", "ต้มยำกุ้ง", "เที่ยวน่าน"]
posts.find { |post| post[:views] > 100 }บทความทั้งก้อนชิ้นแรกที่เกิน 100{title: "ต้มยำกุ้ง", published: true, views: 120}
⚠ ทางลัด &:ชื่อ ใช้กับ Hash ไม่ได้

names.map(&:upcase) = names.map { |name| name.upcase } (“ทุกชิ้น เรียก method ชื่อนี้”) · แต่ posts.map(&:title) กับ Array ของ Hash พัง undefined method 'title' for an instance of Hash — Hash มีช่อง :title ไม่ได้มี method title · กับ Hash ใช้ { |post| post[:title] } · &:title ใช้ได้กับ Post ตัวจริงใน Rails (Day 2)

5 · 5 ไวยากรณ์ที่เจอในโค้ด Rails (บท 97)

โค้ด Rails ที่อ่านเหมือนภาษาอังกฤษ ไม่มีเวทมนตร์ — ทุกบรรทัดคือการเรียก method ธรรมดาที่เขียนย่อ · อ่านแล้วงงเมื่อไหร่ ให้นึกภาพ “แบบเขียนเต็ม” ในคอลัมน์กลาง

ชิ้นเห็นในโค้ด Railsแบบเขียนเต็ม (ไว้อ่าน)ภาษาคนใน Go
1 · วงเล็บละได้greet "ต้น"greet("ต้น")เรียก method ตัวเดียวกันต้องมีวงเล็บเสมอ
2 · hash ตัวสุดท้ายละปีกกาได้validates :title, presence: truevalidates(:title, { presence: true })เห็น ชื่อ: ค่า ท้ายบรรทัด = hash ก้อนเดียวที่ถอดปีกกาต้องสร้าง struct แล้วใส่ปีกกาเอง
2 · (อีกตัวอย่าง)redirect_to @post, notice: "saved"redirect_to(@post, { notice: "saved" })ชิ้นแรกคือ @post · ที่เหลือรวมเป็น hash—
3 · block ที่ได้รับของform_with model: @post do |f|form_with(model: @post) do |f| — ห้ามใส่ปีกกาform_with สร้างกล่องเครื่องมือ แล้ว yield ยื่นให้ block → กลายเป็น fc ใน func(c *gin.Context) ที่ Gin ยื่นให้
4 · symbol ทุกที่f.text_field :title · status: :see_otherf.text_field(:title)โคลอนหน้า (:title) หรือหลัง (presence:) ก็เป็น symbol ทั้งคู่ใช้ string เป็นชื่อช่อง gin.H{"title": …}
5 · method ที่ Rails สร้างตอนรันpost.title ทั้งที่ post.rb ว่าง(เหมือนเดิม)Rails อ่านคอลัมน์ในตาราง แล้วสร้าง title · title= ให้ · ดูว่ามีช่องอะไรที่ db/schema.rbเขียน field ทุกตัวใน struct เอง
⚠ เขียนเต็มไว้ “อ่าน” เท่านั้น อย่าเติมปีกกาเองตอนเรียก form_with

form_with รับของแบบ keyword (ส่งด้วยชื่อ) · ใส่ปีกกาแล้วกลายเป็น hash หนึ่งก้อน ได้ error จริง: wrong number of arguments (given 1, expected 0; required keyword: model) (ArgumentError) · เขียนแบบไม่มีปีกกา (แบบในคู่มือ Rails) ใช้ได้กับทุกตัว

ท่า “method ยื่นของให้ block” ที่จะเจออีก: <%= yield %> ใน layout (บท 103) · create_table :posts do |t| ใน migration (Day 2) · respond_to do |format| ใน controller (Day 3) · เจอ |x| ที่ไม่รู้จัก ให้ถามว่า “method ไหนยื่นของชิ้นนี้มา”

6 · rails new · โฟลเดอร์ · Gemfile (บท 98)

ท่อนของ rails new wongnok_blog --database=postgresql --css=tailwindภาษาคนในร้านอาหาร
rails newสร้างโปรเจกต์ใหม่ (ได้ 109 ไฟล์ · เปิดเป็นเว็บได้ทันที)ขอเปิดสาขาใหม่จากบริษัทแม่
wongnok_blogชื่อโปรเจกต์ = ชื่อโฟลเดอร์ชื่อสาขา
--database=postgresqlใช้ PostgreSQL (ไม่ใส่ = SQLite ซึ่งเป็น database ไฟล์เดียว)ขอตู้เก็บแฟ้มยี่ห้อนี้
--css=tailwindติดตั้ง Tailwind ไว้แต่งหน้าเว็บขอชุดตกแต่งร้าน

Go — เช่าห้องเปล่า

$ go mod init blog
go: creating new go.mod: module blog
$ ls -A
go.mod

Rails — ได้ร้านครบ (แต่ยังไม่ commit)

$ git log
fatal: your current branch 'main' does not have any commits yet
$ git add -A
$ git commit -m "rails new"

rails new แค่ git init ให้ ไม่ได้ commit ให้ — commit แรกเองทันที จะได้เทียบได้ว่าแก้อะไรไป

ห้องในร้าน — Day 1 แตะแค่ไม่กี่ห้อง

โฟลเดอร์ / ไฟล์มีไว้ทำอะไรในร้านอาหารใน Go ที่เคยทำDay 1
app/controllers/หัวหน้าเชฟแต่ละแผนกห้องเชฟhandler✅ บ่อย
app/views/ไฟล์ HTML ที่ส่งให้ลูกค้าโต๊ะจัดจาน(ฝั่ง Go ส่ง JSON ด้วย c.JSON)✅ บ่อย
app/models/ดูแลข้อมูลในตู้แฟ้มห้องวัตถุดิบstruct + GORM❌ รอ Day 2
config/routes.rbรายการ URL ทั้งแอปรายการในมือพนักงานต้อนรับr.GET(…) ใน main.go✅ บ่อย
bin/สคริปต์คำสั่ง เช่น bin/railsแผงสวิตช์ของร้านgo run .✅ ใช้ทุกวัน ไม่ต้องแก้
Gemfile · Gemfile.lockgem ที่ใช้ · รุ่นจริงที่ได้มารายการซื้อของ · ใบเสร็จgo.mod · go.sum👀 เปิดอ่านบ้าง
log/บันทึกทุก requestสมุดจดใบสั่งlog ของ Gin👀 ดูตอนพัง
db/เรื่องของ database (db/migrate/ · db/schema.rb เกิดตอนสร้างตารางแรก)ตู้เก็บแฟ้มไฟล์ migration ของ goose❌ รอ Day 2
test/โค้ดทดสอบห้องชิมก่อนเสิร์ฟ_test.go❌ รอ Day 4
public/ไฟล์ที่ส่งตรงไม่ผ่านครัว (เช่นหน้า 404)ใบปลิวหน้าร้าน—❌
ที่เหลือ (lib/ tmp/ storage/ vendor/ …)ของเครื่องมือ · deploy · ไฟล์ชั่วคราวห้องเก็บของหลังร้าน—❌ ไม่ต้องสนใจ

Gemfile เทียบ go.mod

บรรทัดใน Gemfileภาษาคนใน Go คือ
source "https://rubygems.org"ไปซื้อของที่คลังไหนแหล่งที่ go get โหลด module
gem "rails", "~> 8.1.4"เรียก method gem แบบละวงเล็บ: ชื่อ gem + เงื่อนไขรุ่นrequire github.com/… v1.12.0
gem "pg", "~> 1.1"ตัวคุยกับ PostgreSQLdriver postgres ที่ GORM ใช้
gem "puma", ">= 5.0"web server (ประตูร้าน) · รุ่น 5.0 ขึ้นไปnet/http ที่ Gin ใช้
group :development, :test do … endใช้เฉพาะเครื่องเรากับตอนเทส ไม่โหลดบนเว็บจริง—
gem "brakeman", require: falseติดตั้งไว้ แต่ไม่โหลดเข้าแอปเอง (เครื่องมือเรียกจาก terminal)—
เงื่อนไขรุ่น ~> (“ขยับได้ แต่ไม่ไกล”)ผ่านไม่ผ่าน
~> 1.2 = ตั้งแต่ 1.2 แต่ต่ำกว่า 2.01.2.0 · 1.14.0 · 1.99.91.1.0 · 2.0.0
~> 8.1.4 = ตั้งแต่ 8.1.4 แต่ต่ำกว่า 8.28.1.4 · 8.1.98.1.3 · 8.2.0

7 · คำสั่งที่ใช้บ่อยใน Day 1

ในโฟลเดอร์โปรเจกต์ใช้ bin/rails เสมอ (ใช้ gem รุ่นตาม Gemfile.lock ของโปรเจกต์นี้) · rails เฉย ๆ ใช้แค่ตอน rails new

อยากทำอะไรพิมพ์ควรเห็น / ต้องรู้ใน Go คือ
เช็ครุ่น Rubyruby -vขึ้นต้น ruby 4. · ถ้าได้ ruby 2.6.10… = Ruby รุ่นเก่าที่ติดมากับ Mac (terminal ยังไม่ได้ปลุก mise — ตัวสลับรุ่น Ruby ที่ติดตั้งในบท 91)go version
รันไฟล์ Rubyruby ไฟล์.rbอ่านไปรันไปทีละบรรทัด · บรรทัดก่อนจุดพังทำงานไปแล้วgo run main.go
ตรวจไวยากรณ์อย่างเดียวruby -c ไฟล์.rbผ่านได้ Syntax OK · ใช้หา end ที่หายgo build (ส่วนตรวจ)
ลองโค้ดทีละบรรทัดirbprompt irb(main):001> · ผลขึ้นหลัง =>ไม่มี
สร้างโปรเจกต์rails new wongnok_blog --database=postgresql --css=tailwindแล้ว commit แรกเองgo mod init
ติดตั้ง gem ของโปรเจกต์bundle installBundle complete! …go mod download
สร้าง databasebin/rails db:prepareCreated database '…' · ต้องตั้ง DATABASE_URL ก่อน—
เปิดร้านbin/rails server (ย่อ s)Listening on http://127.0.0.1:3000 · ปิดด้วย Ctrl-C · แก้ routes/controller/view แล้ว refresh ได้เลยr.Run()
ครัวทดลองbin/rails console (ย่อ c)prompt wongnok-blog(dev):001> · ออกด้วย exitไม่มี
ครัวทดลองแบบลองแล้วทิ้งbin/rails console --sandboxAny modifications you make will be rolled back on exitdefer tx.Rollback()
รันไฟล์ Ruby ในแอปbin/rails runner ไฟล์.rb · bin/rails runner '…'รู้จัก Post โดยไม่ต้อง import—
สร้าง model (สาธิตบท 99)bin/rails generate model Post title:string body:text (ย่อ g)ได้ไฟล์ migration (ใบสั่งสร้างตาราง) · app/models/post.rb · ไฟล์เทส test/models/post_test.rb · ข้อมูลตัวอย่างไว้เทส test/fixtures/posts.ymlเขียน struct เอง
สร้างตารางตาม migrationbin/rails db:migrate== … CreatePosts: migratedgoose (บท 31)
ย้อน migration ล่าสุดbin/rails db:rollbackต้อง rollback ก่อน destroy—
ลบไฟล์ที่ generate ไว้bin/rails destroy model Categoryบรรทัด remove …—
ดู URL ของแผนกหนึ่งbin/rails routes -c postsตาราง Prefix · Verb · URI Pattern · Controller#Actionอ่าน main.go เอง
ดูด่านหน้าครัวbin/rails middlewareบรรทัด use … เรียงตามลำดับที่ใบสั่งผ่านr.Use(…) ที่เขียนไว้
แอปนี้ใช้อะไรรุ่นไหนbin/rails aboutRails · Ruby · Database adapter · schema version—
มีคำสั่งอะไรบ้างbin/rails --help · bin/rails generate model --helpสามตัวบนสุด: generate · console · servergo help
ขอหน้าเว็บจาก terminalcurl -s http://localhost:3000/postsได้ HTML เป็นตัวหนังสือ · -i ดู header · -L เดินตาม redirect · -w '%{http_code}\n' ดูเลขสถานะเหมือนกัน
หา debugger ที่ลืมไว้grep -rn debugger app/ไม่ขึ้นอะไรเลย = สะอาด พร้อม commit—

8 · routes · resources 8 เส้น · _path (บท 101)

routes คือพนักงานต้อนรับ — ดู verb (อยากทำอะไร: GET = ขอดู · POST = ส่งของใหม่) กับ path (เรื่องอะไร) แล้วชี้ว่าแผนกไหน เมนูอะไร · ไม่ทำอาหาร ไม่แตะ database

Go · Gin

r.GET("/posts", h.List)
r.GET("/posts/:id", h.Get)

Rails · config/routes.rb

Rails.application.routes.draw do
  get "/posts",     to: "posts#index"
  get "/posts/:id", to: "posts#show", as: :post
  root "posts#index"
end
ชิ้นภาษาคนใน Gin
get "/posts", to: "posts#index"ใบสั่ง GET /posts → แผนก posts (PostsController) เมนู index · get คือ method ที่ละวงเล็บ · to: คือ hash ไม่มีปีกกาr.GET("/posts", h.List)
"posts#index"ส่งแค่ชื่อเป็นข้อความ · # ในเครื่องหมายคำพูดเป็นตัวคั่น ไม่ใช่ commentส่งตัวฟังก์ชัน · พิมพ์ผิด build ไม่ผ่าน
:id ใน pathช่องว่างในใบสั่ง รับอะไรก็ได้ → params[:id] · เป็น String เสมอ ("42") · /posts/abc ก็เข้าc.Param("id") ได้ string
root "posts#index"หน้าแรกของร้าน (path /) · มีได้ตัวเดียวr.GET("/", …)
as: :postตั้งชื่อเล่นให้ route → ได้ helper post_path · path ที่มี : ต้องตั้งเองไม่มี
post_path(42) · posts_path · root_path"/posts/42" · "/posts" · "/" · เปลี่ยน URL ในไฟล์เดียว ลิงก์เปลี่ยนตามทุกที่fmt.Sprintf("/posts/%d", id)
post_url(42)URL เต็มพร้อมโดเมน http://localhost:3000/posts/42 (ใช้ในอีเมล) · ลิงก์ในเว็บใช้ _path พอ—
namespace :admin do … endโซนหลังร้าน: URL /admin/posts · แผนก Admin::PostsController · helper admin_posts_path · ไม่ได้ล็อกอะไรr.Group("/admin") (ใกล้ scope)

resources :posts — ชุดเมนูมาตรฐาน 8 เส้นในบรรทัดเดียว

ใบสั่งเมนู (action)ภาษาคนhelper
GET /postsindexดูบทความทั้งหมดposts_path
GET /posts/42showดูบทความชิ้นเดียวpost_path(42)
GET /posts/newnewเปิดฟอร์มเขียนใหม่new_post_path
POST /postscreateกดบันทึกบทความใหม่(ใช้ posts_path)
GET /posts/42/editeditเปิดฟอร์มแก้edit_post_path(42)
PATCH /posts/42updateกดบันทึกที่แก้(ใช้ post_path(42))
PUT /posts/42updateบันทึกที่แก้ อีกวิธีเรียก (เมนูเดียวกับ PATCH)(ใช้ post_path(42))
DELETE /posts/42destroyลบบทความ(ใช้ post_path(42))

resources ให้แค่รายการของพนักงานต้อนรับ — เมนูใน controller ยังต้องเขียน def index · def show … เอง · เลือกบางเส้นได้: resources :posts, only: [:index, :show] · ใช้ครบ 8 เส้น (CRUD = สร้าง · อ่าน · แก้ · ลบ) ใน Day 2 · ใน Gin ต้องเขียน r.GET r.POST r.PATCH r.DELETE เองทีละบรรทัด

⚠ ลำดับบรรทัดสำคัญ — บรรทัดแรกที่เข้าเงื่อนไขชนะ (ต่างจาก Gin)
get "/posts/:id",     to: "posts#show"      # ← :id รับทุกอย่าง รวมคำว่า popular
get "/posts/popular", to: "posts#popular"   # ← ไม่มีวันถูกเรียก
# $ curl localhost:3000/posts/popular
แผนก posts · เมนู show · params[:id] = "popular"

กฎ: เจาะจงไว้บน กว้างไว้ล่าง · ไม่มีอะไรเตือน · Gin เลือกตัวที่เจาะจงกว่าให้เอง แต่ Rails ไม่ · (/posts กับ /posts/:id สลับกันได้ เพราะไม่แย่ง URL เดียวกัน)

9 · controller · params · render กับ redirect_to (บท 100 · 102)

Go · Gin — handler ตัวเดียวทำทุกอย่าง

r.GET("/hello", func(c *gin.Context) {
	name := c.Query("name")
	if name == "" {
		name = "PEA"
	}
	c.String(http.StatusOK, "Hello %s", name)
})

Rails — 3 ไฟล์ ตามทางที่ใบสั่งเดิน

# config/routes.rb
get "/hello", to: "greetings#index"
# app/controllers/greetings_controller.rb
class GreetingsController < ApplicationController
  def index
    @name = params[:name].presence || "PEA"
  end
end
# app/views/greetings/index.html.erb
<h1>Hello <%= @name %></h1>
เรื่องภาษาคนใน Gin
class GreetingsController < ApplicationControllerแผนก greetings · สืบทอดจากแม่แบบที่ rails new สร้างไว้ จึงได้ params · render · redirect_to มาฟรี · เขียน < บรรทัดเดียวกับ classhandler รับ c *gin.Context
def indexเมนู (action) · ชื่อต้องตรงกับขวาของ # ใน route · ไม่มี argumenthandler หนึ่งตัว
params[:name]รายละเอียดในใบสั่ง · มาได้ทั้ง ?name=Somchai และ /hello/:name ลงกล่องเดียวกัน · เป็นข้อความเสมอ (อยากได้เลขใช้ .to_i)c.Query("name") · c.Param("name")
?name= (มี = แต่ว่าง)ได้ "" ไม่ใช่ nil → "" || "PEA" ได้ "" · แก้ด้วย .presenceเช็ค name == "" ตรง ๆ
@name = …ถาดหน้าครัว — ทางเดียวที่ view มองเห็น · ลืม @ ได้ 200 กับค่าว่าง ไม่มี errorc.HTML(200, "index.tmpl", gin.H{"name": name})
ไม่ได้สั่ง renderRails เปิด app/views/<แผนก>/<เมนู>.html.erb ให้เอง (implicit render)ต้องสั่งตอบเองทุกครั้ง
render plain: "…"ตอบเป็นข้อความเปล่า ไม่ใช้ไฟล์ viewc.String(…)
before_action :set_post, only: %i[show edit update]“ก่อนเข้าเมนูพวกนี้ เรียก set_post ก่อน” · %i[…] = [:show, :edit, :update] · method ใต้ private ไม่ใช่เมนูmiddleware ที่ผูกบาง route

render (ยกจาน) กับ redirect_to (ชี้ไปเคาน์เตอร์อื่น)

renderredirect_to "/hello"
จำนวน request1 รอบ จบ2 รอบ (browser ยิงรอบสองเอง)
ที่เห็นใน curl -iHTTP/1.1 200 OK + หน้าเว็บHTTP/1.1 302 Found + location: http://localhost:3000/hello · content-length: 0
URL บนแถบที่อยู่ไม่เปลี่ยนเปลี่ยนเป็นที่อยู่ใหม่
@ตัวแปรใช้ต่อใน view ได้หายหมด — อยากส่งต่อใส่ใน URL เช่น "/hello?name=Rails"
ใช้ตอนไหนกรอกฟอร์มไม่ผ่าน → โชว์ฟอร์มเดิม (ของที่พิมพ์ยังอยู่)ทำสำเร็จ → พาไปหน้าผลลัพธ์ (refresh แล้วไม่ส่งซ้ำ)
ใน Ginc.HTML(…) · c.String(…)c.Redirect(http.StatusFound, "/hello")

เลขสถานะที่เจอใน log

เลขชื่อใน Railsใน Goในร้านอาหารเจอตอนไหน
200:okhttp.StatusOKจานถึงโต๊ะเรียบร้อยปกติ
302:foundhttp.StatusFound“เมนูนี้ไปสั่งเคาน์เตอร์ 2 นะคะ”redirect_to
403:forbiddenhttp.StatusForbiddenด่านหน้าครัวไม่ให้เข้าapp.get ใน console ก่อน app.host!
404:not_foundhttp.StatusNotFoundร้านไม่มีเมนูนี้ไม่มี route · ไม่มี action
406:not_acceptablehttp.StatusNotAcceptableเชฟทำเสร็จแต่หาแบบจัดจานไม่เจอไม่มีไฟล์ view (เปิดด้วย browser)
422:unprocessable_contenthttp.StatusUnprocessableEntityใบสั่งใช้ไม่ได้ ส่งคืนไม่มี CSRF token · ฟอร์มไม่ผ่าน (Day 2)
500:internal_server_errorhttp.StatusInternalServerErrorครัวไฟไหม้ — ความผิดของร้านโค้ดเรา error · ต่อ database ไม่ติด

อ่าน log ของ server ทีละบรรทัด

บรรทัดใน logแปลว่าในร้านอาหาร
Started GET "/posts" for ::1request เข้ามา (::1 = เครื่องตัวเอง)ด่านหน้าครัวจดใบสั่งใหม่
Processing by PostsController#index as HTMLroutes จับคู่ได้ ส่งถึง action แล้ว · ไม่มีบรรทัดนี้ = routes หาไม่เจอหัวหน้าเชฟรับใบสั่ง
Parameters: {"name" => "Somchai"}ของในกล่อง paramsรายละเอียดในใบสั่ง
Rendering posts/index.html.erb within layouts/applicationสร้าง HTML จาก view วางใน layoutจัดจานบนจานรอง
Redirected to http://localhost:3000/postsสั่ง redirect · ไม่มีบรรทัด Renderingชี้ไปเคาน์เตอร์อื่น
Completed 200 OK in 18msตอบแล้ว · เลขสถานะ · เวลารวม · ActiveRecord: 0.0ms = ไม่ได้แตะ databaseยกจานถึงโต๊ะ

10 · view · ERB · layout · link_to · ความปลอดภัยที่ได้ฟรี (บท 103)

ไฟล์ view ลงท้าย .html.erb — ERB = HTML ธรรมดาที่เจาะช่องให้ Ruby เติมได้ · เหมือนใบแบบจัดจานที่เว้นช่อง “ใส่ชื่อเมนูตรงนี้” ไว้ · ช่องมีสามแบบตามตารางนี้

แท็ก ERBทำอะไรลูกค้าเห็นไหมใน Go html/template
<%= … %>รัน Ruby แล้วพิมพ์ผลลงตรงนั้น (= = “เอาออกไปโชว์”)✅ เห็นผล{{.Title}}
<% … %>รันเฉย ๆ ไม่พิมพ์ — ใช้กับ if · each · end❌ ตัวมันไม่เห็น (HTML ที่มันคุมเห็นได้){{if …}} · {{range .}} · {{end}}
<%# … %>comment ไม่รันเลย❌ ไม่เห็นสักตัว—

วนรายการบทความ (จำลองด้วย Hash — มี model จริงแล้วเขียน post.title ได้ใน Day 2):

<h1>บทความทั้งหมด</h1>
<ul>
  <% @posts.each do |post| %>         <%# ไม่มี = เพราะเป็นคำสั่งถึงคนจัดจาน %>
    <li><%= link_to post[:title], post_path(post[:id]) %></li>
  <% end %>
</ul>

ผลจริงหนึ่งแถว: <li><a href="/posts/1">ข้าวมันไก่ร้านดัง</a></li> · link_to ข้อความ, ที่อยู่ สร้างแท็ก <a> · ใช้ post_path แทนพิมพ์ URL เอง

กับดัก ERBเห็นอะไรแก้
ลืม = เช่น <% @post[:id] %>ช่องนั้นว่าง ไม่มี errorใส่ =
ใส่ = ที่บรรทัด each <%= @posts.each do |post| %>ข้อมูลดิบทั้งก้อน [{id: 1, …}] โผล่ท้ายรายการลบ =
@post.title ทั้งที่ @post เป็น Hash จำลองundefined method 'title' for an instance of Hash@post[:title] (แบบจุดใช้ได้ Day 2)

layout — จานรองแบบเดียวกันทุกจาน

<%# app/views/layouts/application.html.erb (ของจริงจาก rails new · ย่อ) %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= content_for(:title) || "Wongnok Blog" %></title>
    <%= csrf_meta_tags %>
  </head>
  <body>
    <main class="container mx-auto mt-28 px-5 flex">
      <%= yield %>                       <%# ← view ของแต่ละหน้าถูกวางตรงนี้ %>
    </main>
  </body>
</html>

view ที่เราเขียนไม่ต้องมี <html> · Rails วางลงตรง <%= yield %> ให้เอง (เหมือน {children} ใน app/layout.tsx ของ Next.js) · Go ไม่มี layout ให้อัตโนมัติ · ในโหมด development Rails แปะ comment <!-- BEGIN app/views/… --> บอกว่า HTML ท่อนนี้มาจากไฟล์ไหน

ความปลอดภัยที่ได้ฟรี — ไม่ต้องเขียนอะไรเพิ่ม

ภัยภาษาคนRails ทำให้แล้ว (ผลจริง)ปิดตัวกันเองถ้า…
XSSคนร้ายตั้งชื่อบทความเป็น <script>… ให้ไปรันใน browser คนอื่นทุก <%= %> แปลง < → &lt; · > → &gt; · ' → &#39; · จอโชว์ตัวอักษร แต่ไม่รัน (Go html/template ก็ทำแบบนี้)ใช้ raw หรือ .html_safe กับข้อมูลที่ผู้ใช้พิมพ์ — ได้ <script> ตัวจริง ไม่มีใครเตือน
CSRFเว็บคนร้ายแอบส่งคำสั่ง “ลบบทความ” มา โดยอาศัย cookie ของเราทุกหน้ามีรหัสลับ csrf-token ใน <head> · คำสั่งที่ไม่ใช่ GET ไม่แนบรหัส → 422 + Can't verify CSRF token authenticity. · action ไม่ได้ทำงานเลยปิดการตรวจเอง — ฟอร์มและปุ่มของ Rails แนบรหัสให้อยู่แล้ว (Day 2–3)

11 · debugger · console (บท 104)

เครื่องมือภาษาคนในร้านอาหารใช้เมื่อใน Go
puts · p ใน actionพิมพ์ค่า · ผลขึ้นใน terminal ของ server ไม่ใช่บนหน้าเว็บกล้องที่ติดไว้ไม่กี่มุมรู้อยู่แล้วว่าจะดูค่าไหนfmt.Println
debuggerหยุดโค้ดบรรทัดนั้น แล้วถามค่าอะไรก็ได้กดหยุดเวลาในครัวต้องมีใบสั่งวิ่งมา เช่นอยากดู paramsDelve (dlv)
bin/rails consoleโหลดแอปทั้งตัว ให้พิมพ์ทีละบรรทัด ไม่ต้องเปิด serverครัวทดลองหลังร้านลองชื่อ route · helper · แปลงชื่อไม่มี
bin/rails console --sandboxเหมือน console แต่ทุกการเปลี่ยนใน database ถูกยกเลิกตอน exitเขียนบนแผ่นใสที่วางทับแฟ้มลองคำสั่งน่ากลัวกับข้อมูล (Day 2)defer tx.Rollback()

พิมพ์ได้ตอน debugger หยุด (prompt (rdbg))

พิมพ์ย่อทำอะไร
ชื่อตัวแปร / โค้ด Ruby—ดูค่าทันที เช่น @name · params[:name]
nextnทำบรรทัดนี้ แล้วหยุดบรรทัดถัดไป · ลูกศร => ชี้บรรทัดที่ “ยังไม่ทำ”
stepsมุดเข้าไปใน method ที่บรรทัดนี้เรียก (ระวังหลงเข้าโค้ดของ Rails)
info—ดูตัวแปรทุกตัว · ที่ขึ้นต้น @_ เป็นของ Rails
continuecปล่อยวิ่งต่อ · หน้าเว็บจะขึ้น (ระหว่างหยุด browser หมุนค้าง)

ใช้ใน console เท่านั้น (ผลจริง)

พิมพ์ได้ภาษาคน
app.hello_path"/hello"app = browser จำลอง · ถามชื่อ route ได้
helper.link_to "หน้าทักทาย", "/hello""<a href=\"/hello\">หน้าทักทาย</a>"helper = เรียก helper ของ view
app.get "/hello?name=Somchai"=> 403 + Blocked hosts: www.example.comติดด่านเช็คชื่อเว็บ — ตั้งใจ ไม่ใช่บั๊ก
app.host! "localhost" แล้ว app.get …=> 200บอก browser จำลองให้ใช้ชื่อ localhost (ตั้งครั้งเดียวต่อการเปิด console)
app.response.bodyHTML ทั้งหน้าดูจานที่ได้
reload!Reloading... · => nilแก้ไฟล์ใน app/ ระหว่าง console เปิด ต้องสั่งก่อนลองใหม่
⚠ ลบ debugger ก่อน commit ทุกครั้ง

gem debug อยู่ใน group :development, :test — เว็บจริง (production) ไม่โหลด · ลืมไว้ = ทุกคนที่เปิดหน้านั้นได้ 500 (NameError (undefined local variable or method 'debugger' …)) · ก่อน commit รัน grep -rn debugger app/ ให้ว่างเปล่า · และเปิด server ด้วย bin/rails server ไม่ใช่ bin/dev ตอนจะใช้ debugger

12 · ตาราง error → แปลว่า → แก้ยังไง

อ่านหน้าแดง 3 จังหวะ (บท 105): ① ชื่อ error + เกิดที่แผนกไหน (NameError in PagesController#about) → ② ประโยคตัวแดง + Did you mean? → ③ หา path ที่ขึ้นต้นด้วย app/ กับเลขบรรทัด · ถ้าไม่มีไฟล์ของเราเลย = ปัญหาอยู่ที่ชื่อ (route · ชื่อไฟล์ · ชื่อ class)

Ruby (ไฟล์ .rb และในแอป)

ข้อความที่เห็นแปลว่าแก้ยังไง
undefined local variable or method 'titel' for main (NameError) + Did you mean? titleไม่รู้จักชื่อนี้สะกดผิด — ดูชื่อที่ Ruby เดาให้
undefined method 'upcase' for nil (NoMethodError)ค่าตรงนั้นเป็น nil (กล่องว่าง)อย่าแก้ที่บรรทัดที่พัง — ย้อนหาว่าทำไมเป็น nil · หรือ &.
undefined method 'upcase' for an instance of Integer (NoMethodError)ของในมือเป็นชนิดอื่น (ตัวเลข) ไม่มีคำสั่งนี้ดูท่อน for an instance of … ว่าเป็นอะไรจริง
undefined method 'days' for an instance of Integer · … 'blank?' for an instance of Stringเป็นของ Active Support ไม่ใช่ Ruby เปล่าใช้ในแอป Rails หรือรัน ruby -ractive_support/all ไฟล์.rb
undefined method 'title' for an instance of Hash (NoMethodError)ใช้ .title หรือ &:title กับ Hashpost[:title] · { |post| post[:title] }
undefined method 'title' for an instance of Postclass ของเราไม่มี method อ่านค่าเติม attr_accessor :title (หรือ attr_reader)
undefined method 'length' for nil หลัง upcase!upcase! ไม่มีอะไรต้องเปลี่ยน เลยคืน nilเรียก method ! เป็นบรรทัดเดี่ยว ไม่เอาค่าไปต่อ
no implicit conversion of Integer into String (TypeError)ต่อข้อความกับตัวเลขด้วย +"อ่าน #{views}" หรือ views.to_s
nil can't be coerced into Integer (TypeError)เอา nil ไปบวก (มักหยิบ key ผิดชนิด post["views"])หยิบด้วย symbol post[:views]
key not found: :author (KeyError)fetch ช่องที่ไม่มีเพิ่มช่อง หรือให้ค่าสำรอง fetch(:author, "ไม่ระบุ")
missing keyword: :title (ArgumentError)ลืมส่ง keyword ที่บังคับส่ง title: … · ดู caller (บรรทัดที่เรียก) กับ callee (บรรทัดที่ประกาศ)
wrong number of arguments (given 2, expected 1) (ArgumentError)ส่งจำนวน argument ไม่ตรงนับให้ตรง · ใน initialize ของลูกใช้ super(title) ไม่ใช่ super เปล่า
wrong number of arguments (given 1, expected 0; required keyword: model)ใส่ปีกกาให้ของที่ต้องส่งแบบ keywordform_with model: post do |f| ไม่มีปีกกา
no block given (yield) (LocalJumpError)method มี yield แต่ไม่ได้แนบ blockเรียกพร้อม do … end
uninitialized constant Post::Slugable (NameError) + Did you mean? Sluggableชื่อตัวใหญ่ (class · module) ที่ไม่รู้จักสะกดให้ตรง
syntax errors found + expected an `end` to close the `def` statementลืม end · บรรทัดที่ชี้อาจไม่ใช่บรรทัดที่ลืมไล่คู่ def/class/do กับ end · ตรวจด้วย ruby -c
divided by 0 (ZeroDivisionError)หารด้วยศูนย์ ไม่มีใครรับ โปรแกรมหยุดเช็คก่อน หรือ rescue ZeroDivisionError
warning: already initialized constant BLOG_NAMEตั้งค่าคงที่ซ้ำ (เปลี่ยนแล้วจริง แค่เตือน)ลบบรรทัดที่ตั้งซ้ำ
#<Enumerator:0x…> โผล่แทนผลputs … do … end — block ไปติดกับ putsใช้ { } หรือใส่วงเล็บ puts( … )
method ได้ nil ทั้งที่คำนวณถูกบรรทัดสุดท้ายเป็น putsให้ค่าที่อยากส่งกลับอยู่บรรทัดสุดท้าย

Rails (หน้าแดง · log ของ server · terminal)

ข้อความที่เห็นstatusแปลว่าแก้ที่ไหน
ActiveRecord::ConnectionNotEstablished (connection to server on socket "/tmp/.s.PGSQL.5432" failed …500ไม่รู้ว่า PostgreSQL อยู่ไหน / Postgres ไม่เปิดexport DATABASE_URL=… ในหน้าต่างเดียวกับ server
ActiveRecord::NoDatabaseError (Database not found: …)500ยังไม่ได้สร้าง databaseปิด server → bin/rails db:prepare → เปิดใหม่
ActiveRecord::DatabaseConnectionError: There is an issue connecting with your hostname: 127.0.0.1.—ต่อ database ไม่ได้Postgres เปิดไหม · พอร์ตใน DATABASE_URL · หน้าต่างใหม่ต้อง export ซ้ำ
A server is already running (pid: …)—มี server เปิดค้างอีกหน้าต่างไปกด Ctrl-C ที่หน้าต่างนั้น
Routing Error · No route matches [GET] "/comments"404พนักงานต้อนรับไม่มีกฎนี้ (log ไม่มี Processing by)config/routes.rb · save แล้วหรือยัง
Unknown action · The action 'popular' could not be found for PostsController404route มี แต่แผนกไม่มีเมนูนี้เพิ่ม def popular ใน controller
ActionDispatch::MissingController in PageController#about · uninitialized constant PageController500หา controller ชื่อนี้ไม่เจอ (มักขาด s)ให้ตรงกันสามที่: คำใน route · ชื่อไฟล์ · ชื่อ class
No view template for interactive request · PagesController#about is missing a template for request formats: text/html406หาไฟล์ view ไม่เจอชื่อไฟล์/โฟลเดอร์ใน app/views/ — ย่อหน้า NOTE บอกชื่อที่ถูกให้
NameError in PagesController#about · undefined local variable or method 'parms'500พิมพ์ชื่อผิดในโค้ดบรรทัดที่กรอบโค้ดชี้ · ดู Did you mean?
ArgumentError · Reported error must be an Exception, got: #<SyntaxError:…500หัวข้อหลอกตา — ตัวจริงคือ SyntaxError ข้างใน (มักลืม end)อ่านใน terminal ของ server (Caused by: SyntaxError) · ruby -c ไฟล์
syntax error found · unexpected '<', ignoring it500< ApplicationController อยู่คนละบรรทัดกับ classย้ายขึ้นบรรทัดเดียวกัน
Invalid route name, already in use: 'root' (ArgumentError)500เขียน root สองบรรทัดเหลือบรรทัดเดียว
undefined method 'post_path'500route นั้นไม่มีชื่อ / ไม่ได้เปิด :showใส่ as: :post หรือ only: [:index, :show]
ActionView::Template::Error (undefined method 'each' for nil)500view หา @posts ไม่เจอชื่อ @… ใน controller ให้ตรงกับ view (เช่นลืม s)
หน้าขึ้นปกติแต่ค่าหาย (Hello )200@ตัวแปร สะกดไม่ตรง · ลืม @ · ลืม = ใน ERB · หรือ params key ผิดเทียบชื่อระหว่าง controller กับ view · ใช้ debugger ดู
PG::UndefinedTable: ERROR: relation "categories" does not exist—มีใบสั่งช่าง (migration) แต่ยังไม่สั่งลงมือbin/rails db:migrate
uninitialized constant Post (NameError) ใน console—ไม่มีไฟล์ app/models/post.rb หรือชื่อไฟล์ไม่ตรง classชื่อไฟล์ = ชื่อ class ตัวเล็ก
ApplicationRecord is an abstract class and cannot be instantiated.—ใช้แม่แบบกลางสร้างของตรง ๆใช้ model จริง เช่น Post.new
Can't verify CSRF token authenticity. · ActionController::InvalidAuthenticityToken422คำสั่งที่ไม่ใช่ GET ไม่ได้แนบรหัสลับใช้ฟอร์ม/ปุ่มของ Rails (แนบให้เอง) — ยิง curl เปล่า ๆ ได้ 422 ไม่ใช่บั๊ก
ActionNotFound … but it is listed in the controller's :only option404ใส่ action ใน only: ที่ยังไม่ได้เขียนใส่เฉพาะ action ที่เขียนแล้ว
NameError (undefined local variable or method 'debugger' …)500ลืม debugger ไว้ แล้วรันในโหมดที่ไม่มี gem debugลบ · grep -rn debugger app/
DEBUGGER: wait for debugger connection... แล้วพิมพ์อะไรก็ไม่ตอบ—เปิดด้วย bin/devCtrl-C แล้วเปิดด้วย bin/rails server
Blocked hosts: www.example.com · => 403403app.get ใน console ติดด่านชื่อเว็บapp.host! "localhost"
eval error: undefined local variable or method 'app'—พิมพ์ app ใน (rdbg) ของ debuggerapp · helper มีเฉพาะใน bin/rails console
URI must be ascii only—ใส่ตัวอักษรไทยใน URL ของ app.getใช้ชื่อภาษาอังกฤษ
ArgumentError: Missing host to link to!—เรียก post_url ใน bin/rails runner (ไม่มีใบสั่งให้ยืมโดเมน)ใช้ใน controller/view หรือใช้ _path
fatal: your current branch 'main' does not have any commits yet—ไม่ใช่ error จริง — rails new ไม่ได้ commit ให้git add -A · git commit -m "rails new"

13 · ศัพท์ภาษาคน

ศัพท์ภาษาคนในร้านอาหารใน Go
frameworkชุดเครื่องมือทำเว็บสำเร็จรูป ที่เป็นฝ่ายเรียกโค้ดของเราร้านแฟรนไชส์Gin (แต่ Rails ให้มาครบกว่ามาก)
Ruby · RailsRuby = ภาษา · Rails = framework ที่เขียนด้วย Rubyภาษาที่พูดในครัว · ตัวร้านGo · Gin
MVCแบ่งโค้ดเป็น ข้อมูล (Model) · หน้าตา (View) · ผู้ประสานงาน (Controller)ห้องวัตถุดิบ · คนจัดจาน · หัวหน้าเชฟhandler เดียวทำทั้งสามงาน
Convention over Configurationตั้งชื่อตามกฎ แทนเขียนตั้งค่าเองทุกสาขาวางของที่เดิมต้องบอกทุกอย่างเอง
gem · Bundlerชุดโค้ดที่คนอื่นเขียนให้ลงมาใช้ · ตัวติดตั้ง gem ตาม Gemfileอุปกรณ์สำเร็จรูป · คนถือรายการไปซื้อmodule · go mod
Active Supportส่วนของ Rails ที่เติม method ให้ของพื้นฐานของ Ruby (blank? · 3.days.ago)สมุดศัพท์ประจำร้านไม่มี
object · methodค่าที่มีคำสั่งติดตัว · คำสั่งแต่ละตัว เรียกด้วยจุดของในบ้านที่มีปุ่มติดมาGo: ชนิดพื้นฐานไม่มี method
symbolป้ายชื่อที่ขึ้นต้นด้วย : เช่น :titleป้ายลิ้นชักใกล้สุดคือ const string
block · |x| · yieldก้อนโค้ดที่ฝากไปกับ method · ชื่อของที่ส่งเข้ามา · คำสั่งเรียกก้อนนั้นสูตรที่ฝากไว้กับแม่ครัวfunc ที่ส่งเป็น argument
truthy · falsyค่าที่ผ่าน / ไม่ผ่าน if · Ruby ไม่ผ่านแค่ nil กับ falseรปภ. ที่ห้ามแค่ 2 คนif รับแค่ bool
instance variable (@x)ตัวแปรที่ติดอยู่กับ object · ใน controller คือทางส่งของให้ viewถาดหน้าครัวfield ของ struct · gin.H{…}
inheritance (<) · superclassclass ลูกได้ทุกอย่างของ class แม่ · แม่เรียก superclassคู่มือบริษัทแม่ → คู่มือสาขา → แผนกไม่มี (ใช้ฝัง struct)
module · mixin (include) · concernก้อน method ที่หลาย class ยืมไปใช้ · concern = module ที่วางใน concerns/ชุดเครื่องมือกลางของครัวembedding
exception · rescue · raiseerror ที่ถูกโยน · ที่รับ · การโยนเองกริ่งไฟไหม้ · เจ้าหน้าที่ประจำชั้น · กดกริ่งเองerror ที่ return · if err != nil · errors.New
duck typingใช้ได้ถ้า “ทำได้” (มี method) ไม่สนว่าเป็น class อะไรร้องก๊าบ ๆ ได้ ก็ถือว่าเป็นเป็ดinterface ที่ compiler ตรวจ
DSLโค้ดที่อ่านเหมือนภาษาเฉพาะเรื่อง — จริง ๆ คือเรียก method ที่ละวงเล็บใบสั่งแบบพูดย่อ—
route · controller#actionกฎหนึ่งบรรทัด จับคู่ verb + path กับแผนก + เมนูรายการในมือพนักงานต้อนรับr.GET(path, handler)
paramsทุกอย่างที่ผู้ใช้ส่งมากับ request รวมไว้กล่องเดียว · เป็นข้อความเสมอรายละเอียดในใบสั่งc.Param · c.Query
implicit renderaction จบโดยไม่สั่ง → Rails เปิด view ชื่อตรงกันให้เชฟวางของเสร็จ คนจัดจานรู้เองว่าจัดจานไหนต้องสั่ง c.HTML เอง
ERB · layout · helperHTML ที่แทรก Ruby ได้ · กรอบที่ครอบทุกหน้า · method สำเร็จรูปที่คืน HTMLใบแบบจัดจาน · จานรอง · เครื่องพิมพ์ป้ายhtml/template · จัดเอง · ต่อข้อความเอง
named route · _path · _urlroute ที่มีชื่อ · ได้ method สร้าง path / URL เต็มชื่อเล่นโต๊ะfmt.Sprintf เอง
resources · CRUDroute มาตรฐาน 8 เส้น · สร้าง อ่าน แก้ ลบเซ็ตเมนูมาตรฐานของแฟรนไชส์เขียน r.GET r.POST … เองทีละเส้น
middlewareโค้ดที่ทุก request ต้องผ่านก่อนถึง routes (ขาเข้าและขากลับ)ด่านหน้าครัวr.Use(…)
environmentโหมดที่แอปรัน · development = เครื่องเรา · production = เว็บจริงสาขาทดลอง · ร้านจริง—
autoload (Zeitwerk)Rails เปิดไฟล์ให้เองจากชื่อ class ไม่ต้อง import · ชื่อไฟล์ต้องตรงชื่อ classบรรณารักษ์ที่อ่านแค่สันหนังสือimport "blog/models"
model · attributeclass ที่แทนข้อมูลหนึ่งชนิด · ช่องข้อมูลที่ Rails สร้าง method ให้จากคอลัมน์คนดูแลสต็อก · ช่องในแฟ้มstruct · field
migration · schemaไฟล์สั่งสร้าง/แก้ตาราง · db/schema.rb สรุปโครงตารางทั้งหมดใบสั่งช่างต่อเติมตู้ · ผังตู้goose · ไม่มี
XSS · escapeยัดโปรแกรมลงในข้อมูลให้ไปรันที่ browser คนอื่น · แปลง < เป็นตัวอักษรธรรมดาคนจัดจานที่ไม่ทำตามคำสั่งแปลก ๆ ในใบสั่งhtml/template escape ให้เหมือนกัน
CSRF · authenticity tokenปลอมคำสั่งจากเว็บอื่นโดยอาศัย cookie · รหัสลับที่ Rails ฝังในหน้าเว็บบัตรคิวที่แจกเฉพาะลูกค้าในร้านต้องหาเครื่องมือมาเสียบเอง
breakpoint · backtraceจุดที่สั่งให้หยุด · รายการชั้นการเรียกที่พามาถึงจุดพัง (Application Trace = ไฟล์ของเรา)จุดหยุดเวลา · เส้นทางใบสั่งDelve · stack trace ของ panic
REPLอ่าน → รัน → โชว์ผล → วนใหม่ · irb และ bin/rails consoleกระดาษทด · ครัวทดลองไม่มี

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

เฉพาะจุดที่พิมพ์ตามสไลด์แล้วพัง · ที่เหลือดูได้ในตารางท้ายแต่ละบท

หน้าสไลด์เขียนของจริงทำยังไง
21nums.min / nums.max · nums.sort / nums.uniq/ คือหาร · ได้ 0 และ NoMethodErrorแยกพิมพ์ทีละบรรทัด
20–21posts.map(&:title)posts เป็น Hash จะพัง undefined method 'title' for an instance of Hashใช้ { |post| post[:title] } จน Day 2
29Post.new.slug # ใช้ได้เลยNameError — Post ในสไลด์ไม่มี titleเติม attr_accessor :title + initialize
40“git repo ที่ commit แรกให้แล้ว” · พิมพ์ bin/rails server ต่อได้เลยไม่ได้ commit · หน้าแรกได้ 500 ถ้าไม่บอกที่อยู่ Postgrescommit เอง · ตั้ง DATABASE_URL + db:prepare
60class GreetingsController กับ < ApplicationController คนละบรรทัดSyntaxError · หน้าเว็บ 500เขียนบรรทัดเดียวกัน
63only: %i[show edit update] แต่โชว์แค่ def showทุก URL ของ controller นั้นได้ 404ใส่ใน only: เฉพาะ action ที่เขียนแล้ว
64stylesheet_link_tag "application"ไฟล์จริงใช้ :app (รวม Tailwind)อย่าแก้ตามสไลด์
68app.get "/posts" ได้ => 200ได้ 403 + Blocked hostsapp.host! "localhost" ก่อน

15 · อ้างอิง