SoftDev Concept Notes
สรุปเนื้อหาเพื่อเตรียมสอบ (เน้น concept)

จาก CRUD ในไฟล์ สู่ระบบ POS ร้านหมูกระทะ

ทั้ง 5 บทคือโปรเจกต์เดียวที่ค่อย ๆ โตขึ้น: เริ่มจากเก็บข้อมูลใน ArrayList + ไฟล์ → ย้ายไปฐานข้อมูล SQLite ผ่าน JDBC → แยกโค้ดเป็นชั้น (Layer) → ใส่หน้าจอ Swing → ทำ flow ขายจริง (เปิดโต๊ะ / สั่ง / เช็คบิล)

เส้นทาง 5 บท กดไอคอนได้
Week 7 ArrayList + ไฟล์ Week 9 SQLite + JDBC Week 10 แบ่งชั้น + DAO Week 11 หน้าจอ Swing POS Flow ระบบขายจริง
กดไอคอนเพื่อดูว่าสัปดาห์นั้นมีชั้นอะไรบ้าง (ผลแสดงในกล่องถัดไป) · จุดที่วิ่งบนเส้นคือ "ข้อมูล" ที่ย้ายที่อยู่ไปเรื่อย ๆ จากไฟล์ → ฐานข้อมูล
ลองกด: แต่ละสัปดาห์มีชั้นอะไรบ้าง interactive
UI — หน้าจอ Swing (JFrame / JDialog)คุยกับ service เท่านั้น
Service — กฎธุรกิจ (login, เปิดโต๊ะ, เช็คบิล)ตัดสินว่า "ทำได้ไหม"
DAO — SQL ของแต่ละตารางชั้นเดียวที่เขียน SQL
Model — กล่องข้อมูล (User, Employee, Bill)วิ่งผ่านทุกชั้น
Helper / Connection — DatabaseHelperเปิด–ปิด Connection
ที่เก็บข้อมูล — ไฟล์ .dat / SQLite .db

แก่นที่ต้องจำ 5 ข้อ (ถ้าจำอะไรไม่ได้เลย จำอันนี้)

1. CRUD
Create · Read · Update · Delete — งานพื้นฐานของทุกระบบข้อมูล ทุกบททำเรื่องนี้ซ้ำในรูปแบบที่ดีขึ้น
2. Separation of Concerns
แต่ละชั้นมีหน้าที่เดียว แก้ชั้นหนึ่งไม่กระทบชั้นอื่น (UI → Service → DAO → DB)
3. ฐานข้อมูลคุมโครงสร้าง, Service คุมกฎ, UI คุมการแสดงผล
กฎ 3 ชั้นของ POS — constraint อยู่ DB, "ห้ามเปิดโต๊ะซ้ำ" อยู่ service
4. PreparedStatement + ?
แยกข้อมูลออกจากคำสั่ง SQL กัน SQL injection
5. Single source of truth
หลังแก้ข้อมูลให้อ่านจาก DB ใหม่เสมอ ไม่จำค่าไว้เองในหน้าจอ
+ Event-driven
GUI รอเหตุการณ์ (กดปุ่ม) แล้วเรียก handler ไม่ได้รันจากบนลงล่างแบบ console
บทที่ 1 · Week 7

CRUD ผู้ใช้ด้วย Swing + ArrayList + ไฟล์

โปรแกรม User Management: หน้าจอ Swing ที่เพิ่ม/แก้/ลบผู้ใช้ได้ ข้อมูลอยู่ใน ArrayList<User> และบันทึกลงไฟล์ user.dat ด้วย Serialization ยังไม่มีฐานข้อมูล

ที่มา: โค้ด week7 (UserManagement) ในเครื่อง — repo week7-crud บน GitLab เปิดไม่ได้ (private / มีแค่ initial commit)

บทนี้ทำให้เราเรียนรู้

  • CRUD คืออะไร และทำ CRUD ครบวงจรบนหน้าจอเดียว
  • แยก "ข้อมูล" (Model: User) ออกจาก "ตัวจัดการข้อมูล" (UserService) และ "หน้าจอ" (UserFrame)
  • เก็บ object ลงไฟล์ (Serialization) เพื่อให้ปิดโปรแกรมแล้วข้อมูลไม่หาย
  • JTable ไม่ได้เก็บข้อมูลเอง — มันถาม TableModel
  • เห็นข้อจำกัดของการเก็บในไฟล์ ซึ่งเป็นเหตุผลที่บทถัดไปย้ายไปใช้ฐานข้อมูล

Concept หลัก

CRUD พื้นฐาน

Create เพิ่ม · Read อ่าน · Update แก้ · Delete ลบ ในบทนี้ map เป็นเมธอด addUser, getUsers/getUser, updateUser, deleteUser

Model class + Encapsulation OOP

User มีฟิลด์ private (id, login, name, password, gender, role) เข้าถึงผ่าน getter/setter และมี toString() ไว้พิมพ์ดู

Service แบบ static โครงสร้าง

UserService ถือ static ArrayList และ lastId เพื่อแจก id อัตโนมัติ (lastId++) ใช้ static {} block ใส่ข้อมูลตั้งต้น (admin, user1, user2)

สังเกต: ตอนนี้ service ทำทั้ง "เก็บข้อมูล" และ "อ่าน/เขียนไฟล์" ปนกัน — บท Week 10 จะแยกเป็น DAO

Serialization ไฟล์

คลาสต้อง implements Serializable แล้วใช้ ObjectOutputStream เขียนทั้ง list ลงไฟล์ / ObjectInputStream อ่านกลับ

