GrabDuck

MyBatis как более быстрая альтернатива Hibernate

:

В Java сообществе Hibernate framework де-факто считается стандартом для удобной работы с базой данных. Разработчику трудно выбрать другой фреймфорк, потому что порой он не знает о существовании альтернатив. В этой статье я проведу курс молодого бойца по работе с MyBatis framework. Полностью охватить весь framework не получится, но информации будет достаточно, что бы увидеть преимущества и слабые стороны данного framework'а и начать работать с MyBatis.

MyBatis не реализует JPA спеки, а является альтернативой JPA. Основное отличие MyBatis от Hibernate — это то как производится мапинг объектов. Hibernate мапит таблицы БД на сущности, давая нам доступ к данным. Для получения данных Hibernate генерирует SQL запросы, а генерируемые запросы хороши до поры — до времени, а потом они съедают кучу времени, становятся громоздкими и не управляемыми. MyBatis мапится не на таблицы, а на SQL запросы, за формирование запросов отвечает разработчик и только от него будет зависеть как быстро будет работать приложение.

С преамбулой закончили, теперь можно перейти непосредственно к созданию небольшого проекта с использованием MyBatis, что бы познакомиться с ним поближе. Не буду оригинальным, сделаем пару запросов к БД с использованием MyBatis. В примере буду использовать СУБД MySQL, а вы можете использовать любую другую СУБД, которая вам по душе.

Создадим БД mybatis:

CREATE DATABASE `mybatis`;

Создадим таблицы subscriber, tariff, payments:

subscriber:

CREATE TABLE `mybatis`.`subscriber` (
`id` INT( 10 ) NOT NULL ,
`name` VARCHAR( 255 ) NOT NULL ,
`ref_tariff` VARCHAR( 10 ) NOT NULL ,
PRIMARY KEY ( `id` ) 
) ENGINE = MYISAM

tariff:
CREATE TABLE `mybatis`.`tariff` (
`id` INT( 10 ) NOT NULL ,
`descr` VARCHAR( 255 ) NOT NULL ,
PRIMARY KEY ( `id` ) 
) ENGINE = MYISAM

payments:
CREATE TABLE `mybatis`.`payments` (
`id` INT( 10 ) NOT NULL ,
`ref_subscriber` INT( 10 ) NOT NULL ,
`summa` INT( 10 ) NOT NULL ,
PRIMARY KEY ( `id` ) 
) ENGINE = MYISAM

Самое скучное позади — у нас есть БД, из которой мы будем получать данные, теперь приступим непосредственно к работе с MyBatis. Для начала нам необходима библиотека MyBatis. Для получения библиотеки мы будем использовать maven, необходимо добавить зависимость в настройки проекта(pom.xml):
<dependency> 
    <groupId>org.mybatis</groupId> 
    <artifactId>mybatis</artifactId> 
    <version>3.2.8</version> 
</dependency>

На момент написания статьи последняя версия MyBatis 3.2.8

После того как библиотека успешно загрузилась, необходимо настроить подключение к БД. Настройки осуществляются в конфигурационном файле 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> 
    <properties resource="config.properties"> <!--ссылка на файл со свойствами(ссылка на СУБД, логин, пароль и тп.)-->
    </properties> 
    <settings><!--в данном блоке можно настроить много параметров. Полный список параметров http://mybatis.github.io/mybatis-3/configuration.html#settings-->
        <setting name="logImpl" value="LOG4J"/> 
    </settings> 
    <environments default="development"><!--в данном блоке настраиваются подключения к БД-->
        <environment id="development"> 
            <transactionManager type="JDBC"/> 
            <dataSource type="POOLED"> 
                <property name="driver" value="${db.driver}"/> 
                <property name="url" value="${db.url}"/> 
                <property name="username" value="${db.username}"/> 
                <property name="password" value="${db.password}"/> 
            </dataSource> 
        </environment> 
    </environments> 
    <mappers><!--в данном блоке необходимо описать маперы, которые используются в проекте-->
        <mapper class="kz.jazzsoft.mapper.SubscriberMapper"/> 
        <mapper class="kz.jazzsoft.mapper.TariffMapper"/>
        <mapper class="kz.jazzsoft.mapper.PaymentMapper"/>
    </mappers> 
