- request / response — ใบสั่งที่ browser ส่งมา / จานที่ server ยกกลับไป (บท 90)
- ร้านอาหาร = แอป Rails — พนักงานต้อนรับคือ routes · หัวหน้าเชฟคือ controller · จัดจานคือ view (บท 90)
Postกับposts— บทความหนึ่งชิ้น (ตัวใหญ่ เอกพจน์) กับหลายชิ้น / URL / โฟลเดอร์ (ตัวเล็ก พหูพจน์) (บท 99)@ตัวแปร— ตัวแปรที่ติดอยู่กับ object ตัวนั้น ไม่หายเมื่อ method จบ (บท 95)- handler ของ Gin — ฟังก์ชัน
func(c *gin.Context)ที่รับ request แล้วตอบกลับ (บท 38)
ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · Puma 8.0.2 (สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 ต่างกันแค่รุ่นย่อย ผลเหมือนกัน) · ทุก log และทุกหน้า HTML ในบทนี้ก็อปมาจากการรันจริงบนพอร์ต 3000 · เวลา · PID · ตัวเลข ms ของคุณจะต่างไป เป็นเรื่องปกติ
ภาพรวม — ใบสั่งหนึ่งใบเดินผ่านใครบ้าง
เวลาคุณพิมพ์ localhost:3000/posts แล้วกด Enter browser จะส่งใบสั่ง (request) ไปที่ร้าน
ใบสั่งใบนี้ไม่ได้ไปถึงคนทำอาหารทันที มันต้องส่งต่อกันหลายมือ แต่ละมือทำงานแค่อย่างเดียว
บทนี้ทั้งบทคือการเดินตามเส้นข้างล่างนี้ทีละช่วง จำภาพนี้ไว้ก่อน เดี๋ยวเราจะเห็นของจริงทุกช่อง
ลูกค้าเขียนใบสั่ง “ขอดูบทความทั้งหมด” ยื่นที่ประตูร้าน · ใบสั่งผ่านด่านหน้าครัวที่จดลงสมุดว่ามีใบสั่งเข้ามา · พนักงานต้อนรับอ่านแล้วชี้ว่า “ใบนี้ของแผนกบทความ เมนู index” · หัวหน้าเชฟแผนกบทความเตรียมของ · คนจัดจานจัดลงจานสวย ๆ · แล้วยกไปเสิร์ฟที่โต๊ะ
ที่ไม่เหมือนกัน: ในร้านจริงแต่ละคนเป็นคนละคน เดินส่งใบสั่งกันจริง ๆ · แต่ใน Rails ทุก “คน” คือโค้ดที่อยู่ในโปรแกรมเดียวกัน เรียกต่อกันเป็นทอด ๆ ทั้งเส้นใช้เวลาแค่ไม่กี่มิลลิวินาที และคุณเขียนเองแค่ 3 ช่อง (routes · controller · view) ที่เหลือ Rails เตรียมไว้ให้แล้ว
ถ้าเทียบกับ Go: ใน Gin คุณเขียนเกือบทุกช่องเอง — สร้าง router เอง (gin.Default()) ·
ใส่ middleware เอง (r.Use(...)) · ลง route เอง · เขียน handler ที่ทำทั้งเตรียมข้อมูลและตอบกลับ ·
แล้วสั่ง r.Run() เปิดประตูเอง ·
Rails คือร้านแฟรนไชส์ที่ได้ครัวครบชุด คุณแค่วางของลงช่องที่แฟรนไชส์กำหนด
- request cycle / request lifecycle
- เส้นทางทั้งหมดของ request หนึ่งตัว ตั้งแต่เข้ามาจนตอบกลับ · พิมพ์ถาม AI ได้ว่า “walk me through the Rails request cycle for GET /posts”
- controller#action
- วิธีเขียนสั้น ๆ ว่า “แผนกไหน เมนูอะไร” เช่น
posts#index= actionindexของPostsController· จะเห็นคำนี้ทั้งในroutes.rbและใน log
ก่อนเปิดร้าน — เกิดอะไรขึ้นตอนพิมพ์ bin/rails server
ร้านอาหารไม่ได้เปิดประตูปุ๊บแล้วขายได้เลย ต้องเตรียมครัวก่อน — เปิดไฟ ตั้งเตา เช็ควัตถุดิบ Rails ก็เหมือนกัน ช่วงเตรียมตัวก่อนรับใบสั่งแรกนี้เรียกว่า boot
คุณเคยเปิดร้านมาแล้วหนึ่งครั้งใน บท 98 · รอบนี้เราจะอ่านช้าลงว่าแต่ละบรรทัดบอกอะไร ·
เข้าไปในโฟลเดอร์โปรเจกต์ wongnok_blog แล้วพิมพ์คำสั่งนี้ในเทอร์มินัล
(bin/rails คือโปรแกรมประจำโปรเจกต์ที่รู้จักแล้วในบท 98):
ใช้โปรเจกต์เดิมจากบท 98 ที่ตั้ง DATABASE_URL และสั่ง bin/rails db:prepare แล้ว ·
ถ้ายังไม่ได้ทำ server จะเปิดได้ แต่พอเปิดหน้าเว็บจะได้ error 500 เพราะต่อ database ไม่ติด (วิธีแก้อยู่ในบท 98)
$ bin/rails server
=> Booting Puma
=> Rails 8.1.4 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 8.0.2 ("Into the Arena")
* Ruby version: ruby 4.0.9 (2026-10-23 revision 3e93b23afc) +PRISM [arm64-darwin25]
* Min threads: 3
* Max threads: 3
* Environment: development
* PID: 2674
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
อ่านทีละบรรทัด:
=> Booting Puma— กำลังเตรียมเปิดร้าน · Puma คือโปรแกรม “ประตูร้าน” ที่ Rails ใช้รับ request (เหมือนr.Run()ของ Gin ที่เปิดพอร์ตรอ)Puma version·Ruby version— แค่บอกรุ่นของโปรแกรมที่ใช้ · ตัวอักษรยาว ๆ ท้ายบรรทัดไม่ต้องอ่านstarting in development— เปิดในโหมดสาขาทดลอง สำหรับนั่งเขียนโค้ดบนเครื่องตัวเอง (อีกโหมดคือ production = ร้านจริงที่ลูกค้าใช้)Min threads: 3 / Max threads: 3— ประตูนี้มีพนักงานรับใบสั่ง 3 คน รับพร้อมกันได้ 3 ใบ · ตอนนี้ไม่ต้องสนใจPID: 2674— เลขประจำตัวของโปรแกรมนี้ในเครื่อง · ของคุณจะเป็นเลขอื่นListening on http://127.0.0.1:3000— ประตูเปิดแล้ว ที่พอร์ต 3000 ·127.0.0.1กับ[::1]แปลว่า “เครื่องตัวเอง” ทั้งคู่ (แบบเก่ากับแบบใหม่ของที่อยู่ในเครือข่าย)Use Ctrl-C to stop— กด Ctrl กับ C พร้อมกัน = ปิดร้าน
ก่อนบรรทัด Listening on จะโผล่ Rails แอบทำงานเตรียมครัวไปหลายอย่าง
โดยอ่านไฟล์ตั้งค่าในโฟลเดอร์ config/ เช่น
config/application.rb (ตั้งค่าของทั้งร้าน) ·
config/environments/development.rb (กฎเฉพาะสาขาทดลอง) ·
และไฟล์ใน config/initializers/ (งานเตรียมเล็ก ๆ ทีละข้อ)
คุณไม่ต้องจำลำดับไฟล์ · จำแค่ว่า “ตั้งค่าอยู่ใน config/ และถูกอ่านตอนเปิดร้าน” ก็พอ
เช้าก่อนเปิดร้าน ผู้จัดการเดินดูตามเช็คลิสต์: เปิดแก๊ส · ตั้งหม้อน้ำซุป · เช็คของในตู้ · ติดเมนูหน้าร้าน ·
แล้วค่อยพลิกป้ายหน้าประตูเป็น “เปิด” · ป้าย “เปิด” นั่นแหละคือบรรทัด Listening on
ที่ไม่เหมือนกัน: ร้านจริงต้องปิดร้านทุกครั้งที่อยากเปลี่ยนเมนู ·
แต่ในโหมด development คุณแก้ routes · controller · view แล้วกด refresh ได้เลย ไม่ต้องปิดเปิด server
(ผมลองแก้ทั้งสามไฟล์ตอน server เปิดอยู่ หน้าเว็บเปลี่ยนตามทันที) ·
ยกเว้นไฟล์ใน config/initializers/ ที่อ่านแค่ตอนเปิดร้าน แก้แล้วต้องปิดเปิดใหม่
- boot
- ช่วงที่แอปเตรียมตัวก่อนพร้อมรับ request · ถ้าเปิดช้า คนจะพูดว่า “boot ช้า” · ถาม senior ได้ว่า “แก้ไฟล์ไหนบ้างที่ต้อง restart server”
- Puma (web server)
- โปรแกรมที่เปิดพอร์ตแล้วรับ request จริง แล้วส่งต่อให้ Rails · Rails ใช้ตัวนี้เป็นค่าเริ่มต้น ·
ตั้งค่าที่
config/puma.rb - environment (development / production)
- โหมดที่แอปกำลังรัน · development = เครื่องเรา แก้โค้ดแล้วเห็นผลทันที · production = เว็บจริง · พิมพ์บอก AI ได้ว่า “this should only run in development” เวลาอยากให้โค้ดบางอย่างทำงานแค่บนเครื่องตัวเอง
ลงมือ — ทำหน้า “บทความทั้งหมด” ด้วย 3 ไฟล์
ร้านเปิดแล้ว แต่ยังไม่มีเมนูอะไรเลย · เราจะเพิ่มหน้า /posts ที่โชว์รายชื่อบทความ
โดยแตะแค่ 3 ช่องที่ Rails ให้เราเขียน: พนักงานต้อนรับ → หัวหน้าเชฟ → จัดจาน
วันนี้ยังไม่มี database (รอ Day 2) เราจะจำลองบทความด้วย Hash สองก้อน วางไว้ใน controller ตรง ๆ — เป็นของชั่วคราว ท้ายบทจะเห็นว่ามันควรย้ายไปอยู่ที่ไหน
ไฟล์ที่ 1 · พนักงานต้อนรับ — config/routes.rb
เปิดไฟล์นี้ (มีอยู่แล้วตั้งแต่ rails new) แล้วเพิ่มบรรทัด get "/posts", … เข้าไป ·
ไฟล์จริงมีบรรทัด comment (ขึ้นต้นด้วย #) อีกหลายบรรทัด ข้างล่างตัดออกให้สั้น — จะลบหรือเก็บไว้ก็ได้ ไม่มีผล:
# config/routes.rb
Rails.application.routes.draw do
get "up" => "rails/health#show", as: :rails_health_check # มีมาให้แล้ว ไม่ต้องแตะ
get "/posts", to: "posts#index" # ← เพิ่มบรรทัดนี้
end
Rails.application.routes.draw do … end— “เริ่มเขียนรายการเส้นทางของร้าน” · ของที่อยู่ระหว่างdoกับendคือ block (ก้อนโค้ดที่ส่งให้ method — บท 93)get "up" => …— ของที่rails newใส่มาให้: หน้า/upไว้เช็คว่าร้านยังเปิดอยู่ ·=>ตรงนี้อ่านว่า “ไปที่” · ไม่ต้องแตะ และไม่ต้องเข้าใจตอนนี้ (บท 101)get "/posts"— “ถ้ามีใบสั่งแบบ GET (ขอดู) มาที่/posts…” ·getคือ method ตัวหนึ่ง ที่เรียกโดยละวงเล็บ (บท 97) — เขียนเต็มได้ว่าget("/posts", to: "posts#index")to: "posts#index"— “…ส่งไปแผนกpostsเมนูindex” ·to:คือ hash ท้าย argument ที่ไม่ต้องใส่ปีกกา (บท 97) · ละเอียดของ routes อยู่ใน บท 101
ไฟล์ที่ 2 · หัวหน้าเชฟ — app/controllers/posts_controller.rb
สร้างไฟล์ใหม่ ชื่อต้องเป๊ะตามกฎตั้งชื่อ (บท 99):
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
# ข้อมูลจำลอง — ยังไม่มี database (รอ Day 2)
@posts = [
{ title: "ข้าวซอยเจ้าเด็ด", author: "สมชาย" },
{ title: "ส้มตำปูปลาร้า", author: "สมหญิง" }
]
end
end
class PostsController < ApplicationController— แผนกบทความ ·<แปลว่า “สืบทอดความสามารถมาจาก” (บท 95) ·ApplicationControllerคือ controller แม่ที่rails newสร้างให้def index … end— เมนูindexของแผนกนี้ คือ method ธรรมดาตัวหนึ่ง (บท 93) · ชื่อต้องตรงกับคำหลัง#ใน routes@posts = [ … ]— Array (รายการ ใส่ใน[ ]) ของ Hash สองก้อน (บท 92) · แต่ละก้อนจำลองบทความหนึ่งชิ้น ·{ title: "…", author: "…" }คือ Hash ที่มีช่อง:titleกับ:author(เขียนtitle:= ช่องชื่อ:title) · ต้องมี@เพราะ@ตัวแปรคือถาดที่ส่งต่อให้คนจัดจาน — view อ่านได้ · ตัวแปรธรรมดาที่ไม่มี@view มองไม่เห็น (ละเอียดใน บท 102)- สังเกตว่าไม่มีบรรทัดไหนสั่งให้ส่งหน้าเว็บกลับ · ถ้า action จบเฉย ๆ Rails จะไปหา view ที่ชื่อตรงกันเอง (ไฟล์ที่ 3)
ไฟล์ที่ 3 · จัดจาน — app/views/posts/index.html.erb
สร้างโฟลเดอร์ app/views/posts/ แล้วสร้างไฟล์ index.html.erb ข้างใน:
<h1>บทความทั้งหมด</h1>
<% @posts.each do |post| %>
<h2><%= post[:title] %></h2>
<p>โดย <%= post[:author] %></p>
<% end %>
- ไฟล์
.html.erbคือ HTML ที่แทรกโค้ด Ruby ได้ (ERB — ละเอียดใน บท 103) <% … %>— รันโค้ด Ruby แต่ไม่พิมพ์อะไรลงหน้า · ใช้กับคำสั่งวนลูป<%= … %>(มี=) — รันแล้วพิมพ์ผลลงหน้า@posts.each do |post| … end— วนทีละบทความ แต่ละรอบเรียกบทความนั้นว่าpost(บท 93)post[:title]— หยิบค่าจาก Hash ด้วย symbol:title(บท 92)
ทำไม Rails รู้ว่าต้องใช้ไฟล์นี้? กฎตั้งชื่อจาก บท 99:
PostsController + action index → app/views/posts/index.html.erb
ไม่ต้องเขียนบอกเลย
เปิดดูผล
เปิด browser ไปที่ http://localhost:3000/posts จะเห็นหัวข้อ “บทความทั้งหมด” กับบทความสองชิ้น ·
ถ้าดู HTML ที่ได้กลับมา (กด View Source) ช่วงท้ายของหน้า ตั้งแต่ <main> จะเป็นแบบนี้:
<main class="container mx-auto mt-28 px-5 flex">
<!-- BEGIN app/views/posts/index.html.erb
--><h1>บทความทั้งหมด</h1>
<h2>ข้าวซอยเจ้าเด็ด</h2>
<p>โดย สมชาย</p>
<h2>ส้มตำปูปลาร้า</h2>
<p>โดย สมหญิง</p>
<!-- END app/views/posts/index.html.erb -->
</main>
</body>
</html>
<!-- END app/views/layouts/application.html.erb -->
- view ของเรามี 6 บรรทัด แต่หน้าที่ได้มี
<html><head><main>ครบ — เพราะ Rails เอาไปวางบนจานรองที่ใช้ทุกหน้า คือไฟล์app/views/layouts/application.html.erb(เรียกว่า layout · บท 103) class="container mx-auto …"— คลาสแต่งหน้าตาของ Tailwind ที่มากับ layout ตอนrails new … --css=tailwind· ไม่ใช่ของเรา ไม่ต้องสนใจ<!-- BEGIN … -->กับ<!-- END … -->คือป้ายที่ Rails แปะให้ในโหมด development บอกว่าส่วนนี้มาจากไฟล์ไหน · ช่วยมากเวลาหาว่าข้อความบนหน้ามาจากไฟล์อะไร
อ่าน log — สมุดบันทึกของร้าน
กลับไปดูเทอร์มินัลที่เปิด server ไว้ ทุกครั้งที่มีใบสั่งเข้ามา Rails จะจดลงสมุด (log) ให้เอง ·
นี่คือ log จริงตอนเปิดหน้า /posts:
Started GET "/posts" for ::1 at 2026-11-03 20:47:30 +0700
Processing by PostsController#index as HTML
Rendering layout layouts/application.html.erb
Rendering posts/index.html.erb within layouts/application
Rendered posts/index.html.erb within layouts/application (Duration: 0.3ms | GC: 0.0ms)
Rendered layout layouts/application.html.erb (Duration: 6.5ms | GC: 0.0ms)
Completed 200 OK in 18ms (Views: 7.7ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)
อ่านคู่กับภาพร้านอาหาร (ตัวเลขเวลาของคุณจะต่าง):
| บรรทัดใน log | แปลว่า | ในร้านอาหาร |
|---|---|---|
Started GET "/posts" for ::1 |
มี request แบบ GET มาที่ /posts จากเครื่องตัวเอง (::1) |
ด่านหน้าครัวจดว่า “ใบสั่งใหม่เข้ามาแล้ว” |
Processing by PostsController#index as HTML |
routes จับคู่ได้แล้ว ส่งให้ action index ของ PostsController · ลูกค้าขอเป็น HTML |
พนักงานต้อนรับชี้แผนกเสร็จ หัวหน้าเชฟแผนกบทความรับใบสั่ง |
Rendering posts/index.html.erb within layouts/application |
เริ่มสร้าง HTML จาก view ของเรา วางใน layout | เริ่มจัดจาน วางบนจานรองของร้าน |
Rendered … (Duration: 0.3ms …) |
สร้าง HTML เสร็จ ใช้เวลาเท่านี้ | จัดจานเสร็จ |
Completed 200 OK in 18ms |
ส่ง response กลับแล้ว · status 200 = สำเร็จ · ทั้งเส้นใช้ 18 มิลลิวินาที | ยกจานถึงโต๊ะเรียบร้อย |
ส่วน ActiveRecord: 0.0ms (0 queries …) ท้ายบรรทัดสุดท้าย บอกว่ารอบนี้ไม่ได้แตะ database เลย
(Active Record คือส่วนของ Rails ที่คุยกับ database — เจอจริง Day 2)
— ตรงกับที่เราจำลองข้อมูลไว้ใน controller · Day 2 ตัวเลขนี้จะเริ่มขยับ
request แรกหลังเปิด server คุณอาจเห็นบรรทัด ActiveRecord::SchemaMigration Load … แทรกมาอีกหนึ่งบรรทัด —
Rails แค่แง้มดูว่าตู้เก็บแฟ้ม (database) พร้อมไหม ไม่ต้องตกใจ
(:: อ่านว่า “ที่อยู่ข้างใน” แบบที่เจอในบท 99)
ถ้าเทียบกับ Gin: log ของ Gin ต่อหนึ่ง request มีบรรทัดเดียว (ผลจริงจากโปรแกรม Go ในหัวข้อ MVC ข้างล่าง):
[GIN] 2026/11/03 - 21:12:17 | 200 | 86.291µs | ::1 | GET "/posts"
Rails จดละเอียดกว่า เพราะมันแบ่งงานเป็นหลายช่อง แล้วบอกว่า request ผ่านช่องไหนบ้าง · เวลาหน้าเว็บพัง ให้ดู log ก่อนเสมอ — มันบอกว่าใบสั่งไปติดอยู่ที่มือใคร
# routes.rb มีแค่ get "/posts" (ไม่มี /menu)
# เปิด browser ไปที่
http://localhost:3000/menu
log จะมีบรรทัด Processing by … ไหม? แล้วได้ status เลขอะไร?
ไม่มีบรรทัด Processing by เลย · log จริง:
Started GET "/menu" for ::1 at 2026-11-03 20:47:31 +0700
ActionController::RoutingError (No route matches [GET] "/menu"):
และ browser ได้ status 404 พร้อมหน้าแดง ๆ หัวข้อ Routing Error
เขียนว่า No route matches [GET] "/menu"
ใบสั่งผ่านด่านหน้าครัวมาแล้ว (มีบรรทัด Started) แต่พนักงานต้อนรับหาแผนกไม่เจอ
เลยไม่มีหัวหน้าเชฟคนไหนได้รับใบสั่ง (ไม่มี Processing by) ·
เจอ No route matches เมื่อไหร่ ให้ไปดูที่ config/routes.rb ก่อนเลย
ด่านหน้าครัว — middleware
ก่อนใบสั่งจะถึงพนักงานต้อนรับ มันต้องผ่านด่านหลายด่านเรียงกัน แต่ละด่านทำงานหนึ่งอย่างกับทุกใบสั่ง เช่น จดลงสมุด · อ่านบัตรสมาชิกของลูกค้า (cookie) · กันคนแปลกหน้า · ด่านพวกนี้เรียกว่า middleware
บรรทัด Started GET "/posts" … ที่เห็นใน log ก็มาจาก middleware ตัวหนึ่ง — มันคือคนจดสมุดที่ประตูครัว ·
อยากดูว่ามีด่านอะไรบ้าง สั่งได้ด้วยคำสั่งข้างล่าง
(ชื่อแต่ละด่านมี :: คั่น เช่น ActionDispatch::Static = ด่าน Static ที่อยู่ในกล่อง ActionDispatch):
$ bin/rails middleware
use ActionDispatch::HostAuthorization
use Rack::Sendfile
use ActionDispatch::Static
…(ตัดออก)…
run WongnokBlog::Application.routes
ในโปรเจกต์ที่เพิ่ง rails new โหมด development มี 28 ด่าน (บรรทัด use) ·
อ่านจากบนลงล่าง = ลำดับที่ใบสั่งเดินผ่าน · บรรทัดสุดท้าย run … routes บอกว่าผ่านด่านสุดท้ายแล้วส่งให้ routes ·
คุณไม่ต้องจำชื่อสักตัว — รู้แค่ว่ามีด่านพวกนี้ และ Rails ใส่มาให้ครบแล้ว
ร้านอาหารที่ดีมีกติกาว่า ใบสั่งทุกใบต้องผ่านจุดเดียวกันก่อนเข้าครัว: จดเวลารับลงสมุด · ตรวจบัตรสมาชิก · เช็คว่าลูกค้ามาถูกร้าน · ไม่ว่าจะสั่งเมนูอะไร ด่านพวกนี้ไม่สน มันทำงานเหมือนกันทุกใบ
ที่ไม่เหมือนกัน: ด่านในร้านจริงทำงานแค่ขาเข้า ·
แต่ middleware ทำงานทั้งขาเข้าและขากลับ — จานที่ทำเสร็จต้องเดินย้อนผ่านทุกด่านเดิมก่อนถึงลูกค้า
เช่นมีด่านหนึ่งแปะ “รหัสประจำจาน” ตอนขากลับ ให้ browser จำได้ว่าเคยได้จานนี้แล้ว
(รหัสนี้คือ header ชื่อ etag · header = ป้ายข้อมูลเล็ก ๆ ที่แนบไปกับจาน ลูกค้าไม่เห็นบนหน้าเว็บ)
ใน Go คุณรู้จักของนี้แล้ว — gin.Default() ใส่ middleware ให้ 2 ตัว (Logger กับ Recovery) และคุณเพิ่มเองด้วย
r.Use(...) (บท 39) ·
แนวคิดเดียวกันเป๊ะ ต่างแค่ Rails ใส่มาให้ 28 ตัวตั้งแต่วันแรก
- middleware / middleware stack
- โค้ดที่ทุก request ต้องผ่านก่อนถึง routes เรียงกันเป็นแถว (stack) · ดูได้ด้วย
bin/rails middleware· ถาม senior ได้ว่า “ทีมนี้มี middleware ที่เขียนเองไหม ทำหน้าที่อะไร” - log / server log
- ข้อความที่ server จดไว้ทุก request · ใน development โชว์ในเทอร์มินัลและเก็บที่
log/development.log· เวลาถาม AI เรื่อง error ให้ก็อป log ตั้งแต่บรรทัดStartedถึงCompletedไปด้วย มันจะตอบตรงกว่ามาก
เลข status ที่เจอใน log — ภาษาคน
บรรทัด Completed … ทุกครั้งมีเลขสามหลัก เรียกว่า status code — เป็นป้ายสั้น ๆ ที่ติดไปกับจาน
บอกลูกค้าว่าใบสั่งนี้จบแบบไหน (เคยเรียนฝั่ง Go แล้วใน บท 34) ·
ใน Rails มีชื่อเป็น symbol (คำที่ขึ้นต้นด้วย : — บท 92) ให้ใช้แทนตัวเลขได้ ·
วันนี้แค่อ่านเลขใน log ให้ออก ยังไม่ต้องเขียนเอง · คู่ชื่อกับเลขในตารางนี้ผมเช็คกับ Rails 8.1.4 จริงแล้ว:
| เลข | ชื่อใน Rails | ใน Go | ในร้านอาหาร | เจอตอนไหน |
|---|---|---|---|---|
| 200 | :ok | http.StatusOK |
จานถึงโต๊ะเรียบร้อย | ปกติ ทุกอย่างผ่าน |
| 302 | :found | http.StatusFound |
“เมนูนี้ย้ายไปอีกเคาน์เตอร์ค่ะ” ลูกค้าเดินไปสั่งใหม่เอง | redirect — ส่งไปอีกหน้า |
| 404 | :not_found | http.StatusNotFound |
ร้านไม่มีเมนูนี้ | URL ไม่มีใน routes |
| 422 | :unprocessable_content | http.StatusUnprocessableEntity |
ใบสั่งกรอกไม่ครบ ส่งคืนให้แก้ | ฟอร์มกรอกไม่ผ่าน (เจอจริง Day 2) |
| 500 | :internal_server_error | http.StatusInternalServerError |
ครัวไฟไหม้ — ความผิดของร้าน ไม่ใช่ลูกค้า | โค้ดของเรา error |
ลองดู 302 ของจริง · สมมติร้านเคยใช้ URL /articles แล้วย้ายมาเป็น /posts
เราอยากให้คนที่ยังเข้า URL เก่าถูกส่งต่อไป URL ใหม่ · เพิ่ม route กับ action อีกอย่างละหนึ่ง:
# config/routes.rb — เพิ่มใต้ get "/posts"
get "/articles", to: "posts#articles"
# app/controllers/posts_controller.rb — เพิ่มต่อจาก def index … end (ก่อน end ตัวสุดท้าย)
def articles
redirect_to "/posts"
end
redirect_to "/posts" แปลว่า “ไม่ต้องจัดจาน บอกลูกค้าให้ไปสั่งที่ /posts แทน”
(ละเอียดใน บท 102) · เปิด localhost:3000/articles แล้วดู log:
Started GET "/articles" for ::1 at 2026-11-03 20:47:30 +0700
Processing by PostsController#articles as HTML
Redirected to http://localhost:3000/posts
↳ app/controllers/posts_controller.rb:11:in 'PostsController#articles'
Completed 302 Found in 1ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)
Redirected to http://localhost:3000/posts— หัวหน้าเชฟบอกลูกค้าว่าให้ไปที่ไหน↳ app/controllers/posts_controller.rb:11— บรรทัดที่ 11 ของไฟล์ controller คือคนสั่ง redirect (Rails ชี้ให้เองในโหมด development · เลขบรรทัดของคุณอาจต่างถ้าเว้นบรรทัดไม่เท่ากัน)Completed 302 Found— จบด้วยเลข 302 · ไม่มีบรรทัด Rendering เพราะไม่ได้จัดจาน- ถ้าเปิดด้วย browser จะเห็น log ต่อท้ายอีกก้อนเป็น
Started GET "/posts"ทันที — นั่นคือ browser ยื่นใบสั่งใบใหม่ตามที่ถูกบอก รวมเป็น 2 request · ช่องที่อยู่ของ browser ก็จะเปลี่ยนจาก/articlesเป็น/postsเอง
MVC ของจริง — Gin ทำในฟังก์ชันเดียว · Rails แบ่งแผนก
ถึงตรงนี้เราเห็นสองช่องจากสามช่องของ MVC แล้ว: Controller (หัวหน้าเชฟ) กับ View (จัดจาน) · ช่องที่สามคือ Model = ห้องเก็บวัตถุดิบ + คนดูแลสต็อก ที่รู้ว่าข้อมูลอยู่ตู้ไหนใน database · MVC คือการแบ่งโค้ดเป็นสามกองตามหน้าที่ แทนที่จะกองรวมกัน
ลองเขียนหน้าเดียวกันด้วย Gin ดูก่อน (โค้ดนี้ go build ผ่าน · รันแล้วได้บทความสองชิ้นเหมือนกัน แค่ไม่มี layout ห่อ):
// main.go
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type Post struct {
Title string
Author string
}
func main() {
r := gin.Default() // พนักงานต้อนรับ + ด่านหน้าครัว (Logger, Recovery)
r.GET("/posts", func(c *gin.Context) {
// 1) หาข้อมูล (งานของ Model)
posts := []Post{
{"ข้าวซอยเจ้าเด็ด", "สมชาย"},
{"ส้มตำปูปลาร้า", "สมหญิง"},
}
// 2) จัดจานเป็น HTML (งานของ View)
html := "<h1>บทความทั้งหมด</h1>"
for _, p := range posts {
html += fmt.Sprintf("<h2>%s</h2><p>โดย %s</p>", p.Title, p.Author)
}
// 3) ยกจานออกไป (งานของ Controller)
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
})
r.Run(":8080")
}
type Post struct— ประกาศหน้าตาของบทความเอง (Go ต้องบอกทุกช่องชัด ๆ)r.GET("/posts", func(c *gin.Context) { … })— ลง route กับเขียน handler ในบรรทัดเดียวกันfmt.Sprintf(…)— ต่อ string HTML เองทีละบทความc.Data(http.StatusOK, …)— ส่ง HTML กลับพร้อมเลข 200 เอง
สังเกตว่า handler ตัวเดียวทำทั้งสามงาน: หาข้อมูล · ประกอบ HTML · ตอบกลับ · ตอนหน้ามีหน้าเดียวก็สบายดี แต่พอมี 30 หน้า ทุกหน้าต้องมี HTML ปนอยู่ใน Go · ทุกหน้าต้องรู้ว่าข้อมูลอยู่ตาราง SQL ไหน
Rails เตรียมห้องแยกไว้ให้ตั้งแต่ rails new แล้วใช้กฎตั้งชื่อจับคู่ให้: หาข้อมูลไป app/models/ · ประกอบ HTML ไป app/views/ ·
ประสานงานอยู่ app/controllers/ · สไลด์หน้า 52 สรุปไว้แบบนี้:
| Model | View | Controller | |
|---|---|---|---|
| ในร้าน | ห้องเก็บวัตถุดิบ + คนดูแลสต็อก | คนจัดจาน | หัวหน้าเชฟ |
| หน้าที่ | ข้อมูล + กฎของธุรกิจ (เช่น “บทความต้องมีชื่อเรื่อง”) | หน้าตาที่ผู้ใช้เห็น | รับใบสั่ง · ขอข้อมูลจาก model · เลือก view |
| ไม่ควรทำ | ไม่รู้จัก URL · params (รายละเอียดในใบสั่ง เช่น “บทความเลข 42” — บท 102) · redirect | ไม่ควรไปค้นข้อมูลเอง | ควรบางที่สุด ไม่แบกกฎธุรกิจ |
| ไฟล์ | app/models/post.rb | app/views/posts/index.html.erb | app/controllers/posts_controller.rb |
กฎตัดสินใจง่าย ๆ จากสไลด์: เป็นข้อมูลหรือกฎของธุรกิจ → Model · เกี่ยวกับ HTTP เช่น URL, params, redirect → Controller · หน้าตาล้วน ๆ → View
handler ของ Gin เหมือนร้านข้างทางที่เจ้าของทำเองทุกอย่าง — รับออเดอร์ หยิบของในตู้ ผัด จัดจาน เสิร์ฟ คนเดียวจบ ·
เร็วและง่ายตอนร้านเล็ก แต่พอเมนูเป็นร้อย ทุกอย่างอยู่ในหัวคนคนเดียว
Rails เหมือนร้านแฟรนไชส์ — มีห้องเก็บของ (Model) · หัวหน้าเชฟ (Controller) · คนจัดจาน (View) แยกกัน ·
และทุกสาขาวางของตำแหน่งเดียวกัน ใครย้ายมาทำงานสาขาไหนก็หาของเจอ
ที่ไม่เหมือนกัน: ในแฟรนไชส์จริง คนจัดจานเดินเข้าห้องเก็บของไม่ได้ · แต่ใน Rails ไม่มีใครห้าม — view จะเขียนโค้ดไปค้น database เองก็ทำงานได้ ไม่มี error · การแบ่งแผนกเป็นแค่ข้อตกลงที่ทีมต้องรักษาเอง สไลด์ถึงเรียกว่า “เส้นแบ่งที่คนข้ามบ่อยที่สุด” · และใน Go คุณก็แยกได้เหมือนกัน (แตกเป็นหลายฟังก์ชัน หลายไฟล์) แค่ต้องตั้งโครงเอง
แล้ว Model ของเราอยู่ไหน? วันนี้ยังไม่มี · ข้อมูลจำลองที่วางใน controller คือ
หัวหน้าเชฟเก็บวัตถุดิบไว้ในกระเป๋าตัวเองชั่วคราว ซึ่งผิดแผนก ·
Day 2 เราจะสร้าง model Post กับตาราง posts ใน database จริง แล้ว controller จะเหลือบรรทัดเดียวแบบภาพในสไลด์หน้า 53:
# Day 2 — ยังรันไม่ได้วันนี้ เพราะยังไม่มี model Post และตาราง posts
def index
@posts = Post.all # "ห้องเก็บของ ขอบทความทั้งหมด"
end
Post (ตัวใหญ่ เอกพจน์) คือคนดูแลสต็อกของบทความ · .all คือขอ “ทุกชิ้น” ·
Post ไม่ได้โผล่มาเฉย ๆ — Day 2 จะมีไฟล์ app/models/post.rb ที่ประกาศ class Post ไว้
แล้ว Rails หาเจอเองจากชื่อ ไม่ต้อง import (autoload — บท 99) ·
เส้นทางทั้งเส้นในสไลด์หน้า 53 จึงเป็น: browser → routes → PostsController → Post → database → กลับมาที่ controller
→ ส่ง @posts ให้ view → HTML กลับไปที่ browser — เส้นเดียวกับที่เราทำวันนี้ แค่เพิ่มห้องเก็บของเข้ามา
- MVC (Model–View–Controller)
- การแบ่งโค้ดเป็นสามกองตามหน้าที่: ข้อมูล · หน้าตา · ผู้ประสานงาน · ถาม senior ได้ว่า “ทีมนี้ยอมให้ logic อยู่ใน controller ได้แค่ไหน” — แต่ละทีมขีดเส้นไม่เท่ากัน
- skinny controller
- คำขวัญของชาว Rails = controller ควรบาง ทำแค่ประสานงาน · กฎของธุรกิจย้ายไปอยู่ model · พิมพ์บอก AI ได้ว่า “keep the controller skinny, move this rule into the model”
- business logic
- กฎที่ยังจริงอยู่แม้ไม่มีเว็บ เช่น “บทความต้องมีชื่อเรื่อง” · ลองถามตัวเองว่า ถ้าทำเป็นแอปมือถือแทนเว็บ กฎนี้ยังต้องมีไหม — ถ้าต้องมี มันคือของ Model
- status code
- เลขสามหลักที่บอกว่า request จบแบบไหน (200 · 302 · 404 · 422 · 500) · เวลารายงานบั๊กให้บอกเลขนี้ด้วยเสมอ เช่น “หน้า /posts ได้ 500” ชัดกว่า “หน้าพัง” มาก
ภารกิจ — เพิ่มหน้า “บทความแรก” ด้วยมือตัวเอง
🎯 ทำหน้า /posts/first (ประมาณ 10 นาที)
ใช้โปรเจกต์เดิมที่ทำหน้า /posts ไว้แล้ว เปิด server ทิ้งไว้ได้เลย ไม่ต้องปิด
- พนักงานต้อนรับ: ใน
config/routes.rbเพิ่มget "/posts/first", to: "posts#first"ต่อจาก route ที่มีอยู่ - หัวหน้าเชฟ: ใน
PostsControllerเพิ่ม actionfirstที่ตั้ง@postเป็น Hash บทความหนึ่งชิ้น (ข้อมูลจำลอง) - จัดจาน: สร้างไฟล์
app/views/posts/first.html.erbโชว์ชื่อบทความใน<h1>และชื่อคนเขียนใน<p> - เปิด
http://localhost:3000/posts/firstควรเห็นชื่อบทความตัวใหญ่ - ดู log ในเทอร์มินัล หาให้เจอ 3 บรรทัด:
Started GET·Processing by·Completed 200 OKแล้วพูดออกมาว่าแต่ละบรรทัดคือใครในร้าน
ถ้าเจอ error ให้ดูตรงนี้ก่อน (ทุกข้อความก็อปจากที่ผมลองทำพังจริง):
- หน้า Routing Error +
No route matches [GET] "/posts/first"(404) → พนักงานต้อนรับไม่รู้จัก · ลืมเพิ่ม route หรือสะกด URL ไม่ตรง · ไปดูconfig/routes.rb - หน้า No view template for interactive request +
PostsController#first is missing a template for request formats: text/html(406) → หาจานไม่เจอ · ไฟล์ view ไม่มี หรืออยู่ผิดโฟลเดอร์ / ชื่อผิด · ต้องเป็นapp/views/posts/first.html.erbเป๊ะ NoMethodError+undefined method '[]' for nil(500) → ถาดว่าง · ส่วนใหญ่คือลืม@ใน controller (เขียนpost = …) · ตัวแปรที่ไม่มี@ส่งไม่ถึง view · view เลยได้nil(ไม่มีค่า — บท 94)
# config/routes.rb
Rails.application.routes.draw do
get "up" => "rails/health#show", as: :rails_health_check
get "/posts", to: "posts#index"
get "/articles", to: "posts#articles"
get "/posts/first", to: "posts#first"
end
# app/controllers/posts_controller.rb — เพิ่ม action นี้ (ตรงไหนก็ได้ระหว่าง class … end)
def first
@post = { title: "ข้าวซอยเจ้าเด็ด", author: "สมชาย" }
end
app/views/posts/first.html.erb
<h1><%= @post[:title] %></h1>
<p>โดย <%= @post[:author] %></p>
log จริง (เวลาของคุณจะต่าง):
Started GET "/posts/first" for ::1 at 2026-11-03 20:46:21 +0700
Processing by PostsController#first as HTML
Rendering layout layouts/application.html.erb
Rendering posts/first.html.erb within layouts/application
Rendered posts/first.html.erb within layouts/application (Duration: 0.1ms | GC: 0.0ms)
Rendered layout layouts/application.html.erb (Duration: 1.2ms | GC: 0.0ms)
Completed 200 OK in 3ms (Views: 2.0ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)
Started GET "/posts/first"= ด่านหน้าครัวจดว่ามีใบสั่งเข้ามาProcessing by PostsController#first= พนักงานต้อนรับส่งถึงหัวหน้าเชฟแผนกบทความ เมนูfirstCompleted 200 OK= ยกจานถึงโต๊ะเรียบร้อย
ถ้าคุณทำข้อ 3 ไม่ทันแล้วเปิดหน้าดูก่อน log จะเป็น
Completed 406 Not Acceptable ตามด้วย ActionController::MissingExactTemplate —
แปลว่าหัวหน้าเชฟรับใบสั่งแล้ว (มี Processing by) แต่ไม่มีจานให้จัด
ทดสอบความเข้าใจ
พิมพ์ localhost:3000/posts แล้ว ใบสั่งเดินผ่านลำดับไหน?
ตัวเลือกที่ 3 ถูก · ประตูร้าน (Puma) รับใบสั่ง → ผ่านด่านหน้าครัว (middleware) → พนักงานต้อนรับ (routes) ชี้แผนก → หัวหน้าเชฟ (controller) เตรียมข้อมูล → จัดจาน (view) → ยกกลับไป
ตัวเลือกที่ 1 ให้หัวหน้าเชฟรับใบสั่งก่อนพนักงานต้อนรับ — ไม่มีใครบอกเชฟว่าใบนี้ของแผนกไหน · ตัวเลือกที่ 2 จัดจานก่อนเชฟทำอาหาร · ตัวเลือกที่ 4 กลับหัวกลับหางทั้งเส้น · จุดที่ต้องจำ: ประตู → ด่าน → ต้อนรับ → เชฟ → จัดจาน
log มีบรรทัด Started GET "/menu" แล้วตามด้วย No route matches [GET] "/menu" ทันที ไม่มี Processing by · ควรไปแก้ที่ไหนก่อน?
ตัวเลือกที่ 2 ถูก · ไม่มี Processing by แปลว่าใบสั่งไปไม่ถึงหัวหน้าเชฟ —
ติดที่พนักงานต้อนรับ ซึ่งก็คือ routes
ตัวเลือกที่ 1 กับ 3 เป็นปัญหาที่เกิดหลัง Processing by (เชฟรับใบสั่งแล้ว) ·
ตัวเลือกที่ 4 ไม่จำเป็น เพราะโหมด development อ่าน routes ใหม่ให้เองเมื่อไฟล์เปลี่ยน ·
จุดที่ต้องจำ: ดูว่า log ไปหยุดที่บรรทัดไหน แล้วจะรู้ว่าต้องแก้ไฟล์ไหน
log จบด้วย Redirected to http://localhost:3000/posts และ Completed 302 Found · แปลว่าอะไร?
ตัวเลือกที่ 4 ถูก · 302 คือ “เมนูนี้ย้ายไปอีกเคาน์เตอร์” · หัวหน้าเชฟไม่ได้จัดจาน (ไม่มีบรรทัด Rendering)
แค่บอกที่อยู่ใหม่ แล้ว browser ยื่นใบสั่งใบที่สองไปที่ /posts เอง
ตัวเลือกที่ 1 สับสนกับ 500 · ตัวเลือกที่ 2 สับสนกับ 404 (Rails ไม่เดา URL ให้) · ตัวเลือกที่ 3 ผิดเพราะ 302 กับ 200 เป็นคนละความหมาย · จุดที่ต้องจำ: redirect = 2 request · ใบแรกได้ 302 ใบที่สองได้หน้าจริง
วันนี้เราวางข้อมูลบทความจำลองไว้ใน PostsController · ตามหลัก MVC ข้อมูลนี้ควรย้ายไปอยู่ที่ไหน?
ตัวเลือกที่ 1 ถูก · ข้อมูลกับกฎของธุรกิจเป็นงานของห้องเก็บวัตถุดิบ (Model) ·
Day 2 controller จะเหลือแค่ @posts = Post.all คือ “ขอของจากห้องเก็บ” แล้วส่งต่อให้ view
ตัวเลือกที่ 2 ผิด — view แค่จัดจาน ไม่ควรเก็บหรือค้นข้อมูลเอง · ตัวเลือกที่ 3 ผิด — routes แค่ชี้ว่า URL ไหนไปแผนกไหน ไม่แตะข้อมูล · ตัวเลือกที่ 4 ผิด — layout คือจานรองที่ใช้ทุกหน้า ไม่ใช่ที่เก็บข้อมูล · จุดที่ต้องจำ: ข้อมูล/กฎ → Model · HTTP/redirect → Controller · หน้าตา → View
ติดตรงไหน ถามกลับได้เลย · ถ้าหน้าเว็บพัง ก็อป log ตั้งแต่ Started ถึง Completed มาทั้งก้อน
พร้อมบอกว่าเปิด URL อะไร · แค่นั้นก็เดาได้แล้วเกือบทุกครั้งว่าติดที่มือใครในร้าน
ถ้าเปิดสไลด์ดูประกอบ
สไลด์หน้า 49–53 สรุปไว้สั้น บางจุดไม่ตรงกับที่เห็นในเครื่องจริง · ทั้งหมดนี้ไม่ทำให้โค้ดในบทนี้พัง อ่านผ่าน ๆ ได้
| หน้า | สไลด์เขียน | ของจริง (Rails 8.1.4) | ต้องสนใจไหม |
|---|---|---|---|
| 49 | config.ru คือจุดเริ่ม แล้วไล่ไป boot.rb → application.rb |
สั่งผ่าน bin/rails server จะโหลด boot.rb กับ application.rb ก่อน แล้ว Puma ค่อยอ่าน config.ru |
ไม่ต้อง — ภาพรวม “ตั้งค่าก่อน แล้วค่อยเปิดประตู” ยังถูก |
| 49 | ไม่มีไฟล์ config/environment.rb ในลำดับ |
มีจริง เป็นไฟล์ที่สั่งให้ environments กับ initializers ทำงาน | ไม่ต้อง |
| 49 | โหลด routes.rb ก่อน Puma เปิดพอร์ต |
จริงใน production · ใน development โหลดตอน request แรกมาถึง | นิดเดียว — ถ้า routes.rb พิมพ์ผิด server ยังเปิดได้ แล้วไปพังตอนเปิดหน้าแรก |
| 50 | CSRF อยู่ในกล่อง Middleware | การตรวจ CSRF เกิดใน controller (หลังบรรทัด Processing by) ไม่ใช่ middleware |
ไม่ต้องวันนี้ — CSRF อยู่ใน บท 103 |
| 51 · 62 · 77 | หน้า 51 ใช้ :unprocessable_content · หน้า 62/77 ใช้ :unprocessable_entity |
ได้ 422 ทั้งคู่ · ชื่อเก่า (_entity) โดน Rack (ชิ้นส่วนพื้นฐานที่ Rails ใช้คุยกับ Puma) เตือนว่ากำลังจะเลิกใช้ |
เขียนใหม่ให้ใช้ :unprocessable_content |
อ้างอิง
- Rails Guides — Getting Started — อ่านหัวข้อ Model-View-Controller Basics (สั้น) แล้วต่อด้วย A Request's Journey Through Rails ที่เล่าเส้นทางเดียวกับบทนี้ (route → controller → view) ด้วยตัวอย่างแอปขายสินค้า
- Rails Guides — Layouts and Rendering — อ่านหัวข้อ Rendering by Default ที่อธิบายว่าทำไม action ว่าง ๆ ถึงหา view ชื่อเดียวกันเจอเอง
- Rails Guides — Rails on Rack —
อ่านแค่หัวข้อ Inspecting the Middleware Stack (คำสั่ง
bin/rails middleware) · ส่วนที่เหลือลึกเกินสำหรับตอนนี้ - Rails Guides — Debugging Rails Applications —
อ่านหัวข้อ The Logger ว่า log เก็บไว้ไฟล์ไหน และ Verbose Redirect Logs
ที่เป็นที่มาของบรรทัด
↳ app/controllers/…ใน log ของ redirect - Rails Guides — The Rails Initialization Process — สำหรับคนอยากรู้ว่าตอน boot อ่านไฟล์ไหนก่อนหลัง · ลึกมาก ไม่ต้องอ่านตอนนี้ กลับมาอ่านหลังจบ Day 1 ได้