// writeFile(): เขียนทั้งก้อนทับไฟล์เดิม
oos.writeObject(userList);
// readFile(): อ่านทั้งก้อนกลับมา
userList = (ArrayList<User>) ois.readObject();

JTable + TableModel Swing

JTable เป็นแค่ "หน้าตา" มันจะถาม model ว่ามีกี่แถว (getRowCount) กี่คอลัมน์ (getColumnCount) และช่องนี้แสดงอะไร (getValueAt) — บทนี้ใช้ AbstractTableModel ที่ดึงจาก list โดยตรง

โหมดฟอร์ม: เพิ่ม vs แก้ไข UI logic

ปุ่ม Add เปิดฟอร์มว่าง / ปุ่ม Edit เอาแถวที่เลือกมาเติมฟอร์ม (fillForm) ปุ่ม Save ต้องรู้ว่ากำลังเพิ่มหรือแก้ เพื่อเรียก add หรือ update และใช้ enabledForm(true/false) เปิด–ปิดช่องกรอก

ลองกด: ข้อมูลไหลยังไงใน Week 7 interactive
UserFrame idloginname โปรแกรมปิดอยู่ RAM · ArrayList<User> Object Stream user.dat บนดิสก์ (ถาวร) AC ED 00 05 …
กด เปิดโปรแกรม ก่อน แล้วลองเพิ่ม/แก้/ลบ ดูว่าข้อมูลวิ่งไปไหน · ลองเอาติ๊ก writeFile() ออกแล้วปิดโปรแกรม จะเห็นว่าข้อมูลหาย
ดูภาพ: JTable ถาม TableModel ทีละช่อง animation
JTable (หน้าตา) AbstractTableModel อ่านจาก UserService.getUsers() ถาม ตอบ
JTable ไม่ได้เก็บข้อมูลเอง — ทุกครั้งที่ต้องวาด มันจะถาม model ว่ามีกี่แถว กี่คอลัมน์ และแต่ละช่องคืออะไร
ข้อจำกัดของการเก็บในไฟล์ (ทำไมต้องไปฐานข้อมูล) ต้องเขียนทับทั้งไฟล์ทุกครั้ง · ค้นหาแบบมีเงื่อนไขไม่ได้ (ต้องวนลูปเอง) · ไม่มีกฎกันข้อมูลผิด (ชื่อซ้ำ, ค่าว่าง) · หลายโปรแกรมใช้พร้อมกันไม่ได้ · ถ้าเปลี่ยนโครงสร้างคลาส ไฟล์เก่าอาจอ่านไม่ได้

แบบทดสอบท้ายบท

บทที่ 2 · Week 9 (mookata_jdbc — Lab พื้นฐานก่อน Week 8)

SQLite + Letos + Java JDBC พื้นฐาน

สร้างฐานข้อมูลไฟล์เดียว moogata-basic.db มีตาราง menu_category ตารางเดียว ทำ CRUD ใน Letos ก่อน แล้วทำคำสั่งเดิมผ่าน Java JDBC — เขียน 2 แบบ: try-catch-finally และ try-with-resources

ที่มา: mookata_jdbc.md, jdbc-sql-sequence.md, resource-management-comparison.md · โค้ดของเรา: repo week9 (คลาส ...Finally)

บทนี้ทำให้เราเรียนรู้

  • SQLite คือฐานข้อมูลในไฟล์เดียว — Letos กับ Java เปิด "ไฟล์เดียวกัน" ไม่ได้คุยกันเอง
  • SQL พื้นฐาน: CREATE TABLE (DDL) และ SELECT/INSERT/UPDATE/DELETE (DML)
  • ลำดับการใช้ JDBC 6 ขั้น และความต่างของ executeQuery vs executeUpdate
  • ResultSet อ่านทีละแถวด้วย cursor (next())
  • การปิด resource ให้ถูกต้อง (finally vs try-with-resources)
ภาพรวม: Java คุยกับฐานข้อมูลผ่านอะไร animation
โปรแกรม Java SelectDatabase JDBC API java.sql.* Driver sqlite-jdbc moogata- basic.db Letos GUI SQL แถวข้อมูล
โค้ดเราเรียก JDBC API ชุดเดิมเสมอ ส่วน Driver ที่ Maven โหลดมาจะแปลให้ SQLite เข้าใจ · Letos ไม่ได้อยู่ในเส้นทางนี้ มันแค่เปิดไฟล์ .db เดียวกัน

Concept หลัก

SQLite = Embedded Database DB

ทั้งฐานข้อมูลอยู่ในไฟล์ .db เดียว ไม่ต้องมี server / user / รหัสผ่าน (ต่างจาก MySQL) Letos (ชื่อเดิม SQLiteStudio) คือโปรแกรมเปิดดู/แก้ไฟล์นั้น

Letos → moogata-basic.db ← SQLite JDBC ← Java

Constraint ในตาราง DB

PRIMARY KEY ระบุแถวไม่ซ้ำ · AUTOINCREMENT ให้ DB แจกเลขเอง · NOT NULL ห้ามว่าง · UNIQUE ห้ามซ้ำ

CREATE TABLE menu_category (
  category_id   INTEGER PRIMARY KEY AUTOINCREMENT,
  category_name TEXT NOT NULL UNIQUE
);

DDL vs DML SQL

DDL = คำสั่งโครงสร้าง (CREATE TABLE) · DML = คำสั่งข้อมูล (SELECT / INSERT / UPDATE / DELETE)

WHERE สำคัญมาก SQL

