суббота, 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, не было необходимости как-то усложнять данный функционал.

четверг, 11 октября 2012 г.

MyBatis и JTA UserTransaction

В нескольких проектах мне пришлось использовать очень хорошую библиотеку работы с базой данных MyBatis. Самая интересная часть MyBatis - это возможность спрятать всю работу с базой данных за простыми Java интерфейсами. Самая ужасная часть MyBatis - это система управления текущей сессией и транзакциями. Многие предлагают спрятать MyBatis за механизмом DAO объектов Spring Framework. Данный подход частично скроет от нас систему работы с текущей сессией и предоставит собственную наработку ведения транзакций.

Попробовав написать код, я окончательно запутался в xml описании компонентов (bean) для Spring. Да тут еще и появилась возможность реализовать систему на основе Apache Camel Framework. Изучая данный framework, я понял, что мне прямая дорога в OSGi.

В относительно новой редакции OSGi V4.2 есть своя реализация xml описании компонентов, похожая на Spring и очень сильно отличающаяся. Короче, мне не по душе стало склеивать OSGi Blueprint и Spring Framework. Но сама идея OSGi Blueprint не подразумевает AOP ориентированного подхода. В Spring за всех старается некий AOP Invoker при обнаружении аннотации @Transaction. Мне так нравятся эти аннотации. Значит, надо искать что-то из CDI подобных реализаций. Для MyBatis есть специальная библиотека MyBatis-Guice. Это как раз то, что мне надо. В OSGi пока только рассматривается возможность использования CDI совместно с xml описанием OSGi Blueprint. Проект Google Guice успешно работает под управлением OSGi.

Мои муки выбора остановились на использовании MyBatis-Guice. Главное достоинство - это лаконичный java код с использованием аннотаций @Transaction и @Injection для моих любимых интерфейсов, скрывающих все муки JDBC SQL запросов.

И так, приступим к самому интересному. Это транзакции. Как известно, транзакции бывают локальными и глобальными. Не буду вдаваясь в детали описаний. Локальные - это те, что происходят на уровне текущего соединения с базой данных java.sql.Connection. Глобальные - это те, что помогают выполнить действия разных систем (сервисов, служб) в рамках одной задачи.

Короче, в MyBatis напрочь отсутствует система взаимодействия с глобальными транзакциями. Ну, это частично понятно. Реализация данного функционала тянет за собой кусок Java EE спецификации и требует наличия менеджера управления транзакциями. В небольшом java проекте весь этот ворох дополнительных библиотек будет только вызывать одно раздражение.

Поскольку я решился взять за основу OSGi Enterprise V4.2 c OSGi Blueprint, то мне менеджер транзакций достается в качестве полезного бонуса. В старой версии iBatis была поддержка JTA транзакций. Мне надо всего лишь переработать старый код под новый функционал MyBatis.

Далее идет большое количество кода.

Класс JtaTransactionFactory

