вторник, 14 января 2014 г.

DAO на голом JDBC без использования Spring Framework и AOP. Хотите немного функциональной магии?

Здравствуйте, уважаемые подписчики. Праздники кончились и очень хочется немного поработать. Сейчас Суровый разрабатывает небольшое интеграционное решение, оперирующее коллекциями разнообразных объектов. Так как есть весьма критичные требования к производительности, а ассоциации между объектами практически отсутствуют, то было принято решение отказаться от использования ORM и ограничиться чистым JDBC. А так как мы работаем с коллекциями, то всегда найдется место для функциональной магии. В данной статье я расскажу об организации "коллекционно-ориентированного" слоя доступа к данным.

Используем паттерн QueryObject


В интеграционном проекте приходится решать две основные задачи: извлекать коллекции объектов из одного хранилища, например базы данных, и записывать их в другое. Соответственно, концептуально мы имеем две операции: load и save. При этом типов объектов и алгоритмов их загрузки/извлечения может быть очень много. Для организации DAO можно описать все операции в виде методов в одном классе. Получится класс, состоящий из N*2 методов, где N - количество типов объектов. Данный класс будет очень большим и сложным, по сути у нас получится просто некоторое кладбище SQL-запросов, которое будет чрезвычайно трудоемко как сопровождать, так и тестировать.

Можно разделить передаваемые типы данных на группы по функциональному признаку и создать не один класс DAO, а несколько. Однако опыт подсказывает, что при интеграции как правило выделяется одна функциональная область, число типов данных в которой значительно превышает все остальные. Речь идет о нормативно-справочной информации (НСИ). Создание нескольких классов по два-три метода и одного с двадцатью методами не является решением проблемы. Антипаттерн "волшебный объект" все еще остается в системе.

Пойдем другим путем. Воспользуемся паттерном QueryObject. При использовании данного паттерна мы инкапсулируем логику загрузки каждого типа объекта в свой класс. Аналогично поступаем с логикой сохранения данных. Таким образом у нас получается N*2 маленьких классов, состоящих из одного бизнес-метода. Данные классы легко сопровождать и тестировать. В случае необходимости очень просто сделать заглушку (mock) - необходимо подменить всего один метод. Вероятность конфликтов при работе с системой контроля версий так же уменьшается.


Перейдем от слов к делу. Используемая реализация паттерна QueryObject выглядит следующим образом. Есть интерфейс, описывающий запрос для извлечения данных:


package ru.rt.integration.rms.services.connector;

import java.util.List;

/**
 * Реализация паттерна <b>QueryObject</b> - класс-запрос, извлекающий список
 * данных типа <code>&lt;T&gt;</code>.
 *
 * @param <T> тип элементов извлекаемого списка данных
 */
public interface TypedLoadQuery<T> {
 
    /**
     * Загрузка списка данных типа <code>&lt;T&gt;</code>, удовлетворяющих
     * критерию <code>criteria</code>
     * @param criteria критерий выбора данных
     * @return список данных типа <code>&lt;T&gt;</code> для филиала 
     * <code>billingId</code>
     * @throws QueryExecutionException при невозможности извлечь данные
     *  из хранилища
     */
    public List<T> load(Criteria criteria) 
            throws QueryExecutionException; 
}

Аналогичным образом присутствует интерфейс для описания запроса на сохранение данных:


package ru.rt.integration.rms.services.connector;


/**
 * Реализация паттерна <b>QueryObject</b> - класс-запрос, сохраняющий данные
 * типа <code>&lt;T&gt;</code> в хранилище.
 *
 * @param <T> тип сохраняемых данных
 */
public interface TypedSaveQuery<T> {    
    
    /**
     * Сохраняет <code>param</code> в хранилище данных
     * @param param сохраняемый параметр     
     * @throws QueryExecutionException при невозможности сохранить данные 
     *  в хранилище
     */
    public void save(T param) throws QueryExecutionException;    
}

Использование данных классов мало чем отличается от применения классического DAO в сервисах:


@Inject @SettlementTypeLoader
private TypedLoadQuery<SettlementType> settlTypeLoader;
    
@Inject @SettlementTypeSaver
private TypedSaveQuery<List<SettlementType>> settlTypeSaver;

...

@WebMethod
public void loadSettlementTypes(@WebParam(name = "billingId") String billingId, 
   @WebParam(name = "count") int count)
   throws DataLoadException {
    List<SettlementType> settlementTypes = null;
    try {
        settlementTypes = settlTypeLoader.load(new Criteria(...));
        settlTypeSaver.save(settlementTypes);
    ...
}

В приведенном примере инжекция DAO осуществляется с помощью CDI, но вы всегда можете использовать свой любимый IoC-контейнер или вообще обойтись без него.

Реализации данных интерфейсов могут использовать различные средства доступа к данным: ORM (в частности JPA), Spring JDBC или голый JDBC. Именно последний мы используем в своем проекте, при этом с некоторыми особенностями.

Вызов хранимых процедур Oracle из слоя DAO


Реализацию DAO с помощью JDBC можно сделать по-разному. Во-первых, можно прямо в DAO получать соединение с БД и использовать его напрямую. Во-вторых, можно работать через некоторую обертку, берущую на себя решение общих для всех QueryObject'ов задач.

В нашем приложении мы используем внешний API информационной системы, реализованный с помощью хранимых процедур. Каждая процедура принимает на вход несколько коллекций определенных пользователем типов Oracle, при этом каждая коллекция это один из примитивных атрибутов объекта.

Попробуем разобраться. Предположим, есть справочник типов клиентов. Элементы данного справочника характеризуются идентификатором, названием и филиалом. Получается такая структура:


public class CustomerType {
    private String localSystemId;
    private Long branchId;
    private String title;

    ...
}

Легко представить себе список таких элементов:


List<CustomerType> customerTypes = ...;

Так вот. Хранимая процедура принимает на вход три параметра, каждый из которых представляет собой массив: массив идентификаторов, массив названий и массив филиалов:


create or replace type t_string_list is table of varchar2(4000);
/
create or replace type t_number_list is table of number;
/
PROCEDURE add_customer_types
(
    ctyp_id   IN t_string_list,
    title     IN t_string_list,
    branch_id IN t_number_list
);
/

Соответственно, у нас появляется общая для всех DAO задача: преобразование коллекции структур в массив коллекций примитивных типов. Писать в каждом DAO цикл обхода коллекции структур, раскадывать на каждой итерации структуру на элементы, формировать массивы элементов и вызывать хранимую процедуру через CallableStatement очень утомительно. При этом делать это необходимо в каждом QueryObject'е. Почему бы не разделить ответственности классов: отделить класс, выполняющий все подготовительные операции по вызову хранимой процедуры от логики конкретного запроса? От циклов и итераций тоже неплохо бы избавиться, задействовав имеющиеся в Java примитивные средства функционального программирования.

Опишем класс, инкапсулирующий в себе массив параметров примитивных типов. Данный массив характеризуется типом параметров в терминах Oracle и списком значений:


package ru.rt.integration.rms.services.connector.rms.dao;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;


/**
 * Представление массива, выступающего в качестве одного параметра 
 * информационной системы
 */
public class ArgArray<T> {
    
    private Type type;
    
    private List<T> values;
    
    public ArgArray(Type type) {
        super();
        this.type = type;
        this.values = new LinkedList<T>();
    }
    
    public Type getType() {
        return type;
    }
    
    public List<T> getValues() {
        return Collections.unmodifiableList(values);
    }
    
    /**
     * Создаем экземпляр класса {@link ArgArray}типа
     * <code>type</code>, заполняя его результатом конвертации коллекции бинов
     * <code>collection</code> типа <code>R</code> в коллекцию <code>values</code>
     * типа <code>T</code>.
     * @param <R> тип элементов конвертируемой коллекции бинов
     * @param <T> тип элементов массива
     * @param collection коллекция бинов
     * @param mapper трансформатор для бина типа <code>R</code> в элементы типа
     * <code>T</code>
     */
    public static <R, T> ArgArray<T> fill(Type type, Collection<R> collection, 
        Mapper<R, T> mapper) throws MapperException {
        ArgArray<T> array = new ArgArray<T>(type);
        for (R bean : collection) {
            array.values.add(mapper.map(bean));
        }
        return array;
    }
    
    /**
     * Трансформатор для бинов типа <code>R</code> в элементы типа <code>T</code>
     * @param <T> тип результата трансформации
     * @param <R> тип трансформируемого бина
     */
    public static interface Mapper<R, T> {
        public T map(R bean) throws MapperException;
    }
    
    public static class MapperException extends Exception {
        @SuppressWarnings("compatibility:3805429975583783355")
        private static final long serialVersionUID = -7806625410044206947L;

        public MapperException(String message) {
            super(message);
        }
        
        public MapperException(String message, Throwable t) {
            super(message, t);
        }
    }
}

package ru.rt.integration.rms.services.connector.rms.dao;


/**
 * Типы аргументов API
 */
public enum Type {
    T_DATE_LIST, T_NUMBER_LIST, T_STRING_LIST;
}

Наиболее интересным участком приведенного исходного кода является метод fill. Данный фабричный метод строит экземпляры списка ArgArray, заполняя их значениями параметров записей, извлеченными из переданной на вход коллекции бинов collection с помощью преобразователя Mapper. Вместо того, чтобы в каждом DAO обходить в цикле коллекцию бинов и вызывать что-то вроде ArgArray#getValues().add(...) мы просто описываем алгоритм извлечения параметра и передаем его при построении массива ArgArray. Кстати, сам список, возвращаемый с помощью метода getValues(), является неизменяемым.

Для упрощения построения цепочки массивов ArgArray введем класс ArgArrayList, реализующий паттерн Fluent Interface:


package ru.rt.integration.rms.services.connector.rms.dao;

import java.util.LinkedList;
import java.util.List;


/**
 * Обертка для списка массивов аргументов {@link ru.rt.integration.rms.services.connector.rms.dao.ArgArray}. Реализует паттерн
 * <i>fluent interface</i>.
 */
public class ArgArrayList {
    
    private List<ArgArray<?>> argArraysList = new LinkedList<>();
    
    public ArgArrayList() {
        super();
    }
    
    public List<ArgArray<?>> arrays() {
        return argArraysList;
    }
    
    public <T> ArgArrayList add(ArgArray<T> array) {
        argArraysList.add(array);
        return this;
    }
}

Данный класс позволяет присоединять массивы со значениями параметров хранимой процедуры друг к другу путем последовательного вызова методов add.

Теперь мы можем выделить класс, выполняющий непосредственный вызов хранимой процедуры БД Oracle. В качестве параметров хранимой процедуры метод call данного класса будет принимать на вход экземпляры ArgArrayList:


package ru.rt.integration.rms.services.connector.rms.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;

import java.text.MessageFormat;

import java.util.Iterator;

import javax.annotation.Resource;

import javax.sql.DataSource;

import oracle.jdbc.OraclePreparedStatement;

import oracle.sql.ARRAY;
import oracle.sql.ArrayDescriptor;

import org.apache.log4j.Logger;


/**
 * Реализация паттерна <i>DAO</i>, основанная на выполнении операций,
 * принимающих в качестве аргументов список массивов
 * {@link ru.rt.integration.rms.services.connector.rms.dao.ArgArray}в виде вызова
 * хранимых процедур СУБД <i>Oracle</i>.
 */
public class OracleGenericCallableDao implements GenericCallableDao {
    
    private static final String PACKAGE = "ADAPTER";
    
    private static final String METHOD_EXECUTION_ERROR = "Could not execute " +
        " operation " + PACKAGE + ".{0}: {1}";

    private static final String PARAMS_IS_NULL_ERROR =
        "ArgArrayList params should not be null";
    
    private static final String PARAMS_IS_EMPTY_ERROR =
        "ArgArrayList params is empty";

    private static final String CURRENT_PARAM_IS_NULL_ERROR =
        "ArgArray param should not be null";

    private static final String CURRENT_PARAM_IS_EMPTY_ERROR =
        "ArgArray param is empty";

    private static final String DATASOURCE_IS_NULL =
        "The DataSource is null, may be connection is not established or broken";
    
    private static final Logger logger = Logger
        .getLogger(OracleGenericCallableDao.class);

    @Resource(name = "jdbc/systemA")
    private DataSource dataSource;
        
    public OracleGenericCallableDao() {
        super();
    }

    /**
     * {@inheritDoc}     
     */
    @Override    
    public void call(String operationName, ArgArrayList params) 
            throws DaoException {

        if (dataSource == null)
            throw new DaoException(DATASOURCE_IS_NULL);
        
        if (params == null)
            throw new DaoException(PARAMS_IS_NULL_ERROR);        
        
        if (params.arrays().size() == 0) {
            logger.info(PARAMS_IS_EMPTY_ERROR);
            return;
        }
        
        try (Connection conn 
                 = dataSource.getConnection(); 
             PreparedStatement pstmt 
                 = conn.prepareStatement(buildQuery(operationName, params))) {
            int i = 0;
            Iterator<ArgArray<?>> paramiterator = params.arrays().iterator();
            while(paramiterator.hasNext()) {
                ArgArray<?> arg = paramiterator.next();

                if (arg == null) {
                    throw new DaoException(CURRENT_PARAM_IS_NULL_ERROR);            
                }
                
                if (arg.getValues().size() == 0) {
                    logger.info(CURRENT_PARAM_IS_EMPTY_ERROR);
                    return;
                }

                ArrayDescriptor descriptor = ArrayDescriptor.createDescriptor(
                    arg.getType().toString(), conn);                
                ARRAY array_to_pass = new ARRAY(descriptor, conn, 
                    arg.getValues().toArray());                
                ((OraclePreparedStatement) pstmt).setARRAY(++i, array_to_pass);                
            }

            pstmt.execute();
        }
        catch (Exception e) {
            throw new DaoException(MessageFormat.format(METHOD_EXECUTION_ERROR, 
                operationName, e.getMessage()), e);
        }        
    }
    
    private String buildQuery(String operationName, ArgArrayList params) {
        StringBuilder queryBuilder = new StringBuilder();
        queryBuilder.append("{ call " + PACKAGE + ".");
        queryBuilder.append(operationName);
        queryBuilder.append("(");            
        for (int i = 0; i < params.arrays().size() - 1; i++) {
            queryBuilder.append("?, ");
        }
        queryBuilder.append("?) }");
        
        return queryBuilder.toString();
    }
}

При вызове хранимых процедур Oracle используем специфичное API JDBC-драйвера данной СУБД: классы ArrayDescriptor и ARRAY.

В классах-реализациях паттерна QueryObject теперь необходимо только описать алгоритм преобразования коллекции записей в массив коллекций их элементов. Делается это в функциональном стиле благодаря нашим наработкам: классам ArgArray и ArgArrayList:


new ArgArrayList().add(ArgArray.fill(Type.T_STRING_LIST,
            param, 
            new Mapper<T, String>() {
                @Override
                public String map(T bean) {
                    return bean.getLocalSystemId();
                }
            })).add(ArgArray.fill(Type.T_STRING_LIST, param,
                new Mapper<T, String>() {
                    @Override
                    public String map(T bean) {
                        return bean.getTitle();
                    }
            })).add(ArgArray.fill(Type.T_NUMBER_LIST, param,
                new Mapper<T, String>() {
                    @Override
                    public String map(T bean) throws MapperException {
                        try {
                            return bean.getApp() == null ? null :
                                branches.getCode(bean.getApp().getPkId());
                        }
                        catch (MapNotFoundException e) {
                            throw new MapperException(e.getMessage(), e);
                        }
                    }
            }));

Не знаю почему, но в последнее время с трудом представляю себе программирование без использования подобных конструкций.

Сохранение данных в таблицах БД


Не ко всем информационным системам существует API в виде хранимых процедур, иногда требуется писать в таблицы БД напрямую с помощью SQL-запросов. Казалось бы что может быть проще - нужно лишь получить соединение в DAO, создать PreparedStatement и выполнить запросы, подставив требуемые параметры. Плохо здесь только то, что все данные действия нужно будет повторять в каждом классе-запросе, для каждого сохраняемого типа данных.

Отделим как и в предыдущем примере формирование параметров от выполнения запросов. В одном классе мы будем работать с БД. Метод query данного класса будет принимать на вход текст SQL-запроса и массив параметров.


package ru.rt.integration.rms.services.connector.mdm.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;

import java.sql.SQLException;

import java.text.MessageFormat;

import javax.annotation.Resource;

import javax.sql.DataSource;

import org.apache.log4j.Logger;

import ru.rt.integration.rms.services.LoaderService;

public class OracleGenericSaveDao implements GenericSaveDao {

    private static final Logger logger = Logger
        .getLogger(OracleGenericSaveDao.class);

    private static final String PARAMS_IS_NULL_ERROR =
        "Query params should not be null";
    
    private static final String PARAMS_DEBUG_FMR =
        "index: {0}, value: {1}";

    private static final String DATASOURCE_IS_NULL =
        "The MDM DataSource is null, may be connection is not established " +
        "or broken";

    @Resource(name = "jdbc/mdm")
    private DataSource dataSource;


    public OracleGenericSaveDao() {
        super();
    }

    @Override
    public <T> void query(String query, Params params) throws DaoException {
        if (params == null)
            throw new DaoException(PARAMS_IS_NULL_ERROR);
        
        if (dataSource == null)
            throw new DaoException(DATASOURCE_IS_NULL);
        
        try (Connection conn = dataSource.getConnection();
            PreparedStatement pstmt = conn.prepareStatement(query)) {
            
            // выполняем запрос столько раз, сколько элементов в списке params            
            for (Object[] paramarray : params.params()) {
                // TODO пока считаем, что все изменения выполняются по принципу 
                // "все или ничего", хотя если DAO вызывается вне транзакции,
                // то изменения, выполненные до ошибки на N-й строчке будут
                // закоммичены в БД.
                for (int i = 0; i < paramarray.length; i++) {
                    logger.debug(MessageFormat.format(PARAMS_DEBUG_FMR, (i + 1), 
                        paramarray[i]));
                    pstmt.setObject(i + 1, paramarray[i]);
                }
                pstmt.executeUpdate();
                // готовим новое исполнение запроса
                pstmt.clearParameters();                            
            }
        }
        catch (Exception ex) {
            throw new DaoException(ex.getMessage(), ex);
        }
    }
}

Массив параметров икапсулирован в класс Params. Данный класс похож на ArgArray: он точно так же содержит метод fill, осуществляющий преобразование списка данных data в массив массивов объектов. Каждый элемент списка data пропускается через преобразователь Mapper (T -> Object[]).


package ru.rt.integration.rms.services.connector.mdm.dao;

import java.util.LinkedList;
import java.util.List;

/**
 * Представление массива параметров запроса на сохранение данных
 */
public class Params {

    private static final String DATA_OR_MAPPER_IS_NULL_ERROR = "Parameters " +
        "list or mapper should not be null";

    private List<Object[]> paramsList = new LinkedList<>();
    
    private Params() {
        super();
    }
    
    public List<Object[]> params() {
        return paramsList;
    }
    
    /**
     * Заполнение массива параметров из списка <code>data</code> типа
     * <code>&lt;T&gt;</code>. Для преобразование элемента списка в массив
     * объектов используется трансформатор 
     * {@link ru.rt.integration.rms.services.connector.mdm.dao.Params.Mapper}
     * @param <T> тип элементов преобразуемого списка
     * @param data список, преобразованными элементами которого заполняется 
     *      массив параметров
     * @param mapper преобразователь элемента списка в массив параметров
     * @return заполненный массив параметров
     * @throws IllegalArgumentException в случае передачи <code>null</code>
     *      в качестве одного из параметров метода
     */
    public static <T> Params fill(List<T> data, Mapper<T> mapper) {
        if (data == null || mapper == null)
            throw new IllegalArgumentException(DATA_OR_MAPPER_IS_NULL_ERROR);
        Params params = new Params();
        for (T bean : data) {
            params.paramsList.add(mapper.map(bean));
        }
        return params;
    }

    /**
     * Трансформатор для параметров типа <code>T</code> в массив объектов
     * @param <T> тип трансформируемого параметра
     */
    public static interface Mapper<T> {
        public Object[] map(T bean);
    }

}

Вернемся к разделению DAO на классы. Другими классами будут выступать наши реализации QueryObject'ов. Их задача - описать преобразователь из бина конкретного класса в массив объектов:


dao.query(EM_SAVE_QUERY, Params.fill(param, new Mapper<EntityMapping>() {
                @Override
                public Object[] map(EntityMapping em) {                
                    Object[] objects = new Object[12];
                    objects[0] = em.getAttributeEndDate();
                    objects[1] = new Timestamp(em.getAttributeStartDate().getTime());
                    objects[2] = ...
                    ...
                    return objects;
                }
            }));

Чтение коллекций объектов из БД


Последняя операция, которую необходимо реализовать в слое доступа к данным, - загрузка коллекций объектов из хранилища. При использовании JDBC самая не то чтобы трудоемкая, но муторная операция - обработка ResultSet'ов. Воспользуемся идеей из Spring Framework: класс для работы с БД после считывания данных пропускает полученный ResultSet через переданный в метод преобразователь. Задача преобразователя - построить java-объект по ResultSet'у.

Класс для работы с БД выглядит следующим образом:


package ru.rt.integration.rms.services.connector.mdm.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Resource;

import javax.sql.DataSource;

public class OracleGenericLoadDao implements GenericLoadDao {
    
    private static final String PARAMS_IS_NULL_ERROR =
        "Query params should not be null";

    private static final String DATASOURCE_IS_NULL =
        "The MDM DataSource is null, may be connection is not established or broken";

    @Resource(name = "jdbc/mdm")
    private DataSource dataSource;
    
    public OracleGenericLoadDao() {
        super();        
    }

    /**
     * {@inheritDoc}
     * Передаваемый <code>query</code> должен быть подготовлен к выполнению
     * через {@link java.sql.PreparedStatement}
     */
    @Override
    public <T> List<T> query(String query, Object[] params,
        int pageSize, RowMapper<T> mapper) throws DaoException {
        
        if (params == null)
            throw new DaoException(PARAMS_IS_NULL_ERROR);
        
        if (dataSource == null)
            throw new DaoException(DATASOURCE_IS_NULL);
        
        List<T> result = new ArrayList<>();
        
        try (Connection conn = dataSource.getConnection();
            PreparedStatement pstmt = conn.prepareStatement(query)) {
            pstmt.setFetchSize(pageSize);
            for (int i = 0; i < params.length; i++) {
                pstmt.setObject(i + 1, params[i]);
            }            

            int i = 0;
            try (ResultSet rs = pstmt.executeQuery()) {
                while(rs.next()) {
                    result.add(mapper.mapRow(rs, i++));                    
                }
            } catch (Exception ex) {
                throw new DaoException(ex.getMessage(), ex);
            }
        } catch (Exception ex) {
            throw new DaoException(ex.getMessage(), ex);
        }
        
        return result;
    }
}

Наиболее интересная для нас часть данного класса - тот участок, в котором каждая строчка ResultSet пропускается через преобразователь:


while(rs.next()) {
    result.add(mapper.mapRow(rs, i++));                    
}

Помимо преобразователя метод query DAO принимает на вход текст запроса, массив параметров и размер считываемой страницы:


return dao.query(QUERY, new Object[] {...}, pageSize, 
    new RowMapper<ClientType>() {
        @Override
        public ClientType mapRow(ResultSet rs, int rowNum) 
                throws SQLException {
            ClientType clientType = new ClientType();
            clientType.setLocalSystemId(rs.getString(8));
            clientType.setOwner(rs.getString(9));
            clientType.setTitle(rs.getString(3));

            ...

            return clientType;
        }
});

Уверен, что читателям, знакомым со Spring JDBC, данный код покажется тривиальным.

Выводы


Написать уровень доступа к данным без использования ORM можно, более того, в большинстве случаев это несложно. Часто встречаются ситуации, когда ORM не нужен и его использование объяснимо только тем, что разработчики не знают и не хотят знать SQL и особенности используемых СУБД. Для них ORM это не столько средство сокрытия работы с БД от остального кода приложения, сколько сокрытие собственной некомпетентности.

Наиболее характерными "без-ORM'ными" областями является аналитика (т.н. OLAP-приложения) и интеграция. Если требования к производительности высоки, данные логически слабосвязаны и мы работаем не с отдельными объектами, а в первую очередь с их коллекциями, то подключать к приложению и настраивать громоздкий ORM-фреймворк невыгодно.

Очень сильно помочь в разработке и самое главное - использовании - DAO, основанного на JDBC, могут элементы функционального программирования, даже такие примитивные, как в Java. Пока речь идет об анонимных классах. Их использование позволяет скрыть все обходы коллекций, формирования массивов и служебных структур внутри тех классов, в которые обычно не ступает IDE разработчика. Все, что нужно пользователю этого кода - это передать правильную реализацию Mapper'а, Translator'а, PropertyExtractor'а и т.д. Код получается более декларативным и на мой взгляд лучше читается. С приходом же Java 8 с ее анонимными функциями и "умными" коллекциями данный код еще и не будет смотреться несколько чужеродно с точки зрения среднестатистического Java-программиста.

P.S. Еще один пример реализации паттерна QueryObject, но на платформе .NET приведен в статье Dapper + QueryObject, как замена ORM.

Понравилось сообщение - подпишитесь на блог

13 комментариев:

Колесников Михаил aka Gloomy комментирует...

Очень полезная статья, спасибо! Я так и не осилил рефакторинг прошлого наследия и ограничился разделением дао на обособленные кусочки...

Ваша методика вполне поможет одному из восьми макрорегиональных филиалов ОАО "Ростелеком" :)

