บทเรียน 99 · Rails · Day 1

ตั้งชื่อตามกฎ แล้ว Rails หาของเอง — Post กับ posts

~20 นาที · ควรผ่าน บทเรียน 98 (rails new · โฟลเดอร์ในโปรเจกต์) และ บทเรียน 95 (class · สืบทอดด้วย <) มาก่อน

ถ้าคุณเคยเปิดโค้ด Rails แล้วสงสัยว่า “posts คืออะไร” กับ “Post โผล่มาจากไหน ทั้งที่ไม่มีใคร import” — บทนี้ตอบสองข้อนั้นให้จบ
อ่านจบแล้วคุณจะรู้ว่าชื่อเดียวอย่าง “post” แตกเป็นชื่อไฟล์ ชื่อตาราง ชื่อ URL ได้ยังไง · สร้าง Post ตัวจริงด้วยคำสั่งเดียว · อ่าน error ตอนตั้งชื่อผิดออก · และรู้สามคำสั่งที่ใช้สำรวจโปรเจกต์ที่ไม่เคยเห็นมาก่อน

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

🧰 เครื่องที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 17 (สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 — กฎตั้งชื่อในบทนี้เหมือนกันทุกตัว) · ผลรันทุกกล่องก็อปมาจากเครื่องจริง ย่อเฉพาะ path ยาว ๆ เป็น …

posts คืออะไร — คำว่า post ในแอปบล็อก

แอปในสไลด์คือบล็อก — เว็บที่มีบทความให้อ่าน · ของหลักของบล็อกจึงเป็นบทความ ซึ่งภาษาอังกฤษเรียกว่า post
Rails ใช้คำนี้สองแบบ และสองแบบนี้ไม่ใช่ของชิ้นเดียวกัน:

ข่าวดีคือคุณเคยเขียนแบบนี้มาแล้วใน Go · ตอน GORM ใน บทเรียน 27 มี type User struct (หนึ่งคน) กับ var users []User (หลายคน) และ GORM ก็เดาชื่อตาราง users ให้เอง:

// Go — ท่อนหนึ่งในไฟล์ (เทียบให้ดูเฉย ๆ)
type Post struct {   // แม่พิมพ์ของบทความ "หนึ่งชิ้น" → ตัวใหญ่ ไม่มี s
    Title string
    Body  string
}

var posts []Post      // บทความ "หลายชิ้น" → ตัวเล็ก มี s

Rails ใช้หลักเดียวกันเป๊ะ แค่เข้มกว่า: ไม่ใช่แค่ชื่อตัวแปร แต่ชื่อไฟล์ ชื่อตาราง ชื่อ URL ทั้งหมดต้องตามกฎนี้ · หัวข้อถัดไปจะไล่ให้ดูทีละช่อง

🗂 เทียบกับของจริง — แบบฟอร์มกับลิ้นชักแฟ้ม

นึกถึงตู้เก็บแฟ้มหลังร้าน · Post คือแบบฟอร์มเปล่าหนึ่งแผ่น ที่พิมพ์หัวไว้ว่า “หัวข้อ: ____ · เนื้อหา: ____” · ส่วน posts คือลิ้นชักที่ติดป้ายว่า “บทความ” ข้างในมีแบบฟอร์มที่กรอกแล้วหลายสิบแผ่น (หนึ่งแผ่น = บทความหนึ่งชิ้น)
พูดถึง “แบบ” ใช้เอกพจน์ · พูดถึง “กอง” ใช้พหูพจน์ — แค่นั้นเลย · (ในภาพร้านอาหารของคอร์ส Post ยังเป็นคนดูแลสต็อกที่ถือแบบฟอร์มนี้และรู้ว่าลิ้นชักอยู่ไหนด้วย — เห็นชัดในหัวข้อ “สร้าง Post ของจริง”)

ที่ไม่เหมือนกัน: ในออฟฟิศจริง คุณจะติดป้ายลิ้นชักว่าอะไรก็ได้ เช่น “งานเขียน” · แต่ Rails ไม่ให้เลือก — ป้ายลิ้นชักต้องเป็นชื่อแบบฟอร์มแปลงเป็นพหูพจน์เท่านั้น (Post → posts) ถ้าตั้งเป็นอย่างอื่น Rails จะหาไม่เจอ

🗣 ศัพท์ที่เพิ่งเรียนไป
model
class ที่เป็นตัวแทนของข้อมูลหนึ่งชนิดในแอป เช่น Post แทนบทความ · ชื่อ model เป็นเอกพจน์ ตัวใหญ่เสมอ · ถาม senior ได้ว่า “แอปนี้มี model อะไรบ้าง ตัวไหนสำคัญที่สุด” — เป็นคำถามแรกที่ดีเวลาเปิดโปรเจกต์ใหม่
table · row (record)
table คือลิ้นชักใน database ที่เก็บของชนิดเดียวกัน (posts) · row หรือ record คือของหนึ่งชิ้นในนั้น (บทความหนึ่งบทความ) · พิมพ์บอก AI ได้ว่า “the posts table has one row per Post”
singular / plural
เอกพจน์ / พหูพจน์ — คำที่ senior ใช้เวลาพูดถึงกฎตั้งชื่อของ Rails เช่น “model is singular, table is plural”

กฎตั้งชื่อ — ชื่อเดียว แตกไปอยู่ได้ 6 ที่

พอตัดสินใจว่าของหลักชื่อ “post” แล้ว ชื่ออื่นทุกตัวในโปรเจกต์ถูกกำหนดตามมาเอง ไม่ต้องคิดเพิ่ม · ตารางนี้คือหัวใจของบท อ่านคอลัมน์ขวาสุดให้ดี:

ของชื่อชิ้นเดียว หรือ หลายชิ้นเหตุผลภาษาคน
model (class)Postชิ้นเดียวบอกหน้าตาของบทความหนึ่งชิ้น เหมือนแบบฟอร์มหนึ่งแผ่น
ไฟล์ modelapp/models/post.rbชิ้นเดียวไฟล์นี้เก็บ class Post · ชื่อไฟล์ = ชื่อ class แบบตัวเล็ก
ตารางใน databasepostsหลายชิ้นลิ้นชักที่เก็บบทความทุกชิ้น
URL/postsหลายชิ้นหน้ารวมบทความ · ส่วน /posts/42 อ่านว่า “บทความเบอร์ 42 ในกลุ่มบทความ”
controller (class)PostsControllerหลายชิ้นดูแลงานของบทความทั้งกลุ่ม ทั้งโชว์รายการ และเปิดดูทีละชิ้น
ไฟล์ controllerapp/controllers/posts_controller.rbหลายชิ้นชื่อไฟล์ = ชื่อ class แบบตัวเล็กคั่นด้วย _
โฟลเดอร์ viewapp/views/posts/หลายชิ้นเก็บหน้าตาทุกหน้าของแผนกบทความ

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

