понедельник, июля 26, 2010

Как получить курсор из хранимой процедуры Oracle - SimpleJdbcCall

Понадобилось получить набор данных из хранимой процедуры Oracle. Данные получить эти надо в java-приложение.
Посмотрел несколько вариантов, наиболее симпатичным (что, впрочем, предсказуемо) оказался вариант и использованием спрингового SimpleJdbcCall.

Итак, есть такая табличка - разумеется, что здесь всё упрощено насколько это можно - и даже чуть более

CREATE TABLE BOOK (
    ID INTEGER NOT NULL PRIMARY KEY, 
    NAME VARCHAR2(20))

Есть такая вот хранимая процедура

PROCEDURE GET_BOOKS_BY_NAME (book_cursor OUT SYS_REFCURSOR, name_template IN VARCHAR2)
IS
BEGIN
  OPEN book_cursor FOR
    SELECT b.ID as ID, b.NAME as NAME 
      FROM BOOK b
     WHERE b.NAME LIKE name_template
     ORDER BY ID;    
  
END GET_BOOKS_BY_NAME;

Осталась самая малость - получить от этой процедуры содержимое курсора book_cursor.

Для начала, понадобится получить экземпляр SimpleJdbcCall. Его можно создать руками, можно прописать в контексте спринга - по вкусу.

Для представления данных удобно создать отдельный DTO


public class BookItem {
    private Integer id;
    private String name;
    public BookItem() {}
    // getters и setters опущены 
}


На экземпляры этого класса будут отображены данные из получаемого курсора.

А теперь собираем всё вместе и получаем данные


    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("app-config.xml");


        SimpleJdbcCall simpleJdbcCall = context.getBean("simpleJdbcCall", SimpleJdbcCall.class);


        // Этот экземпляр отвечает за отображение набора данных, полученных из
        // базы данных в курсоре, на список объектов
        RowMapper rowMapper = ParameterizedBeanPropertyRowMapper.newInstance(BookItem.class);
        // Указывается имя вызываемой хранимой процедуры
        simpleJdbcCall.withProcedureName("GET_BOOKS_BY_NAME");
        // Связывается курсор, получаемый из хранимой процедуры, с отображателем
        // данных
        simpleJdbcCall.returningResultSet("BOOK_CURSOR", rowMapper);


        // Задаются входные параметры для хранимой процедуры
        SqlParameterSource params = new MapSqlParameterSource().addValue("NAME_TEMPLATE", "NEW%");


        // Теперь производится непосредственный вызов хранимой процедуры.
        // В возвращаемый в результате вызова map помещается имя курсора
        // (параметр типа String) и сами полученные данные в виде ArrayList.
        // Для того, чтобы эти данные из ArrayList разобрать, и используется
        // rowMapper
        Map resultMap = simpleJdbcCall.execute(params);


        // При этом вызове производится отображение полученных данных на тип,
        // указанный в параметре rowMapper
        @SuppressWarnings("unchecked")
        List resultList = (List) resultMap.get("BOOK_CURSOR");


        // А теперь просто работаем с тем, что получили от БД
        System.out.println("Получено записей: " + resultList.size());
        for (BookItem item : resultList) {
            System.out.println(item);
        }
    }

Вот и всё.

вторник, июля 06, 2010

Glassfish, Spring, и RAR5031:System Exception

Приложение имеет следующий простой вид: EJB SLSB фасад, который обращается с контексту Спринга, получает из него реализацию сервиса и делегирует вызов этом полученному сервису.

В ряде совершенно вроде бы безобидных случаев вываливался exception, начинающийся вот так:

