четверг, 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 в качестве фабрики создания транзакций.

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

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