Извлечение данных


Каждому кто занимается промышленной разработкой кровавым энтерпрайзом не раз приходилось сталкиваться с написанием слоя работы с базой данных. С этим столкнулись и мы.


Наш проект построен на финском фреймворке Vaadin и чистым JDBC в основе слоя работы с базой данных. Без опыта работы с JDBC мы нагородили достаточно большой слой спагетти кода, а потом доблестно с ним разобрались.


О том как мы с этим боролись и какой велосипед изобрели под катом.



Что привело к такому решению


В Vaadin отображать данные в UI компоненты можно с помощью BeanItem Container, можно почитать здесь.


Коротко о предметной области

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


Сущности описывались с помощью спецификации Bean:


public class Element implements Serializable{
    private Integer id = 0;
    private String name = "";
    private Float price = 0.0F;

    // getter and setter for property
}

Контейнер для такого элемента создается просто:


BeanItemContainer<Element> container = new BeanItemContainer<>(Element.class);

И дальше этот контейнер подставляется в UI с помощью методов setContainerDataSource(...).


Чтобы получить этот контейнер, его нужно предварительно разобрать на экземпляры класса Element из ResultSet полученного с помощью запроса к базе данных.


Изначально решение получилось такого вида:


public Element(ResutlSet rs){
    try {
        id = rs.getInt("id");
    }catch(SqlException e){
        id = 0; 
        e.printStackTrace();
    }

    try {
        name = rs.getString("name");
    }catch(SqlException e){
        name = "";
        e.printStackTrace();
    }

    try {
        price = rs.getFloat("price");
    }catch(SqlException e){ 
        price = 0.0f;
        e.printStackTrace();
    }
}

И тут на меня должны быть направлены гневные взгляды гуру Java и карма полететь в минусы.
Но концепция такая: если во время разбора данных из ResultSet поле вызвало ошибку, то система не должна вывалиться с Exception и продолжить работать и записать логи ошибок (запись логов здесь и дальше описывать не будет тема для отдельной статьи).


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


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


Реализация


Используя паттерн Декоратор создали маленькую библиотеку Executor.


Эта библиотека расширяет функционал ResultSet, добавляя безопасные методы для получения данных.


public Integer getInt(String columnName) {
        Integer i = 0;
        try {
            i = rs.getInt(columnName);
        } catch (SQLException ignored) {
        }
        return i;
    }

 public Integer getIntNull(String columnName) {
        Integer i = 0;
        try {
            i = rs.getInt(columnName);
            if (rs.wasNull())
                return null;
        } catch (SQLException ignored) {
        }
        return i;
    }

    public String getString(String columnName) {
        String s = "";
        try {
            s = rs.getString(columnName);
            if (s == null)
                return "";
        } catch (SQLException ignored) {
        }
        return s;
    }

Таким образом 100% гарантия получения результата и работа без Exception при обработке данных, которые в Vaadin выглядят чуть-чуть пугающе.


И это в окошке пользователя


На вопрос, а если нужен Exception, ответ: в планах добавить конструктор для Executor с типом вызываемого Exception, потому что простой SqlExecption не информативен, и реализовать методы для корректной работы.


Планируется API следующего вида (UPD: внесены предложения из комментария):


public Executor(ResultSet rs, Class<? extends RuntimeException> exceptionClass){
    this.rs = rs;
    this.exceptionClass = exceptionClass;
}

public Integer getIntThrow(String columnName) {
        Integer i = 0;
        try {
            i = rs.getInt(columnName);
            if (rs.wasNull())
                return null;
        } catch (SQLException ex) {
            throw exceptionClass.newInstance();
        }
        return i;
    }

И вариант использования


public Element(ResutlSet rs){
    Executor ex = new Executor(rs, CreateElementException.class);
    id = ex.getIntThrow("id");
    name = ex.getStringThrow("name");
    price = ex.getFloatThrow("price");
}

private class CreateElementException extends RuntimeException{
    private String message = "";
    public CreateElementException(String message){
        this.message = message;
    }

    @Override
    public String getMessage(){
        return "Exception with access to column with name " + this.message;
    }
}

Примеры использования


Что получается с кодом, после использования данной библиотеки. Конструктор для класса Element изменился на следующий:


public Element(ResutlSet rs){
    Executor ex = new Executor(rs);
    id = ex.getInt("id");
    name = ex.getString("name");
    price = ex.getFloat("price");
}

В результате:


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

Заключение


Сделан еще один велосипед велосипеды нужно тоже уметь делать, который обеспечивает универсальный и безопасный доступ к получению данных из БД. Люди желающие воспользоваться библиотекой исходники добро пожаловать на GitHub. Хотелось бы получить оценку решения велосипеда и конструктивных предложений и замечаний.

Целесообразно ли такое решение?

Проголосовало 55 человек. Воздержалось 26 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Поделиться с друзьями
-->

