
Привет, Хабр! Меня зовут Пётр Гусаров, я Java‑программист в CDEK. В этой статье расскажу про не очень распространённый фреймворк MyBatis.
Почему MyBatis? Потому что мы в CDEK используем его в большинстве проектов, и в деле он весьма неплохо себя показал. Немного сложен и непривычен на этапе входа, но все эти минусы перекрываются его гибкостью. «Да есть Hibernate, Jooq, JDBC и еще что‑то», — скажут бывалые. Есть, но в данной статье речь пойдёт о MyBatis.
Статья будет полезна новичкам, которые хотели попробовать данный фреймворк или попробовали, но что‑то не получилось.
Содержание
Что мы сделаем
Почему MyBatis
Запустим птичку
Стартуем на малых оборотах
Посмотрим, как это всё работает
Занавес
Что мы сделаем
Посмотрим на плюсы и минусы данного фреймворка
Развернём MyBatis на основе Spring Boot (так шустрее)
Напишем и запустим пару‑тройку запросов
Посмотрим, в каких случаях применять MyBatis лучше, чем другие фреймворки.
Почему MyBatis
У меня две новости: хорошая и плохая. С какой начать? Хорошо, начнём с плохой, точнее — с недостатков:
все запросы придётся писать на нативном SQL в XML-файлах. Стойте, не уходите! Не все так страшно, как звучит :)

Теперь к достоинствам:
полный контроль над запросами к БД;
намного легче накладывать логику на legacy‑схемы БД (просто мапим запросы на сущности, остальное делает «птичка»);
при развитии и усложнении продукта вы потратите меньше времени на оптимизацию запросов, чем в других фреймворках;
скорость обработки данных выше. Но здесь есть нюансы: за формирование запросов отвечает разработчик, и только от него зависит, как быстро будет работать обмен данными между приложением и БД;
не нужны знания о дополнительных состояниях вашего объекта, как в Hibernate‑е.
Итак, начнём. Что многие обычно делают, когда начинают изучать новый фреймворк? Открывают официальную документацию подключают его и начинают «тыкать» с разных сторон. В крайнем случае пробуют найти готовый примерчик в сети. Фреймворк считается успешным, если получается поднять его, добавив зависимость и пару‑тройку аннотаций, и всё работает. С MyBatis немного по‑другому — здесь подходит фраза из мультика: «лучше день потерять, потом за 5 минут долететь».