UPDATE/DELETE ที่ไม่มี WHERE จะกระทบ ทุกแถว และเลข id ที่ลบแล้วไม่จำเป็นต้องถูกใช้ซ้ำ จึงไม่ควรเดาเลข id ถัดไป

JDBC + Driver Java

JDBC คือ API มาตรฐานของ Java ที่ใช้คุยกับฐานข้อมูล ตัวแปลจริงคือ Driver (sqlite-jdbc) ที่ Maven โหลดให้จาก pom.xml — ไม่ต้องเพิ่ม JAR เอง

executeQuery vs executeUpdate Java

executeQuery() ใช้กับ SELECT ได้ ResultSet · executeUpdate() ใช้กับ INSERT/UPDATE/DELETE ได้ int = จำนวนแถวที่เปลี่ยน (ไม่ใช่ id ใหม่ — ถ้าอยากได้ id ใช้ getGeneratedKeys())

ได้ 0 แถว ≠ SQL ผิด (SQL ผิดจะเกิด SQLException)

PreparedStatement + ? ความปลอดภัย

? คือตำแหน่งของ ค่า ใส่ด้วย setString(1, …) (นับจาก 1) ไม่ต้องใส่ quote รอบ ? และใช้แทนชื่อตาราง/คอลัมน์ไม่ได้

Working directory กับดัก

URL jdbc:sqlite:moogata-basic.db เป็น path สัมพัทธ์ → หาไฟล์จากโฟลเดอร์ที่รันโปรแกรม ถ้าวางผิดที่ SQLite จะ สร้างไฟล์เปล่าใหม่ แล้วเจอ no such table

ลองกด: ลำดับ JDBC 6 ขั้น interactive
Java .db Connection ? ตัวที่ 1 SQL
ลองกด: ResultSet cursor ทำงานยังไง interactive

ResultSet เริ่มอยู่ ก่อนแถวแรก ต้องเรียก next() ก่อนถึงจะอ่าน getInt/getString ได้

category_idcategory_name
ตอนนี้ cursor อยู่ ก่อนแถวแรก (ยังอ่านค่าไม่ได้)

ปิด resource: 2 แบบ

try-catch-finally แบบหลักของคาบ

1) ประกาศตัวแปรเป็น null ก่อน try 2) เปิดใน try 3) finally ตรวจ != null แล้วปิดย้อนลำดับ ResultSet → Statement → Connection 4) แต่ละ close มี try/catch ของตัวเอง 5) ห้าม return ใน finally

Connection conn = null; PreparedStatement st = null;
try { conn = DriverManager.getConnection(url); ... }
catch (SQLException e) { ... }
finally { if (st != null) try { st.close(); } catch ... }

try-with-resources แบบย่อ

ประกาศ resource ในวงเล็บ try (...) Java ปิดให้อัตโนมัติ ย้อนลำดับ ถ้าตอนปิดพลาดด้วย error จะถูกเก็บเป็น suppressed exception

try (Connection c = DriverManager.getConnection(url);
     PreparedStatement st = c.prepareStatement(sql)) {
  ...
} catch (SQLException e) { ... }
ดูภาพ: เปิดทีหลัง ต้องปิดก่อน animation
Connection PreparedStatement ResultSetมี stmt ก่อน ถึงจะมี rs ✗ close() พัง ลำดับที่เกิดขึ้น
ResultSet อยู่ใน Statement ซึ่งอยู่ใน Connection — ปิดย้อนลำดับ เหมือนถอดกล่องซ้อนจากในออกนอก (LIFO)
finally ไม่ได้ปิดให้อัตโนมัติ เราต้องเขียน close() เอง และ finally ไม่รับประกันว่าจะรันถ้า JVM ถูกยุติ (System.exit()) · ถ้า Letos ค้าง transaction (ยังไม่ Commit) Java จะเจอ database is locked
ภาพประกอบ: กับดัก working directory
✓ รันจากโฟลเดอร์โปรเจกต์ moogata-jdbc-basic/ working dir pom.xml moogata-basic.db มี 4 แถว ✓ src/ ✗ รันจากโฟลเดอร์อื่น Downloads/ working dir moogata-basic.db 0 KB · SQLite สร้างไฟล์เปล่าให้ใหม่! SQLException: no such table: menu_category
URL jdbc:sqlite:moogata-basic.db เป็น path สัมพัทธ์ ถ้าหาไฟล์ไม่เจอ SQLite จะสร้างไฟล์เปล่าให้ (ตอนเชื่อมต่อจึงไม่ error) แต่จะไปพังตอน query

แบบทดสอบท้ายบท

บทที่ 3 · Week 10 (Lab 8 README)

Layer Pattern + DAO + Service (ล็อกอิน)

ฐานข้อมูลร้าน BONUS SUKI 3 ตาราง (menu_category, menu_item, employee) แล้วแบ่งโค้ดเป็นชั้น: helper → model → dao → service ทำทีละ "ช่วง" 7 ช่วง แต่ละช่วงจบด้วย commit

ที่มา: Lab 8 README.md · โค้ดของเรา: repo week (commit ช่วง 1–7)

บทนี้ทำให้เราเรียนรู้

  • ทำไมต้องแบ่งเลเยอร์ และแต่ละชั้นรับผิดชอบอะไร
  • ออกแบบตารางมีความสัมพันธ์: PK, FK, 1:N, constraint, normalization
  • OOP ที่ใช้จริง: encapsulation, constructor overloading, static, interface, generics
  • Pattern: Layered, DAO, Singleton/shared connection, Row mapping
  • วางกฎธุรกิจ (login) ไว้ที่ service ไม่ใช่ที่ DAO หรือ UI