RAR5031:System Exception.
java.lang.NullPointerException
at com.sun.enterprise.resource.ConnectorXAResource.getResourceHandle(ConnectorXAResource.java:228)
at com.sun.enterprise.resource.ConnectorXAResource.start(ConnectorXAResource.java:124)
at com.sun.enterprise.distributedtx.J2EETransactionManagerOpt.enlistResource(J2EETransactionManagerOpt.java:159)
at com.sun.enterprise.resource.ResourceManagerImpl.registerResource(ResourceManagerImpl.java:144)
at com.sun.enterprise.resource.ResourceManagerImpl.enlistResource(ResourceManagerImpl.java:102)
at com.sun.enterprise.resource.PoolManagerImpl.getResource(PoolManagerImpl.java:216)
at com.sun.enterprise.connectors.ConnectionManagerImpl.internalGetConnection(ConnectionManagerImpl.java:327)
at com.sun.enterprise.connectors.ConnectionManagerImpl.allocateConnection(ConnectionManagerImpl.java:189)
at com.sun.enterprise.connectors.ConnectionManagerImpl.allocateConnection(ConnectionManagerImpl.java:165)
at com.sun.enterprise.connectors.ConnectionManagerImpl.allocateConnection(ConnectionManagerImpl.java:158)
at com.sun.gjc.spi.base.DataSource.getConnection(DataSource.java:108)
at org.hibernate.ejb.connection.InjectedDataSourceConnectionProvider.getConnection

Случаи были совсем безобидные - вида (автоматического, разумеется) обращения к БД для генерации идентификатора (через TableGenerator) или в случае вызова тразакционного спрингового компонента из транзакционного же EJB SLSB... Короче, во всех этих случаях ничего ломаться бы и не должно было - но ломалось.

Стэктрейс в этой ситуации был реально большим, вот здесь я оставил из него только классы исключений, убрав имена и адреса методов

RAR5031:System Exception.
java.lang.NullPointerException

RAR5031:System Exception.
com.sun.enterprise.resource.PoolingException: java.lang.NullPointerException
RAR5029:Unexpected exception while registering component.
java.lang.RuntimeException: Got exception during XAResource.start:
poolmgr.err_enlisting_res_in_getconn
RAR5117 : Failed to obtain/create connection from connection pool [ ]. Reason : java.lang.RuntimeException: Got exception during XAResource.start:
RAR5114 : Error allocating connection : [Error in allocating a connection. Cause: java.lang.RuntimeException: Got exception during XAResource.start:]

--In J2EETransaction.rollback, jtsTx=null nonXAResource=null
ejbDestroyed: BookServiceBean; id: [B@159b1a0
--In J2EETransaction.rollback, jtsTx=null nonXAResource=75
context with empty container in ContainerSynchronization.afterCompletion
EJB5018: An exception was thrown during an ejb invocation on [BookServiceBean]

javax.ejb.EJBException
Caused by: javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCException: Cannot open connection
Caused by: org.hibernate.exception.GenericJDBCException: Cannot open connection
Caused by: java.sql.SQLException: Error in allocating a connection. Cause: java.lang.RuntimeException: Got exception during XAResource.start:
Caused by: javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCException: Cannot open connection
Caused by: org.hibernate.exception.GenericJDBCException: Cannot open connection
Caused by: java.sql.SQLException: Error in allocating a connection. Cause: java.lang.RuntimeException: Got exception during XAResource.start:
Caused by: java.rmi.RemoteException: null; nested exception is:
Caused by: javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCException: Cannot open connection
Caused by: org.hibernate.exception.GenericJDBCException: Cannot open connection
Caused by: java.sql.SQLException: Error in allocating a connection. Cause: java.lang.RuntimeException: Got exception during XAResource.start:
javax.ejb.EJBException: nested exception is: java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.RemoteException: null; nested exception is:
javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCException: Cannot open connection

Проблема оказалась в следующем: не было установлено свойство allow-non-component-callers в описании пула соединений. После того, как свойство это установил - все проблемы кончились.

К слову, документация по этому поводу сообщает:
You can allow non-Java-EE components, such as servlet filters and third party persistence managers, to use this JDBC connection pool. The returned connection is automatically enlisted with the transaction context obtained from the transaction manager. Standard Java EE components can also use such pools. Connections obtained by non-component callers are not automatically closed at the end of a transaction by the container. They must be explicitly closed by the caller.