Запустим птичку
Для самых нетерпеливых ссылка на пример лежит здесь.
Создадим проект на основе Spring Boot. Не буду описывать подробности, вы и так знаете, как это делается. Кто не в курсе — вам сюда. Добавьте такие зависимости как mybatis, h2, lombok. Или просто возьмите их из этого pom‑файла:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.3</version>
<relativePath/>
</parent>
<groupId>ru.gpm.example</groupId>
<artifactId>mybatis-minimum</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mybatis-minimum</name>
<description>Demo MyBatis Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Можем уже запустить проект. Он запустится, но делать ничего не будет. Это самая лучшая программа: ничего не делает, ничего не ломает. Но нас не так учили: нам нужен движ, печеньки и зарплата :)
Стартуем на малых оборотах
Мы не будем писать «hello world», а напишем более приближенный к реальности проект, потому что именно такой подход, по моему мнению, покажет этот инструмент в деле. Сделаем сервис по управлению складом и остатками товара на нём.
Настроим MyBatis (здесь будет немного XML). Рядом с application.yml положим файл mybatis‑config.xml следующего содержания:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="ru.gpm.example.mybatis.min.domain"/>
</typeAliases>
</configuration>
Здесь мы показали «птичке», где у нас будут лежать объекты, отображающие данные из БД.
Создадим файл schema.sql и положим его в корень ресурсов приложения. Это будет скелет нашей высоконагруженной БД — как мы любим.
schema.sql
set mode MySQL;
CREATE TABLE IF NOT EXISTS product (
id integer NOT NULL auto_increment,
name varchar,
sku varchar,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS warehouse (
id integer NOT NULL auto_increment,
name varchar,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS stock (
product_id integer,
warehouse_id integer,
qty integer,
PRIMARY KEY (product_id, warehouse_id)
)
mode MySql нужен для поддержки базой H2 некоторых удобных функций при обновлении данных upsert.
Там же создадим папку с названием mappers, в нее мы будем класть наши файлы с запросами к базе. Теперь скажем Spring'у, где у нас этот XML‑файл лежит и откуда брать запросы к БД. Для этого в application.yml напишем следующее:
mybatis:
config-location: classpath:mybatis-config.xml
mapper-locations: classpath*:mappers/*.xml
spring.sql.init.mode: alwaysconfig‑location — показывает, где настройки MyBatis;
mapper‑locations — где «птичка» будет брать запросы;
spring.sql.init.mode — указывает Spring'у, когда инициировать скрипт schema.sql (в нашем случае — каждый раз при запуске).
Закончили с настройками, приступим к написанию кода. Создадим 3 класса в пакете domain: товар, склад и остатки.
@Data
@Accessors(chain = true)
public class Product {
private Integer id;
private String name;
private String sku;
}
@Data
@Accessors(chain = true)
public class Stock {
private Product product;
private Warehouse warehouse;
private int count;
}
@Data
@Accessors(chain = true)
public class Warehouse {
private Integer id;
private String name;
}Далее напишем сами запросы, репозитории и сервисы в привычном нам стиле. Начнём с репозиториев. Точнее, здесь они называются «мапперы».
По‑хорошему, лучше использовать репозитории с DI мапперов в них, так как это создает правильный слой данных и даст возможность управлять форматом данных в репозитории. Но у нас в рамках примера таких сложностей не ожидается.
@Mapper
public interface ProductRepository {
void save(Product product);
List<Product> findAll();
}
@Mapper
public interface WarehouseRepository {
void save(Warehouse warehouse);
Warehouse findOne(int id);
}
@Mapper
public interface StockRepository {
List<Stock> findStockByWarehouse(Warehouse warehouse);
void save(Stock stock);
}
Почти всё готово, осталось написать запросы SQL. Да‑да, запросы на старом добром SQL ).
mappers/product-mapper.xml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE mapper PUBLIC '-//mybatis.org//DTD Mapper 3.0//EN'
'http://mybatis.org/dtd/mybatis-3-mapper.dtd'>
<mapper namespace="ru.gpm.example.mybatis.min.repository.ProductRepository">
<insert id="save" keyProperty="id" useGeneratedKeys="true">
INSERT INTO product (name, sku)
VALUES (#{name}, #{sku});
</insert>
<select id="findAll" resultMap="ProductMap">
SELECT id, name, sku
FROM product;
</select>
<resultMap id="ProductMap" type="Product" autoMapping="true"/>
</mapper>
mappers/warehouse-mapper.xml
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE mapper PUBLIC '-//mybatis.org//DTD Mapper 3.0//EN'
'http://mybatis.org/dtd/mybatis-3-mapper.dtd'>
<mapper namespace="ru.gpm.example.mybatis.min.repository.WarehouseRepository">
<insert id="save" useGeneratedKeys="true" keyProperty="id">
<if test="id == null">
INSERT INTO warehouse (name) values (#{name});
</if>
<if test="id != null">
UPDATE warehouse SET name=#{name} where id=#{id};
</if>
</insert>
<select id="findOne" resultMap="WarehouseMap">
SELECT id, name FROM warehouse WHERE id = #{id};
</select>
<resultMap id="WarehouseMap" type="Warehouse" autoMapping="true"/>
</mapper>
Обратите внимание: id блоков в XML‑схеме полностью идентичны наименованию методов в интерфейсах репозиториев. Так запросы в XML автоматически линкуются с Java‑интерфейсами репозиториев. Есть и другие варианты, но усложняться не будем.
Здесь мы задействовали такие элементы как insert select и resultMap. Немного остановимся на них:
insert — выполняет вставку в БД.
- id = "..." — это id statement'а для соответствия репозиторию, который будет мапиться на этот запрос;
- useGeneratedKeys="true" говорит о том, что запрос генерирует значение ключа;
- keyProperty="id" сообщает «птичке», какое свойство у класса‑модели отвечает за ID и устанавливает его в значение в объекте после сохранения.select — выполняет чтение данных из базы.
- resultMap = "StockMap" используется в блоке <select> и сообщает «птичке», какой маппер использовать для выгрузки данных в объект.resultMap — собственно, сам маппер. Здесь описываются правила маппинга результата запроса (описанного в блоке <select>) на доменный объект. Вариантов множество. В рамках данной статьи все рассматривать не будем. Остановимся на основных.
- autoMapping="true" объявляет «птичке»: «Cделай все сама». Но это работает, когда у класса и алиаса в ответе однотипные именования полей, иначе придется описывать правила маппинга.
На следующем маппере (mappers/stock‑mapper.xml) остановимся подробнее.
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE mapper PUBLIC '-//mybatis.org//DTD Mapper 3.0//EN'
'http://mybatis.org/dtd/mybatis-3-mapper.dtd'>
<!--suppress ALL -->
<mapper namespace="ru.gpm.example.mybatis.min.repository.StockRepository">
<insert id="save">
INSERT INTO stock (product_id, warehouse_id, qty)
VALUES (#{product.id}, #{warehouse.id}, #{count}) ON DUPLICATE KEY
UPDATE qty = #{count}
</insert>
<sql id="stock-select-request">
SELECT
p.id AS p_id,
p.name AS p_name,
p.sku AS p_sku,
w.id AS w_id,
w.name AS w_name,
s.qty
FROM stock s
LEFT JOIN product p ON s.product_id = p.id
LEFT JOIN warehouse w ON s.warehouse_id = w.id
</sql>
<select id="findStockByWarehouse" resultMap="StockMap" parameterType="Warehouse">
<include refid="stock-select-request"/>
WHERE w.id = #{id}
</select>
<select id="findStockByWarehouseAndProduct" resultMap="StockMap">
<include refid="stock-select-request"/>
WHERE w.id = #{warehouseId} AND p.id = #{productId}
</select>
<resultMap id="StockMap" type="Stock">
<result property="count" column="qty"/>
<association property="product" columnPrefix="p_" javaType="Product">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="sku" column="sku"/>
</association>
<association property="warehouse" columnPrefix="w_" javaType="Warehouse">
<result property="id" column="id"/>
<result property="name" column="name"/>
</association>
</resultMap>
</mapper>Рассмотрим новые элементы:
sql — шаблонная конструкция запроса, которая может неоднократно использоваться.
- id = "..." — это id шаблона.include refid = "..." — собственно, сама вставка шаблона. Она применяется здесь в двух запросах с разными условиями where.
parameterType="Warehouse" сообщает «птичке», какой класс объекта передается в параметрах запроса.
resultMap — более развернутый маппер. Здесь видно, как алиасы ответа накладываются на вложенные объекты.
- result property = "count" column = "qty" — настраивает отношение свойств класса и наименования полей ответа;
- association — настраивает отношение вложенных объектов в класс. Таким образом, мы реализуем отношение one‑to‑one. Где property указывает свойство класса вложенного объекта, а columnPrefix — это своеобразный фильтр алиасов в ответе для данного объекта.

И самое сердце нашего приложения — сервисы. Чтобы не грузить логикой этот пример, напишем один сервис для получения остатков товаров на складах. Сохранение сделаем в тестах напрямую через репозитории.
@Service
@RequiredArgsConstructor
public class StockService {
private final StockRepository repository;
public Stock save(Stock stock) {
repository.save(stock);
return stock;
}
public List<Stock> getAllByWarehouse(Warehouse warehouse) {
return repository.findStockByWarehouse(warehouse);
}
public Stock getBy(Warehouse warehouse, Product product) {
return repository.findStockByWarehouseAndProduct(warehouse.getId(), product.getId());
}
}
Вот и всё — можем запускать наш проект и ловить баги наслаждаться жизнью. Не будем дергать это всё великолепие REST‑контроллерами, а просто напишем тест, который покажет, как это всё работает.
Посмотрим, как это всё работает

@Slf4j
@SpringBootTest
class MyBatisApplicationTest {
@Autowired
private StockService stockService;
@Autowired
private ProductRepository productRepository;
@Autowired
private WarehouseRepository warehouseRepository;
@Test
void init() {
// Добавим товары в БД
productRepository.save(new Product().setName("name-1").setSku("sku-1"));
productRepository.save(new Product().setName("name-2").setSku("sku-2"));
final List<Product> all = productRepository.findAll();
Assertions.assertEquals(2, all.size());
// Добавим склад.
final Warehouse warehouse = new Warehouse().setName("склад-1");
warehouseRepository.save(warehouse);
Assertions.assertNotNull(warehouseRepository.findOne(warehouse.getId()));
// Сохраним остатки по товарам на складе
final Stock stock1 = new Stock().setProduct(all.get(0)).setWarehouse(warehouse).setCount(10);
final Stock stock2 = new Stock().setProduct(all.get(1)).setWarehouse(warehouse).setCount(50);
stockService.save(stock1);
stockService.save(stock2);
// Получим текущие остатки на складе
List<Stock> allByWarehouse = stockService.getAllByWarehouse(warehouse);
Assertions.assertEquals(2, allByWarehouse.size());
log.info("{}", allByWarehouse);
// Поменяем остаток товара
stockService.save(stock1.setCount(20));
allByWarehouse = stockService.getAllByWarehouse(warehouse);
Assertions.assertEquals(2, allByWarehouse.size());
final Stock stockEdit = stockService.getBy(warehouse, stock1.getProduct());
Assertions.assertNotNull(stockEdit);
Assertions.assertEquals(20, stockEdit.getCount());
log.info("{}", allByWarehouse);
}
}
Занавес
Думаю, достаточно для первого знакомства с «птичкой». Подозреваю, многие скажут, что неудобно всё это ручками писать, если есть фреймворки, делающие всё за тебя. Отчасти вы будете правы: они очень удобны, чтобы быстренько написать стандартное приложение. Но когда приходится подключать эти «суперавтоматы» к legacy‑базам или требуется оптимизация запросов в связи с возросшим размером данных, то MyBatis реально выручает.
Возможно, кому‑то данная статья поможет в выборе технологии, а у кого‑то я просто украл время.
Всегда готов к конструктивной критике, так как она поднимает знания и опыт критикуемого :)
Ссылки: GitHub примера, Мультфильм: «Крылья, ноги и хвосты» (скрины и цитаты).