ทีนี้เทียบกับ Go · ตอนเขียน Gin คุณบอกเองทุกอย่าง — URL ไหนไปฟังก์ชันไหน (r.GET("/posts", listPosts)) ตั้งชื่อฟังก์ชันว่าอะไรก็ได้ · Rails ไม่มีบรรทัดแบบนั้นให้เขียน เพราะมันเดาจากชื่อแทน

🏪 เทียบกับของจริง — ร้านแฟรนไชส์

Go เหมือนเปิดร้านเองตั้งแต่ศูนย์ · คุณจะวางครัวตรงไหน ติดป้ายแผนกว่าอะไรก็ได้ แต่ต้องเขียนผังร้านแปะไว้ให้พนักงานใหม่อ่านเอง
Rails เหมือนร้านแฟรนไชส์ · ทุกสาขาวางของตำแหน่งเดียวกัน ป้ายแผนกตั้งตามกฎกลาง — แผนกบทความต้องชื่อ “Posts” ห้องเก็บของต้องชื่อ “posts” · พนักงานที่ย้ายมาจากสาขาอื่นเดินเข้ามาก็หาของเจอทันทีโดยไม่ต้องดูผัง

ที่ไม่เหมือนกัน: พนักงานจริงเห็นป้าย “Post” แทน “Posts” ก็ยังเดาออกว่าคือแผนกเดียวกัน · แต่ Rails ไม่เดาใกล้เคียง — ขาด s ตัวเดียวก็หาไม่เจอแล้ว (เดี๋ยวจะทำให้พังจริงให้ดูในหัวข้อ “ตั้งชื่อผิดตัวเดียว”)

🗣 ศัพท์ที่เพิ่งเรียนไป
Convention over Configuration
หลักของ Rails: “ตั้งชื่อตามกฎ แทนการเขียนตั้งค่าเอง” · convention = ข้อตกลงที่ทุกคนทำเหมือนกัน · configuration = การเขียนบอกเองว่าอะไรอยู่ไหน · ถาม senior ได้ว่า “โปรเจกต์นี้มีจุดไหนที่ไม่ตั้งชื่อตาม convention บ้าง” เพราะจุดพวกนั้นคือที่ที่คนใหม่หลงบ่อยที่สุด
CamelCase · snake_case
CamelCase = ตัวใหญ่ต้นคำแล้วเขียนติดกัน (PostsController) ใช้กับชื่อ class · snake_case = ตัวเล็กคั่นด้วย _ (posts_controller) ใช้กับชื่อไฟล์และตาราง · บอก AI ได้ว่า “rename this file to snake_case to match the class name”
controller
class ที่รับคำขอจาก browser แล้วตัดสินใจว่าจะตอบอะไร — ในภาพร้านอาหารคือหัวหน้าเชฟของแผนก · ตอนนี้รู้แค่ว่าชื่อต้องพหูพจน์ต่อท้ายด้วย Controller ก็พอ · ละเอียดใน บทเรียน 102

Rails แปลงชื่อเอง — ลองสั่งดูด้วยมือ

Rails ไม่ได้มีสมุดจดไว้ว่า Post คู่กับ posts · มันคำนวณชื่อใหม่ทุกครั้งด้วย method ของ string ที่ Active Support (ของเสริมที่ Rails เติมให้ Ruby · บท 90) เพิ่มเข้ามา
เราเรียก method พวกนั้นเองได้ · ที่ที่สั่งง่ายที่สุดคือ bin/rails console (ช่องพิมพ์ Ruby ที่รู้จักทุกอย่างในแอป — เหมือนครัวทดลองหลังร้าน ลองของได้โดยไม่ต้องเปิดร้าน · ละเอียดใน บทเรียน 104)

ในโฟลเดอร์โปรเจกต์ สั่ง bin/rails console แล้วพิมพ์ทีละบรรทัด (ผลจริง):

$ bin/rails console
Loading development environment (Rails 8.1.4)
wongnok-blog(dev):001> "Post".tableize
=> "posts"
wongnok-blog(dev):002> "post".pluralize
=> "posts"
wongnok-blog(dev):003> "person".pluralize
=> "people"
wongnok-blog(dev):004> "people".singularize
=> "person"
wongnok-blog(dev):005> "posts".classify
=> "Post"

อ่านทีละส่วน:

พิมพ์ exit เพื่อออกจาก console

"Category".tableize   # หมวดหมู่
"Person".tableize     # คน
"Mouse".tableize      # หนู
"Sheep".tableize      # แกะ

สี่บรรทัดนี้จะได้ชื่อตารางอะไรบ้าง? คิดก่อนกดเฉลย (ใบ้: ภาษาอังกฤษมีคำที่พหูพจน์ไม่ได้แค่เติม s)

wongnok-blog(dev):001> "Category".tableize
=> "categories"
wongnok-blog(dev):002> "Person".tableize
=> "people"
wongnok-blog(dev):003> "Mouse".tableize
=> "mice"
wongnok-blog(dev):004> "Sheep".tableize
=> "sheep"

ถูกตามภาษาอังกฤษทุกตัว · Category เปลี่ยน y เป็น ies · Mouse เป็น mice · Sheep พหูพจน์หน้าตาเหมือนเดิม

⚠️ Rails ไม่ได้รู้ภาษาอังกฤษจริง มันมีแค่รายการกฎตัดท้ายคำ · ผลจริง: "human".pluralize ได้ "humen" (ที่ถูกคือ humans) เพราะไปโดนกฎ man → men · ก่อนสร้าง model ด้วยคำที่ไม่คุ้น สั่ง "ชื่อ".tableize ใน console ดูก่อนเสมอ ใช้เวลาสองวินาที

ไม่ต้อง import — Rails หาไฟล์จากชื่อให้