ilinchik комментирует...

На данный момент есть ли у вас какие-то результаты, можете сделать выводы о том правильно ли было отказаться от ORM? Действительно ли ваше решение выигрывает в плане производительности? ORM кроме того что освобождает от написания SQL это еще и кеширование, управление ресурсами, sequence preallocation, batch writing/fetching и прочие радости.

Так как в вашем проекте "ассоциации между объектами практически отсутствуют" мне кажется чистый JDBC не даст ощутимого преимущества так как и ORM не стал бы поднимать пол базы при каждом запросе.

Интересно ваше мнение, может быть со временем оно изменится, когда вы будете искать очередной connection leak :)

Pavel Samolisov комментирует...

Да, мои выводы сделаны не на пустом месте. Я сравнивал исполнение явных SQL-запросов через TopLink и напрямую через JDBC. Я лично видел, как адаптер OSB (в недрах которого лежит TopLink) не мог переварить несложный update с select-подзапросом. Пришлось разбивать на два запроса: выкачивать сначала объекты в память с, а затем делать update.

Что касается плюшек. Это конечно хорошо, но ORM это прежде всего мэпинг (собственно с этой целью такой класс фреймворков и разрабатывали). Т.е., если есть объекты даже без ассоциаций, но с огромным количеством полей (а такое бывает, вроде и БД у нас нормализована, но вот содержат сведения о клиенте 20-30 полей), то применение ORM может избавить от необходимости руками разбирать элементы ResultSet'а. Если же к такому количеству полей добавить еще и ассоциации, то применимость ORM становится очевидной. Я ни в коем случае не являюсь противником использования ORM, но и пихать его в любое приложение только потому что это модно спортивно молодежно, да еще так советует Фаулер или кто там сейчас в моде, не буду.