/*
 *    Copyright 2009-2012 The MyBatis Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.transaction.jta;

import java.sql.Connection;
import java.util.Properties;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import javax.transaction.UserTransaction;

import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.TransactionFactory;

public class JtaTransactionFactory implements TransactionFactory {

 private UserTransaction userTransaction;
 private boolean closeConnection = true;

 public JtaTransactionFactory() {
 }

 @Override
 public void setProperties(Properties props) {
  if (props != null) {
   String utxName = null;
   try {
    utxName = (String) props.get("UserTransaction");
    InitialContext initCtx = new InitialContext();
    userTransaction = (UserTransaction) initCtx.lookup(utxName);
   } catch (NamingException e) {
    throw ExceptionFactory.wrapException(
      "Error initializing JtaTransactionConfig while looking up UserTransaction (" + utxName + ").", e);
   }

   String closeConnectionProperty = props.getProperty("closeConnection");
   if (closeConnectionProperty != null) {
    closeConnection = Boolean.valueOf(closeConnectionProperty);
   }
  }
 }

 @Override
 public Transaction newTransaction(Connection conn) {
  return new JtaTransaction(userTransaction, conn, closeConnection);
 }

 @Override
 public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {
  return new JtaTransaction(userTransaction, dataSource, level, autoCommit);
 }

}

Класс JtaTransaction

/*
 *    Copyright 2009-2012 The MyBatis Team
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.transaction.jta;

import java.sql.Connection;
import java.sql.SQLException;

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

import org.apache.ibatis.exceptions.ExceptionFactory;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;
import org.apache.ibatis.transaction.Transaction;
import org.apache.ibatis.transaction.managed.ManagedTransaction;

public class JtaTransaction implements Transaction {
 private static final Log log = LogFactory.getLog(ManagedTransaction.class);

 private final UserTransaction userTransaction;
 private DataSource dataSource;
 private TransactionIsolationLevel level;
 private Connection connection;
 private final boolean closeConnection;

 private boolean commmitted = false;
 private boolean newTransaction = false;

 public JtaTransaction(UserTransaction userTransaction, Connection connection, boolean closeConnection) {
  this.userTransaction = userTransaction;
  this.connection = connection;
  this.closeConnection = closeConnection;

  if(userTransaction == null) {
   throw ExceptionFactory.wrapException("JtaTransaction initialization failed. UserTransaction was null.", null);
  }
  if(connection == null) {
   throw ExceptionFactory.wrapException("JtaTransaction initialization failed. Connection was null.", null);
  }

  init();
 }

 public JtaTransaction(UserTransaction userTransaction, DataSource ds, TransactionIsolationLevel level, boolean closeConnection) {
  this.userTransaction = userTransaction;
  this.dataSource = ds;
  this.level = level;
  this.closeConnection = closeConnection;

  if(userTransaction == null) {
   throw ExceptionFactory.wrapException("JtaTransaction initialization failed. UserTransaction was null.", null);
  }
  if(dataSource == null) {
   throw ExceptionFactory.wrapException("JtaTransaction initialization failed. DataSource was null.", null);
  }

  init();
 }

 private void init() {
  try {
   newTransaction = userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION;
   if (newTransaction) {
    userTransaction.begin();
   }
  } catch (Exception e) {
   throw ExceptionFactory.wrapException("JtaTransaction could not start transaction.  Cause: ", e);
  }
 }

 @Override
 public Connection getConnection() throws SQLException {
  if (this.connection == null) {
   openConnection();
  }
  return this.connection;
 }

 @Override
 public void commit() throws SQLException {
  if (commmitted) {
   throw ExceptionFactory.wrapException("JtaTransaction could not commit because this transaction has already been committed.", null);
  }
  try {
   if (newTransaction) {
    userTransaction.commit();
   }
  } catch (Exception e) {
   throw ExceptionFactory.wrapException("JtaTransaction could not commit.  Cause: ", e);
  }
  commmitted = true;
 }

 @Override
 public void rollback() throws SQLException {
  if (!commmitted) {
   try {
    if (userTransaction != null) {
     if (newTransaction) {
      userTransaction.rollback();
     } else {
      userTransaction.setRollbackOnly();
     }
    }
   } catch (Exception e) {
    throw ExceptionFactory.wrapException("JtaTransaction could not rollback.  Cause: ", e);
   }
  }
 }

 @Override
 public void close() throws SQLException {
  if (this.closeConnection && this.connection != null) {
   if (log.isDebugEnabled()) {
    log.debug("Closing JDBC Connection [" + this.connection + "]");
   }
   this.connection.close();
  }
 }

 protected void openConnection() throws SQLException {
  if (log.isDebugEnabled()) {
   log.debug("Openning JDBC Connection");
  }
  this.connection = this.dataSource.getConnection();
  if (level != null) {
   connection.setTransactionIsolation(level.getLevel());
  }
  if (connection.getAutoCommit()) {
   connection.setAutoCommit(false);
  }
 }

}
Теперь осталось дело за малым. Надо создать SqlSessionFactory и в конфигурации указать использование JtaTransactionFactory в качестве фабрики создания транзакций.