นี่คือคำตอบของ “Post มาเฉย ๆ ได้ไง” ครึ่งแรก
ใน Go ถ้าจะใช้ Post ที่อยู่อีก package คุณต้อง import บอกที่อยู่ก่อนเสมอ ไม่งั้น build ไม่ผ่าน:

// Go — ต้องบอกว่า Post อยู่ package ไหน
import "blog/models"

p := models.Post{Title: "สวัสดี"}

ใน Rails คุณพิมพ์ Post ได้เลยในไฟล์ไหนก็ได้ของแอป · พอ Ruby เจอชื่อที่ยังไม่รู้จัก Rails จะแปลงชื่อเป็นชื่อไฟล์ (Post → post.rb) แล้วไปหาไฟล์นั้นในโฟลเดอร์ app/models, app/controllers ฯลฯ ให้เอง
ถ้ายังไม่มีไฟล์นั้น ก็หาไม่เจอ · โปรเจกต์ที่เพิ่ง rails new ยังไม่มี model เลย · ลองเปิด bin/rails console ใหม่ แล้วพิมพ์ Post เฉย ๆ (ผลจริง):

$ bin/rails console
Loading development environment (Rails 8.1.4)
wongnok-blog(dev):001> Post
(wongnok-blog):1:in '<main>': uninitialized constant Post (NameError)

uninitialized constant Post แปลตรง ๆ ว่า “ยังไม่รู้จักชื่อ Post” · constant คือชื่อที่ขึ้นต้นด้วยตัวใหญ่ ซึ่งชื่อ class ทุกตัวเป็นแบบนั้น · ตอนนี้ไม่มี app/models/post.rb Rails เลยหาให้ไม่ได้

📚 เทียบกับของจริง — บรรณารักษ์ที่อ่านแค่สันหนังสือ

Go เหมือนห้องสมุดที่คุณต้องบอกเลขชั้นเองทุกครั้งก่อนหยิบ (import "blog/models") · Rails เหมือนมีบรรณารักษ์ · คุณแค่ตะโกนชื่อเล่ม “Post!” บรรณารักษ์เดินไปหาเล่มที่สันเขียนว่า post.rb แล้วยื่นให้

ที่ไม่เหมือนกัน: บรรณารักษ์จริงหาจากเนื้อเรื่องหรือชื่อผู้แต่งได้ด้วย · แต่ Rails อ่านแค่สันหนังสือ (ชื่อไฟล์) อย่างเดียว — ถ้าข้างในเล่มเป็นเรื่อง Post แต่สันเขียนชื่ออื่น มันจะหาไม่เจอ (หัวข้อ “ตั้งชื่อผิดตัวเดียว” ข้างล่างพิสูจน์ให้ดู)

🗣 ศัพท์ที่เพิ่งเรียนไป
autoload (โหลดอัตโนมัติ)
การที่ Rails ไปเปิดไฟล์ให้เองตอนโค้ดเรียกชื่อ class ครั้งแรก ทำให้ไม่ต้องเขียน require หรือ import · ถาม senior ได้ว่า “ไฟล์ในโฟลเดอร์ lib/ ของโปรเจกต์นี้ autoload ไหม” เพราะบางโฟลเดอร์ไม่ได้เปิดไว้
Zeitwerk
ชื่อของตัวโหลดอัตโนมัติที่ Rails ใช้ (อ่านว่า “ไซท์-แวร์ก”) · กฎหลักของมันคือชื่อไฟล์ต้องตรงกับชื่อ class · เจอคำนี้ใน error ให้เช็คชื่อไฟล์กับชื่อ class ก่อนอย่างอื่น
constant · NameError
constant = ชื่อที่ขึ้นต้นตัวใหญ่ เช่นชื่อ class · NameError พร้อมคำว่า uninitialized constant X = “หาชื่อ X ไม่เจอ” · พิมพ์บอก AI ได้ว่า “I get uninitialized constant Post — which file should define it?”

สร้าง Post ของจริงด้วยคำสั่งเดียว

👀 หัวข้อนี้กับหัวข้อถัดไป อ่านผลตามไปก่อน ยังไม่ต้องพิมพ์ตาม · ได้ลงมือเองในภารกิจท้ายบท (สร้าง Category แล้วเก็บกวาดให้สะอาด) · เหตุผล: บทเรียน 100 จะเริ่มสร้างหน้า /posts จากโปรเจกต์ที่ยังไม่มี Post

ไม่ต้องสร้างไฟล์เองทีละไฟล์ · Rails มีคำสั่ง bin/rails generate (ย่อ g) ที่สร้างไฟล์ทุกไฟล์ให้ถูกชื่อ ถูกที่ ตามกฎในตารางข้างบน
คำสั่งนี้แปลว่า “สร้าง model ชื่อ Post ที่มีช่อง title เป็นข้อความสั้น และช่อง body เป็นข้อความยาว”:

$ bin/rails generate model Post title:string body:text
      invoke  active_record
      create    db/migrate/20261103141245_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml

เปิดไฟล์ model ที่ได้มา · นี่คือทั้งไฟล์จริง ไม่ได้ตัดอะไรออก:

# app/models/post.rb
class Post < ApplicationRecord
end

แค่สองบรรทัด · บรรทัดแรกแปลว่า “สร้าง class ชื่อ Post ที่สืบทอดจาก ApplicationRecord” (< = สืบทอด จาก บท 95) · บรรทัดสองปิด class
แล้ว title กับ body ไปอยู่ไหน? ไม่อยู่ในไฟล์นี้เลย · มันอยู่ในไฟล์ migration:

# db/migrate/20261103141245_create_posts.rb
class CreatePosts < ActiveRecord::Migration[8.1]
  def change
    create_table :posts do |t|   # สร้างตารางชื่อ posts (พหูพจน์!)
      t.string :title               # ช่อง title เก็บข้อความสั้น
      t.text :body                  # ช่อง body เก็บข้อความยาว

      t.timestamps                    # เติมช่องเวลาสร้าง/เวลาแก้ให้เอง
    end
  end
end

ไม่ต้องจำวิธีเขียน แค่อ่านออกพอ:

