Implementing Pagination in Oracle Database with Spring Boot and MyBatis
Pagination is a common requirement when working with large datasets, especially in web applications where performance and user experience are critical. If you're using Oracle Database and MyBatis with Spring Boot, you might find pagination a bit challenging due to the differences in how Oracle handles it compared to databases like MySQL.
In this blog post, we'll dive into how to implement pagination with Oracle Database using MyBatis. We'll cover both modern and legacy approaches, tailored to your Oracle Database version, and walk through a practical implementation.
Oracle Database Pagination Basics
Oracle's approach to pagination depends on the database version you're using:
-
Oracle 12c and Later: Introduced
OFFSET
andFETCH FIRST
clauses, making pagination straightforward. -
Oracle 11g and Earlier: Requires using
ROWNUM
in combination with subqueries to achieve pagination.
Let's explore both methods in detail.
Oracle 12c and Later: Using OFFSET
and
FETCH FIRST
Starting from Oracle 12c, pagination has become much easier with the
introduction of OFFSET
and
FETCH FIRST
clauses. Here is an example of how you
can use these clauses in a MyBatis XML Mapper.
<select id="getChatHistoriesPageable" parameterType="map" resultType="com.example.dto.ChatHistoryDto.List">
SELECT
id,
user_id,
message,
created_at
FROM
chat_history
WHERE
user_id = #{userId}
ORDER BY
created_at DESC
OFFSET #{offset} ROWS FETCH NEXT #{size} ROWS ONLY
</select>
Explanation
-
OFFSET #{offset} ROWS
: Skips the specified number of rows. -
FETCH NEXT #{size} ROWS ONLY
: Retrieves the specified number of rows.
Example:
-
Page 1 with 10 rows per page:
offset = 0
-
Page 2 with 10 rows per page:
offset = 10
Oracle 11g and Earlier: Using ROWNUM
with
Subqueries
If you're working with Oracle 11g or an older version, you'll need to use
ROWNUM
in combination with subqueries. Here's how you
can define the query in a MyBatis XML Mapper:
<select id="getChatHistoriesPageable" parameterType="map" resultType="com.example.dto.ChatHistoryDto.List">
SELECT * FROM (
SELECT
a.*,
ROWNUM AS rn
FROM (
SELECT
id,
user_id,
message,
created_at
FROM
chat_history
WHERE
user_id = #{userId}
ORDER BY
created_at DESC
) a
WHERE ROWNUM <= #{offset} + #{size}
)
WHERE rn > #{offset}
</select>
Explanation
-
Inner Query: Retrieves rows along with their
ROWNUM
values. -
Outer Query: Filters the rows based on
ROWNUM
to return the desired range.
Integrating MyBatis with Spring Boot
Now that we understand the query structure, let's see how to integrate this
with MyBatis and Spring Boot.
MyBatis Mapper Interface
First, define the MyBatis Mapper interface that will interact with the XML Mapper.
package com.example.mapper;
import com.example.dto.ChatHistoryDto;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface ChatHistoryMapper {
List<ChatHistoryDto.List> getChatHistoriesPageable(@Param("userId") Long userId,
@Param("offset") int offset,
@Param("size") int size);
}
-
@Param
Annotation: This binds the method parameters to the named parameters in the SQL query.
DTO Class for Result Mapping
Create a DTO class to map the query results.
package com.example.dto;
import lombok.Data;
import java.time.LocalDateTime;
public class ChatHistoryDto {
@Data
public static class List {
private Long id;
private Long userId;
private String message;
private LocalDateTime createdAt;
}
}
Service Layer Implementation
The service layer fetches the paginated data and wraps it in a
Page
object.
package com.example.service;
import com.example.dto.ChatHistoryDto;
import com.example.mapper.ChatHistoryMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ChatHistoryService {
private final ChatHistoryMapper chatHistoryMapper;
public Page<ChatHistoryDto.List> getChatHistories(Long userId, int page, int size) {
int offset = page * size; // Calculate the offset
List<ChatHistoryDto.List> results = chatHistoryMapper.getChatHistoriesPageable(userId, offset, size);
// Total count should ideally be fetched via a separate query
int totalCount = 100; // Example value
return new PageImpl<>(results, PageRequest.of(page, size), totalCount);
}
}
-
PageImpl
Class: Used to create a pageable response. -
PageRequest
Class: Encapsulates pagination information like page number and size.
REST Controller Implementation
Finally, expose the service method via a REST API.
package com.example.controller;
import com.example.dto.ChatHistoryDto;
import com.example.service.ChatHistoryService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class ChatHistoryController {
private final ChatHistoryService chatHistoryService;
@GetMapping("/api/chat-histories")
public Page<ChatHistoryDto.List> getChatHistories(
@RequestParam Long userId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
return chatHistoryService.getChatHistories(userId, page, size);
}
}
API Endpoint Example:
GET /api/chat-histories?userId=1&page=0&size=10
Conclusion
Implementing pagination with Oracle Database and MyBatis in a Spring Boot
application can be straightforward once you understand the appropriate SQL
syntax for your Oracle version. By leveraging MyBatis XML Mappers and Spring
Data's Page
abstraction, you can create a clean and
maintainable solution for paginated APIs.
We hope this guide helps you implement pagination in your projects. Feel free to leave comments or questions below!
Comments
Post a Comment