</configuration>

В листинге выше я указал 3 мапера — все взаимодействие с БД будет осуществляться через маперы и чем детальнее вы будете понимать как работать с маперами и формировать запросы, тем более производительней будут ваши приложения.

Для корректной работы с MyBatis необходимо создать интерфейс мапера в котором будут предопределены методы, которые будут использоваться и xml файл настроек в котором будут описаны sql запросы, правила их мапинга на объекты и тп.

Создадим интерфейс kz.jazzsoft.mapper.SubscriberMapper:

package kz.jazzsoft.mapper;

import kz.jazzsoft.dal.Subscriber;

public interface SubscriberMapper {

Subscriber getSubscriberById(Integer id);

List getSubscriber();

}

В данном интерфейсе мы определили два метода:

1. getSubscriberById — вернет одного пользователя по id;

2. getSubscriber — вернет список пользователей;

Но что бы данные методы заработали необходимо создать xml маппер с sql запросами.

<!DOCTYPE mapper     PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kz.jazzsoft.mapper.SubscriberMapper">
    <select 
id="getSubscriberById" <!--название метода-->
parameterType="java.lang.Integer" <!--тип входящих параметров, может быть поистине разнообразным, начиная от Map и заканчивая EntityBean.--> 
>
        select * from subscriber where id = #{id}  <!-- поле в фигурных скобках это параметр, который прилетел в метод. Если это Map — то {имя} это ключ к переменной. Если в метод передаем EntityBean то {имя} — это название переменной данного bean.-->
    </select>
    
    <select id="getSubscriber">
        select * from subscriber
    </select>
</mapper>

Я упустил еще один момент, который необходимо было сделать — это создать классы beanEntity, на которые мы будем мапить результаты выполнения запросов.

Subscriber:

package kz.jazzsoft.dal;
import java.util.List;
public class Subscriber {
    private Long id;
    private String name;
    private Tariff tariff;
	private List<Payment> payments
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Tariff getTariff() {
        return tariff;
    }
    public void setTariff(Tariff tariff) {
        this.tariff = tariff;
    }
    public List<Payment> getPayments() {
        return paymentList;
    }
    public void setPayments(List<Payment> payments) {
        this.payments = payments;
    }
    public List<Connection> getConnections() {
        return connections;
    }
}

Tariff:
package kz.jazzsoft.dal;
public class Tariff {
    private Long id;
    private String descr;
   
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getDescr() {
        return descr;
    }
    public void setDescr(String descr) {
        this.descr = descr;
    }
}

Payment:
package kz.jazzsoft.dal;
public class Payment {
    private Long id;
    private Integer summa;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Integer getSumma() {
        return discount;
    }
    public void getSumma(Integer summa) {
        this.summa = summa;
    }
}

Можно сразу посмотреть, как работает код, для этого необходимо подключиться к БД и инициализировать нужный мапер, а пока он у нас один (SubscriberMapper). Создадим класс, в котором будем работать:

Work:

package kz.jazzsoft;
import kz.jazzsoft.mapper.SubscriberMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.Reader;
public class Work {
    public static void main(String[] args) {
        SqlSessionFactory sqlSessionFactory;
        SubscriberMapper subscriberMapper;
        Reader reader = null;
        try {
            reader = Resources
                    .getResourceAsReader("mybatis-config.xml"); //Читаем файл с настройками подключения и настройками MyBatis
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
            subscriberMapper = sqlSessionFactory.openSession().getMapper(SubscriberMapper.class); //Создаем маппер, из которого и будем вызывать методы getSubscriberById и getSubscribers
List<Subscriber> subscribers = subscriberMapper.getSubscribers();
Subscriber subscriber = subscriberMapper.getSubscriberById(101);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Запросы выполнились и у нас есть объекты, с которыми мы можем работать. Вы можете посмотреть, что у объектов есть id и другие поля заполнены, но не все. Тут есть один нюанс, если колонка в БД имеет такое же имя как переменная, то она автоматически смапиться на нее. Что бы расширить возможности мапинга и создавать сложные структуры в арсенале MyBatis есть тег ResultMap, который позволяет настраивать произвольный мапинг. Делать связи one-to-one и one-to-many.

ResultMap представляет из себя описание правил связи полей EntityBean с колонками из таблиц. Пример для Subscriber:

<resultMap id="subscriber" type="kz.jazzsoft.dal.Subscriber">
        <id property="id" column="id"/>
        <result property="name" column="name"/> <!--можно поменять поле name в Subscriber и посмотреть результат, соотвественно поменяв свойство property-->
</resultMap>

В итоге маппер для Subscriber будет выглядеть следующим образом:
<!DOCTYPE mapper     PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kz.jazzsoft.mapper.SubscriberMapper">
<resultMap id="subscriber" type="kz.jazzsoft.dal.Subscriber">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
</resultMap>
    <select id="getSubscriberById" parameterType="java.lang.Integer"
resultMap="subscriber">  <!-- ссылка на ResultMap по которому и будет происходить мапинг-->
        select * from subscriber where id = #{id}
    </select>
    
    <select id="getSubscribers" resultMap="tariff">
        select * from subscriber
    </select>
</mapper>

Связь one-to-one осуществляется не сложнее примера выше. Но нам сначала необходимо будет описать следующий мапер Tariff. Через него мы будем получать данные для связанного поля в Subscriber.

Создаем сущность Tariff:

package kz.jazzsoft.dal;
public class Tariff {
    private Long id;
    private String descr;
   
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getDescr() {
        return descr;
    }
    public void setDescr(String descr) {
        this.descr = descr;
    }
}

Создаем интерфейс мапера TariffMapper, нам понадобится только один метод:
package kz.jazzsoft.mapper;
import kz.jazzsoft.dal.Tariff;
public interface TariffMapper {
    Tariff getTariffById(Integer id);
}

Создаем мапер:
<!DOCTYPE mapper     PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kz.jazzsoft.mapper.TariffMapper">
    <resultMap id="tariff" type="kz.jazzsoft.dal.Tariff">
        <id property="id" column="id"/>
        <result property="descr" column="descr"/>
    </resultMap>
    <select id="getTariffById" resultMap="tariff" parameterType="java.lang.Integer">
        select * from tariff where id = #{id}
    </select>
</mapper>

Теперь можно добавить Subscriber c Tariff в SubscriberMaper, в resultMap необходимо добавить правило связи:
<resultMap id="subscriber" type="kz.jazzsoft.dal.Subscriber">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <association 
		property="tariff"  <!--свойство в Subscriber -->
		column="ref_tariff"  <!-- колонка в таблице Subscriber, по которой собственно и будет происходить связь с нужным тарифом-->
                    	javaType="kz.jazzsoft.dal.Tariff"  <!--описание типа, который у нас будет возвращаться-->
                     	select="kz.jazzsoft .mapper.TariffMapper.getTariffById"  <!-- Тут у нас работа передается тарифному мапперу, который выполнит sql и замапит все согласно своим настройкам.-->
                     	fetchType="eager"  <!-- Тип запроса--> />
</resultMap>

Необходимо заменить данный resultMap и можно будет узнать на каком тарифном(Tariff) плане у нас находится Абонент(Subscriber)

Subscriber.gettariff().getDescr();

Добавим к Абоненту(Subscriber) список его платежей(Payments)(one-to-many):

Для начала необходимо создать EntityBean Payment:

package kz.jazzsoft.dal;
public class Payment {
    private Long id;
    private Integer summa;
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Integer getSumma() {
        return discount;
    }
    public void getSumma(Integer summa) {
        this.summa = summa;
    }
}

Теперь необходимо создать интерфейс мапера PaymentMapper, он будет простой. Только один метод получения списка платежей по id пользователя.
package kz.jazzsoft.mapper;
import kz.jazzsoft.dal.Payment;
import java.util.List;
public interface PaymentMapper {
    List<Payment> getPaymentsByIdSub(Integer id);
}

Необходимо создать xml мапер:
<!DOCTYPE mapper     PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kz.jazzsoft.mapper.PaymentMapper">
    <resultMap id="payment" type="kz.jazzsoft.dal.Payment">
        <id property="id" column="id"/>
        <result property="summa" column="date"/>
    </resultMap>
    <select id="getPaymentsByIdSub" resultMap="payment" parameterType="java.lang.Integer">
        select * from payment where ref_subscriber = #{id}
    </select>
</mapper>
Свяжем список платежей (payment) с абонентом (Subscriber):

<source lang="xml">
<resultMap id="subscriber" type="kz.jazzsoft.dal.Subscriber">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <association property="tariff" column="ref_tariff"
                    	javaType="kz.jazzsoft.dal.Tariff"
                     	select="kz.jazzsoft.mapper.TariffMapper.getTariffById"	fetchType="eager"/>
        <collection 
			property="payments" <!--свойство в bean Subscriber-->
			column="id" <!--id Subscriber--> 
			javaType="List" <!--тип который получим на выходе(список)-->
			ofType="Payment" <!--а этим типом будет заполнен список-->
			select="kz.jazzsoft.mapper.PaymentMapper.getPaymentsByIdSub" <!-- метод, который необходимо выполнить, что бы получить список платежей по id Абонента-->
            fetchType="eager" <!--тип запроса-->             
            />
</resultMap>

Полученный resultMap заменяем в SubscriberMapper и можно посмотреть все платежи пользователя. Но на этом все самое интересное только начинается.

MyBatis имеет функционал, который позволяет формировать sql запросы динамически в зависимости от параметров, которые были в него переданы. Например нам нет необходимости создавать кучу sql на каждое действие(выборки из одной таблицы, но по разным параметрам), можно отделаться одним методом, который будет фильтровать тех же абонентов по нескольким колонкам или вообще не будет фильтровать и вернет всех в зависимости от входных данных, но обо всем по порядку.

Для динамического формирования SQL запросов в арсенале MyBatis имеется достаточно компонентов для решения большинства задач. Рассматривать все мы не будем, так как их достаточно много и их можно комбинировать и тп. Для примера расмотрим IF оператор, больше информации можно прочитать в официальном руководстве: mybatis.github.io/mybatis-3/dynamic-sql.html

IF Оператор:

<select id="getSubscribersWithParam" parameterType="map">
	select * from subscriber where (1=1)
	<if test="descr != null" >
		and decr = #{descr}
	</if>
</select>

В запросе приведенном выше выполняется проверка, на то что объект в Map по ключу descr не null, тогда в запрос будет добавлена строка в блоке if и таких блоков может быть сколько угодно, они могу быть вложенными.
<update id="updateSubscriber" parameterType="kz.jazzsoft.dal.Subscriber">
	udpate subscriber
    	<set>
		<if test="descr != null">
			descr = #{descr},
        </if>
   	 </set>
	where id = #{id}
</update>

MyBatis при разумном использовании может дать ощутимый прирост в скорости работы приложения. Может показаться страшно писать самому запросы и правила мапинга, но это только кажется, Hibirnate тоже не так уж прост.

Не существует одного универсального решения, которое подошло бы всем, в каждом выборе нужен четкий расчет. MyBatis можно использовать совместно с Hibernate там где это действительно нужно, а это сможете определить только вы.