Что касается багов - вопрос философский. А вы уверены, что у всех реализаций ORM нет багов? JIRAHibernate или там OpenJPA никогда не смотрели? Я вот помню, как на WebSphere Application Server 7 боролись с багом OpenJPA, который вроде как уже отмечен исправленным, а все равно в новых версиях библиотеки воспроизводится. Сonnection Leak это еще довольно легко диагностируемая ошибка (но, согласен, не всегда легко исправляемая, хотя спасибо разработчикам Java 7 за помощь всем нам). Хотя вот интересно, подозрения на такие утечки я наблюдал как раз таки при использовании ORM. Впрочем, допускаю, что это у меня просто опыт такой, писал бы больше на голом JDBC, наблюдал бы больше в таких приложениях.

ilinchik комментирует...

В JIRA`y hibernate не заглядывал, но согласен что баги есть и в ORM и их диагностика и тем более исправление уже куда более сложная задача, чем поиск незакрытого connection`а в своем коде.
Спасибо, хорошая стать, интересный опыт. Как вывод можно сказать что не нужно бояться писать на чистом JDBC, ваше решение тому подтверждение.

Pavel Samolisov комментирует...

Справедливости ради, код выйдет в продакшн только через несколько месяцев. Пока есть лишь результаты тестов. Кстати, сегодня играл с Java 8, есть идеи по упрощению кода. В любом случае, если будет интересно, то напишу потом об опыте поддержки кода, взаимодействующего с БД без ORM.

