🔁 ทวนก่อนเริ่ม — คำจากบทก่อนที่บทนี้ใช้
- terminal (บท 91) — หน้าต่างที่พิมพ์คำสั่งแล้วกด Enter
- gem (บท 90) — ของสำเร็จรูปที่คนอื่นเขียนไว้ให้ Ruby ใช้ · บทนี้จะเจอละเอียด
- Convention over Configuration (บท 90) — วางของตามที่ตกลงกันไว้ แล้ว Rails หาเจอเอง
- symbol · block · hash ท้าย argument (บท 92 · 93 · 97) —
:test·do … end·gem "pg", require: false - ร้านอาหาร (บท 90) — routes = พนักงานต้อนรับ · controller = หัวหน้าเชฟ · model = ห้องวัตถุดิบ · view = การจัดจาน
🧰 เครื่องที่ใช้ตรวจ: Ruby 4.0.9 · Rails 8.1.4 · PostgreSQL 17 —
สไลด์ใช้ Ruby 4.0.6 · Rails 8.1.3 ต่างกันแค่เลขท้าย · จุดที่บทนี้เห็น 8.1.4 ของคุณอาจเห็น 8.1.3
ทุกผลรันในบทนี้ก็อปมาจากเครื่องจริง · path ยาว ๆ ของเครื่องตรวจย่อเป็น … · กล่องไหนตัดบรรทัดออกจะเขียนกำกับไว้
Go เริ่มจากห้องเปล่า — Rails ได้ร้านมาทั้งร้าน
ตอนเริ่มโปรเจกต์ Go คุณพิมพ์ go mod init แล้วได้ไฟล์มาไฟล์เดียว
ที่เหลือต้องสร้างเองทั้งหมด: main.go · router ของ Gin · handler · โค้ดต่อ database
# ผลรันจริง — โฟลเดอร์เปล่า แล้วสั่ง go mod init
$ go mod init blog
go: creating new go.mod: module blog
$ ls -A
go.mod
rails new ทำกลับกัน — คำสั่งเดียวได้ไฟล์มา 109 ไฟล์
และเปิดเป็นเว็บได้ทันทีโดยยังไม่ต้องเขียนโค้ดสักบรรทัด
ข้อดีคือเริ่มได้เร็ว · ข้อเสียคือวันแรกมีไฟล์ที่ไม่รู้จักเต็มไปหมด — บทนี้จะพาดูเฉพาะห้องที่ต้องรู้
Go คือเช่าห้องเปล่าแล้วเปิดร้านเอง — go mod init ให้แค่กุญแจห้อง
คุณต้องจ้างพนักงานต้อนรับเอง (Gin router) · เขียนใบงานเชฟเอง (handler) · ซื้อตู้เก็บแฟ้มเอง (GORM + database)
Rails คือซื้อแฟรนไชส์ — วันแรกได้ครัวครบชุด เคาน์เตอร์ ตู้แฟ้ม ป้ายร้าน
และทุกสาขาวางของตำแหน่งเดียวกัน ใครย้ายมาจากสาขาอื่นก็หาของเจอ
ที่ไม่เหมือนกัน: แฟรนไชส์จริงห้ามรื้อห้องที่บริษัทแม่ให้มา · แต่ห้องไหนใน Rails ที่ไม่ใช้ คุณลบทิ้งได้เลย (หรือสั่งตั้งแต่แรกว่าไม่เอา ด้วย flag ในหัวข้อถัดไป) และไม่มีใครเก็บค่าธรรมเนียม — Rails ฟรีทั้งหมด
- generator (
rails new) - คำสั่งที่ "สร้างไฟล์ให้" ตามแบบที่กำหนดไว้ ·
rails newคือ generator ตัวใหญ่สุด สร้างทั้งโปรเจกต์ · ถาม senior ได้ว่า "ทีมเราใช้ rails new กับ flag อะไรบ้างตอนเริ่มโปรเจกต์นี้" - boilerplate / project skeleton
- โครงโปรเจกต์สำเร็จรูปที่ยังไม่มีเนื้อหาของเรา · พิมพ์บอก AI ได้ว่า "explain which files in a fresh rails new app I can ignore on day one"
อ่านคำสั่ง rails new ทีละท่อน
คำสั่งที่สไลด์ให้พิมพ์คือบรรทัดนี้ — ยาว แต่แตกได้เป็นห้าท่อน
$ rails new wongnok_blog --database=postgresql --css=tailwind
| ท่อน | แปลว่า | ในร้านอาหาร |
|---|---|---|
rails | โปรแกรม rails ที่ติดตั้งไว้ในเครื่อง (ได้มาตอนติดตั้ง gem rails) | บริษัทแม่แฟรนไชส์ |
new | "สร้างโปรเจกต์ใหม่" | "ขอเปิดสาขาใหม่" |
wongnok_blog | ชื่อโปรเจกต์ = ชื่อโฟลเดอร์ที่จะถูกสร้าง | ชื่อสาขา |
--database=postgresql | ให้ตั้งค่าไว้สำหรับ PostgreSQL · ถ้าไม่ใส่จะได้ SQLite | ขอตู้เก็บแฟ้มยี่ห้อ PostgreSQL |
--css=tailwind | ติดตั้ง Tailwind (เครื่องมือแต่งหน้าเว็บที่เคยเห็นฝั่ง FRONT) ไว้ให้ | ขอชุดตกแต่งร้านแบบ Tailwind |
ของที่ขึ้นต้นด้วย -- เรียกว่า flag = ตัวเลือกเสริมของคำสั่ง
เหมือน -v ใน go test -v ที่เคยใช้ · ไม่ใส่ก็ได้ แต่จะได้ค่าเริ่มต้นแทน
ทำไมเลือก PostgreSQL? เพราะเป็น database ตัวเดียวกับที่ใช้ฝั่ง Go และ Day 2 จะใช้จริง ·
ส่วน SQLite (ค่าเริ่มต้น) คือ database ที่เป็นไฟล์เดียวอยู่ในโปรเจกต์ ไม่ต้องเปิดโปรแกรม database แยก
สไลด์หน้า 41 ยก flag อื่นมาอีกห้าตัว — วันนี้ยังไม่ใช้ ไม่ต้องจำ รู้แค่ว่ามี:
| flag | ภาษาคน |
|---|---|
--api | ไม่เอาหน้าเว็บ ส่งแต่ข้อมูล JSON — สำหรับแอปที่มี frontend แยก (เช่น Next.js) · สไลด์บอกว่าตัวนี้เปลี่ยนใจทีหลังยากสุด |
--skip-solid | ไม่เอาชุด Solid — ของเสริมที่ Rails 8 ใส่มาให้ ใช้เก็บงานค้างและข้อมูลชั่วคราวไว้ใน database (Day 3 จะเจอ) |
--javascript=esbuild | ให้ไฟล์ JavaScript ต้องผ่านขั้น build ก่อน (คล้ายตอนทำ Next.js ฝั่ง FRONT) · ค่าเริ่มต้นของ Rails ไม่ต้อง build |
--devcontainer | สร้างไฟล์ตั้งค่าให้ VS Code เปิดโปรเจกต์ข้างใน Docker |
--skip-test | ไม่สร้างโฟลเดอร์ test/ |
กดสร้างจริง — แล้วดูว่ามันทำอะไรไปบ้าง
บนเครื่องตรวจใช้เวลาราว 13 วินาที (gem ทุกตัวลงไว้แล้ว) · เครื่องคุณครั้งแรกอาจนานเป็นนาที เพราะต้องโหลด gem จากเน็ต · ผลจริงยาว 202 บรรทัด นี่คือช่วงที่ควรดู:
# ผลรันจริง (ย่อ — ตัดบรรทัดกลางออก)
$ rails new wongnok_blog --database=postgresql --css=tailwind
create
create README.md
create Rakefile
create .ruby-version
create config.ru
create .gitignore
create .gitattributes
create Gemfile
run git init -b main from "."
Initialized empty Git repository in …/wongnok_blog/.git/
create app
create app/assets/stylesheets/application.css
create app/controllers/application_controller.rb
…(ตัดออก)…
run bundle install --quiet
…(ตัดออก)…
Ensure foreman is installed
run gem install foreman from "."
Successfully installed foreman-0.90.0
…(ตัดออก)…
อ่านทีละแบบ:
create= สร้างไฟล์หรือโฟลเดอร์นั้น · บรรทัดแบบนี้คือส่วนใหญ่ของ 202 บรรทัดrun git init -b main= สร้างที่เก็บประวัติ git ให้ (branch ชื่อmain)run bundle install= ไปติดตั้ง gem ทุกตัวที่โปรเจกต์ต้องใช้ — อธิบายในหัวข้อ Gemfile ด้านล่างgem install foreman= Tailwind ขอเครื่องมือเพิ่มอีกตัว (ใช้กับคำสั่งbin/dev) · Day 1 ไม่ต้องสนใจ
$ cd wongnok_blog
$ git log
เห็นบรรทัด git init ในผลข้างบนแล้ว และสไลด์หน้า 40 บอกว่าได้ "git repo ที่ commit แรกให้แล้ว" — git log จะแสดงอะไร?
# ผลรันจริง
$ git log
fatal: your current branch 'main' does not have any commits yet
ยังไม่มี commit เลย · rails new แค่สร้างที่เก็บ git เปล่า ๆ ให้ ไม่ได้ commit ให้ ·
fatal ตรงนี้ไม่ใช่เรื่องร้าย มันแค่บอกว่ายังว่าง
ทำ commit แรกเองทันที จะได้มี "จุดเริ่มที่สะอาด" ไว้เทียบทีหลังว่าคุณแก้อะไรไปบ้าง:
# ผลรันจริง (เลข commit ของคุณจะต่าง)
$ git add -A
$ git commit -m "rails new"
…(ผลของ git commit ยาวร้อยกว่าบรรทัด ตัดออก)…
$ git log --oneline
2b53597 rails new
$ git show --stat --format= | tail -1
109 files changed, 3219 insertions(+)
git show --stat --format= | tail -1 = "สรุป commit ล่าสุดให้ดู แต่เอาแค่บรรทัดท้าย" — บรรทัดที่บอกว่า commit นี้มีกี่ไฟล์
เดินดูร้าน — ทีละห้อง
เข้าไปในโฟลเดอร์แล้วดูว่ามีอะไรบ้าง · ls -1F = แสดงบรรทัดละชื่อ และเติม / ท้ายชื่อที่เป็นโฟลเดอร์
# ผลรันจริง
$ ls -1F
Dockerfile
Gemfile
Gemfile.lock
Procfile.dev
README.md
Rakefile
app/
bin/
config/
config.ru
db/
lib/
log/
public/
script/
storage/
test/
tmp/
vendor/
19 ชื่อ ดูเยอะ แต่ Day 1 ใช้จริงแค่ไม่กี่ห้อง · ตารางนี้คือแผนผังร้าน:
| โฟลเดอร์ / ไฟล์ | มีไว้ทำอะไร | ในร้านอาหาร | Day 1 แตะบ่อยไหม |
|---|---|---|---|
app/ | โค้ดของเราเกือบทั้งหมด | ครัว | ✅ บ่อยที่สุด |
config/ | การตั้งค่า · มี routes.rb ที่บอกว่า URL ไหนไปหาใคร | ห้องผู้จัดการ · routes.rb ข้างในคือรายการในมือพนักงานต้อนรับ (ใบสั่งแบบไหนส่งแผนกไหน) | ✅ เฉพาะ routes.rb |
bin/ | สคริปต์คำสั่ง เช่น bin/rails | แผงสวิตช์ของร้าน | ✅ ใช้ทุกวัน แต่ไม่ต้องแก้ |
Gemfile · Gemfile.lock | รายการ gem ที่ใช้ · รุ่นจริงที่ได้มา | รายการซื้อของ · ใบเสร็จ | 👀 เปิดอ่านบ้าง |
log/ | บันทึกทุก request ที่เข้ามา | สมุดจดใบสั่งของร้าน | 👀 เปิดดูตอนพัง |
db/ | เรื่องของ database | ตู้เก็บแฟ้ม | ❌ รอ Day 2 |
test/ | โค้ดทดสอบ | ห้องชิมอาหารก่อนเสิร์ฟ | ❌ รอ Day 4 |
public/ | ไฟล์ที่ส่งให้ลูกค้าตรง ๆ ไม่ผ่านครัว (เช่นหน้า 404) | ใบปลิวหน้าร้านที่ลูกค้าหยิบเองได้ | ❌ |
ที่เหลือ (lib/ tmp/ storage/ vendor/ script/ Dockerfile config.ru Rakefile …) | ของสำหรับเครื่องมือ · deploy · ไฟล์ชั่วคราว | ห้องเก็บของหลังร้าน | ❌ ไม่ต้องสนใจ |
ห้องครัว — app/
# ผลรันจริง
$ ls -1F app
assets/
controllers/
helpers/
javascript/
jobs/
mailers/
models/
views/
มี 8 ห้องย่อย แต่ Day 1 ใช้แค่สองห้อง และ Day 2 จะเพิ่มห้องที่สาม — ตรงกับสามตัวละครในร้านอาหาร:
controllers/= ห้องของหัวหน้าเชฟแต่ละแผนก (บท 102)views/= โต๊ะจัดจาน — ไฟล์ HTML ที่จะส่งให้ลูกค้า (บท 103)models/= ห้องวัตถุดิบ — คนดูแลข้อมูลในตู้แฟ้ม (Day 2)- ที่เหลือ:
helpers/เครื่องมือช่วยจัดจาน (บท 103) ·jobs/งานเบื้องหลัง ·mailers/ส่งอีเมล ·javascript/·assets/ไฟล์ JavaScript CSS รูปภาพ — ยังไม่ใช้วันนี้
แล้วในห้องเชฟกับห้องวัตถุดิบตอนนี้มีอะไร?
# ผลรันจริง
$ ls -1AF app/controllers app/models
app/controllers:
application_controller.rb
concerns/
app/models:
application_record.rb
concerns/
(-A = โชว์ไฟล์ที่ซ่อนอยู่ด้วย จะได้แน่ใจว่าไม่มีอะไรแอบอยู่ · concerns/ = ลิ้นชักเปล่าไว้เก็บของที่หลายแผนกใช้ร่วมกัน คล้าย module ในบท 95 · ยังไม่ใช้)
ยังไม่มี Post (แม่พิมพ์ของบทความ) และยังไม่มี PostsController (หัวหน้าเชฟแผนกบทความ) — ร้านเพิ่งเปิดใหม่ ยังไม่มีแผนกบทความ
ไฟล์ที่ขึ้นต้นด้วย application_ คือ "แม่แบบกลาง" ที่ทุกแผนกจะสืบทอดต่อ (สืบทอด = class ลูกได้ของทั้งหมดของ class แม่ บท 95) ·
บท 99 จะสาธิตการสร้าง Post ด้วยคำสั่ง bin/rails generate ให้ดู แล้วคุณจะเห็นว่า Post โผล่มาจากไหน ·
ส่วนแผนกบทความ (PostsController) คุณจะเขียนเองในบท 100
ห้องผู้จัดการ — config/
config/ มีของ 20 อย่าง (ไฟล์ + โฟลเดอร์ย่อย) แต่ Day 1 เปิดแค่ config/routes.rb — รายการในมือพนักงานต้อนรับ ที่บอกว่า URL ไหนไปหาแผนกไหน ·
ดูของจริงที่ได้มา:
# ผลรันจริง — config/routes.rb (ไฟล์เต็ม)
Rails.application.routes.draw do
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
# Can be used by load balancers and uptime monitors to verify that the app is live.
get "up" => "rails/health#show", as: :rails_health_check
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
# Defines the root path route ("/")
# root "posts#index"
end
Rails.application.routes.draw do … end= "วาดผัง URL ของแอปนี้" · ของข้างในdo … endคือ block (บท 93)- บรรทัดที่ขึ้นต้นด้วย
#คือ comment — Ruby ไม่อ่าน (comment ภาษาอังกฤษพวกนี้ Rails เขียนมาให้ อ่านข้ามได้) · ไฟล์นี้จึงมี route (หนึ่งบรรทัดในผัง URL) ที่ใช้งานจริงอยู่บรรทัดเดียว get "up" => …= ถ้ามีคนขอ URL/upให้ส่งไปแผนกตรวจสุขภาพของ Rails เอง (ใช้เช็คว่าร้านยังเปิดอยู่ไหม) ·=>อ่านว่า "ส่งไปที่" ·"rails/health#show"อ่านว่า "แผนกrails/healthเมนูshow" (ซ้ายของ#คือแผนก = controller · ขวาคือเมนู = action) · ส่วนas:คือชื่อเล่นของ route (บท 101)# root "posts#index"(แผนก posts เมนู index) ยังเป็น comment = ยังไม่มีหน้าแรกของเราเอง · เดี๋ยวตอนเปิด server จะเห็นผลของบรรทัดนี้
ไวยากรณ์ของ routes ทั้งหมดรอไว้บท 101 · ตอนนี้แค่รู้ว่าอยากรู้ว่าแอปมี URL อะไรบ้าง ให้เปิดไฟล์นี้ไฟล์เดียว
ตู้เก็บแฟ้ม — db/
# ผลรันจริง
$ ls -1F db
cable_schema.rb
cache_schema.rb
queue_schema.rb
seeds.rb
สามไฟล์แรกเป็นของชุด Solid (Day 3) · seeds.rb คือที่ใส่ข้อมูลตัวอย่าง (Day 2) ·
สไลด์หน้า 42 วาด db/migrate/ กับ db/schema.rb ไว้ด้วย แต่แอปใหม่ยังไม่มีสองตัวนี้
— มันจะเกิดตอนคุณสร้างตาราง posts ครั้งแรกใน Day 2 · เปิดหาไม่เจอไม่ใช่คุณทำผิด
เทียบกับโปรเจกต์ Go ที่เคยทำ
วิธีจำผังที่เร็วที่สุดคือถามว่า "ของที่เคยเขียนเองใน Go ย้ายไปอยู่ไหน":
| หน้าที่ | Go (ที่คุณเขียนเอง) | Rails (ได้มาแล้ว) |
|---|---|---|
| จุดเริ่มโปรแกรม ต่อสายทุกอย่าง | main.go | ไม่มีให้เขียน — Rails ต่อสายเองจากชื่อไฟล์ (บท 99–100) |
| รายการ URL | r.GET(…) กระจายใน main.go | config/routes.rb ไฟล์เดียว |
| handler | ฟังก์ชัน func(c *gin.Context) | app/controllers/ |
| หน้าตาข้อมูล + คุยกับ DB | struct + GORM | app/models/ |
| หน้าตาที่ส่งให้ลูกค้า | ฝั่ง Go ที่เรียนส่ง JSON ด้วย c.JSON(…) (ยังไม่เคยทำหน้า HTML) | app/views/ (ไฟล์ HTML) |
| รายการ dependency | go.mod | Gemfile |
| รุ่นที่ได้มาจริง + รหัสเช็คว่าไฟล์ไม่ถูกแก้ | go.sum | Gemfile.lock |
พนักงานใหม่ไม่ต้องรู้ทุกห้อง วันแรกรู้แค่ ครัวอยู่ไหน (app/) ·
รายการของพนักงานต้อนรับ (ผังส่งใบสั่ง) เก็บอยู่ห้องผู้จัดการ (config/routes.rb) ·
สวิตช์เปิดร้านอยู่ตรงไหน (bin/rails) ·
และสมุดจดใบสั่งวางไว้ไหน (log/) — ห้องเก็บของหลังร้านค่อยเรียนทีหลัง
และเพราะทุกสาขาผังเหมือนกัน ไปเปิดโปรเจกต์ Rails ของบริษัทไหนก็เจอ controller ที่ app/controllers/ เสมอ
ที่ไม่เหมือนกัน: ในร้านจริง ถ้าเอาหม้อไปวางผิดห้อง คนยังเดินหาจนเจอได้ · แต่ใน Rails ถ้าวางไฟล์ผิดโฟลเดอร์หรือตั้งชื่อไม่ตรงกฎ Rails หาไม่เจอเลย และ error มักไม่บอกตรง ๆ ว่าวางผิด — กฎตั้งชื่อนี้คือเรื่องทั้งบทของบท 99
- directory structure
- ผังโฟลเดอร์มาตรฐานของ Rails ที่ทุกโปรเจกต์เหมือนกัน · ถาม senior ได้ว่า "โปรเจกต์นี้มีโฟลเดอร์ไหนที่ไม่ใช่ของมาตรฐาน Rails บ้าง" — ที่ไม่ใช่มาตรฐานคือที่ที่คนใหม่หลงทาง
config/routes.rb- ไฟล์รวม URL ทั้งแอป · พิมพ์บอก AI ได้ว่า "read config/routes.rb and list every URL this app serves" จะได้ภาพรวมแอปเร็วกว่าไล่อ่าน controller ทีละไฟล์
app/(models · views · controllers)- สามห้องหลักของ MVC (Model · View · Controller — ภาพร้านอาหารในบท 90) · ถาม AI ได้ว่า "in a Rails app, which folder should this code go in: models, controllers or views?"
Gemfile — รายการซื้อของของร้าน
gem คือของสำเร็จรูปที่คนอื่นเขียนไว้ เช่นตัวคุยกับ PostgreSQL หรือ web server ·
Gemfile คือรายการว่าแอปนี้ต้องใช้ gem อะไร รุ่นไหน ·
Bundler คือโปรแกรมที่อ่านรายการแล้วไปติดตั้งให้ ด้วยคำสั่ง bundle install (rails new สั่งให้แล้วรอบหนึ่ง)
gem ตรงกับอะไรใน Go — module ไม่ใช่ package
ใน Go package คือโฟลเดอร์โค้ดหนึ่งโฟลเดอร์ที่ import ได้ (เช่น net/http) ·
module คือชุดของ package ที่ปล่อยออกมาพร้อมกัน มีชื่อและเลขรุ่นเดียวกัน = หนึ่งบรรทัดใน go.mod
gem มีชื่อ มีเลขรุ่น และติดตั้งทั้งก้อน → ตรงกับ module · ข้างใน gem หนึ่งตัวมีไฟล์ย่อยหลายไฟล์ เหมือน module หนึ่งตัวมีหลาย package
นี่คือ Gemfile ที่ได้มาจริง ตัดเหลือบรรทัดที่ควรอ่านเป็น (ไฟล์เต็มยาว 68 บรรทัด ส่วนใหญ่เป็น comment):
# ผลรันจริง — Gemfile (ย่อ)
source "https://rubygems.org"
gem "rails", "~> 8.1.4"
gem "pg", "~> 1.1"
gem "puma", ">= 5.0"
gem "tailwindcss-rails"
…(ตัดออก)…
group :development, :test do
…(ตัดออก)…
gem "brakeman", require: false
…(ตัดออก)…
end
Gemfile เป็นไฟล์ Ruby จริง ๆ · ทุกบรรทัดคือการเรียก method แบบละวงเล็บ (บท 97) — อ่านทีละบรรทัด:
source "https://rubygems.org"= ไปซื้อของที่ร้านไหน · rubygems.org คือคลังกลางของ gem (เหมือนแหล่งที่go getไปโหลด module มาให้)gem "rails", "~> 8.1.4"= เรียก methodgemส่งของสองอย่าง: ชื่อ gem กับเงื่อนไขรุ่น · เท่ากับgem("rails", "~> 8.1.4")·~>อ่านว่าอะไร อยู่หัวข้อถัดไปgem "pg", …= ตัวคุยกับ PostgreSQL — หน้าที่เดียวกับ driver postgres ที่ GORM ใช้ฝั่ง Gogem "puma", ">= 5.0"= web server (ประตูร้านที่รับใบสั่งเข้ามาแล้วส่งต่อเข้าร้าน · ละเอียดในบท 100) ·>== รุ่น 5.0 ขึ้นไปอะไรก็ได้gem "tailwindcss-rails"ไม่มีเงื่อนไขรุ่น = รุ่นไหนก็ได้ ให้ Bundler เลือกgroup :development, :test do … end= gem ข้างใน block นี้ใช้เฉพาะตอนพัฒนาและตอนเทส ไม่โหลดตอนเปิดร้านจริง (production) ·:developmentคือ symbol (บท 92)require: false= hash ท้าย argument (บท 97) แปลว่า "ติดตั้งไว้ แต่ไม่ต้องโหลดเข้าแอปเอง" · ใช้กับเครื่องมือที่เรียกจาก terminal เช่นbrakeman(ตัวสแกนช่องโหว่)
Gemfile.lock — ใบเสร็จที่บอกรุ่นจริง
Gemfile บอก "อยากได้อะไร" · Gemfile.lock บอก "ได้อะไรมาจริง" — ชื่อ เลขรุ่นจริง และลายนิ้วมือของทุก gem ·
ไฟล์นี้ Bundler เขียนให้เอง ไม่ต้องแก้ด้วยมือ · ลองสั่ง bundle install อีกรอบดูสรุป:
# ผลรันจริง (ในโฟลเดอร์ wongnok_blog)
$ bundle install
Bundle complete! 24 Gemfile dependencies, 124 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
1 installed gem you directly depend on is looking for funding.
Run `bundle fund` for details
(สองบรรทัดท้ายเรื่อง funding = ชวนบริจาคให้คนทำ gem · ข้ามได้)
เขียนในรายการ 24 ตัว ได้มาจริง 124 ตัว — เพราะ gem หนึ่งตัวลาก gem อื่นที่มันต้องใช้มาด้วย ·
ตัวอย่างชัดที่สุดคือ rails เอง ใน Gemfile.lock เขียนไว้แบบนี้:
# ผลรันจริง — ท่อนหนึ่งของ Gemfile.lock
rails (8.1.4)
actioncable (= 8.1.4)
actionmailbox (= 8.1.4)
actionmailer (= 8.1.4)
actionpack (= 8.1.4)
actiontext (= 8.1.4)
actionview (= 8.1.4)
activejob (= 8.1.4)
activemodel (= 8.1.4)
activerecord (= 8.1.4)
activestorage (= 8.1.4)
activesupport (= 8.1.4)
bundler (>= 1.15.0)
railties (= 8.1.4)
…(ตัดออก)…
CHECKSUMS
…(ตัดออก)…
rails (8.1.4) sha256=5f3ecdf9a2a3b79b2e31b8fee3b64a00f7284d76e5631f87d0e58e665911a33b
- บรรทัด
rails (8.1.4)= ได้ rails รุ่น 8.1.4 มาจริง (ไม่ใช่ "8.1 อะไรก็ได้" แล้ว) - บรรทัดที่ย่อหน้าเข้าไปใต้มัน = ของที่ rails ลากมาด้วย · นี่คือ "กล่อง" ที่บท 90 เล่า — เขียนบรรทัดเดียว ได้มาทั้งชุด
- ท่อน
CHECKSUMS= ลายนิ้วมือของแต่ละ gem (รหัสยาว ๆ ที่คำนวณจากเนื้อไฟล์ · ไฟล์ถูกแก้แม้นิดเดียว รหัสก็เปลี่ยน) · Bundler เช็คทุกครั้งตอนติดตั้ง ไม่ตรงไม่ยอมลง — หน้าที่เดียวกับgo.sum
Gemfile.lock ต้อง commit เข้า git เสมอ (คู่มือ Bundler บอกตรง ๆ ว่าสำหรับแอปให้ commit ทุกครั้ง) · ไม่งั้นเพื่อนที่ clone ไปจะได้ gem คนละรุ่นกับคุณ แล้ว "เครื่องฉันรันได้ เครื่องเธอพัง"
Gemfile คือรายการซื้อของที่คุณเขียนเอง: "น้ำปลา 1 ขวด ยี่ห้อเดิม ล็อตไหนก็ได้" · "ข้าวสาร 5 กิโล ยี่ห้ออะไรก็ได้"
Bundler คือคนที่ถือรายการไปซื้อของ ·
Gemfile.lock คือใบเสร็จที่ได้กลับมา — ระบุยี่ห้อ รุ่น และบาร์โค้ดของทุกชิ้นจริง ๆ
รวมของแถมที่ติดมาด้วยโดยคุณไม่ได้สั่ง (24 อย่างในรายการ ได้ของกลับมา 124 ชิ้น)
อยากให้สาขาอื่นได้ของชุดเดียวกันเป๊ะ ต้องส่งใบเสร็จให้ ไม่ใช่ส่งแค่รายการ
ที่ไม่เหมือนกัน: ใบเสร็จจริงเป็นแค่หลักฐานหลังจ่ายเงิน ไม่มีใครเอาไปยื่นให้ร้านจัดของตาม ·
แต่ bundle install อ่าน Gemfile.lock ก่อนเสมอ แล้วติดตั้งตามรุ่นในนั้นเป๊ะ ๆ —
มันจึงเป็นทั้งใบเสร็จและใบสั่งซื้อซ้ำในไฟล์เดียว
- gem · Gemfile
- gem = แพ็กเกจของ Ruby หนึ่งตัว (เทียบ module ของ Go) · Gemfile = รายการ gem ที่แอปต้องการ · ถาม senior ได้ว่า "gem ตัวนี้ทำอะไร ทำไมโปรเจกต์ถึงต้องใช้" ทุกครั้งที่เห็นชื่อแปลกใน Gemfile
- Bundler ·
bundle install - ตัวจัดการ gem ของโปรเจกต์ (เทียบ
go mod downloadของ Go) · พิมพ์บอก AI ได้ว่า "I just cloned a Rails app, what do I run before bin/rails server?" คำตอบแรกเกือบทุกครั้งคือbundle install Gemfile.lock(lockfile)- รุ่นจริงที่เลือกแล้ว + checksum ของทุก gem · commit เสมอ ห้ามแก้ด้วยมือ · ถาม AI ได้ว่า "diff Gemfile.lock between these two commits and list the version changes" เวลาอยากรู้ว่าใครแอบอัปเกรดอะไร
- group
- ป้ายบอกว่า gem ใช้ใน environment ไหน (development · test · production) · ถาม senior ได้ว่า "gem นี้ควรอยู่ group ไหน ใช้ตอน production ด้วยหรือเปล่า"
ก่อนเพิ่ม gem ใหม่ (เก็บไว้ใช้ทีหลัง) — สไลด์หน้า 45 ให้ถามตัวเองสี่ข้อ: ยังมีคนดูแลอยู่ไหม (commit ล่าสุดเกินปีให้สงสัยไว้ก่อน) · ใช้กับ Rails รุ่นเราได้ไหม · ลาก gem อื่นมาด้วยกี่ตัว · และRails มีให้อยู่แล้วหรือเปล่า — ข้อสุดท้ายสำคัญที่สุด
~> อ่านว่าอะไร
~> แปลว่า "ขยับรุ่นได้ แต่ไม่ไกล" · วิธีอ่าน: ตัวเลขตัวท้ายสุดที่เขียนไว้ ขยับขึ้นได้ ส่วนตัวเลขข้างหน้ามันล็อก
~> 1.2→ ตัวท้ายคือ2ขยับขึ้นได้ ·1ล็อก → ตั้งแต่ 1.2 แต่ต่ำกว่า 2.0~> 8.1.4→ ตัวท้ายคือ4ขยับขึ้นได้ ·8.1ล็อก → ตั้งแต่ 8.1.4 แต่ต่ำกว่า 8.2
ทำไมต้องล็อก? เพราะเลขหลักหน้าที่เปลี่ยนมักแปลว่า "มีของเปลี่ยนจนโค้ดเก่าอาจพัง" — ~> ยอมรับแค่การแก้บั๊กเล็ก ๆ
~> 1.2 เหมือนเขียนในรายการซื้อของว่า "น้ำปลาสูตร 1 ล็อต 2 ขึ้นไป ล็อตใหม่แค่ไหนก็ได้ แต่ห้ามเอาสูตร 2 มา" ·
คนซื้อ (Bundler) จะหยิบล็อตใหม่สุดที่ยังเป็นสูตร 1 แล้วจดลงใบเสร็จ (Gemfile.lock)
ที่ไม่เหมือนกัน: คนซื้อของจริงดูฉลากแล้วใช้วิจารณญาณได้ · Bundler ไม่รู้ว่าข้างใน gem เปลี่ยนไปแค่ไหน มันแค่เทียบตัวเลขทีละช่องตามกฎเป๊ะ ๆ — ถ้าคนทำ gem ตั้งเลขรุ่นไม่ตรงกับที่เปลี่ยนจริง ของที่ทำโค้ดพังก็หลุดเข้ามาได้
ไม่ต้องเชื่อคำอธิบาย ให้ RubyGems (ตัวที่ Bundler ใช้ตัดสินจริง) ตอบเอง:
# tilde.rb — ถาม RubyGems ว่า "~> 1.2" ยอมรับรุ่นไหนบ้าง
want = Gem::Requirement.new("~> 1.2")
["1.1.0", "1.2.0", "1.14.0", "1.99.9", "2.0.0"].each do |v|
puts "#{v} => #{want.satisfied_by?(Gem::Version.new(v))}"
end
อ่านโค้ดก่อน: Gem::Requirement.new("~> 1.2") = สร้างเงื่อนไขรุ่นแบบเดียวกับที่เขียนใน Gemfile
(Gem::Requirement = class ชื่อ Requirement ที่อยู่ข้างใน module Gem — :: อ่านว่า "ข้างใน" · Ruby โหลด Gem ไว้ให้เองไม่ต้อง install อะไร) ·
[ … ] = array ของเลขรุ่น (บท 92) ·
.each do |v| … end = วนทีละรุ่น (บท 93) ·
satisfied_by? = "รุ่นนี้ผ่านเงื่อนไขไหม" ตอบ true/false (ชื่อลงท้าย ? บท 93)
ห้ารุ่นนี้ รุ่นไหนได้ true บ้าง? ระวัง 1.14.0 — เลข 14 กับเลข 2 อะไรใหญ่กว่า?
# ผลรันจริง
$ ruby tilde.rb
1.1.0 => false
1.2.0 => true
1.14.0 => true
1.99.9 => true
2.0.0 => false
1.1.0 ต่ำกว่าขั้นต่ำ · 2.0.0 ข้ามเลขหน้าที่ล็อกไว้ ·
1.14.0 ผ่าน เพราะเลขรุ่นเทียบเป็นตัวเลขทีละช่อง (14 มากกว่า 2) ไม่ได้เทียบแบบตัวอักษร
ลองแบบสามหลักบ้าง — เงื่อนไขของ rails ใน Gemfile:
# ผลรันจริง — tilde3.rb ใช้ Gem::Requirement.new("~> 8.1.4") กับอีกสี่รุ่น
$ ruby tilde3.rb
8.1.3 => false
8.1.4 => true
8.1.9 => true
8.2.0 => false
สามหลักล็อกแน่นกว่าสองหลัก: ~> 8.1.4 ยอมแค่ 8.1.x · ~> 1.2 ยอมถึง 1.99
เทียบกับ Go: ใน go.mod คุณเขียน require github.com/gin-gonic/gin v1.12.0
แปลว่า "อย่างน้อยรุ่นนี้" ไม่มีเพดานให้เขียน · Go ใช้รุ่นนี้ไปเรื่อย ๆ จนกว่าคุณจะสั่งอัปเกรดเอง ·
ส่วน Ruby ให้เขียนเพดานด้วย ~> แล้ว Bundler เลือกรุ่นใหม่สุดที่ยังอยู่ใต้เพดาน
- pessimistic version constraint (
~>) - ชื่อทางการของ
~>("มองโลกแง่ร้าย" = กลัวรุ่นใหม่ทำของพัง) · ถาม senior ได้ว่า "gem นี้ควรล็อก ~> สองหลักหรือสามหลัก"
bin/rails — คำสั่ง rails ประจำโปรเจกต์
ตอน rails new คุณพิมพ์ rails เฉย ๆ เพราะยังไม่มีโปรเจกต์ ·
แต่หลังจากนี้ ในโฟลเดอร์โปรเจกต์ให้พิมพ์ bin/rails เสมอ —
มันคือไฟล์ชื่อ rails ในโฟลเดอร์ bin/ ที่ใช้ gem ตามรุ่นใน Gemfile.lock ของโปรเจกต์นี้
ไม่ใช่รุ่นไหนก็ได้ที่ลงไว้ในเครื่อง
# ผลรันจริง — ไฟล์ bin/rails ทั้งไฟล์
$ cat bin/rails
#!/usr/bin/env ruby
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../config/boot"
require "rails/commands"
- บรรทัด 1 = "ให้ใช้ ruby รันไฟล์นี้" (เลยพิมพ์
bin/railsได้ตรง ๆ ไม่ต้องพิมพ์ruby bin/rails) - บรรทัด 2 = จดไว้ว่าไฟล์ตั้งค่าหลักของแอปอยู่ไหน เก็บในค่าคงที่
APP_PATH(ชื่อตัวพิมพ์ใหญ่ = ค่าคงที่ บท 91) ·__dir__= "โฟลเดอร์ที่ไฟล์นี้อยู่" · ไม่ต้องจำ - บรรทัด 3 =
require_relativeแปลว่า "โหลดไฟล์ Ruby อีกไฟล์เข้ามาใช้ นับทางจากไฟล์นี้" (ไม่ต้องเขียน.rb) — ใกล้เคียงimportของ Go แต่โหลดเป็นไฟล์ · ไฟล์ที่โหลดคือconfig/boot.rbซึ่งข้างในมีrequire "bundler/setup"— ตัวนี้แหละที่ล็อกให้ใช้รุ่นตาม Gemfile.lock - บรรทัด 4 =
require(ไม่มี_relative) โหลดของจาก gem · ที่โหลดคือชุดคำสั่งของ rails แล้วทำตามที่คุณพิมพ์ต่อท้าย (server·console·generate…)
# ผลรันจริง (ย่อ — ตัดรายการคำสั่งย่อยยาว ๆ ท้ายออก)
$ bin/rails --help
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")
…(ตัดออก)…
Day 1 ใช้สามตัว: server (บทนี้) · generate (บท 99) · console (บท 104) ·
เทียบ Go: เหมือน go run . ที่อ่าน go.mod ในโฟลเดอร์นั้นเสมอ
rails ที่ลงไว้ในเครื่องคือบริษัทแม่ — มีหน้าที่หลักคือมาเปิดสาขาใหม่ให้ (rails new)
bin/rails คือผู้จัดการประจำสาขา — สั่งเปิดร้าน เปิดครัวทดลอง สร้างแผนกใหม่ ·
และใช้อุปกรณ์ตามใบเสร็จของสาขานี้ (Gemfile.lock) เท่านั้น สาขาอื่นจะใช้รุ่นไหนไม่เกี่ยว
ที่ไม่เหมือนกัน: ผู้จัดการจริงคิดเองได้ · bin/rails เป็นแค่ไฟล์ 4 บรรทัดที่ส่งต่อคำสั่ง ·
และมันอยู่ในโฟลเดอร์โปรเจกต์ — ถ้าคุณยืนอยู่นอกโฟลเดอร์ wongnok_blog แล้วพิมพ์ bin/rails terminal จะหาไฟล์ไม่เจอ
- binstub (
bin/rails) - สคริปต์เล็ก ๆ ในโฟลเดอร์
bin/ที่รันคำสั่งด้วย gem รุ่นของโปรเจกต์ · พิมพ์บอก AI ได้ว่า "use bin/rails instead of the global rails command" ถ้ามันแนะนำให้พิมพ์railsเฉย ๆ ในโปรเจกต์ - environment (development · test · production)
- โหมดที่แอปรันอยู่ ·
bin/rails serverบนเครื่องคุณคือ development เสมอ (เห็นในผลรันข้างล่าง) · ถาม senior ได้ว่า "ตอนนี้ server ตัวนี้รันใน environment ไหน" ทุกครั้งที่ของบนเครื่องกับบน server ทำงานต่างกัน
เปิดร้านครั้งแรก — bin/rails server
bin/rails server = เปิดร้าน — แอปรอรับใบสั่ง (request) ที่พอร์ต 3000 จนกว่าคุณจะปิด ·
ใน Go คุณต้องเขียน r.Run() เองใน main.go · ที่นี่ไม่ต้องเขียนอะไรเลย
ถ้ายังไม่ได้บอก Rails ว่า PostgreSQL อยู่ไหน server ขึ้นได้ปกติ แต่เปิดหน้าแรกแล้วได้ error 500 · ดูผลจริงด้านล่าง แล้วทำตามวิธีแก้ต่อจากนั้น
# ผลรันจริง — หน้าต่างที่ 1: เปิดร้าน
$ 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: 4989
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
อ่านเฉพาะที่สำคัญ: Puma คือ web server จาก Gemfile · development = โหมดพัฒนา ·
Listening on http://127.0.0.1:3000 = ร้านเปิดแล้ว รอที่พอร์ต 3000 ·
127.0.0.1 กับ [::1] = ที่อยู่ของ "เครื่องตัวเอง" สองแบบ (คนอื่นในเน็ตเข้าไม่ได้) ·
Use Ctrl-C to stop = ปิดร้านด้วย Ctrl-C · (PID = เลขประจำตัวของโปรแกรมที่กำลังรัน ของคุณจะได้เลขอื่น ·
threads = จำนวนใบสั่งที่รับพร้อมกันได้ ข้ามได้)
# ผลรันจริง — หน้าต่างที่ 2: ลองเป็นลูกค้า
$ curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:3000/
HTTP 500
# ส่วนหนึ่งของสิ่งที่หน้าต่างที่ 1 พิมพ์ออกมา (ย่อ)
Started GET "/" for ::1 at 2026-11-03 20:53:57 +0700
ActiveRecord::ConnectionNotEstablished (connection to server on socket "/tmp/.s.PGSQL.5432" failed: No such file or directory
Is the server running locally and accepting connections on that socket?
)
…(ตัดออก)…
curl= เบราว์เซอร์ใน terminal ·-sไม่พิมพ์แถบความคืบหน้า ·-o /dev/nullทิ้งเนื้อหน้าเว็บ ·-w 'HTTP %{http_code}\n'พิมพ์แค่ status codeStarted GET "/"= ใบสั่งเข้ามาแล้ว (::1คือเครื่องตัวเอง)ConnectionNotEstablished= ต่อ database ไม่ติด · ค่าเริ่มต้นของ Rails มองหา PostgreSQL ที่ติดตั้งในเครื่องตรง ๆ ผ่านช่องทางชื่อ/tmp/.s.PGSQL.5432· ถ้า Postgres ของคุณอยู่ใน Docker ช่องทางนี้ไม่มี
แปลกตรงที่ร้านเปิดได้ แต่ลูกค้าคนแรกเข้ามาแล้วพัง — เพราะ Rails ไม่ต่อ database ตอนเปิดร้าน
มันไปต่อตอนมีใบสั่งเข้ามา (โหมด development จะเช็ค database ก่อนเสิร์ฟหน้าแรก) ·
ต่างจาก Go ที่บท 47 สอนให้เรียก db.Ping() ตอน start เพื่อให้พังตั้งแต่เปิดร้าน
ประตูร้านเปิดได้ (server ขึ้น) แต่ตู้เก็บแฟ้ม (PostgreSQL) ตั้งอยู่อีกตึก ·
Rails เดาว่าตู้อยู่ในห้องเดียวกัน (ช่องทาง /tmp/.s.PGSQL.5432 ในเครื่อง) — พอลูกค้าคนแรกสั่ง คนดูแลสต็อกเดินไปหาไม่เจอ จานเลยออกมาเป็น error 500
DATABASE_URL คือกระดาษจดที่อยู่ตู้แฟ้มที่คุณยื่นให้ร้านก่อนเปิด
ที่ไม่เหมือนกัน: พนักงานจริงหาไม่เจอก็โทรถามหรือให้ลูกค้ารอได้ · Rails ไม่รอ มันตอบ error กลับไปทันที แล้วจดสาเหตุละเอียดไว้ในหน้าต่างที่รัน server — เวลาพังให้ไปอ่านที่หน้าต่างนั้นก่อน
วิธีแก้ — บอกที่อยู่ database ด้วย DATABASE_URL
ปิด server (Ctrl-C) แล้วตั้งตัวแปร DATABASE_URL — หน้าตาเดียวกับ DSN (สตริงบอกที่อยู่ database) ที่เคยใช้กับ goose ในบท 31
(postgres://postgres:secret@localhost:55433/shopdb…) ·
แล้วสั่ง bin/rails db:prepare ให้สร้าง database (รายละเอียดคำสั่งนี้รอ Day 2):
# ผลรันจริง — ค่าใน URL เป็นของเครื่องตรวจ
$ export DATABASE_URL=postgres://postgres:sandbox@127.0.0.1:55432/a98
$ bin/rails db:prepare
Created database 'a98'
แปล URL ทีละท่อน: postgres:// ชนิด database · postgres:sandbox ชื่อผู้ใช้:รหัสผ่าน ·
127.0.0.1:55432 เครื่องและพอร์ต · a98 ชื่อ database
ของคุณใช้ค่าตาม Postgres ที่คุณเปิดไว้ เช่น Postgres ใน Docker พอร์ตปกติก็จะเป็น
postgres://postgres:รหัสของคุณ@127.0.0.1:5432/wongnok_blog_development ·
และ export มีผลแค่หน้าต่าง terminal นั้น — ต้องรัน server ในหน้าต่างเดียวกัน
ยังไม่มี Postgres เปิดอยู่เลย? เปิด container แบบเดียวกับบท 31 ได้ แล้วใส่ชื่อผู้ใช้ รหัส และพอร์ตของ container นั้นลงใน URL
DATABASE_URL(connection string)- ที่อยู่ database แบบบรรทัดเดียว ส่งให้แอปผ่านตัวแปรของ terminal · ถ้าตั้งไว้ Rails จะใช้ค่านี้ก่อนค่าในไฟล์ตั้งค่า database (Day 2) · ถาม senior ได้ว่า "เครื่องเราต้องตั้ง DATABASE_URL เองไหม หรือทีมใส่ไว้ใน config แล้ว"
- HTTP 500 · Internal Server Error
- เลข status ที่แปลว่า "ฝั่ง server พังเอง" ไม่ใช่ลูกค้าขอผิด (เลข status เคยเจอในบท 34) · เจอ 500 ให้ไปอ่าน log ฝั่ง server ไม่ใช่ไปแก้ URL · พิมพ์บอก AI ได้ว่า "my Rails app returns 500, here is the server log: …" แล้วแปะบรรทัด error มาทั้งบรรทัด
# ผลรันจริง — เปิดร้านใหม่ในหน้าต่างเดิม แล้วลองเป็นลูกค้าอีกรอบ
$ bin/rails server
…(บรรทัดตอนเปิดเหมือนเดิม)…
* Listening on http://127.0.0.1:3000
# หน้าต่างที่ 2
$ curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:3000/
HTTP 200
# สิ่งที่หน้าต่าง server พิมพ์ (ย่อ path ยาว)
Started GET "/" for ::1 at 2026-11-03 20:53:59 +0700
…(ตัดบรรทัด SQL ที่ Rails ใช้เช็ค database ออก)…
Processing by Rails::WelcomeController#index as */*
Rendering …/railties-8.1.4/lib/rails/templates/rails/welcome/index.html.erb
Rendered …/railties-8.1.4/lib/rails/templates/rails/welcome/index.html.erb (Duration: 0.2ms | GC: 0.0ms)
Completed 200 OK in 4ms (Views: 1.1ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.2ms)
อ่าน log เป็นภาษาร้านอาหาร:
Started GET "/"= ใบสั่งเข้ามา ขอหน้า/Processing by Rails::WelcomeController#index= แผนกที่รับคือ "แผนก Welcome" (หัวหน้าเชฟที่ Rails แถมมา ทำได้เมนูเดียวคือหน้ายินดีต้อนรับ) ของ Rails เอง ไม่ใช่ของเรา — เพราะrootในroutes.rbยังเป็น comment อยู่ Rails เลยโชว์หน้าต้อนรับแทน · (ท้ายบรรทัดas */*= ลูกค้าบอกว่ารับของได้ทุกแบบ ข้ามได้)Rendering … index.html.erb= จัดจาน (view)Completed 200 OK= เสิร์ฟสำเร็จ
เปิด http://localhost:3000 ในเบราว์เซอร์ จะเห็นโลโก้ Rails กับเลขรุ่น · ตัวหนังสือทั้งหน้ามีแค่นี้ (ผลจริง ตัดแท็ก HTML ออก ·
Rack = ตัวกลางมาตรฐานที่ให้ Puma คุยกับ Rails ได้ ตอนนี้ไม่ต้องสนใจ):
Ruby on Rails 8.1.4 Rails version: 8.1.4 Rack version: 3.2.8 Ruby version: ruby 4.0.9 (2026-10-23 revision 3e93b23afc) +PRISM [arm64-darwin25]
เพื่อนบางคนใช้ bin/dev แทน — มันเปิด server พร้อมตัว build Tailwind ไปด้วยกัน (ดูได้ในไฟล์ Procfile.dev) ·
Day 1 ใช้ bin/rails server ก็พอ
🎯 ภารกิจ: เปิดสาขาของคุณเอง (~10 นาที)
- เปิด terminal ไปที่โฟลเดอร์เก็บงาน (อย่าซ้อนในโปรเจกต์ Rails อื่น) แล้วพิมพ์
rails new wongnok_blog --database=postgresql --css=tailwind· รอจนกลับมาที่ prompt cd wongnok_blog· พิมพ์git log— ควรเห็นfatal: your current branch 'main' does not have any commits yet- ทำ commit แรก:
git add -Aแล้วgit commit -m "rails new"— ควรเห็นบรรทัด… files changed(เครื่องตรวจได้ 109 · ถ้า Rails ของคุณคนละรุ่น ตัวเลขอาจต่างนิดหน่อย) - พิมพ์
ls -1AF app/controllers app/modelsแล้วตอบ: ตอนนี้มีไฟล์ของ Post หรือยัง? - ตั้ง
DATABASE_URLให้ชี้ไป Postgres ของคุณ (ยังไม่มี → เปิด container แบบบท 31) →bin/rails db:prepareควรเห็นCreated database '…'→bin/rails server - เปิดหน้าต่าง terminal ที่สอง:
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://localhost:3000/— ควรได้HTTP 200· เปิดเบราว์เซอร์ไปที่http://localhost:3000ควรเห็นโลโก้ Rails - กลับไปหน้าต่างแรก ดูบรรทัด
Processing by …แล้วตอบ: แผนกไหนรับใบสั่งนี้ และทำไมไม่ใช่ของเรา? · ปิดร้านด้วย Ctrl-C
ถ้าเจอ error นี้ แปลว่า…
ActiveRecord::ConnectionNotEstablished (connection to server on socket "/tmp/.s.PGSQL.5432" failed …→ Rails ยังไม่รู้ที่อยู่ database · เช็คว่าexport DATABASE_URL=…ในหน้าต่างเดียวกับที่รัน server และ Postgres เปิดอยู่A server is already running (pid: 979, file: …/wongnok_blog/tmp/pids/server.pid).(ผลจริงตอนเปิด server ตัวที่สองซ้อน · เลข pid ของคุณจะต่าง) → มี server ตัวเก่าเปิดค้างอยู่ในอีกหน้าต่าง · ไปกด Ctrl-C ที่หน้าต่างนั้นก่อนfatal: your current branch 'main' does not have any commits yet→ ไม่ใช่ error จริง แค่ยังไม่มี commit (ข้อ 2)
ข้อ 4: ยังไม่มี · ผลจริงคือแค่นี้:
$ ls -1AF app/controllers app/models
app/controllers:
application_controller.rb
concerns/
app/models:
application_record.rb
concerns/
Post ยังไม่มีเพราะยังไม่มีใครสร้าง — Rails ไม่ได้รู้ล่วงหน้าว่าคุณจะทำบล็อก ·
บท 99 จะสาธิต bin/rails generate สร้าง Post ให้ดู (ไฟล์โผล่ใน app/models/ ตามกฎตั้งชื่อ) · ส่วนแผนกบทความใน app/controllers/ คุณเขียนเองในบท 100
ข้อ 7: Rails::WelcomeController — แผนก Welcome ที่มากับ Rails (หัวหน้าเชฟที่ทำได้แค่หน้ายินดีต้อนรับ) ·
มันรับหน้า / เพราะ config/routes.rb ยังไม่มี root ของเราเอง (บรรทัด # root "posts#index" ยังเป็น comment) ·
บท 101 จะเขียน route เอง แล้วหน้าต้อนรับจะหายไป
ถ้าทำครบเจ็ดข้อ แปลว่าคุณมีโปรเจกต์ที่พร้อมใช้ต่อทั้ง Day 1 แล้ว · เก็บไว้อย่าลบ บท 99–105 ใช้ต่อ
ทดสอบความเข้าใจ
ข้อ 1 · หลัง rails new เสร็จ คุณได้อะไรที่ go mod init ไม่ให้?
ตัวเลือกที่ 2 ถูก · ได้ 109 ไฟล์ เปิด server แล้วได้ 200 แต่หน้าที่เห็นคือหน้าต้อนรับของ Rails เอง (Rails::WelcomeController)
ตัวเลือกที่ 1 ผิด เพราะนั่นคือภาพของ Go · ตัวเลือกที่ 3 ผิด เพราะ app/controllers กับ app/models ยังไม่มีอะไรเกี่ยวกับ Post เลย ·
ตัวเลือกที่ 4 ผิด เพราะ git log ได้ fatal: … does not have any commits yet — commit แรกต้องทำเอง
จุดที่ต้องจำ: rails new ให้ร้านเปล่าที่ครบชุด ไม่ใช่แอปที่เสร็จแล้ว · และไม่ได้ commit ให้
ข้อ 2 · เพื่อนจะ clone โปรเจกต์ของคุณ แล้วอยากได้ gem รุ่นเดียวกับคุณเป๊ะ ต้องมีไฟล์ไหนอยู่ใน git?
ตัวเลือกที่ 4 ถูก · Gemfile บอกว่าอยากได้อะไร (รายการซื้อของ) · Gemfile.lock บอกว่าได้รุ่นไหนมาจริง (ใบเสร็จ) — Bundler ใช้ทั้งสองไฟล์
ตัวเลือกที่ 1 ผิด เพราะถ้าไม่มี lock Bundler จะเลือกรุ่นใหม่สุดที่ผ่านเงื่อนไข ณ วันที่เพื่อนติดตั้ง ซึ่งอาจไม่ใช่รุ่นของคุณ · ตัวเลือกที่ 2 ผิด เพราะ Bundler ต้องอ่าน Gemfile ด้วยว่าแอปต้องการ gem อะไร (lock เป็นผลลัพธ์ของ Gemfile) · ตัวเลือกที่ 3 ผิด เพราะเพื่อนไม่ได้รัน rails new — เขา clone โปรเจกต์ที่มีอยู่แล้ว
จุดที่ต้องจำ: commit ทั้ง Gemfile และ Gemfile.lock เสมอ · lock คือสิ่งที่ทำให้ทุกเครื่องได้รุ่นเดียวกัน
ข้อ 3 · Gemfile เขียน gem "image_processing", "~> 1.2" · ถ้ามีรุ่น 1.14.0 และ 2.2.0 ออกมาแล้ว Bundler ยอมรับรุ่นไหน?
ตัวเลือกที่ 1 ถูก · ~> 1.2 = ตั้งแต่ 1.2 แต่ต่ำกว่า 2.0 · ตัวเลขท้ายสุดที่เขียน (2) ขยับขึ้นได้ ตัวหน้า (1) ล็อก
ตัวเลือกที่ 2 ผิด เพราะนั่นคือความหมายของการเขียนสามหลัก ~> 1.2.0 ·
ตัวเลือกที่ 3 ผิด เพราะ ~> มีเพดานเสมอ ถ้าอยากได้ "ใหม่กว่าอย่างเดียว" ต้องเขียน >= แบบ puma ·
ตัวเลือกที่ 4 ผิด เพราะเลขรุ่นเทียบเป็นตัวเลขทีละช่อง (14 มากกว่า 2) — ผลรัน tilde.rb ได้ 1.14.0 => true
จุดที่ต้องจำ: ~> ล็อกทุกหลักยกเว้นหลักสุดท้ายที่เขียน · สามหลักแน่นกว่าสองหลัก
ข้อ 4 · คุณได้โปรเจกต์ Rails ของคนอื่นมา อยากรู้เร็ว ๆ ว่าแอปนี้มี URL อะไรบ้าง ควรเปิดไฟล์ไหนก่อน?
ตัวเลือกที่ 3 ถูก · URL ทั้งแอปอยู่ใน config/routes.rb ไฟล์เดียว — ห้องผู้จัดการที่แปะผังส่งใบสั่งไว้
ตัวเลือกที่ 1 ผิด เพราะนั่นคือแม่แบบกลางของ controller ไม่ได้บอกว่ามี URL อะไร · ตัวเลือกที่ 2 ผิด เพราะ Gemfile บอกเครื่องมือที่ใช้ ไม่ใช่หน้าที่แอปมี · ตัวเลือกที่ 4 ผิด เพราะ log จดเฉพาะ URL ที่มีคนเคยเข้ามาแล้ว (และขอ URL ที่ไม่มีอยู่จริงก็ถูกจดด้วย)
จุดที่ต้องจำ: อยากรู้ว่าแอปทำอะไรได้ เปิด config/routes.rb ก่อนเสมอ
ติดข้อไหน หรือเครื่องคุณได้ผลไม่ตรงกับบทนี้ แปะ error บรรทัดแรกมาทั้งบรรทัดแล้วถามได้เลย
ถ้าเปิดสไลด์ดูประกอบ
จุดที่ของจริงไม่ตรงกับสไลด์ (เช็คบน Rails 8.1.4):
| หน้า | สไลด์เขียน | ของจริง | ต้องสนใจไหม |
|---|---|---|---|
| 40 | "git repo ที่ commit แรกให้แล้ว" | มีแค่ git init · git log ได้ fatal: … does not have any commits yet | ✅ ต้อง commit แรกเอง |
| 40 | "สร้างไฟล์ให้ประมาณ 80 ไฟล์" | 109 ไฟล์ (นับหลัง commit แรก) | ไม่ต้อง |
| 40 | พิมพ์ bin/rails server ต่อได้เลย | server ขึ้น แต่หน้าแรกได้ 500 ถ้ายังไม่ได้บอกที่อยู่ Postgres | ✅ ตั้ง DATABASE_URL + db:prepare |
| 39 | Rails มี 10 ชิ้น | gem rails ลากมา 12 ตัว (เกินมา actionmailbox กับ railties — ตัวหลังคือเจ้าของคำสั่ง rails) + bundler | ไม่ต้อง |
| 42 | มี db/migrate/ และ db/schema.rb | แอปใหม่ยังไม่มี · จะเกิดตอนสร้างตารางแรก (Day 2) | ไม่ต้องตกใจ |
| 44 · 45 | Gemfile มี ruby "4.0.6" · bundle info บอก "what it needs" | Gemfile จริงไม่มีบรรทัด ruby (รุ่นอยู่ในไฟล์ .ruby-version) · bundle info บอกที่อยู่และใครพึ่ง gem นั้น ไม่ได้บอกว่ามันพึ่งอะไร | ไม่ต้อง |
อ้างอิง
- Rails Guides — Getting Started: Directory Structure — อ่านตาราง File/Folder · Purpose ทั้งตาราง · เป็นแผนผังร้านฉบับทางการ ตรงกับตารางในหัวข้อ "เดินดูร้าน"
- Rails Guides — The Rails Command Line: Creating a New Rails Application —
อ่านหัวข้อ 2 ทั้งหัวข้อ · มีตัวอย่าง
rails newพร้อมบรรทัดcreate …แบบที่คุณเห็น และวิธีเลือก database อื่น - Bundler — Gemfile: Version —
อ่านย่อหน้าที่อธิบาย pessimistic specifier (
~> 2.0.3เท่ากับ>= 2.0.3และ< 2.1) · แล้วอ่านต่อหัวข้อ Require As (เรื่องrequire: false) กับ Groups - Bundler — Gemfile.lock: When to commit it — อ่านย่อหน้าแรก "For an application, always commit Gemfile.lock" · แล้วย้อนขึ้นไปดูหัวข้อ Checksums ที่ทำหน้าที่แบบ go.sum
- Go Modules Reference — Modules, packages, and versions — อ่านย่อหน้าแรกย่อหน้าเดียว · นิยามว่า module คือชุดของ package ที่ปล่อยพร้อมกัน — เหตุผลที่บทนี้บอกว่า gem ตรงกับ module ไม่ใช่ package
- Rails Guides — Configuring: Connection Preference —
อ่านตัวอย่างแรก ๆ ของหัวข้อนี้ · อธิบายว่า
DATABASE_URLกับconfig/database.ymlทำงานร่วมกันยังไง (ละเอียดใน Day 2)