เหตุใดจึงเลือก MyBatis แทน JPA สำหรับ Spring Boot API Server
บทนำ
เมื่อสร้าง REST API ด้วย Spring Boot tutorial ส่วนใหญ่ใช้ Spring Data JPA
ตอนแรกผมก็พยายาม implement ด้วย JPA เช่นกัน
อย่างไรก็ตาม สำหรับการ implement DVD rental API ฝั่งลูกค้า เราเลือก MyBatis
บทความนี้ไม่ใช่ “JPA ไม่ดี” — มันเป็นเรื่อง “ทำไม MyBatis ถึงเหมาะกว่าในกรณีนั้น”
จุดที่เราสะดุดกับ JPA
DVD rental DB นั้นอิงจาก PostgreSQL sample DB dvdrental
มีประมาณ 15 table ที่มี relation ซับซ้อนพันกัน
film ←→ film_actor ←→ actor
film ←→ film_category ←→ category
inventory → film
rental → inventory → film
payment → rental
เมื่อพยายามสร้าง endpoint ที่ return “รายการภาพยนตร์ (พร้อม categories)” สำหรับ customer API เราพบปัญหาเหล่านี้กับ JPA
ปัญหาที่ 1: N+1 Queries
เมื่อเชื่อม film และ category ด้วย @ManyToMany query จะรันเพื่อดึง category ทุกครั้งที่ดึงภาพยนตร์ 1 รายการ
ถ้ามี 1,000 ภาพยนตร์ จะ execute SQL 1,001 คำสั่ง
// ถ้ามี 1000 รายการนี้กลายเป็น 1001 SQL statements
List<Film> films = filmRepository.findAll();
films.forEach(f -> f.getCategories()); // ← N+1 เกิดที่นี่
สามารถแก้ไขได้ด้วย @EntityGraph หรือ fetch join แต่ code จะซับซ้อน
ปัญหาที่ 2: Aggregate Queries เขียนยาก
สำหรับการรวมข้อมูลที่ต้องใช้ GROUP BY หรือ subquery — “จำนวนภาพยนตร์ตาม category”, “รายการภาพยนตร์ที่มี stock เหลือ” — การเขียนใน JPQL ลด readability
// การเขียน GROUP BY ใน JPQL จะเป็นแบบนี้
@Query("SELECT c.name, COUNT(fc) FROM FilmCategory fc " +
"JOIN fc.category c GROUP BY c.name ORDER BY COUNT(fc) DESC")
List<Object[]> countByCategory();
return type กลายเป็น List<Object[]> ทำให้สูญเสีย type safety
เหตุใดจึงเลือก MyBatis
เหตุผลที่ 1: สามารถเขียน SQL ตรงๆ ได้
MyBatis อนุญาตให้เขียน SQL ตรงๆ ใน XML หรือ annotations
<!-- FilmMapper.xml -->
<select id="findAllWithCategory" resultType="PublicFilmSummary">
SELECT
f.film_id,
f.title,
c.name AS category_name,
f.length,
f.description
FROM film f
LEFT JOIN film_category fc ON f.film_id = fc.film_id
LEFT JOIN category c ON fc.category_id = c.category_id
ORDER BY f.title
</select>
SQL ที่คุณเขียน execute ตามที่เป็น พฤติกรรมคาดเดาได้และ debug ง่าย
เหตุผลที่ 2: Aggregate Queries ที่ซับซ้อนเขียนได้เป็นธรรมชาติ
<select id="findFilmsInStock" resultType="PublicFilmSummary">
SELECT DISTINCT
f.film_id,
f.title,
COUNT(i.inventory_id) AS stock_count
FROM film f
JOIN inventory i ON f.film_id = i.film_id
LEFT JOIN rental r ON i.inventory_id = r.inventory_id
AND r.return_date IS NULL
WHERE r.rental_id IS NULL
GROUP BY f.film_id, f.title
HAVING COUNT(i.inventory_id) > 0
</select>
การเขียนสิ่งเดียวกันด้วย JPQL ของ JPA จะซับซ้อนมาก
เหตุผลที่ 3: SQL แม่นยำกว่าเมื่อทำงานกับ DB ที่มีอยู่
dvdrental เป็น DB ที่มีอยู่ ชื่อ table และชื่อ column ถูกกำหนดไว้แล้ว
การเขียน SQL ตรงๆ มีข้อผิดพลาดน้อยกว่าการแนบ annotation @Column(name = "...") กับทุก field ใน JPA
การ Setup MyBatis
<!-- pom.xml -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
// Mapper interface
@Mapper
public interface PublicFilmMapper {
List<PublicFilmSummary> findAllWithCategory();
Optional<PublicFilmDetail> findById(int filmId);
}
# application.yml
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true # snake_case → camelCase แปลงอัตโนมัติ
เมื่อใดควรใช้ JPA
MyBatis ไม่ใช่คำตอบเสมอ — ขึ้นอยู่กับกรณี
| กรณี | เทคโนโลยีที่เหมาะ |
|---|---|
| CRUD อย่างง่ายเป็นหลัก | JPA (Spring Data JPA) |
| JOIN และการรวมข้อมูลที่ซับซ้อนมาก | MyBatis |
| ทำงานกับ DB ที่มีอยู่ | MyBatis |
| ออกแบบ DB ตั้งแต่ต้น | JPA |
| ต้องการ type-safe query builder | jOOQ |
สรุป
- DVD rental API ฝั่งลูกค้ามี JOIN และการรวมข้อมูลที่ซับซ้อนมาก จึงเลือก MyBatis
- MyBatis อนุญาตให้เขียน SQL ตรงๆ ทำให้ทำงานกับ DB ที่มีอยู่ได้ง่ายและคาดเดาพฤติกรรมได้
- ไม่ต้องกังวลเรื่อง N+1 SQL ที่เขียน execute ตรงๆ — นำไปสู่ debug ที่ง่ายขึ้น
- “อันไหนดีกว่า” เป็นคำถามที่ผิด “เลือกตามสิ่งที่คุณกำลังสร้าง” คือคำตอบที่ถูกต้อง
แผนที่บทความของ Series นี้
→ การสร้างแอพ DVD Rental สำหรับผู้ใช้ควบคู่กับหน้าจัดการ — ภาพรวมโครงสร้าง Vue 3 + Spring Boot