Pavel Samolisov комментирует...

Возвращаясь к вопросу о багах, нет в жизни счастья. Connections leak парни не словили, но поймали Memory leak на JDBC-драйвере!.

marx комментирует...

Не сочтите за рекламу :) но можно посмотреть на мой велосипед. Описание - https://bitbucket.org/marx/libsql/wiki/UserGuide, код там же.

Когда то тоже устал от ORM-ов и реализовал QueryObject самостоятельно, правда с некоторой спецификой (подгрузка "основы" запроса из файла). Потом и RowMapper добавился, очень похожий на MyBatis и совместимый со Spring JDBC. Библиотека используется в паре крупных проектов в связке с Spring JDBC и без ORM.

Pavel Samolisov комментирует...

Выглядит довольно интересно, спасибо за ссылку.

Quester комментирует...

Спасибо за толковую статью и отличный пример.
P.S. Просто интересно, сколько времени (человеко часов, что ли) ушло на реализацию, от идеи до рабочего состояния?

Pavel Samolisov комментирует...

@Quester

Если говорить о прототипе, описанном в статье, то на его реализацию ушло порядка 2-х человеко/дней, от идеи до тестирования. Если говорить о реальном проекте, то там сейчас 52 класса объектов-запросов различной сложности: от самых простых, заполняющих 5 полей, до реально сложных, формирующих сущности с 40-60 полями. Основной код сосредоточен в реализациях интерфейса RowMapper. На написание всего приложения ушло порядка 2-х человеко-месяцев, но работа была потрачена не только на собственно разработку, но еще и на решение аналитических вопросов, согласование наборов полей и т.д. Чистое время разработки DAO и объектов-запросов посчитать не могу.

ilyas комментирует...

У нас тоже есть проекты, где необходимы вызовы "хранимок". Пишет их другая команда. Параметры в них могут быть сколь угодно сложными по вложенности, т.е. объекты БД table или object, которые содержат другие table или object, чтобы было удобно и быстро мы решили генерировать java-классы из объектов БД, а из хранимых процедур Spring JDBC-шные StoreProcedure. DAO слой так же генериться, методы получают названия одноименные хранимым процедурам, параметры для которых выше сгенерированные java-классы.

Pavel Samolisov комментирует...

Интересный экспириенс. А с помощью каких утилит/библиотек генерируете, если не секрет?

ilyas комментирует...

Мета-информацию по хранимым процедурам и их параметрам получаем через java.sql.DatabaseMetaData.
Дальнейшее создание java-классов из спортивного интереса пишется на scale.

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

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!