MyBatis «на минималках»

от автора

Привет, Хабр! Меня зовут Пётр Гусаров, я Java‑программист в CDEK. В этой статье расскажу про не очень распространённый фреймворк MyBatis.

Почему MyBatis? Потому что мы в CDEK используем его в большинстве проектов, и в деле он весьма неплохо себя показал. Немного сложен и непривычен на этапе входа, но все эти минусы перекрываются его гибкостью. «Да есть Hibernate, Jooq, JDBC и еще что‑то», — скажут бывалые. Есть, но в данной статье речь пойдёт о MyBatis.

Статья будет полезна новичкам, которые хотели попробовать данный фреймворк или попробовали, но что‑то не получилось.

Содержание

Что мы сделаем

  • Посмотрим на плюсы и минусы данного фреймворка

  • Развернём MyBatis на основе Spring Boot (так шустрее)

  • Напишем и запустим пару‑тройку запросов

  • Посмотрим, в каких случаях применять MyBatis лучше, чем другие фреймворки.

Почему MyBatis

У меня две новости: хорошая и плохая. С какой начать? Хорошо, начнём с плохой, точнее — с недостатков:

  • все запросы придётся писать на нативном SQL в XML-файлах. Стойте, не уходите! Не все так страшно, как звучит 🙂

А теперь послушай, птичка: ты будешь писать 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.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: 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 примера, Мультфильм: «Крылья, ноги и хвосты» (скрины и цитаты).


ссылка на оригинал статьи https://habr.com/ru/articles/771714/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *