🔁 ทวนก่อนเริ่ม — คำจากบทก่อน ๆ ที่บทนี้ใช้
- controller / action (บท 102) — controller คือ "หัวหน้าเชฟของแผนก" · action คือ "เมนูหนึ่งอย่าง" ของแผนกนั้น ·
PagesController#helloอ่านว่า actionhelloของแผนก pages - params (บท 102) — รายละเอียดในใบสั่ง เช่น
?name=Somchaiท้าย URL @ตัวแปร(บท 95, 102) — ตัวแปรที่ controller เตรียมไว้ แล้ว view หยิบไปใช้ได้nil(บท 94) — แปลว่า "ไม่มีค่า" (คล้ายnilของ Go)puts/p(บท 93) — พิมพ์ค่าออกจอ ·pโชว์หน้าตาจริงของค่า เช่นnilจะเห็นเป็นคำว่าnil
🧰 ตรวจด้วย Ruby 4.0.9 · Rails 8.1.4 · gem debug (rdbg 1.11.1) (สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — รุ่นแก้บั๊กในสายเดียวกัน ใช้แทนกันได้)
ผลทุกก้อนในบทนี้รันจริงแล้วก็อปมา · จัดแค่ย่อ path ยาวเป็น ~/wongnok_blog และตัดบรรทัดที่ไม่เกี่ยวเป็น … ·
เวลา (ms) · วันที่ · เลข 0x… ของคุณจะไม่ตรงกับในบท และจอคุณจะมีสี
ปัญหา: หน้าเว็บทักทายแต่ไม่มีชื่อ
เริ่มจากแอปเล็ก ๆ ที่ใช้คำจากบท 101–103 ครบทุกตัว — หน้า /hello ที่ทักทายตามชื่อในใบสั่ง
ถ้าเปิด /hello?name=Somchai ควรได้หน้า "สวัสดีคุณ Somchai!"
# config/routes.rb — พนักงานต้อนรับ: ใบสั่ง GET /hello ส่งไปแผนก pages เมนู hello
Rails.application.routes.draw do
get "/hello", to: "pages#hello"
end
# app/controllers/pages_controller.rb — หัวหน้าเชฟแผนก pages
class PagesController < ApplicationController
def hello
@name = params[:nmae]
@message = "สวัสดีคุณ #{@name}!"
end
end
<%# app/views/pages/hello.html.erb — จัดจาน: เอา @message มาวางในหัวข้อ %>
<h1><%= @message %></h1>
ทวนทีละบรรทัด:
get "/hello", to: "pages#hello"— ใบสั่งGET /helloส่งไป actionhelloของPagesController(บท 101) · Rails ตั้งชื่อ route นี้ให้เองว่าhelloด้วย เดี๋ยวได้ใช้ในหัวข้อ consoleclass PagesController < ApplicationController— สร้าง controller ใหม่ที่สืบทอด (<บท 95) ความสามารถทั้งหมดจาก controller แม่ของแอปparams[:nmae]— หยิบค่าจากใบสั่ง ช่องชื่อnmae· สังเกตดี ๆ ว่าสะกดผิด (ควรเป็นname) — นี่คือบั๊กที่เราจงใจใส่ไว้@name = …— เก็บไว้ในตัวแปร@เพื่อให้บรรทัดถัดไปและ view ใช้ได้"สวัสดีคุณ #{@name}!"— string ที่เอาค่าของ@nameมาแทรกตรง#{}(บท 91)- ใน view:
<%# … %>คือโน้ตของเชฟ ลูกค้าไม่เห็น ·<%= @message %>คือเอาค่า@messageมาวางลงจาน (บท 103)
# เปิด bin/rails server แล้วเข้าเบราว์เซอร์ที่
http://localhost:3000/hello?name=Somchai
หัวข้อบนหน้าเว็บจะเขียนว่าอะไร? แล้วจะมี error ขึ้นไหม?
ผลจริง (ดึง HTML ด้วย curl แล้วตัดมาเฉพาะแท็กหัวข้อ) — ไม่มี error สักตัว แต่ชื่อหายไป:
<h1>สวัสดีคุณ !</h1>
ใบสั่งมีช่อง name แต่โค้ดไปหยิบช่อง nmae ซึ่งไม่มี → ได้ nil (ไม่มีค่า) →
#{nil} กลายเป็นข้อความว่าง ·
Ruby ไม่ฟ้อง เพราะการถามหาช่องที่ไม่มีใน params เป็นเรื่องปกติ — ตรงนี้แหละที่ยาก:
ถ้าเราไม่รู้คำตอบล่วงหน้า เราจะไม่รู้เลยว่าพังตรงไหน
ทางแรกที่คุ้นมือ: พิมพ์ค่าออกมาดูด้วย puts
ตอนเขียน Go คุณหาบั๊กด้วยการแทรก fmt.Println("name =", name) ไว้ตรงที่สงสัย แล้วรันใหม่
ใน Ruby ทำแบบเดียวกันได้ด้วย puts หรือ p — ลองแทรกสองบรรทัดนี้ใน action:
def hello
@name = params[:nmae]
puts "ชื่อที่ได้: #{@name}" # เหมือน fmt.Println ของ Go
p params # p = พิมพ์หน้าตาจริงของค่า
@message = "สวัสดีคุณ #{@name}!"
end
รีเฟรชหน้าเว็บ · ผลไม่ได้ขึ้นในเบราว์เซอร์ แต่ขึ้นในหน้าต่างเทอร์มินัลที่รัน bin/rails server อยู่ (ผลจริง):
Started GET "/hello?name=Somchai" for ::1 at 2026-11-03 21:04:30 +0700
Processing by PagesController#hello as */*
Parameters: {"name" => "Somchai"}
ชื่อที่ได้:
#<ActionController::Parameters {"name" => "Somchai", "controller" => "pages", "action" => "hello"} permitted: false>
Rendering layout layouts/application.html.erb
…
Completed 200 OK in 10ms (Views: 6.4ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)
Started GET … for ::1— มีใบสั่งเข้ามา ·::1คือที่อยู่ของเครื่องตัวเอง (อีกชื่อของ localhost)Processing by PagesController#hello— ส่งถึงแผนก pages เมนู hello แล้ว · ส่วนท้ายas */*ไม่ต้องสนใจ (ผมยิงด้วยคำสั่งcurl· เปิดจากเบราว์เซอร์จะเห็นคำอื่น)ชื่อที่ได้:— ผลของputs· ว่างเปล่า บอกว่า@nameไม่มีค่า แต่ไม่บอกว่าทำไม- บรรทัด
#<ActionController::Parameters {…}>— ผลของp paramsคือใบสั่งทั้งใบ · เห็นว่ามีช่อง"name"ไม่ใช่"nmae"→ เจอบั๊กแล้ว · คำว่าpermitted: falseท้ายบรรทัดเป็นเรื่องความปลอดภัยของฟอร์ม รอวันที่ 2 Completed 200 OK in 10ms— ทำเสร็จ ส่งจานออกไปแล้ว ใช้เวลา 10 มิลลิวินาที ·ActiveRecord: 0.0ms= เวลาที่ใช้คุยกับฐานข้อมูล (หน้านี้ไม่ได้ใช้ จึงเป็น 0)
ได้ผลจริง แต่มีปัญหาสองข้อ: ต้องเดาล่วงหน้าว่าจะพิมพ์ค่าไหน (เดาไม่ถูกก็ต้องแก้โค้ดแล้วรีเฟรชใหม่อีกรอบ) · และพอเจอบั๊กแล้วต้องตามลบ puts ทุกบรรทัด
puts เหมือนติดกล้องที่ถ่ายรูปอัตโนมัติไว้ไม่กี่มุมในครัว · ร้านเปิด ใบสั่งวิ่งผ่าน กล้องกดชัตเตอร์ ·
คุณได้รูปมาดูทีหลัง — แต่เห็นแค่มุมที่ติดกล้องไว้
ถ้าหม้อที่อยากดูไม่อยู่ในรูป ต้องไปติดกล้องเพิ่มแล้วรอใบสั่งใบใหม่
ที่ไม่เหมือนกัน: กล้องจริงไม่ทำให้อาหารช้าลง และถอดออกเมื่อไหร่ก็ได้ ·
แต่ puts เป็นโค้ดจริงที่อยู่ในไฟล์ — ลืมลบแล้ว push ขึ้นไป มันจะพิมพ์ทุกครั้งที่มีคนเข้าเว็บ จนเต็ม log ของ server จริง
- print debugging
- การหาบั๊กด้วยการแทรกคำสั่งพิมพ์ค่า (
puts·p·fmt.Println) · ง่ายและใช้ได้ทุกภาษา แต่ต้องเดาว่าจะพิมพ์อะไร · ถาม senior ได้ว่า “ทีมนี้หาบั๊กใน Rails ด้วย debugger หรือ print debugging มากกว่ากัน” - server log
- ข้อความที่
bin/rails serverพิมพ์ในเทอร์มินัลทุกครั้งที่มี request เข้ามา (Started GET …·Completed 200 OK …) · ผลของputsใน controller จะไปโผล่ตรงนี้ ไม่ใช่บนหน้าเว็บ · พิมพ์บอก AI ได้ว่า “here is my Rails server log for this request” แล้วแปะทั้งก้อน — ได้คำตอบตรงกว่าเล่าเป็นคำพูด
debugger — กดหยุดเวลาในครัว
debugger คือคำสั่งที่บอก Ruby ว่า "พอมาถึงบรรทัดนี้ หยุดก่อน แล้วเปิดช่องให้ฉันพิมพ์ถาม"
ระหว่างหยุด คุณพิมพ์ชื่อตัวแปรตัวไหนก็ได้เพื่อดูค่า — ไม่ต้องเดาล่วงหน้า · ดูพอแล้วสั่งให้วิ่งต่อ
ใบสั่งวิ่งเข้าครัว เชฟกำลังทำอาหาร แล้วเวลาหยุดนิ่งตรงจุดที่คุณแปะป้าย debugger ไว้ ·
คุณเดินไปเปิดหม้อใบไหนก็ได้ (พิมพ์ @name) · หยิบใบสั่งขึ้นมาอ่าน (พิมพ์ params) ·
แล้วกดให้เวลาเดินทีละก้าว (next) หรือเดินต่อตามปกติ (continue)
ที่ไม่เหมือนกัน: ในนิยายเวลาหยุดแล้วไม่มีใครรู้ตัว ·
แต่ในเว็บ ลูกค้ารอจริง — เบราว์เซอร์จะหมุนค้างตลอดเวลาที่คุณยังไม่สั่ง continue ·
และถ้าคุณพิมพ์โค้ดที่ไปเปลี่ยนค่า (เช่น @name = "x") ระหว่างหยุด มันเปลี่ยนจริง ไม่ใช่แค่ดู
เทียบกับ Go: Go ก็มีเครื่องมือแบบนี้ ชื่อ Delve (คำสั่ง dlv) แต่ไม่ได้มากับ Go ต้องติดตั้งเพิ่มเอง ·
ฝั่ง Rails ไม่ต้องติดตั้งอะไร เพราะ rails new ใส่บรรทัดนี้ไว้ใน Gemfile ให้แล้ว (ของจริงจากแอปที่สร้างใหม่):
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"
…
end
gem "debug"— ขอใช้อุปกรณ์สำเร็จรูปชื่อ debug (gem = อุปกรณ์ที่ซื้อมาใส่ครัว บท 98)group :development, :test do … end— ใช้เฉพาะตอน development (เครื่องเรา) กับ test · ไม่มีคำว่า production (เว็บจริงที่คนใช้) — จำบรรทัดนี้ไว้ อีกสองหัวข้อจะเห็นว่ามันสำคัญมากplatforms:กับrequire:— รายละเอียดการโหลด ข้ามไปได้ ไม่ต้องแก้
- debugger · breakpoint
- debugger คือโปรแกรมที่หยุดโค้ดกลางทางให้เราดูข้างใน · จุดที่สั่งให้หยุดเรียกว่า breakpoint ·
ใน Ruby แค่เขียนคำว่า
debuggerลงในบรรทัดที่อยากให้หยุด · พิมพ์บอก AI ได้ว่า “I put a debugger breakpoint in my Rails controller action, here is what I see” - gem debug · rdbg
- debugger มาตรฐานของ Ruby · ตัวโปรแกรมชื่อ
rdbg— ตอนหยุด จอจะขึ้น prompt ว่า(rdbg)· ถาม senior ได้ว่า “ทีมนี้ใช้ rdbg ในเทอร์มินัล หรือต่อเข้า VS Code”
ลองหยุดจริง: ใส่ debugger ใน action
ลบ puts สองบรรทัดเมื่อกี้ออก แล้วใส่คำว่า debugger หนึ่งบรรทัดแทน:
class PagesController < ApplicationController
def hello
@name = params[:nmae]
debugger # ← หยุดตรงนี้
@message = "สวัสดีคุณ #{@name}!"
end
end
⚠️ เปิด server ด้วย bin/rails server เท่านั้น อย่าใช้ bin/dev (ที่บท 98 บอกว่าเพื่อนบางคนใช้) ·
ผมลองแล้ว: ใต้ bin/dev จอขึ้นแค่ DEBUGGER: wait for debugger connection... แล้วพิมพ์อะไรไปก็ไม่ตอบ เบราว์เซอร์ค้างอย่างเดียว ·
สาเหตุ: bin/dev ตั้งให้ debugger รอโปรแกรมอื่นมาต่อเข้ามา แทนที่จะรับสิ่งที่คุณพิมพ์ในหน้าจอนั้น
เปิด http://localhost:3000/hello?name=Somchai ในเบราว์เซอร์ ·
หน้าเว็บจะหมุนค้าง ไม่ขึ้นอะไร — ไม่ใช่พัง แต่โค้ดกำลังหยุดรอคุณอยู่ ·
ให้ไปดูเทอร์มินัลที่รัน server จะกลายเป็นแบบนี้ (ผลจริง):
Started GET "/hello?name=Somchai" for ::1 at 2026-11-03 21:04:30 +0700
…
Processing by PagesController#hello as */*
Parameters: {"name" => "Somchai"}
[1, 7] in ~/wongnok_blog/app/controllers/pages_controller.rb
1| class PagesController < ApplicationController
2| def hello
3| @name = params[:nmae]
=> 4| debugger
5| @message = "สวัสดีคุณ #{@name}!"
6| end
7| end
=>#0 PagesController#hello at ~/wongnok_blog/app/controllers/pages_controller.rb:4
#1 ActionController::BasicImplicitRender#send_action(method="hello", args=[]) at …
# and 78 frames (use `bt' command for all frames)
(rdbg)
อ่านจอนี้ทีละส่วน:
[1, 7] in …pages_controller.rb— กำลังโชว์บรรทัด 1 ถึง 7 ของไฟล์นี้=> 4|— ลูกศรชี้บรรทัดที่หยุดอยู่#0 … #1 … and 78 frames— ใครเรียกใครมาจนถึงตรงนี้ ·#0คือ action ของเรา · อีก 78 ชั้นข้างใต้เป็นโค้ดของ Rails เอง ไม่ต้องสนใจ(rdbg)— prompt คือเครื่องหมายว่า "รอคุณพิมพ์อยู่"
ทีนี้พิมพ์ถามทีละบรรทัด (กด Enter ทุกบรรทัด) — ผลจริง:
(rdbg) params
#<ActionController::Parameters {"name" => "Somchai", "controller" => "pages", "action" => "hello"} permitted: false>
(ruby) @name
nil
(ruby) params[:name]
"Somchai"
params— ดูใบสั่งทั้งใบ · มีช่อง"name" => "Somchai"อยู่จริง (อีกสองช่อง controller กับ action Rails เติมให้เอง)@name— ได้nil· ยืนยันว่าบรรทัด 3 หยิบอะไรไม่ได้params[:name]— ลองสะกดถูกดู ได้"Somchai"→ เจอบั๊กแล้วโดยไม่ต้องแก้ไฟล์สักตัว- สังเกตว่า prompt บางบรรทัดเปลี่ยนจาก
(rdbg)เป็น(ruby)ระหว่างพิมพ์ — ไม่ต้องสนใจ ทั้งสองแบบใช้ถามค่าได้เหมือนกัน
ต่อไปจะพิมพ์ next = "ทำบรรทัดนี้ แล้วหยุดที่บรรทัดถัดไป" · จะเห็นข้อความ # command ต่อท้าย —
คุณไม่ได้พิมพ์เอง debugger เขียนจาง ๆ ให้ เพื่อบอกว่ามันเข้าใจว่านี่คือคำสั่งของ debugger ไม่ใช่ชื่อตัวแปร
(rdbg) next # command
…
=> 5| @message = "สวัสดีคุณ #{@name}!"
…
(ruby) @message
พิมพ์ next แล้วลูกศรเลื่อนมาชี้บรรทัด 5 · ตอนนี้ถาม @message จะได้ "สวัสดีคุณ !" หรืออย่างอื่น?
ผลจริง — ได้ nil:
(ruby) @message
nil
(rdbg) next # command
…
=> 6| end
…
(ruby) @message
"สวัสดีคุณ !"
ลูกศรชี้บรรทัดที่ "กำลังจะทำ" ไม่ใช่บรรทัดที่ "ทำไปแล้ว" ·
ต้อง next อีกครั้งให้บรรทัด 5 ทำงานก่อน ถึงจะเห็นค่า ·
นี่คือจุดที่มือใหม่ตกใจบ่อยที่สุด — เห็น nil แล้วคิดว่าโค้ดพัง ทั้งที่แค่ยังไม่ถึง
ดูพอแล้ว พิมพ์ continue ปล่อยให้โค้ดวิ่งต่อ · หน้าเว็บจะขึ้นทันที และ log บอกว่า (ผลจริง):
(rdbg) continue # command
Rendering layout layouts/application.html.erb
Rendering pages/hello.html.erb within layouts/application
…
Completed 200 OK in 17946ms (Views: 19.0ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 10.2ms)
17946ms ≈ 18 วินาที — คือเวลาที่ผมหยุดพิมพ์ถามอยู่ · ตลอด 18 วินาทีนั้นเบราว์เซอร์รอค้าง ·
งานจริงของหน้านี้ใช้แค่ราว 10ms (ดูจาก log ตอนใช้ puts ข้างบน)
สรุปคำสั่งที่พิมพ์ได้ตอนหยุด:
| พิมพ์ | ย่อ | ทำอะไร |
|---|---|---|
| ชื่อตัวแปร / โค้ด Ruby | — | ดูค่าทันที เช่น @name · params[:name] |
next | n | ทำบรรทัดนี้ แล้วหยุดที่บรรทัดถัดไป |
step | s | ถ้าบรรทัดนี้เรียก method ให้มุดเข้าไปดูข้างใน · ใน Rails ระวังหลงเข้าไปในโค้ดของ Rails 78 ชั้นนั้น |
info | — | ขอดูตัวแปรทุกตัวที่มองเห็นจากตรงนี้ |
continue | c | ปล่อยให้วิ่งต่อตามปกติ (หน้าเว็บจะขึ้น) |
ลอง info ดูหนึ่งครั้งตอนหยุดอยู่ที่บรรทัด 4 (ผลจริง · ถ้าลองหลัง next จะเห็น @message เพิ่มมาด้วย) — ตัวแปรที่ขึ้นต้นด้วย @_ เป็นของ Rails ทั้งหมด · ของเราคือ @name บรรทัดล่างสุด · %self บรรทัดแรกคือตัว controller ที่กำลังทำงานอยู่:
(rdbg) info # command
%self = #<PagesController:0x00000000006000>
@_action_has_layout = true
@_action_name = "hello"
…
@_request = #<ActionDispatch::Request GET "http://localhost:3000/hello?name=...>
…
@_view_runtime = nil
@name = nil
- step over (
next) · step into (step) - สองแบบของการ "เดินทีละก้าว" · over = ทำบรรทัดนี้ทั้งบรรทัดแล้วหยุดบรรทัดถัดไป · into = มุดเข้าไปใน method ที่บรรทัดนี้เรียก · ถาม senior ได้ว่า “ตรงนี้ควร step into ไหม หรือข้ามไปเพราะเป็นโค้ดของ library”
- frame / backtrace
- "ชั้น" ของการเรียกกันที่ซ้อนอยู่ (
#0= ชั้นที่หยุดอยู่ ·#1= คนที่เรียกมา) · ทั้งกองเรียกว่า backtrace · พิมพ์บอก AI ได้ว่า “here is the backtrace, which frame is my code?” — มันจะชี้ให้ว่าชั้นไหนเป็นของเรา
debugger หยุดอยู่ และลูกศร => ชี้ที่บรรทัด @total = 10 + 5 · พิมพ์ @total จะได้อะไร?
ตัวเลือกที่ 3 ถูก — ลูกศรชี้บรรทัดที่ "กำลังจะทำ" ตรงกับที่เห็นในเฉลย predict: ลูกศรอยู่ที่บรรทัด 5 แต่ @message ยังเป็น nil
ตัวเลือกที่ 1 ผิด เพราะกลับด้าน — ต้อง next หนึ่งครั้งก่อนถึงจะได้ 15 ·
ตัวเลือกที่ 2 ผิด เพราะ debugger หยุดก่อนเริ่มบรรทัด ไม่ใช่หยุดกลางบรรทัด ·
ตัวเลือกที่ 4 ผิด เพราะการพิมพ์ชื่อตัวแปรคือสิ่งที่ debugger มีไว้ให้ทำ
จุดที่ต้องจำ: ลูกศร = บรรทัดที่ยังไม่ทำ · อยากเห็นผลของบรรทัดนั้น กด next ก่อน
ห้ามลืม: เอา debugger ออกก่อน push
แก้บั๊กเสร็จ (เปลี่ยน :nmae เป็น :name) แต่ลืมลบบรรทัด debugger แล้ว push ขึ้นเว็บจริง จะเกิดอะไร?
จำบรรทัด group :development, :test ใน Gemfile ได้ไหม — บนเว็บจริง (production) gem debug ไม่ถูกโหลด → Ruby ไม่รู้จักคำว่า debugger
⚠️ ลืม debugger ไว้ = ทุกคนที่เปิดหน้านั้นบนเว็บจริงเจอ error 500 ·
ผมลองเปิดแอปในโหมด production (โหมดเว็บจริง) บนเครื่องตัวเอง แล้วส่งใบสั่งเข้า /hello —
คำสั่ง app.get ที่ใช้ยิง เดี๋ยวได้เรียนในหัวข้อถัดไป ตอนนี้ดูแค่ผล:
wongnok-blog(prod):001> app.get "/hello?name=Somchai"
[22caf0bb-…] Started GET "/hello?name=Somchai" for 127.0.0.1 at 2026-11-03 21:03:56 +0700
[22caf0bb-…] Processing by PagesController#hello as HTML
[22caf0bb-…] Parameters: {"name" => "Somchai"}
[22caf0bb-…] Completed 500 Internal Server Error in 4ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)
…
[22caf0bb-…] NameError (undefined local variable or method 'debugger' for an instance of PagesController):
[22caf0bb-…] app/controllers/pages_controller.rb:4:in 'PagesController#hello'
…
=> 500
Completed 500 = ครัวพัง ส่งจานไม่ได้ · NameError = "ไม่รู้จักชื่อนี้" · บรรทัดถัดมาชี้ว่าพังที่ไฟล์ไหน บรรทัดที่ 4 ·
[22caf0bb-…] หน้าบรรทัดคือเลขประจำใบสั่ง (ผมย่อให้สั้น ของคุณจะต่าง)
ในเครื่องเราหยุดรอ · บนเว็บจริงพัง — ไม่มีอะไรเตือนให้อัตโนมัติ
นิสัยที่ควรมี: ก่อน commit ค้นหาคำว่า debugger ในโฟลเดอร์ app/ ทุกครั้ง — ผลจริงก่อนและหลังลบ:
$ grep -rn debugger app/
app/controllers/pages_controller.rb:4: debugger
# ↑ ยังเจอ: ชื่อไฟล์ : เลขบรรทัด : เนื้อหาบรรทัด → ไปลบให้หมด
$ grep -rn debugger app/
# ↑ ลบแล้ว: ไม่มีอะไรขึ้นเลย = สะอาด พร้อม commit
grep= คำสั่งค้นหาข้อความในไฟล์ ·-r= ค้นทุกไฟล์ในโฟลเดอร์ ·-n= บอกเลขบรรทัดด้วย
ลืมบรรทัด debugger ไว้ใน action แล้วเอาขึ้นเว็บจริง (deploy) (Gemfile ตามที่ rails new ให้มา) ผู้ใช้เปิดหน้านั้นจะเจออะไร?
ตัวเลือกที่ 4 ถูก — gem debug อยู่ใน group :development, :test · production เลยไม่รู้จัก debugger · ผลจริงคือ NameError และ => 500
ตัวเลือกที่ 1 ผิด เพราะ debugger ในเว็บจริงไม่ใช่คำที่ถูกข้าม แต่เป็นชื่อที่ไม่มีใครรู้จัก ·
ตัวเลือกที่ 2 คือสิ่งที่เกิดในเครื่องเรา (development) ไม่ใช่บนเว็บจริง ·
ตัวเลือกที่ 3 ผิด เพราะไม่มีอะไรห้ามไว้ — เราต้อง grep เช็คเอง
จุดที่ต้องจำ: เครื่องเรา = หยุดรอ · เว็บจริง = 500 · ก่อน commit รัน grep -rn debugger app/ ให้ว่างเปล่า
bin/rails console — ครัวทดลองหลังร้าน
debugger ต้องรอให้มีใบสั่งวิ่งเข้ามาชนก่อน · แต่บางคำถามไม่เกี่ยวกับใบสั่งเลย เช่น
"ชื่อ route นี้ได้ URL อะไร" หรือ "link_to แบบนี้ได้ HTML หน้าตายังไง"
bin/rails console คือหน้าจอที่โหลดแอปทั้งตัวขึ้นมา แล้วให้คุณพิมพ์ Ruby ทีละบรรทัด กด Enter ได้ผลทันที —
ไม่ต้องเปิด server ไม่ต้องเปิดเบราว์เซอร์
ร้านยังไม่เปิด ไม่มีลูกค้า แต่คุณเดินเข้าครัวหลังร้านได้ · ทุกอย่างในครัวเป็นของจริงชุดเดียวกับร้าน — หัวหน้าเชฟทุกแผนก (controller) · รายการในมือพนักงานต้อนรับ (routes) · เครื่องมือจัดจาน (helper) · อยากรู้อะไรก็หยิบมาลองทีละอย่าง ไม่ต้องรอลูกค้าสั่ง
ที่ไม่เหมือนกัน: คนมักคิดว่าครัวทดลองทำอะไรก็ไม่มีผล ·
แต่ console ใช้ตู้เก็บแฟ้ม (database) ใบเดียวกับที่ server ในเครื่องเราใช้ — วันที่ 2 เมื่อมีข้อมูลบทความจริง ถ้าคุณสั่งลบใน console ข้อมูลก็หายจริง ·
อยากได้ครัวที่ลองแล้วทิ้งได้ ต้องเปิดแบบ --sandbox (หัวข้อถัดไป)
เทียบกับ Go: Go ไม่มีหน้าจอแบบนี้ · อยากลองเรียกฟังก์ชันหนึ่งตัวต้องเขียนไฟล์ main.go ใหม่ แล้ว go run ·
ใน Ruby คุณเคยใช้ irb มาแล้ว (บท 91) — bin/rails console ก็คือ irb ที่โหลดแอปทั้งตัวไว้ให้ก่อน
เปิดในโฟลเดอร์โปรเจกต์ (ผลจริง):
$ bin/rails console
Loading development environment (Rails 8.1.4)
wongnok-blog(dev):001>
Loading development environment— โหลดแอปในโหมด development (เครื่องเรา)wongnok-blog— ชื่อแอป (Rails เปลี่ยน_เป็น-ให้) ·(dev)— โหมดที่เปิดอยู่ บนจอจริงเป็นสีน้ำเงิน ·001— บรรทัดที่เท่าไรแล้ว ·>— รอคุณพิมพ์
ลองสามเรื่องที่เรียนมาในบทก่อน ๆ — ผลจริง:
wongnok-blog(dev):001> "post".pluralize
=> "posts"
wongnok-blog(dev):002> "posts".classify
=> "Post"
wongnok-blog(dev):003> app.hello_path
=> "/hello"
wongnok-blog(dev):004> helper.link_to "หน้าทักทาย", "/hello"
=> "<a href=\"/hello\">หน้าทักทาย</a>"
=>นำหน้าผลลัพธ์ — แปลว่า "บรรทัดนี้คืนค่านี้ออกมา""post".pluralize·"posts".classify— ตัวแปลงชื่อที่ Rails ใช้จับคู่Post↔posts(บท 99) · สงสัยว่าชื่อไหนจะกลายเป็นอะไร พิมพ์ถามที่นี่ได้เลยapp.hello_path—appคือเบราว์เซอร์จำลองที่อยู่ใน console · ถามได้ว่า route ชื่อhello(ชื่อที่ Rails ตั้งให้เองจากget "/hello") ได้ URL อะไร ·ชื่อ_pathคือวิธีขอ URL จากชื่อ route (บท 101)helper.link_to …—helperเปิดทางให้เรียก helper ของ view (บท 103) แล้วดู HTML ที่ได้ทันที ·\"คือเครื่องหมาย"ที่อยู่ใน string (console โชว์แบบนี้ ของจริงคือ"ธรรมดา)
ยิง request จาก console ด้วย app.get — และกับดัก 403
app ยังทำได้มากกว่านั้น: app.get "/hello" = สั่งเบราว์เซอร์จำลองส่งใบสั่งเข้าแอป โดยไม่ต้องเปิด server ·
ถ้าคุณเคยเขียน httptest ใน Go (บท 40) นี่คือแนวคิดเดียวกัน — สร้างใบสั่งปลอมแล้วส่งเข้า handler ตรง ๆ · ลองพิมพ์ดู ผลจริง:
wongnok-blog(dev):005> app.get "/hello?name=Somchai"
[ActionDispatch::HostAuthorization::DefaultResponseApp] Blocked hosts: www.example.com
=> 403
403 = ถูกห้ามเข้า · ใบสั่งไม่ถึงพนักงานต้อนรับ (routes) ด้วยซ้ำ — ถูกด่านหน้าครัว (middleware บท 100) ชื่อ HostAuthorization ตีกลับ ·
Blocked hosts: www.example.com บอกเหตุผลตรง ๆ: เบราว์เซอร์จำลองแอบอ้างว่ามาจากเว็บชื่อ www.example.com
แต่ตอน development Rails ยอมรับแค่ชื่ออย่าง localhost (กันการโจมตีแบบปลอมชื่อเว็บ — ตั้งใจ ไม่ใช่บั๊ก)
แก้ด้วยการบอกเบราว์เซอร์จำลองให้ใช้ชื่อ localhost — ผลจริง:
wongnok-blog(dev):006> app.host! "localhost"
=> "localhost"
wongnok-blog(dev):007> app.get "/hello?name=Somchai"
Started GET "/hello?name=Somchai" for 127.0.0.1 at 2026-11-03 21:11:19 +0700
…
Processing by PagesController#hello as HTML
Parameters: {"name" => "Somchai"}
…
Completed 200 OK in 12ms (Views: 6.0ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)
=> 200
wongnok-blog(dev):008> app.response.body[/<h1>.*<\/h1>/]
=> "<h1>สวัสดีคุณ Somchai!</h1>"
app.host! "localhost"— ตั้งชื่อเว็บของเบราว์เซอร์จำลอง · ตั้งครั้งเดียวใช้ได้ตลอดที่ console ยังเปิดอยู่ (ปิดแล้วเปิดใหม่ต้องตั้งใหม่)=> 200— ผ่านแล้ว · log ระหว่างกลางเหมือนตอนเปิดเบราว์เซอร์จริงทุกอย่าง (ตอนนี้แก้:nmaeเป็น:nameแล้ว ชื่อเลยขึ้น)app.response.body— HTML ที่แอปตอบกลับมาทั้งหน้า ·[/<h1>.*<\/h1>/]คือหยิบเฉพาะส่วนที่ตรงกับรูปแบบ<h1>…</h1>(เครื่องหมาย/…/คือ regex = รูปแบบข้อความ ไม่ต้องจำตอนนี้)
⚠️ console โหลดโค้ดครั้งเดียวตอนเปิด · ถ้าไปแก้ไฟล์ใน app/ ระหว่างที่ console เปิดอยู่ ให้พิมพ์ reload! ก่อนลองใหม่
(ผลจริง: ขึ้น Reloading... แล้ว => nil) · ไม่งั้นจะได้ผลของโค้ดเก่า แล้วงงว่าทำไมแก้แล้วไม่เปลี่ยน
- REPL · Rails console
- REPL ย่อมาจาก Read–Eval–Print Loop = อ่านที่พิมพ์ → รัน → โชว์ผล → วนรอบใหม่ ·
irbคือ REPL ของ Ruby ·bin/rails consoleคือ REPL ที่โหลดแอปมาด้วย · ถาม senior ได้ว่า “อันนี้ลองใน rails console ก่อนได้ไหม ก่อนเขียนลงไฟล์” — คนทำ Rails ทำแบบนี้เป็นปกติ app·helper- ตัวช่วยสองตัวที่มีเฉพาะใน console:
app= เบราว์เซอร์จำลอง (app.get·app.host!·app.response) ·helper= เรียก view helper · พิมพ์บอก AI ได้ว่า “in rails console, app.get returns 403 Blocked hosts: www.example.com” แล้วมันจะตอบเรื่อง host ทันที - Host Authorization
- ด่านหน้าครัวที่เช็คว่าใบสั่งมาจากชื่อเว็บที่อนุญาตไหม · เห็น
Blocked hostsแปลว่าติดด่านนี้ ไม่ใช่ route ผิด · ถาม senior ได้ว่า “เว็บจริงของเราตั้ง config.hosts ไว้เป็นชื่อโดเมนอะไร”
เปิด bin/rails console แล้วพิมพ์ app.get "/hello" ได้ => 403 พร้อมข้อความ Blocked hosts: www.example.com ควรทำอะไรต่อ?
ตัวเลือกที่ 2 ถูก — ข้อความบอกว่าติดด่านชื่อเว็บ (www.example.com ไม่ได้รับอนุญาต) · ตั้งเป็น localhost แล้วผลจริงได้ => 200
ตัวเลือกที่ 1 ผิด เพราะ app ไม่ต้องพึ่ง server เลย — ผมยิงได้ 200 ทั้งที่ไม่มี server เปิดอยู่ ·
ตัวเลือกที่ 3 ผิด เพราะถ้าไม่มี route จะได้ 404 ไม่ใช่ 403 ·
ตัวเลือกที่ 4 ผิด เพราะ reload! ใช้ตอนแก้โค้ด ไม่ได้เปลี่ยนชื่อเว็บของเบราว์เซอร์จำลอง
จุดที่ต้องจำ: app.get ได้ 403 + Blocked hosts → app.host! "localhost" ก่อนอย่างอื่น
--sandbox — ลองแล้วทิ้ง (จะสำคัญมากในวันที่ 2)
วันนี้แอปของเรายังไม่ได้เก็บข้อมูลอะไรลงฐานข้อมูล · แต่วันที่ 2 จะมีตารางบทความจริง และคุณจะใช้ console สร้าง แก้ ลบ บทความเยอะมาก · ถ้าอยากลองคำสั่งที่น่ากลัว (เช่น ลบทั้งตาราง) โดยไม่ให้ข้อมูลหายจริง ให้เปิดแบบนี้ (ผลจริง):
$ bin/rails console --sandbox
Loading development environment in sandbox (Rails 8.1.4)
Any modifications you make will be rolled back on exit
wongnok-blog(dev):001> 1 + 1
=> 2
wongnok-blog(dev):002> exit
ข้อความบรรทัดที่สองแปลว่า "ทุกอย่างที่คุณเปลี่ยนในฐานข้อมูล จะถูกย้อนกลับตอนออก" ·
พิมพ์คำสั่งได้เหมือน console ปกติทุกอย่าง ต่างกันแค่ตอน exit ทุกการเปลี่ยนแปลงข้อมูลถูกยกเลิก
database คือตู้เก็บแฟ้มของร้าน · console ปกติคือเขียนปากกาลงแฟ้มจริง ·
--sandbox คือวางแผ่นใสทับแฟ้มแล้วเขียนบนแผ่นใส — มองเห็นของจริงข้างใต้ ขีดฆ่าได้ตามใจ ·
พอ exit ก็ยกแผ่นใสทิ้ง แฟ้มในตู้เหมือนเดิม
ที่ไม่เหมือนกัน: แผ่นใสคุมได้แค่ของในตู้เก็บแฟ้ม (database) · ถ้าคุณสั่งอย่างอื่นที่ออกไปนอกฐานข้อมูล (เช่น ส่งอีเมล หรือเขียนไฟล์) สิ่งนั้นเกิดขึ้นจริง ยกแผ่นใสทิ้งก็ไม่ย้อนกลับ
เทียบกับ Go: จำ defer tx.Rollback() ในบท 8 ได้ไหม — เปิด transaction (กลุ่มคำสั่งที่ "ยืนยันทีเดียว หรือยกเลิกทีเดียว") แล้วยกเลิกทั้งก้อน ·
--sandbox คือ Rails เปิด transaction ครอบไว้ให้ตั้งแต่เปิด console แล้วสั่ง rollback ให้ตอน exit · ไม่ต้องเขียนเอง
สไลด์หน้า 69 ยังพูดถึงอีกสองตัว แค่รู้ไว้ก่อน:
bin/rails console -e production = เปิด console ของเว็บจริง (prompt จะเป็น (prod) สีแดง — เห็นสีแดงให้หยุดมือก่อนกด Enter) ·
bin/rails dbconsole = เข้าโปรแกรมพิมพ์ SQL ของฐานข้อมูลตรง ๆ (ต้องมีโปรแกรม psql ในเครื่อง — รอวันที่ 2)
- sandbox · rollback
- sandbox = โหมดลองเล่นที่ไม่มีผลถาวร · rollback = ยกเลิกการเปลี่ยนแปลงทั้งก้อน (ตรงข้ามกับ commit = ยืนยัน) · กลไกเบื้องหลังคือ transaction แบบเดียวกับ
tx.Rollback()ของ Go · ถาม senior ได้ว่า “จะลองคำสั่งนี้กับข้อมูล ควรเปิด console --sandbox ก่อนไหม” - environment (development / test / production)
- ชุดการตั้งค่าที่แอปใช้ · development = เครื่องเรา · production = เว็บจริงที่ลูกค้าใช้ ·
-eคือเลือกชุด · ถาม senior ได้ว่า “ทีมเราเข้า console ของ production ยังไง ต้องขอสิทธิ์ใคร” — ควรรู้ก่อนวันที่ต้องใช้จริง
จับคู่ปัญหากับเครื่องมือ: "อยากรู้ว่า params ตอนผู้ใช้กดปุ่มส่งฟอร์มมีค่าอะไร" ควรใช้อะไร?
ตัวเลือกที่ 1 ถูก — params คือรายละเอียดในใบสั่ง มีได้เฉพาะตอนที่มีใบสั่งวิ่งเข้ามาจริง · debugger หยุดตรงนั้นแล้วให้ถามได้ (เหมือนที่เราถามเห็น "name" => "Somchai")
ตัวเลือกที่ 2 และ 3 ผิด เพราะใน console ยังไม่มีใบสั่งเข้ามา จึงไม่มี params ให้ดู (console เหมาะกับคำถามที่ไม่ต้องรอลูกค้า เช่น ชื่อ route · ผลของ helper) ·
ตัวเลือกที่ 4 ผิด เพราะ params ไม่ได้ถูกเก็บในฐานข้อมูล
จุดที่ต้องจำ: ต้องมีใบสั่งวิ่งมา → debugger · ลองของได้โดยไม่ต้องมีลูกค้า → console · ลองของที่น่ากลัวกับข้อมูล → console --sandbox
🎯 ภารกิจ — หาบั๊กด้วยตัวเอง (~10 นาที)
ทำในแอป wongnok_blog ของคุณ (ถ้ายังไม่มี ให้ทำตามบท 98 ก่อน) · ทุกขั้นใช้ชื่อภาษาอังกฤษ เช่น Somchai (ชื่อภาษาไทยจะพังในขั้นที่ 6 — ดูท้ายเฉลย)
- เพิ่มบรรทัด
get "/hello", to: "pages#hello"ในconfig/routes.rb(ใต้Rails.application.routes.draw do) · สร้างไฟล์ใหม่app/controllers/pages_controller.rb· สร้างโฟลเดอร์app/views/pages/แล้วสร้างไฟล์hello.html.erbข้างใน · เนื้อหาตามโค้ดในหัวข้อแรกของบทนี้ (ใส่:nmaeสะกดผิดไว้ด้วย) - ใส่
debuggerใต้บรรทัด@name = …· เปิดbin/rails server(ไม่ใช่bin/dev) - เปิดเบราว์เซอร์ที่
http://localhost:3000/hello?name=Somchai(เปลี่ยนเป็นชื่อภาษาอังกฤษของคุณได้) → หน้าเว็บควรหมุนค้าง - ไปที่เทอร์มินัลของ server · พิมพ์
params·@name·params[:name]ทีละบรรทัด แล้วcontinue - แก้
:nmaeเป็น:name· ลบdebugger· รันgrep -rn debugger app/ให้ไม่มีอะไรขึ้น - เปิด
bin/rails console· พิมพ์app.host! "localhost"แล้วapp.get "/hello?name=Somchai"ให้ได้=> 200
ก่อนกดเฉลย: ข้อ 4 ตอนพิมพ์ @name ได้อะไร และข้อ 6 ถ้าข้าม app.host! จะได้เลขอะไร?
สิ่งที่ควรเห็น (จากเครื่องที่ใช้ตรวจ):
- ข้อ 3 — เบราว์เซอร์หมุนค้าง · เทอร์มินัลขึ้นโค้ด 7 บรรทัด ลูกศรชี้
debuggerและ prompt(rdbg) - ข้อ 4 —
paramsมีช่อง"name"·@nameได้nil·params[:name]ได้ชื่อของคุณ · หลังcontinueหน้าเว็บขึ้น และ log บอกCompleted 200 OK in …msเป็นเลขหลักพันถึงหมื่น (เพราะนับเวลาที่หยุดรอด้วย) - ข้อ 5 —
grepไม่พิมพ์อะไรออกมาเลย - ข้อ 6 — ถ้าข้าม
app.host!ได้=> 403+Blocked hosts: www.example.com· ตั้งแล้วได้=> 200
ถ้าเจอ error นี้ แปลว่า…
- เบราว์เซอร์หมุนไม่หยุด แม้แก้โค้ดแล้ว — debugger ยังหยุดรออยู่ในเทอร์มินัลของ server · ไปพิมพ์
continueที่นั่น DEBUGGER: wait for debugger connection...แล้วพิมพ์อะไรก็ไม่ตอบ — คุณเปิดด้วยbin/dev· กด Ctrl-C ปิด แล้วเปิดใหม่ด้วยbin/rails servereval error: undefined local variable or method 'app' for #<PagesController…>— คุณพิมพ์app.getใน(rdbg)ของ debugger แทนที่จะพิมพ์ใน console ·appกับhelperมีเฉพาะในbin/rails console(ผมพลาดแบบนี้เองตอนเตรียมบท ได้ข้อความนี้จริง)URI must be ascii onlyตอนพิมพ์app.get— คุณใส่ชื่อภาษาไทยลงไปใน URL ตรง ๆ · เบราว์เซอร์จริงแปลงตัวอักษรไทยให้เอง แต่เบราว์เซอร์จำลองappไม่แปลง → ใช้ชื่อภาษาอังกฤษแทน (ผมลองแล้ว ได้ error นี้จริง)NameError (undefined local variable or method 'debugger' …)ใน log — มีคนลืมลบdebuggerไว้ แล้วโค้ดไปรันในโหมดที่ไม่มี gem debug
ติดตรงไหน หรือผลของคุณไม่ตรงกับที่เขียนไว้ ถามกลับมาได้เลย พร้อมแปะสิ่งที่จอขึ้นมาทั้งก้อน
ถ้าเปิดสไลด์ดูประกอบ
| หน้า | สไลด์เขียน | ของจริง | ต้องสนใจไหม |
|---|---|---|---|
| 67–68 | ตัวอย่างใช้ Post.all · Post.count · Post.create! | ต้องมีตารางบทความในฐานข้อมูลก่อน ซึ่งเป็นเรื่องวันที่ 2 · บทนี้จึงใช้หน้า /hello แทน | ไม่ต้อง — วันที่ 2 จะได้ลองเอง |
| 67 | ผลใน debugger มี => นำหน้า · params เป็น {…} ธรรมดา | ไม่มี => · params โชว์เป็น #<ActionController::Parameters {…} permitted: false> | ไม่ต้อง — หน้าตาต่าง ความหมายเหมือนกัน |
| 67 | debugger ดีกว่า puts เพราะ "ไม่ลืมลบ puts ทิ้งก่อน commit" | debugger ก็ต้องลบ · ลืมไว้ = เว็บจริง error 500 | สนใจมาก |
| 68 | prompt เป็น irb> | wongnok-blog(dev):001> | ไม่ต้อง |
| 68 | app.get "/posts" ได้ => 200 | ได้ 403 · ต้อง app.host! "localhost" ก่อน | สนใจ — พิมพ์ตามแล้วจะงง |
| 69 | เคล็ดลับ: ตั้งสี prompt ให้ต่างกันระหว่าง dev กับ production | Rails 8.1 ทำให้แล้ว: (dev) สีน้ำเงิน · (prod) สีแดง | ไม่ต้องตั้งเอง |
อ้างอิง
- Rails Guides — Debugging Rails Applications § Debugging with the debug Gem —
อ่านแค่ส่วน Entering a Debugging Session · ยืนยันว่า gem debug มากับแอปใหม่ และพิมพ์
debuggerบรรทัดเดียวก็หยุดได้ · หัวข้อ breakpoint ขั้นสูงข้ามไปก่อนได้ - Rails Guides — The Rails Command Line § bin/rails console —
อ่านต่อถึงหัวข้อ The app Object และ The helper Object · คู่มือบอกเองว่า
appใช้ชื่อเว็บwww.example.comเป็นค่าเริ่ม และให้ส่ง Host เพื่อเลี่ยงด่าน - Rails API — ActionDispatch::Integration::Session —
ชนิดของ
appใน console · ดูแค่host!กับDEFAULT_HOSTพอ ที่เหลือเป็นเรื่องการเขียนเทส (วันที่ 4) - RubyGems — debug —
หน้าทางการของ gem debug · ดูเลขรุ่นล่าสุด และตามลิงก์ Documentation ถ้าอยากเห็นคำสั่งครบทุกตัว (บทนี้ใช้แค่
next·step·info·continue) - Go — Debugging Go Code with GDB — อ่านแค่กล่องหมายเหตุตอนต้นหน้า ที่แนะนำ Delve เป็นทางเลือกที่ดีกว่าสำหรับ Go · ยืนยันว่าฝั่ง Go ต้องหาเครื่องมือมาติดตั้งเพิ่มเอง