суббота, 13 октября 2012 г.

MyBatis-Guice, настройка системы

Настройка системы

В предыдущей статье я упомянул об использовании MyBatis-Guice. В этой статье я покажу способ настройки Guice модуля.

Для начала напишем базовые xml файлы настройки MyBatis. Можно было бы обойтись и без них, но мне не очень нравится избыточное количество аннотаций каждого метода.

mapping.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>
    <typeAlias type="org.myproject.model.ReceiptDoc" alias="ReceiptDoc"/>
    <typeAlias type="org.myproject.impl.model.entity.ReceiptDocEntity" alias="ReceiptDocEntity"/>
  </typeAliases>
  <mappers>
    <mapper resource="org/myproject/db/ReceiptDocMapper.xml"/>
  </mappers>
</configuration>
Файл с SQL запросами

org/myproject/db/ReceiptDocMapper.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="org.myproject.impl.provider.ReceiptDocMapper">
  <resultMap id="docMap" type="ReceiptDocEntity">
    <id property="id" column="ID" jdbcType="VARCHAR"/>
    <result property="dateLoad" column="DATELOAD" jdbcType="TIMESTAMP"/>
    <result property="docUrl" column="DOC_URL" jdbcType="VARCHAR"/>
    <result property="docName" column="DOC_NAME" jdbcType="VARCHAR"/>
    <result property="fileName" column="FILE_NAME" jdbcType="VARCHAR"/>
  </resultMap>

  <!-- Поиск документа по коду -->
  <select id="findReceiptDoc" resultMap="docMap" parameterType="string">
  select ID, DATELOAD, DOC_URL, DOC_NAME, FILE_NAME
  from  RECEIPT_DOC where ID = #{id}
  </select>

  <!-- Поиск документа по названию -->
  <select id="findReceiptDocByName" resultMap="docMap" parameterType="string">
  select ID, DATELOAD, DOC_URL, DOC_NAME, FILE_NAME
  from  RECEIPT_DOC where DOC_NAME = #{name}
  </select>

  <!-- Добавление нового документа -->
  <insert id="insert" parameterType="ReceiptDoc">
  insert into RECEIPT_DOC (
  ID, DATELOAD, DOC_URL, DOC_NAME, FILE_NAME
  ) values (
  #{id, jdbcType=VARCHAR},
  #{dateLoad, jdbcType=TIMESTAMP},
  #{docUrl, jdbcType=VARCHAR},
  #{docName, jdbcType=VARCHAR},
  #{fileName, jdbcType=VARCHAR}
  )
  </insert>
</mapper>

Далее, составим модель объектов.
ReceiptDoc - это интерфейс сохраняемого объекта.
ReceiptDocEntity - это сохраняемый объект.
ReceiptDocMapper - это интерфейс генерируемого объекта, за которым MyBatis прячет весь функционал взаимодействия с базой данных.

Интерфейс манипуляции с базой данных ReceiptDocMapper

package org.myproject.impl.provider;

import org.myproject.model.ReceiptDoc;

public interface ReceiptDocMapper {
    ReceiptDoc findReceiptDoc(String id);
    List<ReceiptDoc> findReceiptDocByName(String name);
    void insert(ReceiptDoc receiptDoc);
}

Интерфейс данных ReceiptDoc

package org.myproject.model;

import java.util.Date;

public interface ReceiptDoc {
    String getId();
    void setId(String id);
    Date getDateLoad();
    void setDateLoad(Date dateLoad);
    String getDocUrl();
    void setDocUrl(String docUrl);
    String getDocName();
    void setDocName(String docName);
    String getFileName();
    voi setFileName(String fileName);
}

Сохраняемый объект ReceiptDocEntity

package org.myproject.impl.model.entity;

import java.util.Date;
import org.myproject.model.ReceiptDoc;

public class ReceiptDocEntity implements ReceiptDoc, Serializable {
    private static final long serialVersionUID = 1L;

    // Далее идет реализация интерфейса ReceiptDoc
}

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

Для этого создадим новый интерфейс объекта, который, для простоты, будет реализовывать те же методы, что и ранее представленный интерфейс ReceiptDocMapper.

package org.myproject.provider;

import org.apache.ibatis.session.SqlSessionFactory;
import org.myproject.model.ReceiptDoc;

public interface Provider {
    ReceiptDoc findReceiptDoc(String id);
    List<ReceiptDoc> findReceiptDocByName(String name);
    void insert(ReceiptDoc receiptDoc);

    SqlSessionFactory getSqlSessionFactory();
}
Теперь напишем реализацию данного интерфейса.
Реализация одного из методов интерфейса Provider могла бы выглядеть примерно так:
private SqlSessionManager sqlSessionManager;