ไฟล์นี้คือใบสั่งช่างมาต่อเติมตู้เก็บแฟ้ม — ยังไม่มีอะไรเกิดขึ้นจนกว่าจะสั่งช่างลงมือด้วย bin/rails db:migrate · วิธีเขียนไฟล์นี้ละเอียดเป็นเรื่องของ Day 2 · วันนี้รู้แค่ว่า “ช่องของข้อมูลถูกจดไว้ที่นี่ แล้วไปเกิดเป็นตารางใน database” ก็พอ

$ bin/rails db:migrate
== 20261103141245 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.0045s
== 20261103141245 CreatePosts: migrated (0.0046s) =============================

ตอนนี้ลิ้นชัก posts มีอยู่จริงแล้ว · ลองสร้างบทความหนึ่งชิ้น เขียนไฟล์ Ruby เล็ก ๆ ไว้ที่โฟลเดอร์บนสุดของโปรเจกต์ แล้วรันด้วย bin/rails runner (รันไฟล์ Ruby โดยโหลดแอปทั้งตัวให้ก่อน — เหมือน ruby ไฟล์.rb แต่รู้จัก Post):

# try_post.rb — ลองสร้างบทความหนึ่งชิ้น (ยังไม่บันทึกลง database)
post = Post.new(title: "สวัสดี Rails", body: "บทความแรกของบล็อก")
p post
puts post.title
puts post.body
$ bin/rails runner try_post.rb
#<Post id: nil, title: "สวัสดี Rails", body: "บทความแรกของบล็อก", created_at: nil, updated_at: nil>
สวัสดี Rails
บทความแรกของบล็อก

นี่คือคำตอบครึ่งหลังของ “Post มาเฉย ๆ ได้ไง” · ตอนรัน Rails ไปเปิดตาราง posts ดูว่ามีช่องอะไร แล้วสร้าง method title body ให้ตามนั้น (method ที่ Rails สร้างให้ตอนรัน จาก บท 97) · เทียบสองฝั่งให้เห็นชัด:

Go — เขียนทุกช่องเองใน struct

type Post struct {
    ID    uint
    Title string
    Body  string
}

compiler รู้ตั้งแต่ตอน build ว่า Post มีช่องอะไร · พิมพ์ p.Titel ผิด build ไม่ผ่านทันที (ผลจริง: p.Titel undefined (type Post has no field or method Titel))

Rails — class ว่าง ช่องมาจากตาราง

class Post < ApplicationRecord
end

ช่องจดไว้ที่เดียวคือตารางใน database · Rails ไปอ่านตอนรัน · พิมพ์ชื่อช่องผิดจะรู้ตอนรันถึงบรรทัดนั้น

# try_author.rb
post = Post.new(title: "สวัสดี Rails")
puts post.author

ตาราง posts ไม่มีช่อง author · สั่ง bin/rails runner try_author.rb แล้วจะได้อะไร?

พังตอนรันถึงบรรทัดที่ 3 ของไฟล์ (บรรทัด 1 คือ comment) · ผลจริง ตัด path ยาวด้านหน้าออก:

…: undefined method 'author' for an instance of Post (NoMethodError)
	from …
	from …/try_author.rb:3:in '<main>'

undefined method 'author' = “บทความไม่มีคำสั่งชื่อ author” · Rails สร้าง method ให้เฉพาะช่องที่มีในตารางจริง ไม่ได้มั่วให้ทุกชื่อ · ต่างจาก Go ตรงที่ Go ฟ้องตั้งแต่ build ส่วน Ruby ฟ้องตอนรันถึงบรรทัดนั้น

📦 เทียบกับของจริง — คนดูแลสต็อกที่ไม่จดรายการเอง

model Post คือคนดูแลสต็อกของลิ้นชักบทความ · ใน Go คนดูแลสต็อกถือสมุดที่จดไว้ว่า “บทความมีช่อง Title, Body” (struct) · ใน Rails คนดูแลสต็อกไม่ถือสมุดเลย ทุกครั้งที่เริ่มงาน เขาเปิดลิ้นชักดูป้ายหัวคอลัมน์ แล้วรู้เองว่ามีช่องอะไรบ้าง · ช่างมาต่อเติมลิ้นชักเพิ่มช่องใหม่เมื่อไหร่ คนดูแลสต็อกก็รู้เองโดยไม่ต้องแก้สมุด

ที่ไม่เหมือนกัน: คนจริงเปิดลิ้นชักแล้วเจอว่าลิ้นชักยังไม่ได้สร้าง จะบอกคุณดี ๆ · Rails จะโยน error ยาวทันที (relation "posts" does not exist ถ้าลืม db:migrate — ลองเจอเองในภารกิจท้ายบท)

🗣 ศัพท์ที่เพิ่งเรียนไป
generator
คำสั่ง bin/rails generate … ที่สร้างไฟล์ให้ถูกชื่อถูกที่ตามกฎ · ทางกลับคือ bin/rails destroy … (ลบไฟล์ชุดเดียวกันออก) · ถาม senior ได้ว่า “ทีมนี้ใช้ generator หรือสร้างไฟล์เอง” — บางทีมปิดการสร้างไฟล์เทสอัตโนมัติไว้
migration
ไฟล์ใน db/migrate/ ที่สั่งสร้างหรือแก้ตาราง — ใบสั่งช่างมาต่อเติมตู้ · สั่งลงมือด้วย bin/rails db:migrate · เรื่องเต็มอยู่ Day 2 · บอก AI ได้ว่า “add a migration that adds a published_at column to posts”
attribute (column)
ช่องของข้อมูล เช่น title body · ใน database เรียก column · ในฝั่ง model เรียก attribute · ถาม senior ได้ว่า “attribute ของ Post มาจาก schema ตอนรันใช่ไหม ถ้าอยากรู้ว่ามีช่องอะไรต้องดูไฟล์ไหน”

ตั้งชื่อผิดตัวเดียว แล้วเจออะไร

มาดูสองแบบที่มือใหม่เจอบ่อยที่สุด · แบบแรก ชื่อไฟล์ model ไม่ตรงชื่อ class · แบบที่สอง ชื่อ controller ขาด s

แบบที่ 1 · ชื่อไฟล์ model ไม่ตรง

$ mv app/models/post.rb app/models/article.rb   # mv = เปลี่ยนชื่อไฟล์ · ข้างในยังเป็น class Post เหมือนเดิม
$ bin/rails console
wongnok-blog(dev):001> Post

