Why We Chose MyBatis Instead of JPA for the Spring Boot API Server
Introduction
When building REST APIs with Spring Boot, most tutorials use Spring Data JPA.
I initially tried to implement with JPA too.
However, for the customer-facing DVD rental API implementation, we chose MyBatis.
This article is not “JPA is bad” — it’s about “why MyBatis was a better fit in that particular case.”
Where We Stumbled with JPA
The DVD rental DB is based on the PostgreSQL sample DB dvdrental.
It has about 15 tables with complex relational entanglements.
film ←→ film_actor ←→ actor
film ←→ film_category ←→ category
inventory → film
rental → inventory → film
payment → rental
When trying to create an endpoint to return “film list (with categories)” for the customer API, we ran into these problems with JPA.
Problem 1: N+1 Queries
When connecting film and category with @ManyToMany, a query runs to fetch categories every time one film is retrieved.
With 1,000 films, 1,001 SQL statements execute.
// With 1000 items this becomes 1001 SQL statements
List<Film> films = filmRepository.findAll();
films.forEach(f -> f.getCategories()); // ← N+1 happens here
You can work around this with @EntityGraph or fetch join, but the code becomes complex.
Problem 2: Aggregate Queries Are Hard to Write
For aggregations requiring GROUP BY or subqueries — “film count by category,” “list of films with remaining stock” — writing in JPQL reduces readability.
// Writing GROUP BY in JPQL ends up like this
@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();
The return type becomes List<Object[]>, losing type safety.
Why We Chose MyBatis
Reason 1: Can Write SQL Directly
MyBatis allows writing SQL directly in XML or 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>
The SQL you write executes as-is. Behavior is predictable and debugging is easy.
Reason 2: Complex Aggregate Queries Write Naturally
<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>
Writing the same thing with JPA’s JPQL becomes considerably complex.
Reason 3: SQL Is More Accurate When Working with an Existing DB
dvdrental is an existing DB. Table names and column names are already defined.
Writing SQL directly involves fewer mistakes than attaching @Column(name = "...") annotations to every field in JPA.
MyBatis Setup
<!-- 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 automatic conversion
When to Use JPA
MyBatis isn’t always the right answer — it depends on the case.
| Case | Suitable Technology |
|---|---|
| Primarily simple CRUD | JPA (Spring Data JPA) |
| Many complex JOINs and aggregations | MyBatis |
| Working with an existing DB | MyBatis |
| Designing DB from scratch | JPA |
| Want a type-safe query builder | jOOQ |
Summary
- The customer-facing DVD rental API had many complex JOINs and aggregations, so we chose MyBatis
- MyBatis allows writing SQL directly, making it easy to work with existing DBs and predict behavior
- Without worrying about N+1 problems, the SQL written executes exactly — leading to easier debugging
- “Which is better” is the wrong question; “choose based on what you’re building” is the right answer