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



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        3.1.3
        
    
    ru.gpm.example
    mybatis-minimum
    0.0.1-SNAPSHOT
    mybatis-minimum
    Demo MyBatis Spring Boot
    
        17
    
    
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            3.0.2
        
        
            com.h2database
            h2
            runtime
        
        
            org.projectlombok
            lombok
            true
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter-test
            3.0.2
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
        
    

Можем уже запустить проект. Он запустится, но делать ничего не будет. Это самая лучшая программа: ничего не делает, ничего не ломает. Но нас не так учили: нам нужен движ, печеньки и зарплата :)

Стартуем на малых оборотах

Мы не будем писать «hello world», а напишем более приближенный к реальности проект, потому что именно такой подход, по моему мнению, покажет этот инструмент в деле. Сделаем сервис по управлению складом и остатками товара на нём.

Настроим MyBatis (здесь будет немного XML). Рядом с application.yml положим файл mybatis‑config.xml следующего содержания:




    
        
    

Здесь мы показали «птичке», где у нас будут лежать объекты, отображающие данные из БД.

Создадим файл 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 findAll();
}

@Mapper
public interface WarehouseRepository {

    void save(Warehouse warehouse);

    Warehouse findOne(int id);
}

@Mapper
public interface StockRepository {

    List findStockByWarehouse(Warehouse warehouse);

    void save(Stock stock);
}

Почти всё готово, осталось написать запросы SQL. Да‑да, запросы на старом добром SQL).

mappers/product-mapper.xml





    
        INSERT INTO product (name, sku)
        VALUES (#{name}, #{sku});
    

    

    

mappers/warehouse-mapper.xml





    
        
            INSERT INTO warehouse (name) values (#{name});
        
        
            UPDATE warehouse SET name=#{name} where id=#{id};
        
    

    

    

Обратите внимание: id блоков в XML‑схеме полностью идентичны наименованию методов в интерфейсах репозиториев. Так запросы в XML автоматически линкуются с Java‑интерфейсами репозиториев. Есть и другие варианты, но усложняться не будем.

Здесь мы задействовали такие элементы как insert select и resultMap. Немного остановимся на них:

  • insert — выполняет вставку в БД.
    id = »…» — это id statement’а для соответствия репозиторию, который будет мапиться на этот запрос;
    useGeneratedKeys=«true» говорит о том, что запрос генерирует значение ключа;
    keyProperty=«id» сообщает «птичке», какое свойство у класса‑модели отвечает за ID и устанавливает его в значение в объекте после сохранения.

  • select — выполняет чтение данных из базы.
    resultMap = «StockMap» используется в блоке ) на доменный объект. Вариантов множество. В рамках данной статьи все рассматривать не будем. Остановимся на основных.
    autoMapping=«true» объявляет «птичке»: «Cделай все сама». Но это работает, когда у класса и алиаса в ответе однотипные именования полей, иначе придется описывать правила маппинга.

На следующем маппере (mappers/stock‑mapper.xml) остановимся подробнее.






    
        INSERT INTO stock (product_id, warehouse_id, qty)
        VALUES (#{product.id}, #{warehouse.id}, #{count}) ON DUPLICATE KEY
        UPDATE qty = #{count}
    

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

© Habrahabr.ru