เปรียบเทียบ: เลเยอร์ = ร้านอาหารที่แบ่งหน้าที่กันชัดเจน animation
หน้าร้าน — พนักงานกดหน้าจอ แสดงผลและรับข้อมูล ไม่รู้ว่าของเก็บอยู่ที่ไหน UI · LoginFrame เคาน์เตอร์ผู้จัดการ — ตรวจกฎก่อนเสมอ รหัสถูกไหม มีสิทธิ์ไหม ตัดสินที่ชั้นนี้ Service · EmployeeService คนเฝ้าคลัง — คนเดียวที่เข้าห้องเก็บของ รู้ว่าของอยู่ตรงไหน = เขียน SQL ทั้งหมดของตาราง DAO · EmployeeDao กุญแจห้องคลัง — มีดอกเดียว ใช้ร่วมกัน เปิดครั้งแรกครั้งเดียว ใครจะเข้าก็ยืมดอกนี้ (static Connection) Helper · DatabaseHelper ห้องเก็บของ ตาราง employee, menu_item, menu_category DB · moogata.db ขอ: somchai
หน้าร้าน (UI) ไม่เดินเข้าห้องเก็บของเอง แต่ส่งใบขอผ่านเคาน์เตอร์ (Service) ที่ตรวจกฎก่อน แล้วคนเฝ้าคลัง (DAO) ซึ่งเป็นคนเดียวที่ใช้กุญแจ (Connection) ก็ไปหยิบของมาให้ · ถ้าย้ายคลังไปที่ใหม่ (เปลี่ยน DB) หน้าร้านไม่ต้องรู้เรื่องเลย
ลองกด: เรียก login() แล้วดูว่าวิ่งผ่านชั้นไหนบ้าง interactive
TestEmployeeService / UIเรียก service.login(name, pw)
EmployeeServiceกฎ: มีคนนี้ไหม + รหัสตรงไหม
EmployeeDaogetByName(name) → SQL ด้วย ?
DatabaseHelpergetConnect() คืน connection เดิม
moogata.dbตาราง employee
เลือกสถานการณ์ด้านบน

แต่ละชั้นทำอะไร

helper / DatabaseHelper ช่วง 3

ศูนย์กลาง Connection: ฟิลด์ static Connection conn ใช้ร่วมทั้งแอป, constructor เป็น private (ห้าม new), getConnect() สร้างครั้งแรกครั้งเดียว และเปิด PRAGMA foreign_keys = ON

synchronized กันสองเธรดสร้าง connection ซ้อนกัน (race condition) — แต่ไม่ได้คุม SQL ที่ DAO รันหลังจากนั้น

model / Employee ช่วง 5

"กล่องข้อมูล" พนักงาน 1 คน ฟิลด์ private + getter/setter, constructor 3 แบบ (ครบ / ไม่มี id / เปล่า) = overloading และ fromRS(ResultSet) เป็นจุดเดียวที่แปลงแถว → object (Row mapping)

dao / Dao<T> + EmployeeDao ช่วง 6

Dao<T> คือ interface = สัญญาว่าทุกตารางต้องมี get, getAll, save, update, delete ส่วน EmployeeDao implements Dao<Employee> คือคนเขียน SQL จริง

public interface Dao<T> {
  T get(int id);   List<T> getAll();
  T save(T obj);  T update(T obj);  int delete(T obj);
}

service / EmployeeService ช่วง 7

"เคาน์เตอร์" ที่ UI จะเรียก ใส่ กฎธุรกิจ เช่น login: ขอข้อมูลจาก DAO แล้วตัดสินเอง

Employee e = employeeDao.getByName(name);
if (e != null && e.getPassword().equals(pw)) return e;
return null;
Class diagram: interface กับคลาสที่ทำตามสัญญา
«interface» Dao<T> + get(int id): T + getAll(): List<T> + save(T) · update(T) · delete(T) implements = ทำตามสัญญาให้ครบ EmployeeDao implements Dao<Employee> + getByName(name) MenuDao implements Dao<Menu> (แบบฝึกหัด 4 · ทำตามแบบเดียวกัน)
เส้นประหัวสามเหลี่ยมโปร่ง = implements · <T> ทำให้สัญญาเดียวใช้ได้กับทุกโมเดล (generics) และ service เรียกเมธอดชื่อเดิมได้ไม่ว่าจะเป็นตารางไหน (polymorphism)

ทฤษฎีที่ใช้

ER Diagram ของ Week 10 ชี้ที่ตารางได้
ขีดสองขีด = ฝั่ง "1" · ตีนกา = ฝั่ง "หลาย" (N) — หมวด 1 หมวดมีเมนูได้หลายรายการ · PK = primary key, FK = foreign key · ลบหมวดที่ยังมีเมนูอ้างอิงไม่ได้ (ON DELETE RESTRICT)

PK / FK / 1:N DB

menu_item.category_id ชี้ไป menu_category (หมวด 1 มีเมนูได้หลายรายการ) · ON DELETE RESTRICT = ห้ามลบหมวดที่ยังมีเมนูอ้างอิง → เจอ FOREIGN KEY constraint failed ไม่ใช่บั๊ก

Normalization DB

แยกข้อมูลที่ซ้ำหลายแถวไปไว้ตารางใหม่ — ชื่อหมวดเก็บที่เดียวใน menu_category แก้ที่เดียวถูกทั้งระบบ

Interface + Polymorphism + Generics OOP