ตอนนี้มี Post แล้ว (จากหัวข้อที่แล้ว) · ถ้าเปลี่ยนชื่อไฟล์ post.rb เป็น article.rb แต่ข้างในยังเขียน class Post อยู่ · พิมพ์ Post ใน console จะเจออะไร?

wongnok-blog(dev):001> Post
(wongnok-blog):1:in '<main>': uninitialized constant Post (NameError)

หาไม่เจอ ทั้งที่ class Post ยังอยู่ในโปรเจกต์ครบทุกตัวอักษร · เพราะ Rails แปลง Post เป็น post.rb แล้วไปหาไฟล์ชื่อนั้น — ไม่ได้เปิดทุกไฟล์มาค้นว่าข้างในมีอะไร · เปลี่ยนชื่อไฟล์กลับเป็น post.rb ก็ใช้ได้เหมือนเดิม (ลองแล้ว)
ใน Go ไม่มีปัญหานี้ เพราะชื่อไฟล์ .go ตั้งอะไรก็ได้ ขอแค่อยู่ใน package เดียวกัน

แบบที่ 2 · ชื่อ controller ขาด s

สมมติเราอยากมีหน้า /posts โชว์รายการบทความ เลยเติมบรรทัดนี้ใน config/routes.rb (ไฟล์ของพนักงานต้อนรับ ที่ดูว่า URL ไหนไปแผนกไหน · ละเอียดใน บทเรียน 101):

get "posts", to: "posts#index"   # URL /posts → แผนก posts · เมนู index

Rails จะแปลงคำว่า posts เป็นชื่อ class PostsController เอง · แต่เราเผลอสร้างไฟล์ controller เป็นเอกพจน์:

# app/controllers/post_controller.rb   ← ขาด s
class PostController < ApplicationController   # ← ขาด s
  def index
  end
end

และสร้างไฟล์หน้าตาของหน้านี้ไว้หนึ่งบรรทัด ที่ app/views/posts/index.html.erb (ข้างในมีแค่ <h1>บทความทั้งหมด</h1> · .html.erb = ไฟล์ HTML ที่แทรกโค้ด Ruby ได้ เรียกว่า ERB ละเอียดใน บทเรียน 103) · จากนั้นเปิด server (bin/rails server) แล้วขอหน้า /posts จากอีกหน้าต่าง terminal (ผลจริง):

$ curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:3000/posts   # curl = ขอหน้าเว็บจาก terminal · -w พิมพ์แค่เลขสถานะ
HTTP 500

500 คือเลขสถานะที่แปลว่า “ฝั่ง server พัง” (ส่วน 200 = สำเร็จ · เคยเจอใน Go ฝั่ง BACK แล้ว)

ใน browser จะเห็นหน้า error สีแดง หัวข้อใหญ่สองบรรทัดนี้:

ActionDispatch::MissingController in PostsController#index
uninitialized constant PostsController

อ่านแบบมือใหม่: Rails ตามหา PostsController (มี s) แต่ไม่เจอ · ในร้านอาหารคือพนักงานต้อนรับตะโกนเรียกแผนก “Posts” แต่ป้ายหน้าครัวเขียนว่า “Post” — ไม่มีใครขานรับ ลูกค้าเลยได้จาน error (HTTP 500)
วิธีแก้: ทำให้ชื่อตรงกฎทั้งชื่อไฟล์และชื่อ class:

# app/controllers/posts_controller.rb   ← เปลี่ยนชื่อไฟล์
class PostsController < ApplicationController   # ← เติม s
  def index
  end
end
$ curl -s -w '\nHTTP %{http_code}\n' http://localhost:3000/posts   # ตัดมาเฉพาะท้าย ๆ
…
      <!-- BEGIN app/views/posts/index.html.erb
--><h1>บทความทั้งหมด</h1>
<!-- END app/views/posts/index.html.erb -->
…
HTTP 200

บรรทัด <!-- BEGIN … --> กับ <!-- END … --> คือ comment ของ HTML ที่ Rails แปะให้เองในโหมดพัฒนา บอกว่า HTML ท่อนนี้มาจากไฟล์ไหน (browser ไม่โชว์ให้เห็นบนหน้าจอ)

ได้ 200 แล้ว · และสังเกตของแถม: เราไม่ได้บอก controller เลยว่าจะใช้หน้าตาไฟล์ไหน แต่ Rails ไปหยิบ app/views/posts/index.html.erb มาเอง — ชื่อแผนก + ชื่อเมนู = ที่อยู่ของไฟล์หน้าตา · กฎตั้งชื่อชุดเดียวกันอีกแล้ว (เรื่อง view ละเอียดใน บทเรียน 103)

แล้วถ้าเป็น Go? Go ส่งตัวฟังก์ชันให้ router ตรง ๆ ไม่ใช่ส่งข้อความ compiler เลยจับชื่อผิดได้ตั้งแต่ยังไม่รัน · ไฟล์นี้ประกาศ postIndex แต่ผูก route ด้วย postsIndex (ใช้ net/http ตัวมาตรฐานแทน Gin ให้ไฟล์สั้น หลักเดียวกัน · ผลจริง go1.26.5):

package main

import (
	"fmt"
	"net/http"
)

func postIndex(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "บทความทั้งหมด")
}

func main() {
	http.HandleFunc("GET /posts", postsIndex)
	http.ListenAndServe(":3000", nil)
}
$ go build ./...
# blog
./main.go:13:32: undefined: postsIndex

สรุปความต่าง: Go ฟ้องตอน build · Rails ฟ้องตอนมีคนเปิดหน้านั้นจริง เพราะ Rails เก็บแค่ข้อความ "posts#index" แล้วค่อยแปลงเป็นชื่อ class ตอนมี request · แลกกันระหว่างความสะดวก (ไม่ต้องเขียนผูกเอง) กับการที่ต้องตั้งชื่อให้ตรงกฎเอง

