Tech Blog

Why We Chose MyBatis Instead of JPA for the Spring Boot API Server

by Tech Writer
Spring Boot MyBatis JPA Java

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.

CaseSuitable Technology
Primarily simple CRUDJPA (Spring Data JPA)
Many complex JOINs and aggregationsMyBatis
Working with an existing DBMyBatis
Designing DB from scratchJPA
Want a type-safe query builderjOOQ

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

Article Map for This Series

Building a DVD Rental End-User App Alongside the Admin Dashboard — Vue 3 + Spring Boot Architecture Overview

Feel free to send a message

Please send a message if you have any technical questions, feedback, or inquiries.