<T> ทำให้เขียนสัญญาครั้งเดียวใช้กับทุกโมเดล: Dao<Employee>, Dao<Menu> · @Override ให้ compiler ฟ้องถ้าพิมพ์ชื่อเมธอดผิด

static OOP

ของคลาส ไม่ผูกกับ object — เรียก DatabaseHelper.getConnect() ได้เลยไม่ต้อง new · ส่วน service ต้อง new EmployeeDao() เพราะเรียก instance method

ใครเป็นเจ้าของ Connection resource

DAO ยืม connection จาก helper จึงปิดแค่ Statement/ResultSet ของตัวเอง ส่วน main ปิด connection ร่วมครั้งเดียวตอนจบ (DatabaseHelper.close())

รหัสผ่าน plain text ความปลอดภัย

lab เทียบด้วย equals เพื่อฝึกเท่านั้น ระบบจริงต้องเก็บ hash (เช่น bcrypt + salt) แล้วเทียบ hash

ลองพิมพ์: ทำไมต้อง PreparedStatement interactive
✗ ต่อ String
✓ PreparedStatement
ค่าที่กรอกถูกส่งแยกเป็น "ข้อมูล" เสมอ ไม่ถูกตีความเป็นคำสั่ง SQL
คำถามชวนคิดที่น่าจะออก 1) เปลี่ยน SQLite → MySQL ต้องแก้ไฟล์ไหน? → helper (URL/driver) และ DAO ถ้า SQL ต่างกัน — UI ไม่ต้องแก้ เพราะ UI คุยกับ service เท่านั้น
2) ทำไม menu_item เก็บแค่ category_id? → normalization แก้ชื่อหมวดที่เดียว
3) getAll(where, order) อันตรายยังไง? → ต่อ string ตรง ๆ ถ้าเอาค่าที่ผู้ใช้กรอกมาใส่ = SQL injection
4) กฎ "มา 4 จ่าย 3" ควรอยู่ชั้นไหน? → service (กฎธุรกิจ)

แบบทดสอบท้ายบท

บทที่ 4 · Week 11

หน้าจอจัดการพนักงานด้วย Java Swing + Login

เพิ่มชั้น UI บนหลังบ้านเดิม (DB / DAO / Service) ทำ 2 แบบที่หลังบ้านเหมือนกัน: MoogataLab ฟอร์มรวม (ฟอร์มบน ตารางล่าง) และ MoogataLabDialog (หน้าหลักมีตาราง ฟอร์มเปิดเป็น dialog)

ที่มา: lab11-88b208.gitlab.io

บทนี้ทำให้เราเรียนรู้

  • โปรแกรม GUI เป็น event-driven และทำงานบน EDT
  • ใช้ component หลักของ Swing และตั้งชื่อ control ให้เป็นระบบ
  • JTable + DefaultTableModel: แยกหน้าตากับข้อมูล และรีเฟรชด้วยการอ่าน DB ใหม่
  • หน้าจอคุยกับ service เท่านั้น ห้ามแตะ SQL
  • กฎสิทธิ์ (role) และ trade-off ระหว่างฟอร์มรวมกับ dialog

ทฤษฎี 7 เรื่อง

Swing GUI

อยู่ใน javax.swing.* มากับ JDK ไม่ต้องเพิ่ม dependency ใน pom.xml · ใน NetBeans ห้ามเลือก Absolute Layout

Container กับ Component GUI

JFrame → contentPane → ปุ่ม / ช่องกรอก / JScrollPane → JTable (JTable ต้องอยู่ใน JScrollPane ถึงจะเห็นหัวตารางและเลื่อนได้)

Event-driven GUI

โปรแกรมรอเหตุการณ์: กดปุ่ม → ActionEvent → ActionListener → เมธอด handler → กลับไปรอ · main แค่เปิดหน้าต่าง

EDT (Event Dispatch Thread) GUI

การวาดและ event ทั้งหมดทำบนเธรดเดียว เปิดหน้าต่างด้วย SwingUtilities.invokeLater และห้ามทำงานหนักบน EDT (หน้าจอจะค้าง)

Modal vs Non-modal Dialog

super(owner, title, true) → dialog ล็อกหน้าหลัก ต้องปิด dialog ก่อนถึงจะกลับไปกดหน้าหลักได้

JTable + DefaultTableModel GUI

JTable = หน้าตา, Model = ข้อมูล · รีเฟรชแบบ "ล้างแล้วเติมใหม่ทั้งชุด" จาก DB (showEmployees()) และตั้งให้ตารางแก้ไขในช่องไม่ได้

JOptionPane GUI

showMessageDialog (ERROR / WARNING) แจ้งผล · showConfirmDialog (YES_NO) ถามยืนยันก่อนลบ

ตั้งชื่อ control convention

ขึ้นต้นตามชนิด: lblLabel, txtTextField, btnButton, tblTable, cmbComboBox, scrScrollPane · รหัสผ่านอ่านด้วย new String(txtPassword.getPassword())

หน้าต่างหนึ่งประกอบด้วยอะไร ชี้หรือแตะเพื่อจับคู่
JFrame · LoginFrame contentPane lblHeader: เข้าสู่ระบบ txtLogin btnLogin scrTable JTable JFrame └─ contentPane ├─ JLabel lblHeader ├─ JTextField txtLogin ├─ JButton btnLogin └─ JScrollPane scrTable └─ JTable tblEmployee
container ซ้อน component เป็นชั้น ๆ (ซ้าย = หน้าตา, ขวา = โครงสร้างใน Navigator ของ NetBeans) · JTable ต้องอยู่ใน JScrollPane ถึงจะเห็นหัวคอลัมน์และเลื่อนได้
ลองล็อกอิน: event วิ่งยังไง + กฎสิทธิ์ role interactive
LoginFrame