⚠️ เห็น uninitialized constant XxxController ให้เช็คสามอย่างให้ตรงกัน: คำใน route (posts#index) · ชื่อไฟล์ (posts_controller.rb) · ชื่อ class (PostsController) · ผิดตัวเดียวก็พัง

ApplicationRecord กับ ApplicationController — แม่แบบกลางที่ทุกตัวสืบทอด

สังเกตไหมว่า Post < ApplicationRecord และ PostsController < ApplicationController · สองตัวนี้ rails new สร้างไว้ให้ตั้งแต่แรก เป็นแม่แบบกลางของแอปเรา ที่ model ทุกตัวและ controller ทุกตัวสืบทอดไป · นี่คือไฟล์จริงทั้งไฟล์:

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
  allow_browser versions: :modern

  # Changes to the importmap will invalidate the etag for HTML responses
  stale_when_importmap_changes
end

ถามใน console ได้ว่าใครเป็นลูกของใคร (superclass = “พ่อของฉันคือใคร” · ผลจริง):

wongnok-blog(dev):001> Post.superclass
=> ApplicationRecord(abstract)
wongnok-blog(dev):002> ApplicationRecord.superclass
=> ActiveRecord::Base
wongnok-blog(dev):003> PostsController.superclass
=> ApplicationController
wongnok-blog(dev):004> ApplicationController.superclass
=> ActionController::Base
wongnok-blog(dev):005> ApplicationRecord.table_name
=> nil
wongnok-blog(dev):006> Post.table_name
=> "posts"
ActiveRecord::Base ← ของ Rails: คุยกับ database ได้ทุกอย่าง ▲ ApplicationRecord (abstract) ← แม่แบบของแอปเรา: ไม่มีตาราง (table_name = nil) ▲ Post ← model จริง: ใช้ตาราง posts ActionController::Base ← ของ Rails: รับ request ตอบ response ▲ ApplicationController ← แม่แบบของแอปเรา: allow_browser มีผลกับทุกตัวข้างล่าง ▲ PostsController ← controller จริงของแผนกบทความ

(abstract) ข้างหลัง ApplicationRecord คือผลของบรรทัด primary_abstract_class · และ table_name เป็น nil = ไม่มีลิ้นชักของตัวเอง ตรงตามที่บอก
ทำไมไม่ให้ Post สืบทอดจาก ActiveRecord::Base ตรง ๆ? เพราะอยากมีที่เดียวไว้ใส่ของที่ทุก model ในแอปเราต้องมี · ใส่ตรงนี้ทีเดียว ทุกตัวได้หมด

Go ไม่มีการสืบทอด · ที่ใกล้ที่สุดคือ r.Use(...) ใน Gin ที่ใส่ middleware (ด่านที่ request ทุกตัวต้องผ่าน · ฝั่ง Rails ละเอียดใน บทเรียน 100) ทีเดียวแล้วมีผลกับทุก route · ApplicationController ทำหน้าที่คล้ายกัน แต่ทำผ่านการสืบทอด class แทน

📘 เทียบกับของจริง — คู่มือกลางของแฟรนไชส์

ActiveRecord::Base / ActionController::Base คือคู่มือจากบริษัทแม่ ที่ทุกร้านในโลกได้เหมือนกัน · ApplicationRecord / ApplicationController คือคู่มือของสาขาเรา ที่เติมกฎเฉพาะร้านลงไป เช่น “ไม่รับลูกค้าที่ใช้ browser รุ่นเก่า” · แต่ละแผนก (PostsController) ถือคู่มือของสาขาอีกที จึงได้ทั้งกฎบริษัทแม่และกฎสาขาโดยอัตโนมัติ

ที่ไม่เหมือนกัน: คู่มือสาขาจริงมักหนาเป็นเล่ม · ของ Rails ตอนเริ่มเกือบว่าง และ ApplicationRecord เป็นแค่ “คู่มือ” — ห้ามเอาไปใช้เป็นแผนกเอง · สั่ง ApplicationRecord.new แล้ว Rails ไม่ยอม (ผลจริง: ApplicationRecord is an abstract class and cannot be instantiated. (NotImplementedError) = “อันนี้เป็นแม่แบบ สร้างของจากมันตรง ๆ ไม่ได้”)

🗣 ศัพท์ที่เพิ่งเรียนไป
superclass · inherit
superclass = class พ่อ · inherit = สืบทอดความสามารถมาจากพ่อ (< ใน Ruby) · ถาม senior ได้ว่า “มีอะไรใส่ไว้ใน ApplicationController บ้าง ที่ทุกหน้าโดนหมด”
base class (แม่แบบกลาง)
class ที่มีไว้ให้คนอื่นสืบทอด เช่น ApplicationRecord · ของที่ทุกตัวต้องมีให้ใส่ที่นี่ · บอก AI ได้ว่า “put this shared method in ApplicationRecord so every model gets it”
abstract class
class ที่ใช้สร้างของตรง ๆ ไม่ได้ มีไว้เป็นพ่ออย่างเดียว · ใน Rails คือผลของ primary_abstract_class · เห็นคำว่า (abstract) ใน console ให้อ่านว่า “อันนี้เป็นแม่แบบ ไม่มีตาราง”

สำรวจโปรเจกต์ — สามคำสั่งที่ใช้จริงตั้งแต่วันแรก

เปิดโปรเจกต์ Rails ที่ไม่เคยเห็น (ของเพื่อน ของทีม หรือของสไลด์) · สามคำสั่งนี้บอกว่าข้างในมีอะไรโดยไม่ต้องเปิดไฟล์ทีละไฟล์

1 · bin/rails about — แอปนี้ใช้อะไรรุ่นไหน

$ bin/rails about     # ตัดบรรทัด Middleware กับ Application root ที่ยาวมากออก
About your application's environment
Rails version             8.1.4
Ruby version              ruby 4.0.9 (2026-10-23 revision 3e93b23afc) +PRISM [arm64-darwin25]
RubyGems version          4.0.20
Rack version              3.2.8
Environment               development
Database adapter          postgresql
Database schema version   20261103141245

ที่ควรดู: Rails version กับ Ruby version (ถาม AI เรื่องโค้ดต้องบอกเลขนี้ด้วย) · Database adapter = ใช้ database ยี่ห้ออะไร · Database schema version = เลขของ migration ล่าสุดที่รันแล้ว (ตรงกับชื่อไฟล์ …_create_posts.rb ข้างบน) · ของคุณจะได้เลขเวอร์ชันและเลข schema คนละชุด

2 · bin/rails routes — URL ไหนไปแผนกไหน

$ bin/rails routes -c posts     # -c posts = ดูเฉพาะของ PostsController
Prefix Verb URI Pattern      Controller#Action
 posts GET  /posts(.:format) posts#index

อ่านทีละคอลัมน์: Prefix = ชื่อเล่นของ route นี้ (เอาไปใช้ตอนทำลิงก์ · บท 101) · Verb = ชนิดคำขอ (GET = ขอดู) · URI Pattern = URL ((.:format) แปลว่าจะต่อท้าย .json ก็ได้ ไม่ต่อก็ได้) · Controller#Action = แผนกกับเมนู — posts#index คือ PostsController เมธอด index · ถ้าไม่ใส่ -c posts จะได้ยาวเกือบ 30 บรรทัด เพราะ Rails มี route ของตัวเองติดมาด้วย

3 · bin/rails --help — มีคำสั่งอะไรให้ใช้บ้าง

$ bin/rails --help     # 120 บรรทัด — ตัดมาแค่ส่วนบน
Usage:
  bin/rails COMMAND [options]

You must specify a command. The most common commands are:

  generate     Generate new code (short-cut alias: "g")
  console      Start the Rails console (short-cut alias: "c")
  server       Start the Rails server (short-cut alias: "s")
  test         Run tests except system tests (short-cut alias: "t")
  test:system  Run system tests
  dbconsole    Start a console for the database specified in config/database.yml
               (short-cut alias: "db")
  plugin new   Create a new Rails railtie or engine

All commands can be run with -h (or --help) for more information.
…

สามตัวบนสุดใช้บ่อยที่สุด และมีชื่อย่อ: bin/rails g = generate · bin/rails c = console · bin/rails s = server · คำสั่งไหนไม่รู้วิธีใช้ ให้ต่อท้ายด้วย --help เช่น bin/rails generate model --help

ภารกิจ — สร้าง model หมวดหมู่ (Category) เอง

🎯 ภารกิจ: บล็อกอยากแบ่งบทความเป็นหมวด ๆ เช่น “อาหารเหนือ”

ทำในโปรเจกต์ wongnok_blog ที่สร้างไว้จาก บทเรียน 98 ใน terminal ที่ตั้ง DATABASE_URL ไว้แล้ว (บท 98) · ใช้เวลาราว 10 นาที

  1. ทายก่อน: model ชื่อ Category จะได้ตารางชื่ออะไร · เช็คด้วย bin/rails console แล้วพิมพ์ "Category".tableize (ออกด้วย exit)
  2. สร้าง model: bin/rails generate model Category name:string · ดูว่าได้ไฟล์อะไรบ้าง แล้วเปิด app/models/category.rb ดู
  3. สร้างไฟล์ try_category.rb ที่โฟลเดอร์บนสุดของโปรเจกต์:
    category = Category.new(name: "อาหารเหนือ")
    puts category.name
    แล้วรัน bin/rails runner try_category.rb ก่อน migrate · จด error ที่เห็น
  4. รัน bin/rails db:migrate แล้วรัน bin/rails runner try_category.rb อีกรอบ
  5. เก็บกวาด (บทความจริงของ Day 2 จะได้ไม่ปน): bin/rails db:rollback → bin/rails destroy model Category → ลบ try_category.rb

ข้อ 1 — ได้ => "categories" (y เปลี่ยนเป็น ies)

ข้อ 2 — ผลจริง (เลขวันเวลาของคุณจะต่าง):

$ bin/rails generate model Category name:string
      invoke  active_record
      create    db/migrate/20261103141437_create_categories.rb
      create    app/models/category.rb
      invoke    test_unit
      create      test/models/category_test.rb
      create      test/fixtures/categories.yml

$ cat app/models/category.rb
class Category < ApplicationRecord
end

ไฟล์ model เอกพจน์ (category.rb) · migration กับ fixture เป็นพหูพจน์ (categories) ตรงกฎทุกตัว · ข้างใน class ว่างเหมือน Post

ข้อ 3 — ก่อน migrate ได้ error นี้ (ตัด path ยาวข้างหน้าออก):

…: PG::UndefinedTable: ERROR:  relation "categories" does not exist (ActiveRecord::StatementInvalid)

อ่านแค่ท่อนกลางพอ (PG::UndefinedTable กับ ActiveRecord::StatementInvalid เป็นแค่ชื่อชนิดของ error) · relation "categories" does not exist = “ลิ้นชัก categories ยังไม่มีในตู้” · มีใบสั่งช่าง (migration) แล้ว แต่ยังไม่ได้สั่งช่างลงมือ

ข้อ 4 — ผลจริง:

$ bin/rails db:migrate
== 20261103141437 CreateCategories: migrating =================================
-- create_table(:categories)
   -> 0.0029s
== 20261103141437 CreateCategories: migrated (0.0029s) ========================

$ bin/rails runner try_category.rb
อาหารเหนือ

ข้อ 5 — ผลจริง:

$ bin/rails db:rollback
== 20261103141437 CreateCategories: reverting =================================
-- drop_table(:categories)
   -> 0.0038s
== 20261103141437 CreateCategories: reverted (0.0063s) ========================

$ bin/rails destroy model Category
      invoke  active_record
      remove    db/migrate/20261103141437_create_categories.rb
      remove    app/models/category.rb
      invoke    test_unit
      remove      test/models/category_test.rb
      remove      test/fixtures/categories.yml

db:rollback = ย้อน migration ล่าสุดหนึ่งตัว (รื้อลิ้นชักที่เพิ่งต่อ) · destroy = ลบไฟล์ชุดเดียวกับที่ generate สร้าง · ต้อง rollback ก่อน destroy ไม่งั้นตารางค้างอยู่ใน database แต่ไฟล์ใบสั่งหายไปแล้ว

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

  • relation "categories" does not exist → ยังไม่ได้ bin/rails db:migrate (ข้อ 3 ตั้งใจให้เจอ)
  • ActiveRecord::DatabaseConnectionError: There is an issue connecting with your hostname: 127.0.0.1. → Rails ต่อ database ไม่ได้ · Postgres ยังไม่เปิด · DATABASE_URL ชี้ผิดพอร์ต · หรือเปิด terminal หน้าต่างใหม่แล้วลืม export DATABASE_URL=… อีกรอบ · ย้อนดูวิธีตั้งใน บทเรียน 98
  • uninitialized constant Category → Rails หาไฟล์ app/models/category.rb ไม่เจอ · generate ไม่สำเร็จ หรือพิมพ์ชื่อผิด (เช่น Catagory)

ลองตอบดู

model ชื่อ Category — Rails จะไปหาตารางใน database ชื่ออะไร?

ตัวเลือกที่ 3 ถูก · ผลจริงของ "Category".tableize คือ "categories"
ตัวเลือกที่ 1 ผิด เพราะตารางเก็บของหลายชิ้นจึงเป็นพหูพจน์เสมอ · ตัวเลือกที่ 2 ผิด เพราะ Rails มีกฎ y → ies ไม่ได้เติม s อย่างเดียว · ตัวเลือกที่ 4 ผิด เพราะชื่อตารางเป็นตัวเล็กทั้งหมด ตัวใหญ่ใช้กับชื่อ class เท่านั้น
จุดที่ต้องจำ: ไม่แน่ใจเมื่อไหร่ สั่ง "ชื่อ".tableize ใน console ดู ไม่ต้องเดา

app/models/post.rb มีแค่ class Post < ApplicationRecord กับ end แต่ post.title ใช้ได้ เพราะอะไร?

ตัวเลือกที่ 2 ถูก · ช่อง title มาจาก migration ที่สร้างตาราง posts แล้ว Rails อ่านตารางตอนรัน
ตัวเลือกที่ 1 ผิด เพราะ Rails ไม่เดาช่อง — post.author ได้ NoMethodError ทันทีเพราะตารางไม่มีช่องนั้น · ตัวเลือกที่ 3 ผิด เพราะไฟล์ที่ generator สร้างมีแค่ 4 ไฟล์ที่เห็นในผลรัน ไม่มีไฟล์ซ่อน · ตัวเลือกที่ 4 ผิด เพราะ ApplicationRecord ไม่มีตารางของตัวเองเลย (table_name เป็น nil) จึงไม่รู้จักช่อง title
จุดที่ต้องจำ: อยากรู้ว่า model มีช่องอะไร อย่าเปิดไฟล์ model — ดูที่ migration หรือตารางใน database

ใน controller เขียน Post.new ได้เลย ไม่ต้อง import อะไร เพราะอะไร?

ตัวเลือกที่ 4 ถูก · นี่คือ autoload · กล่องทายผลเรื่องเปลี่ยนชื่อไฟล์เป็น article.rb พิสูจน์แล้วว่า Rails หาจากชื่อไฟล์
ตัวเลือกที่ 1 ผิด เพราะ Rails หาเฉพาะในโฟลเดอร์ของแอป ไม่ใช่ทั้งเครื่อง · ตัวเลือกที่ 2 ผิด เพราะโค้ดแยกไฟล์จริง ๆ แค่ Rails เป็นคนเปิดไฟล์ให้ · ตัวเลือกที่ 3 ผิด เพราะ post.rb ที่ generator สร้างมีแค่สองบรรทัด ไม่มี import หรือ require เลย
จุดที่ต้องจำ: ชื่อ class กับชื่อไฟล์ต้องตรงกัน (Post ↔ post.rb) autoload ถึงจะทำงาน

route เขียน to: "posts#index" · เปิด /posts แล้วได้ uninitialized constant PostsController · ในโปรเจกต์มี post_controller.rb ที่ประกาศ class PostController · แก้ยังไง?

ตัวเลือกที่ 1 ถูก · ต้องแก้ทั้งชื่อไฟล์และชื่อ class ให้เป็นพหูพจน์ตามที่ route ขอ · ในบทนี้แก้แบบนี้แล้วได้ HTTP 200
ตัวเลือกที่ 2 ผิด เพราะต่อให้โหลดไฟล์เข้ามา ข้างในก็ยังมีแต่ PostController ไม่มี PostsController อยู่ดี · ตัวเลือกที่ 3 ผิด เพราะ db:migrate ยุ่งแค่กับตารางใน database ไม่สร้าง controller · ตัวเลือกที่ 4 ผิด เพราะคำสั่งนั้นสร้าง model (ไม่ใช่ controller) และปัญหาไม่ได้อยู่ที่ model เลย
จุดที่ต้องจำ: route · ชื่อไฟล์ · ชื่อ class ต้องตรงกันทั้งสามที่

✅ ถ้าตอนนี้คุณตอบได้ว่า “posts คือชื่อตาราง/URL ของบทความหลายชิ้น ส่วน Post คือแม่พิมพ์ของบทความหนึ่งชิ้น และ Rails หาไฟล์ post.rb เองจากชื่อ แล้วอ่านช่องจากตาราง” — บทนี้ผ่านแล้ว
อยากรู้ว่าชื่อที่คุณจะใช้ในแอปตัวเองจะถูกแปลงเป็นอะไร เปิด bin/rails console แล้วพิมพ์ "ชื่อ".tableize ดูได้เลย ไม่ต้องเดา

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

สไลด์หน้า 46–48 สอนเรื่องเดียวกับบทนี้ · จุดที่ของจริงไม่ตรงกับสไลด์:

หน้าสไลด์เขียนของจริง (Rails 8.1.4)ต้องสนใจไหม
46ตาราง 6 แถว Post → posts … Sheep → sheep และกล่อง “ลองเองใน console”ตรงกับผลรันจริงทุกช่องไม่ต้อง — เชื่อได้
47ApplicationController ว่างเปล่ามีสองบรรทัด allow_browser … และ stale_when_importmap_changesนิดเดียว — เห็นในไฟล์จริงแล้วไม่ต้องลบ
48กล่อง bin/rails -T มี routes middleware stats notesbin/rails -T ไม่มีสี่ตัวนี้ (มีแต่ task อย่าง db:migrate) · สี่ตัวนี้อยู่ใน bin/rails --help · แต่สั่ง bin/rails routes ตรง ๆ ได้ปกตินิดเดียว — หาใน -T ไม่เจอไม่ต้องตกใจ
48bin/rails about: Rails 8.1.3 · Ruby 4.0.6 · RubyGems 3.6.2 · Rack 3.1.8เครื่องที่ตรวจ: 8.1.4 · 4.0.9 · 4.0.20 · 3.2.8ไม่ต้อง — เลขขึ้นกับเครื่องและโปรเจกต์
48Database schema version 20260215093000แอปที่เพิ่ง rails new ได้ 0 (ยังไม่มี migration) · พอสร้าง Post แล้วได้เลขตามชื่อไฟล์ migrationไม่ต้อง

อ้างอิง