@Override
public ReceiptDoc findReceiptDoc(String id) {
    boolean isSessionInherited = sqlSessionManager.isManagedSessionStarted();

    if(!isSessionInherited) {
        sqlSessionManager.startManagedSession();
    }
    try {
        ReceiptDocMapper mapper = sqlSessionManager.getMapper(ReceiptDocMapper.class);

        // основная задача данного метода
        ReceiptDoc receiptDoc = findReceiptDoc(id);

        if(!isSessionInherited) {
            sqlSessionManager.commit();
        }

        return receiptDoc;
    }
    catch(Exception e) {
        sqlSessionManager.rollback();
    }
    finnaly {
        if(!isSessionInherited) {
            sqlSessionManager.close();
        }        
    }
}
Теперь сделаем то же самое с использованием mybatis-guice
@Inject
private ReceiptDocMapper mapper;

@Override
@Transactional(force = true)
public ReceiptDoc findReceiptDoc(String id) {
    return mapper.findReceiptDoc(id);
}

Вот как красиво и лаконично выглядит функционал взаимодействия с базой данных. Весь функционал управления сессией и транзакциями переместился в обработчик аннотации @Transactional.

Нечто подобное можно постараться реализовать на CDI JSR-299 Weld. Но меня напрягает тот факт, что ReceiptDocMapper - это интерфейс. Рисовать методы провайдера для каждого интерфейса не совсем правильно.

Осталось дело за малым - настроить нашу CDI систему. Напишем небольшой класс, помогающий нам выполнить настройки.

package org.myproject.impl.provider;

import javax.sql.DataSource;

import com.google.inject.Guice;
import com.google.inject.Injector;

public class ServicesUtil {
    /**
     * Создание JSR-330 Guice Injector. Поднятие MyBatis.
     * @param dataSource соединение с базой данных
     * @param useXA учитывать систему распределенных транзакций или нет
     * @param environment окружение для MyBatis
     * @param mappings файл настроек для MyBatis
     * @return
     */
    public static Injector createInjector(DataSource dataSource, boolean useXA, String environment, String mappings) {
        // Нам нужен ClassLoader приватного класса в текущем OSGI Bundle.
        // В настройка MyBatis используются приватные классы.
        SessionModule module = new SessionModule(ServicesUtil.class.getClassLoader(), environment, mappings);
        Injector injector = Guice.createInjector(module);

        module.configure(
            injector.getInstance(Provider.class).getSqlSessionFactory(),
            dataSource,
             useXA);
  
        return injector;
    }
}
На следующий код надо запастись терпением. Он достаточно длинный.
package com.comita.esb.impl.aisgz.exchange.oos.model;

import java.util.Properties;

import javax.sql.DataSource;
import javax.transaction.UserTransaction;

import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.mybatis.guice.XMLMyBatisModule;
import org.mybatis.transaction.jta.JtaTransactionFactory;

import com.google.inject.PrivateModule;

import org.myproject.provider.Provider;

public class SessionModule extends PrivateModule {

    private final String environmentId;
    private final ClassLoader classLoader;
    private final String classPathResource;

    public SessionModule(
        ClassLoader classLoader,
        String environmentId,
        String classPathResource) {

        this.environmentId = environmentId;
        this.classLoader = classLoader;
        this.classPathResource = classPathResource;
    }

    @Override
    protected void configure() {
        install(new XMLMyBatisModule(){
            @Override
            protected void initialize() {
                useResourceClassLoader(classLoader);
                setEnvironmentId(environmentId);
                setClassPathResource(classPathResource);
            }
        });

        bind(Provider.class).to(ProviderImpl.class);
        expose(Provider.class);
    }

    public void configure(
        SqlSessionFactory sqlSessionFactory,
        DataSource dataSource,
        boolean useXaDataSource) {

        Configuration configuration = sqlSessionFactory.getConfiguration();
        TransactionFactory transactionFactory = configTransaction(configuration, useXaDataSource);
        Environment env = new Environment(environmentId, transactionFactory, dataSource);

        configuration.setEnvironment(env);
    }

    private TransactionFactory configTransaction(Configuration configuration, boolean useXaDataSource) {
        TransactionFactory transactionFactory;

        if(useXaDataSource) {
            transactionFactory = new JtaTransactionFactory();

            // запрещаем данному менеджеру автоматически закрывать соединения
            Properties prop = new Properties();
            prop.setProperty("closeConnection", "false");
            prop.setProperty("UserTransaction", "osgi:service/" + UserTransaction.class.getName());

            transactionFactory.setProperties(prop);
        }
        else {
            transactionFactory = new JdbcTransactionFactory();
        }
        return transactionFactory;
    }
}
Обратите внимание, что в свойство "UserTransaction" я указываю название OSGi JNDI ресурс. В JavaEE 6 надо указывать другое название ресурса. Поскольку, весь проект ориентирован на OSGi, не было необходимости как-то усложнять данный функционал.

Комментариев нет:

Отправить комментарий