บัญชีจำลอง: manager (role 1 ผู้จัดการ) · cashier (role 2 พนักงาน) รหัส 1234

1ผู้ใช้กดปุ่ม btnLogin
2Swing สร้าง ActionEvent บน EDT
3ActionListener เรียก btnLoginActionPerformed()
4handler เรียก EmployeeService.login(name, pw)
5ตรวจกฎสิทธิ์: role = 1 เท่านั้นเข้าหน้าจัดการ
6แสดงผล: JOptionPane หรือเปิด EmployeeFrame
EDT วนรอบนี้ตลอดเวลาที่โปรแกรมเปิด main() แค่เปิดหน้าต่างด้วย invokeLater แล้วจบหน้าที่ รอ event (idle) ActionEvent ActionListener btnLoginActionPerformed()
กด btnLogin เพื่อดูเส้นทางของ event
ลองเอง: ทำไมห้ามทำงานหนักบน EDT interactive
 หน้าจอผู้ใช้
ตอบสนองปกติ
คิว event
ว่าง
EDT (เธรดเดียว)
ว่าง
เธรดอื่น
ว่าง
EDT มีเธรดเดียว ทำทั้ง "วาดหน้าจอ" และ "จัดการคลิก" ทีละงานตามคิว — ลองกด query หนักบน EDT แล้วรีบกดคลิกปุ่มหลาย ๆ ครั้ง

ฟอร์มรวม vs Dialog (trade-off)

ส่วนที่ 1 · ฟอร์มรวม editingId สถานะซ่อน ส่วนที่ 2 · ตาราง + Dialog EmployeeDialog (modal)
ซ้าย: ทุกอย่างอยู่หน้าเดียว ต้องจำว่ากำลังเพิ่มหรือแก้ด้วย editingId · ขวา: หน้าหลักมีแค่ตาราง ฟอร์มแยกไปอยู่ใน dialog ที่ล็อกหน้าหลักไว้
ลองเอง: modal dialog + isSaved() interactive
 EmployeeFrame
idชื่อrole

ลองกด btnAdd แล้วคลิกตารางด้านหลัง (modal จะไม่ยอม) · เอาติ๊ก modal ออกแล้วลองใหม่ · ลองบันทึกชื่อว่าง / ชื่อซ้ำ / ยกเลิก

ส่วนที่ 1 · ฟอร์มรวม (MoogataLab)ส่วนที่ 2 · Dialog (MoogataLabDialog)
หน้าตาฟอร์มด้านบน ตารางด้านล่าง ในหน้าเดียวหน้าหลักมีแค่ตาราง + ปุ่ม เพิ่ม/แก้ไข/ลบ/รีเฟรช
แยกโหมดเพิ่ม/แก้ใช้ตัวแปร editingId ในหน้าหลัก (มีสถานะซ่อนอยู่)dialog มี 2 constructor (เพิ่ม / แก้ไข) — หน้าหลักไม่มีสถานะซ่อน
ส่งผลกลับบันทึกแล้วรีเฟรชตารางทันทีdialog บอกผลผ่าน isSaved() → true ค่อยรีเฟรช, กดยกเลิกตารางไม่เปลี่ยน
บันทึกผิดแจ้ง error บนหน้าเดิมแจ้ง error และ ไม่ปิด dialog
กฎธุรกิจในหน้าจอนี้ เฉพาะผู้จัดการ (role = 1) เข้าหน้าจัดการได้ · ข้อความล็อกอินผิด ไม่บอก ว่าชื่อหรือรหัสผิด (กันการเดาบัญชี) · ห้ามลบบัญชีที่ล็อกอินอยู่ · ลบต้องถามยืนยัน · ชื่อซ้ำถูก UNIQUE ปฏิเสธ

แบบทดสอบท้ายบท

บทที่ 5 · POS Flow (ต่อยอด Week 8 + Week 11)

เปิดโต๊ะ · สั่งอาหาร · เช็คบิล

ร้าน BONUS SUKI: บิลมีชีวิต (เกิดตอนเปิดโต๊ะ จบตอนเช็คบิล) และ โต๊ะมีสถานะ (ว่าง → ใช้งาน → ว่าง) เพิ่มตาราง dining_table, bill, bill_item และหน้าจอ 3 หน้า: PosFrame, OrderFrame, CheckoutDialog

ที่มา: pos-flow-2dfbeb.gitlab.io

บทนี้ทำให้เราเรียนรู้

  • สถานะต้องเปลี่ยนตาม กฎ (state machine) ไม่ใช่ใครจะตั้งค่าก็ได้
  • สั่งหลายรอบ = INSERT ทีละแถวลง bill_item
  • Snapshot ปกป้องบิลเก่าจากการแก้ราคาเมนูทีหลัง
  • กฎ 3 ชั้น: DB คุมโครงสร้าง, service คุมกฎธุรกิจ, UI คุมการแสดงผล
  • Transaction (ACID) — และรู้ว่าโค้ด lab ยังไม่ได้ห่อ transaction (ช่องโหว่ที่ตั้งใจให้เห็น)