Комментарии (13)


  1. terryP
    29.12.2016 17:17
    +1

    1. Если тесты настроены правильно, то намного лучше если упадет Exception в тестах, чем вдруг исчезнет значения полей в продакшене. Намного логичнее не съедать ошибки, а сделать unit тесты базы, которые при неверном типе колонки просто не дадут собрать билд. Естественно, unit тесты должны покрывать все сущности (например, банально обходит все DAO классы рефликсией и проверять что ни один метод не упадет на тестовой схеме).

    2. Exception дорогое удовольствие по производительности, почему не использовать getObject(int columnIndex), а потом уже в рантайме не определять какой тип вы получили? Зачем обязательно все делать через игнорирование SQLException?


    1. sah4ez32
      29.12.2016 18:16
      -1

      1. Unit тесты — это святое, но они не могут обеспечить проверку при упавшем подключении к БД. И unit тесты не должны проверять работу с базой данных. Ну в нашем случае у нас не было возможности подымать тестовые бд. Кровавый энтерпрайз он бывает разный.


      2. Встает вопрос как часто может выскочить SqlException. И было желание использовать рефлексию для определения колонки откуда брать данные. Но как предложили в [комментарии] (https://habrahabr.ru/post/318740/?mobile=no#comment_9989940) велосипед уже давно придумано и есть смысл посмотреть это решение.


      1. tigraboris
        30.12.2016 15:01

        Ой, а поясните почему юнит тесты не должны проверять работу с бд? например модуль, отвечающий за соединение с бд?


        1. sah4ez32
          30.12.2016 15:16

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


          Классический вариант юнит теста — это проверка работы всех методов класса, без работы с другими классами, а где надо обращение к другому классу, пишутся mock.


          Но после реакции на эту статью, я уже ни в чем не уверен.


  1. igor_suhorukov
    29.12.2016 17:51
    +1

    Можно упростить жизнь и избегать велосипедостроительства при работе с JDBC через Spring JdbcTemplate… Даже не нужен весь спринг, только этот модуль. Производительность будет та же, кода меньше…


  1. guai
    29.12.2016 18:06

    Вангую, везде будет 1 тип эксепшенов, какой-нибудь DataLayerException.
    Вы прячете исходный эксепшен от реального обработчика. Как он должен реагировать, если не получил инфы?
    Почему бы не делать throw new RuntimeException(sqlException)?
    И вообще, @SneakyThrows из ломбока в помощь.
    Опять же, зачем каст, если можно было объявить тип Class<? extends RuntimeException>?
    Что тут безопасного — тоже не понятно.


  1. gvassilev
    29.12.2016 19:50

    Где логи (смущает пустой catch), как узнать что именно пошло не так и где? Дублирование кода никак не «чистый код» (getInt и getIntNull). Дефолтное значние я бы передавал параметром и то, в отдельном методе.
    id = ex.getInt(«id»); с ID вообще зло, ибо последвстия мержа положат систему.


  1. evkin
    30.12.2016 00:08
    +1

    Как по мне проще сеттить поля в объект просто вызывая все методы подряд в одном блоке try-catch и если в каком то поле вылетает ошибка в catch её отлавливать, логгировать и возвращать новый пустой объект без инициализации любых полей (со значения по умолчанию), чем возвращать полуинициализированый обьект. если пойти дальше, то брать индексы, а лучше названия полей и положить их в аннотацию, которую будет парсить соответствующий обработчик (executor). А если идти еще дальше, то открыть для себя MyBatis и делать это через него вызывая слой дао в том же try-catch описанном выше))


  1. sah4ez32
    30.12.2016 10:33

    Из всех комментариев вижу лишь одно, нет какого-то одного общеизвестного решения. Поэтому и появляются такие велосипеды.


    1. terryP
      30.12.2016 20:11
      +1

      В Java ни в одной области нет одного общеизвестного решения, десятки вебфреймворков, десятки библиотек работы с JSON и т.д. И это хорошо, так как у каждого проекта свои задачи и цели. Есть огромное количество различных решений работы с БД от JPA и hibernate до jOOQ и Spring's JdbcTemplate. Писать свои велосипеды явно не стоит.


      1. sah4ez32
        30.12.2016 21:41

        Эх… и у в нашей Греции есть все… прочитай я Вашу статью раньше, может и не писал бы статью, и не делал велосипеда.


        Но проблема кровавого энтерпрайза фирм не занимающихся профильной разработкой ПО в том, что не всегда есть человек, который может направить на путь истинный. Порой простое решение кажется не подходящим из-за своей простоты. Закрадываются мысли, что только на таком велосипеде можно выехать из сложившейся ситуации.


  1. gkislin
    30.12.2016 14:20

    Вот вам пара ссылок, чтобы велосипедов не писать:
    Lightweight JDBC helper library
    Маппинг объектов в базы данных


    1. sah4ez32
      30.12.2016 15:25

      спасибо за ссылки. сейчас будут выходные, надо будет попробовать разные варианты.