MyBatis «на минималках»
Простой 11 мин Блог компании CDEK Программирование *Java *Doctrine ORM * Туториал
Привет, Хабр! Меня зовут Пётр Гусаров, я Java‑программист в CDEK. В этой статье расскажу про не очень распространённый фреймворк MyBatis.
Почему MyBatis? Потому что мы в CDEK используем его в большинстве проектов, и в деле он весьма неплохо себя показал. Немного сложен и непривычен на этапе входа, но все эти минусы перекрываются его гибкостью. «Да есть Hibernate, Jooq, JDBC и еще что‑то», — скажут бывалые. Есть, но в данной статье речь пойдёт о MyBatis.
Статья будет полезна новичкам, которые хотели попробовать данный фреймворк или попробовали, но что‑то не получилось.
Содержание статьи:
Содержание
-
Что мы сделаем
-
Почему MyBatis
-
Запустим птичку
-
Стартуем на малых оборотах
-
Посмотрим, как это всё работает
-
Занавес
Что мы сделаем
-
Посмотрим на плюсы и минусы данного фреймворка
-
Развернём MyBatis на основе Spring Boot (так шустрее)
-
Напишем и запустим пару‑тройку запросов
-
Посмотрим, в каких случаях применять MyBatis лучше, чем другие фреймворки.
Почему MyBatis
У меня две новости: хорошая и плохая. С какой начать? Хорошо, начнём с плохой, точнее — с недостатков:
-
все запросы придётся писать на нативном SQL в XML-файлах. Стойте, не уходите! Не все так страшно, как звучит 🙂
А теперь послушай, птичка: ты будешь писать 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.sqlset 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: always
-
config‑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 примера, Мультфильм: «Крылья, ноги и хвосты» (скрины и цитаты).
Теги:
- mybatis
- junior
- orm
- orm всегда медленный
Хабы:
- Блог компании CDEK
- Программирование
- Java
- Doctrine ORM