จำลองร้าน: กดโต๊ะ เปิดโต๊ะ สั่ง แก้ราคา เช็คบิล interactive
ว่างใช้งาน
PosFrame (ผังโต๊ะ)
สถานะโต๊ะ เปิดซ้ำ ✗ (SC-4) เปิดโต๊ะ (คน ≥ 1) เช็คบิล ขอบิล ชำระเงิน ว่าง ใช้งาน รอชำระ(แบบฝึกหัด) สถานะบิล ขอบิล ชำระ เช็คบิล (รับเงิน · ทอน) เปิด รอชำระ(แบบฝึกหัด) ปิด

State diagram ด้านบนซิงก์กับร้านจำลอง: เลือกโต๊ะแล้วกรอบสีเน้นคือสถานะตอนนี้ · เปิดโต๊ะ/เช็คบิลแล้วลูกศรจะกะพริบ · เส้นประคือส่วนที่เป็นแบบฝึกหัด

เมนู (ราคาปัจจุบันใน menu_item)กด +10 เพื่อขึ้นราคาแล้วดูว่าบิลเก่าเปลี่ยนไหม
Log: ใครทำอะไร (UI / Service / DAO)

ทฤษฎี 9 เรื่อง

State machine กฎสถานะ

สถานะเปลี่ยนได้เฉพาะตามเหตุการณ์ที่อนุญาต โต๊ะ: ว่าง → ใช้งาน → (รอชำระ) → ว่าง · บิล: เปิด → (รอชำระ) → ปิด · บางเหตุการณ์เปลี่ยน 2 ตารางพร้อมกัน (เปิดโต๊ะ = สร้างบิล + โต๊ะเป็นใช้งาน)

Snapshot ข้อมูล

bill_item เก็บ ชื่อและราคา ณ ตอนสั่ง ไว้เอง ไม่ดึงจาก menu_item ทุกครั้ง → แก้ราคาเมนูทีหลัง บิลเก่าไม่เปลี่ยน

Transaction (ACID) ความถูกต้อง

การกระทำที่ต้องเกิดคู่กัน (เปิดโต๊ะ, เช็คบิล) ต้องสำเร็จทั้งหมดหรือย้อนทั้งหมด · lab ยังไม่ห่อ — แบบฝึกหัดให้แก้ด้วย setAutoCommit(false) → commit() / rollback()

PK / FK / 1:N + constraint DB

โต๊ะ 1 → บิลหลายใบ · บิล 1 → รายการหลายแถว · เมนู 1 → ถูกสั่งได้หลายครั้ง · พนักงาน 1 → เปิดบิลได้หลายใบ · ใช้ NOT NULL, UNIQUE, CHECK, DEFAULT

DAO + PreparedStatement ชั้นข้อมูล

DAO 4 ตัวตามแบบ EmployeeDao เมธอดเด่น: updateStatus, getOpenByTableId, close, save, findByBillId · SQL อยู่ที่ DAO ชั้นเดียว

กฎ 3 ชั้น สถาปัตยกรรม

DB คุม โครงสร้าง (FK, CHECK) · Service คุม กฎธุรกิจ (SC-4 ห้ามเปิดโต๊ะที่ใช้งานอยู่, จำนวนคน ≥ 1) · UI คุม การแสดงผล · ทดลองแล้วว่า SQL ล้วน ๆ ไม่กันการเปิดโต๊ะซ้ำ จึงต้องมีกฎที่ service

Swing event-driven UI

refreshTables() ระบายสีปุ่มโต๊ะจากข้อมูลจริง · tableClicked() แยกทางตามสถานะ · JTable เป็นหน้าตา ข้อมูลอยู่ที่ model · DocumentListener คิดเงินทอนทันทีที่พิมพ์

เงินกับ double กับดัก

double เก็บทศนิยมฐาน 10 ได้ไม่ตรง (0.1 + 0.2 = ) ต้องจัดรูปแบบ 2 ตำแหน่งตอนแสดง (ระบบจริงใช้ BigDecimal หรือเก็บเป็นสตางค์) · Single source of truth: อ่านยอดจาก DB ใหม่ทุกครั้ง ไม่จำเอง

กฎตั้งชื่อ control ใน NetBeans convention

lbl/txt/btn/tbl/scr · JTable ต้องอยู่ใน JScrollPane · ดับเบิลคลิกปุ่มเพื่อผูก event · ข้อมูลที่เปลี่ยนได้ให้เติมตอนรัน ไม่พิมพ์ใน Properties · คลาส ThaiFont กันภาษาไทยเป็นกล่อง □□□

แผนผังข้อมูล (ERD)

6 ตาราง: 3 เดิม + 3 ใหม่ ชี้ที่ตารางได้
ชี้ที่ตารางเพื่อดูว่าโยงกับใคร · แถวสีส้มใน bill_item คือ snapshot (คัดลอกชื่อและราคามาเก็บเอง) · ตารางใหม่คือ dining_table, bill, bill_item
ดูภาพ: ถ้าไม่มี snapshot จะเกิดอะไร animation
menu_item (ราคาปัจจุบัน) บุฟเฟ่ต์หมูกะทะ ฿289.00 JOIN ทุกครั้งที่เปิดดูบิล คัดลอกครั้งเดียวตอนสั่ง ถ้าบิลอ่านราคาจาก menu_item บิล #104 · ปิดแล้ว (รับ 700) บุฟเฟ่ต์หมูกะทะ ×2578.00 หม่าล่า + ชาเย็น ×299.00 รวม677.00 bill_item เก็บ snapshot เอง บิล #104 · ปิดแล้ว (รับ 700) บุฟเฟ่ต์หมูกะทะ ×2578.00 หม่าล่า + ชาเย็น ×299.00 รวม677.00 ตอนนี้ยังตรงกัน ราคาถูกคัดลอกมาเก็บตั้งแต่ตอนสั่ง
บิล #104 ขายไปแล้วตอนบุฟเฟ่ต์ราคา 289 · ลองขึ้นราคาเมนูแล้วดูว่าบิลเก่าแต่ละแบบเป็นยังไง
ดูภาพ: ถ้าไฟดับกลางการเช็คบิล animation
conn.setAutoCommit(false) // BEGIN ขั้น 1 · BillDao.close()UPDATE bill SET status='ปิด' ไฟดับ! หยุดกลางทาง ขั้น 2 · TableDao.updateStatus()UPDATE dining_table SET status='ว่าง' conn.commit() // COMMIT ข้อมูลที่บันทึกจริงในไฟล์ .db bill #104 เปิด dining_table โต๊ะ 4 ใช้งาน
โหมดนี้คือโค้ดใน lab: autoCommit = true แต่ละ UPDATE ถูกบันทึกถาวรทันทีที่รันเสร็จ

3 หน้าจอ + กฎเบื้องหลัง

หน้าจอหน้าที่กฎ
PosFrameปุ่ม 6 โต๊ะ สีตามสถานะ กดโต๊ะว่างเพื่อเปิดSC-4 ห้ามเปิดโต๊ะที่ใช้งานอยู่ — อยู่ใน TableService
OrderFrameเมนูซ้าย บิลขวา สั่งได้หลายรอบ บันทึกลง DB ทุกครั้งบิลปิดแล้วสั่งเพิ่มไม่ได้ · ราคาเป็น snapshot · ปิดโปรแกรมกลางบิล รายการไม่หาย
CheckoutDialogmodal dialog ใส่เงินรับ เห็นเงินทอนทันทีปุ่มยืนยันล็อกจนเงินพอ · ปิดบิล + คืนโต๊ะในจุดเดียว (BillService.checkout) · บิลว่างเช็คไม่ได้
Sequence diagram: กดยืนยันเช็คบิล animation
อ่านจากบนลงล่าง: ลูกศรทึบ = เรียกเมธอด · ลูกศรประ = ส่งผลกลับ · กดเล่นเพื่อดูทีละข้อความ
Flow การกดปุ่มโต๊ะ 4 (Flowchart ในเนื้อหา) หน้าจอ → TableService (ตรวจกฎ) → BillDao + TableDao → ฐานข้อมูล → รีเฟรชหน้าจอ
Sequence เช็คบิล: พิมพ์เงิน → คิดทอนสด → ยืนยัน → BillService ปิดบิลและคืนโต๊ะ → แจ้งผล

แบบทดสอบท้ายบท

ทบทวน

ข้อสอบรวม 50 ข้อ

ปรนัย 4 ตัวเลือกครอบคลุมทั้ง 5 บท สุ่มลำดับข้อและตัวเลือกทุกครั้ง ทำไปแล้วออกไปอ่านบทอื่นได้ คำตอบถูกบันทึกไว้ กลับมาทำต่อได้จากปุ่มมุมขวาล่าง

ทบทวน

Flashcards

กดการ์ดเพื่อพลิกดูคำอธิบาย เลือกบทหรือสุ่มลำดับได้

เบื้องหลังการออกแบบ

หน้านี้ใช้สีแบบไหน ทำไมอ่านสบายตา

โทน "กระดาษอุ่น" สำหรับธีมสว่าง และ "ถ่านอุ่น" สำหรับธีมมืด ใช้สีเน้นสีเดียว (teal หม่น) ที่เหลือเป็นเทาอมน้ำตาล ค่าด้านล่างอ่านจากธีมที่ใช้อยู่ตอนนี้ ลองสลับธีมที่แถบซ้ายแล้วดูตัวเลขเปลี่ยน

Palette ()

Contrast ratio (WCAG)

ตัวหนังสือทั่วไปควรได้อย่างน้อย 4.5:1 (AA) · 7:1 ขึ้นไปเป็นระดับ AAA

หลักที่ใช้

ไม่ใช้ขาวสนิท / ดำสนิท

พื้นขาว #FFF กับตัวดำ #000 ตัดกันแรงเกินไป ทำให้ตาล้าเวลาอ่านนาน ใช้ขาวนวลแบบกระดาษ และตัวอักษรเทาอมน้ำตาลเข้มแทน ธีมมืดก็ใช้เทาถ่าน ไม่ใช่ดำ เพื่อลดอาการตัวหนังสือ "เรือง" (halation)

สีเน้นสีเดียว

teal หม่นใช้กับลิงก์ ปุ่มหลัก และสิ่งที่กำลังเลือกเท่านั้น สายตาเลยรู้ทันทีว่าอะไรสำคัญ ส่วนสีเขียว/ส้ม/แดงเก็บไว้สื่อ "สถานะ" (ถูก / เตือน / ผิด, โต๊ะว่าง / ใช้งาน) อย่างเดียว

ระยะบรรทัดเผื่อภาษาไทย

line-height 1.8 เพราะภาษาไทยมีสระบน–ล่างและวรรณยุกต์ซ้อน ถ้าบรรทัดชิดจะเบียดกัน และจำกัดความกว้างย่อหน้าราว 70 ตัวอักษร ให้ตาไม่ต้องกวาดไกล

ฟอนต์

IBM Plex Sans Thai (หัวกลมอ่านง่ายบนจอ) ถ้าออฟไลน์จะใช้ Sukhumvit Set / Noto Sans Thai ของเครื่องแทน · โค้ดใช้ JetBrains Mono แยก 0/O, 1/l ชัด