java8. Пишем SQL запросы к java коллекциям. (часть I).

 

java8. Пишем SQL запросы к java коллекциям.  (часть I).

Рассматривается возможность реализации в java8 синтаксиса SQL запросов (Select) для работы с java коллекциями записей, имитирующими таблицы БД.

Позиционирование задачи

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

Основной вопрос статьи

Часто для проведения конкретных расчетов в java приложении, требуется только малая часть данных, загруженных из БД в мастер-детальные коллекции. Чтобы получить эти данные, необходимо организовывать многочисленные вложенные циклы на этих коллекциях, отсеивать ненужные записи и т. д. В результате код  получается громоздким. Альтернативой этому могли бы стать SQL запросы к коллекциям. Они имеют компактную и прозрачную форму записи.

Основной вопрос статьи – можно ли, на java8 обеспечить SQL-образный синтаксис для работы с коллекциями?

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

Многое зависит от знания программистом SQL. Язык SQL требует преодоления некоторого барьера для своего освоения, после чего работа с SQL становится ясной, простой и предпочтительной. И программист, владеющий SQL, вероятно предпочтет использовать SQL запросы к java-коллекциям, а не вложенные циклы, а не владеющий SQL выберет циклы.

Лирическое отступление. Сон первый. Богатые женятся на бедных, а бедные на богатых.

В стране к власти пришли демократы(большевики). Они сказали – разброс зарплат слишком большой – надо уровнять.

Имеется БД, в которой есть таблицы:

  • factories – фабрики и заводы страны
  • workers – рабочие и проч. и их зарплаты
  • family – семейный состав рабочих

Зарплаты должны быть скорректированы следующим образом:

  • максимальная зарплата не может быть больше Pmax.
  • минимальная зарплата не может быть меньше Pmin.
  • отношение Pmax/Pmin на предприятии не может быть больше Kmax.
  • сумма зарплат до корректировки должна быть равна сумме после корректировки для каждого предприятия. Если не получается, то излишки забираются в спецфонд или недостатки возмещаются из спецфонда.

Написать ПО корректировки поручено программисту Шарикову.

Да… подумал Шариков, проще всего было бы все реализовать на ORACLE – данные в таблицах, а алгоритм для пересчета на PLSQL. Под каждый алгоритм – один SQL запрос и делать больше нечего. Но демократы(большевики) объявили ORACLE буржуазной лжепрограммой. Алгоритм должен быть написан на HIBERNATE, а данные должны быть легко переносимы в любую БД, которую поддерживает HIBERNATE. И все должно работать быстро!

Алгоритм на java я напишу легко и он будет быстро работать, и данные в HIBERNATE в java коллекции загрузить легко, но загружаться данные будут долго! Логично и проще было бы загружать данные по каждому предприятию и делать по нему корректировку, но чтобы ускорить загрузку придется загружать сразу все данные (всю БД), а не по частям (загрузить из БД в приложение данные одним запросом быстрее, чем многими запросами по частям). Памяти хватит (память сейчас дешевая). Вот только после загрузки, чтобы найти нужные данные придется писать многочсленные вложенные циклы со сложными фильтрами. Работать это будет достаточно быстро, но код получится очень громоздкий и ошибки

Вот, если бы можно было писать к java коллекциям SQL запросы, как в БД к таблицам.

Если бы можно было писать к java коллекциям SQL запросы …

Если бы можно было писать к java коллекциям SQL запросы …

Если бы можно было писать к java коллекциям SQL запросы …

Если бы можно было писать к java коллекциям SQL запросы …

Конец сна.

 

Для кого эта статья.

Эта статья может быть интересна для программистов, использующих java8 и SQL.

Желательно, чтобы читатель был знаком:

  • с SQL запросами select
  • функциональным программированием java8, например, в объеме статьи “Лямбда-выражения в Java 8” (https://habrahabr.ru/post/224593/)
  • java8 Stream, например, в объеме статьи “Шпаргалка Java программиста 4. Java Stream API” (https://habrahabr.ru/company/luxoft/blog/270383/)

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

Что будем делать

  1. Будем формировать класс, поддерживающий синтаксис SQL запроса Select, последовательно добавляя в него такие опции как:
  • простые поля, поле *, калькулируемые поля, агрегируемые поля
  • distinct
  • where
  • order by
  • многоэтажные select
  • group by + having
  • union, minus, intersection
  1. Нас будет интересовать только синтаксис. Скорость работы и объем необходимой памяти не будут исследоваться в данной статье.
  2. Классы значений полей элементов коллекций могут быть только String, Integer, Long. Другие классы не включены, чтобы не перегружать статью подробностями.

Что не будем делать

  • Не будем загружать данные из БД в коллекции и вообще не будем как-либо использовать jdbc. Будем считать, что данные уже загружены.
  • Не будем создавать программный интерфейс для реализации SQL запросов типа insert, update, delete и для DDL.
  • Не будем исследовать работу в параллельном режиме.
  • В данной, первой части статьи мы ограничимся только запросами к одной коллекции (то есть во from будет только одна коллекция). Создание синтаксиса для многих коллекций предполагается осуществить во второй части статьи.

Два основных нововведения java8

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

Функциональное программирование

Функциональное программирование (ФП) предполагает возможность работы с функцией (блоком кода) как единицей программы. Код функции можно, например, присвоить переменной или передать ее в качестве значения входного параметры при вызове метода.

Во многих языках такая возможность присутствует. Допустим в PLSQL (языке БД ORACLE) есть возможность записать код в виде строки и потом выполнить (execute immediate) в нужном месте программы и … получить ошибку, что-то вроде ”колонки не существует”.

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

В java нет таких программных единиц – функция, в java есть только объекты (еще есть примитивы, массивы, классы и интерфейсы).

Между тем, необходимость ФП, в java всегда присутствовала (и была реализована). Представим себе, что надо написать на java визуальный интерфейс, с использованием объектов класса Button. При нажатии мышкой на кнопке – объекте класса Button, должен быть выполнен некий код. Но где задать этот код? В java нет самостоятельной программной единицы – блок кода или функция. Единственным носителем функции (метода) является класс. Объекты только пользуются методами класса. Получается, чтобы написать новый метод надо создать новый класс, у него переопределить метод и в нем написать необходимый блок кода. Кроме того, попутно придется создать объект нового класса. Если надо, чтобы при нажатии на другую кнопку выполнился другой код, мы опять должны создать новый класс и новый объект класса, если нам надо, чтобы код выполнился при двойном клике, то опять надо создавать новый класс и новый объект.

Аналогичными задачами являются выполнение по таймеру и запуск задачи в новом потоке.

Хорошо, еще, что в java есть внутренние классы и упрощенный интерфейс для реализации этих   задач. Однако, все равно, код выглядит очень громоздко.

Что же дает java8 для ФП. По сути ничего нового. По-прежнему, надо создавать новый класс и новый объект. Но разработчики java, как бы говорят – Вам нужна возможность задавать блок кода. Хорошо, мы дадим такой синтаксис, что Вы будете писать только блок кода и список параметров, а все остальное (создание нового класса и объекта) будет автоматически выполнено за сценой.

Такой синтаксис назван лямбда выражением (далее просто лямбда).

Лямбда состоит из трех частей:

  • Списка параметров в скобках
  • Разделителя ->
  • Блока кода

Представим некую функцию, которую мы бы хотели реализовать для ФП.

При написании ее в java программе как лямбда:

  1. Название функции (myFunc) отбрасывается, так как нет особой необходимости в ее использовании
  2. Список параметров с указанием типов превращается в первую часть лямбды, при этом типы переменных, как правило опускаются, так как компилятор java сам может их вычислить
  3. Добавляется значок ->
  4. Блок кода становится третьей частью лямбда – блоком кода лямбды

В java реализовано строгое типизирование переменных. Если лямбду можно присвоить переменной или передать ее в качестве значения параметра при вызове метода, то какого типа должна быть эта переменная и параметр?

Ну точно не String! Мы об этом уже говорили.

Может быть Object?

Пробуем

Получаем ошибку компилирования:

Оказывается, лямбда имеет тип – интерфейс java. К этому интерфейсу правда предъявляется требование – в нем должен быть только один абстрактный метод. Это сделано, что избежать необходимости указывать в лямбда какой метод надо переопределять.

Называется такой интерфейс – функциональным интерфейсом.

Правильно будет написать

Откуда же берутся функциональные интерфейсы? Ответ:

  • Около 50 функциональных интерфейсов в новом пакете java.util.function.
  • Некоторые из уже имевшихся интерфейсов в стандартных библиотеках java являются функциональными интерфейсами, так как у них только один метод.
  • Можно писать самостоятельно функциональные интерфейсы.

Необходимо отметить, что лямбда должна соответствовать функциональному интерфейсу, по списку параметров и типам этих параметров (поэтому в списке параметров лямбды можно опускать типы).

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

Stream

Слово Stream часто употреблялось и ранее в контексте java, однако, Stream который ввело java8 – это совсем новое понятие.

Stream – это альтернатива использованию циклов в java.

Лирическое отступление. Сон второй. Граждане, сдавайте валюту!

Демократы(большевики) объявили использование goto и циклов – буржуазными пережитками и потребовали их срочного и решительного искоренения во всех программах.

Да… подумал Шариков. Ну, положим, goto я никогда и не использовал и сам бы гнал таких программистов, которые используют goto. Но без циклов то никуда не денешься, а если злопыхатели, которые меня подсиживают найдут в коде цикл, то … шутки с демократами(большевиками) плохи.

Первый пункт инструкции по искоренению циклов гласил:

Цикл типа for( int i=0; i<5; i++) заменяется на Strream.of( 0,1,2,3,4 ).

Хм… подумал Шариков – это удобно, и так можно же по любому множеству пробежаться, например Stream.of( ‘Иванов’, ‘Петров’, ‘Сидоров’ ). Но чаще нужны циклы, когда количество элементов заранее не известно.

Второй пункт гласил:

Цикл типа for( int i=0; i<K; i++) заменяется на Strream.iterate( 0, i->i+1 ).limit(K)

Ну что ж – это съедобно. Указываем начальное значение, приращение в виде лямбды и количество элементов через limit.

Далее следовало:

Если надо перебрать элементы коллекции, то пишем list.stream()

Если надо перебрать элементы массива, то пишем Stream.of(array)

Если надо перебрать строки текстового файла, то пишем Files.lines(путь к файлу)

Если надо перебрать …

Если надо перебрать …

Если надо перебрать …

Каждый должен сказать себе – я больше не пользуюсь циклами …

Каждый должен сказать себе – я больше не пользуюсь циклами …

Каждый должен сказать себе – я больше не пользуюсь циклами …

Каждый должен сказать себе – я больше не пользуюсь циклами …

Каждый должен сказать себе – я больше не пользуюсь циклами …

Каждый должен сказать себе – я больше не пользуюсь циклами …

Что-то не так подумал Шариков…Его взгляд выхвадил из кода строку

Ааааааа… я забыл добавить limit(3).

Шариков добавил

и проснулся в холодном поту.

Железный конь (Stream) идет на смену крестьянской лошадке (циклу)!

В java и других языках ООП считается признаком дурного тона использования оператора GOTO, даже если реализована возможность его использования (в java GOTO реализован частично). Теперь можно сказать, что в java к GOTO, в этом смысле добавятся циклы. То-есть использование циклов становится некуртуазным. Это основано на том что:

  • Stream может заменить любой цикл
  • Использование Stream вместо циклов делает код более компактным и читаемым, особенно в случаем сложных циклов, мнократной вложенности
  • Как утверждается Stream обеспечивают более быструю работу
  • Stream работает принципиально по другой методике (по сравнению  с циклом)
  • Stream, для своего освоения, требует определенных усилий и отказ от использования циклов увеличит опыт использования Stream. То-есть, для более быстрого освоения Stream надо сказать себе – Я больше не использую циклы!

Понятно, что далеко не все разделят мнение о том, что надо отказаться от использования циклов. Надо освоить Stream и увидеть его возможности, а освоение Stream требует усилий. Здесь все также как с SQL, если Вы знакомы с SQL, то будете предпочитать работать с ним везде, а если нет, то везде будете выбирать циклы.

Stream можно создавать  на основе коллекций, наборов значений, массивов, строчек файла, массива chars[] строки (String), задания последовательности генерации элементов и др.

Функционирование Stream состоит из двух этапов:

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

  • peek() – позволяет определить действие с каждым элементом Stream
  • map() – позволяет определить преобразование каждого элемента Stream, в результате чего должен получиться Stream с преобразованными элементами
  • filter() – позволяет определить фильтрацию элементов Stream

Следует отметить, что никакие действия с элементами Stream на этом этапе не производятся, а только осуществляется фиксирование инструкций, указываемых при вызове методов. Инструкции представляют собой лямбды.

Формирование инструкций не обязательно происходит в одном месте программы (как скажем это приходится делать при использовании циклов). Добавление инструкций к Stream может быть размещено во многих местах программы. Это возможно потому, что инструкции  Stream представляют лямбды – программный код отложенного действия.

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

  • forEach() – терминальный аналог метода peek() (Stream в этом случае не возвращает результата)
  • reduce()– формирует простую конструкцию для получения результата работы Stream
  • collect() – формирует сложную конструкцию для получения результата работы Stream

Stream, таким образом, может не возвращать результата, а только производить какие-то действия при обработке элементов Stream, имеющимися в нем инструкциями, а может производить действия и также формировать результирующий объект – число, строку, коллекцию и вообще объект любой сложности.

Повторная реализация Stream, а также добавление инструкций к реализованному Stream невозможны.

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

Впрочем есть инструкции (например, инструкция сортировки – sorted(Comparator<? super T> comparator)), которые нарушают это правило, так как не могут иначе работать.

 

Основные моменты, касающиеся SQL

  1. SQL – это реализация реляционной теории.
  2. Краеугольным камнем реляционной теории и SQL является, то что основным объектом приложения действий является таблица и результат действия (запроса select) является также таблица. Это позволяет организовывать цепочки запросов и используя особые правила переставлять запросы местами (или заменять их на эквивалентные), так что общий результат не изменяется. Это делает возможным оптимизацию выполнения запросов по времени выполнения и требуемого для выполнения объема памяти.
  3. В рассматриваемой программе  для выполнения запроса select будет формироваться Stream, состоящий из инструкций (лямбд). Эти инструкции, казалось бы, можно было менять местами для упомянутой в предыдущем пункте оптимизации выполнения запроса. Однако, для оптимизации необходимо знать  содержание лямбд, а сделать это в java невозможно. Рассмотрим данный аспект подробнее. Представим себе следующий код:

Вы глубоко заблуждаетесь, если думаете, что мы получим следующий вывод

Вместо кода лямбды мы увидим некое автоматически сгенерированное легкозапоминаемое имя. Код же лямбды мы никак получить не можем, ведь это просто код одного из методов класса и в java не предусмотрено методик получения кода (java текста) метода. Это очень печально, так как мы не можем:

  • дать хорошей диагностики ошибки (при выполнении s->s+”qw” произошла ошибка), а только можем сообщить, что где-то там произошла ошибка такая-то.
  • провести оптимизацию Stream – переставление местами составляющих его инструкций, на основе их содержимого, так как нам это содержимое не известно.
  1. В данной статье результат выполнения select будем называть курсором, чтобы отличать его от исходной коллекции, которая будет указываться во from. Хотя курсор, конечно, также представляет собой коллекцию.
  2. Элемент коллекции или курсора представляет собой аналог записи таблицы и состоит из набора значений полей. Поля курсора указываются в select() и они бывают:
  • Простые и *. Простые поля – представляют значения полей исходной коллекции. Символ * подразумевает, что он будет заменен в курсоре на все поля исходной коллекции
  • Калькулируемые – значения этих полей вычисляются на основе выражений, используя значения других полей записи исходной коллекции
  • Агрегатные – значения этих полей вычисляются при группировки записей исходной коллекции

 

Как будем работать

 

Сразу скажем о том, что у нас не получится в java.

В plsql языке ORACLE select записывается на “чистом полотне”:

Как видим здесь нет каких-то кавычек или других символов обозначающих начало или конец запроса или строки запроса.

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

Такое написание текста запроса сразу проигрывает в читабельности “чистому полотну”.

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

Мы будем записывать запрос как цепочку вызовов методов.

 

Задача будет реализована в четыре шага.

Первый шаг

Сначала реализуем запрос

Для поддержки запроса создается сопровождающий объект класса OneTableXXXXXXXX. Список имен в select передается как массив String[]. Вообще говоря, нам надо будет также предоставить возможность указания алиаса поля, отделенного от имени поля через < as > или через пробелы или вообще без указания алиаса (по умолчанию тогда алиас будет совпадать с именем поля). Кроме того, мы должны реализовать возможность раскрытия символа <*>, означающего все поля. Надо поднимать ошибку если имя алиаса неуникально.

Для нас важно, чтобы select * from возвращал List<Record>. Тогда полученный результат можно будет использовать снова во from и создавать многоэтажные запросы.

Значения полей курсора, мы получим по имени поля из объекта типа Record исходной коллекции.

Кроме того, на этом шаге формирования программы мы рассмотрим различные утилиты.

Первый шаг представлен в классе OneTableBegin.

Второй шаг

Далее мы должны реализовать калькулируемые поля. Что, собственно, надо сделать?

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

Но как это сделать?!

Записать выражение калькулируемого поля в String и потом парсить – это работа огромной сложности. Такую работу можно выполнить за 1000 и одну ночь и первый релиз можно выпустить под названием “1000  и один баг” 🙂 .

Но оказывается, у java8 есть подходящее решение – лямбда, мы должны его только пристроить в нужное место!

Необходимо передать в метод select() лямбду, и выполнить эту лямбду на значениях каждого record исходной коллекции. Мы напишем что-то вроде:

где r.asInt(“rating”) – возвращает целое значение поля rating в записи r.

Для каждого элемента коллекции в Stream (то есть для каждой записи) мы выполним переданную в select() лямбду калькулируемого поля и получим значение поля для записи курсора.

Реализация калькулируемых полей с использованием любой функциональности имеющейся в java нам далась без всяких усилий.

Что касается синтаксиса, то он конечно, получится сложнее, чем в SQL. SQL занимается самостоятельно приведением типов, а в java мы вынуждены это прописывать сами.

Второй шаг представлен в классе OneTableCalcField.

Третий шаг

Далее реализация where, distinct, order by, многоэтажных запросов.

Реализации where. Здесь все аналогично калькулируемым полям. Мы должны передать в where код лямбды предиката. Выполнение этой лямбды на данных записи коллекции вернет true или false и покажет надо ли запись включать в курсор или нет.

Реализация distinct. В java8 Stream уже реализована данная функциональность. Однако, необходимо переопределить методы hashCode() и equals(), иначе distinct будет проводить сравнение записей как объектов типа Object, а нам необходимо сравнивать значения полей в записях. Переопределение этих двух методов произведено в классе record.

Реализация оrder by. Сортировка уже реализована в java8 Stream и нам остается только определить Comparator, для сравнения записей на основе значений указанных для сортировки полей.

Реализация многоэтажных запросы – у нас эта функциональность заложена с самого начала и здесь просто демонстрируется.

Третий шаг представлен в классе OneTableWhere

Четвертый шаг

Далее реализация group by, having, union, minus, intersection

Реализация GROUP By. Давайте посмотрим, что нам надо сделать:

  • имеется коллекция записей типа record, допустим 100 записей
  • группируем записи по provider_city. Допустим у нас в 100 записях 10 различных значений provider_city, получим 10 групп
  • к каждой группе будет привязано определенное число записей (в них provider_city одинаковый). В сумме количество записей во всех группах будет 100, но в каждой группе количество свое.
  • Теперь мы можем посчитать сколько в группе записей или какой средний рейтинг (provider_rating) в записях группы или какой минимальный и максимальный рейтинг. То-есть мы можем вычислить значения агрегатных полей. Кроме того, мы можем реализвать having для фильтрации групп на основе полей группировки (в данном случае одно поле provider_city) и агрегируемых полей.
  • После этого, мы формируем объект класса Record (запрос же должен вернуть курсор записей типа Record) на основе каждой группы. В этом объекте будут поля группировки и агрегатные поля.

Последний шаг – реализация UNION, INTERSECTION и MINUS.

Как все уже догадались, в java8 Stream вся перечисленная функциональность реализована и надо только как в конструкторе соединить имеющиеся элементы.

Четвертый шаг представлен в классе OneTableGroupBy

Основные и вспомогательные классы

Библиотека (пакет) SQLLib содержит в себе четыре основных классов – OneTableBegin, OneTableCalc, OneTableWhere и  OneTableGroupBy. Эти классы представлены для пошагового добавления функциональности запросов select. При этом, последующий класс включает в себя функциональность предыдущего.

Кроме того, SQLLib имеет три вспомогательных класса:

  • Field
  • TableFields
  • Record

Также для демонстрационных целей в пакете main имеются классы для демонстрационных целей:

  • OneTableTest
  • Provider
  • DB

 

Класс Field

Аналогом объекта данного класса в SQL является тип колонки таблицы.

В данном классе:

  • fieldName – это имя колонки
  • cls – отражает java класс значения поля .

Класс TableFields

Объект данного класса содержит список полей (List<Field>) записи коллекции, или возвращаемого курсора (который также является коллекцией) и соответствует шапке таблицы в SQL.

В этом классе представлены несколько методов:

  • для поиска поля по имени
  • для поиска имени поля по индексу
  • для поиска индекса поля по имени
  • для поиска класса значения поля по имени
  • для получения строкового представления шапки коллекции.

 

Класс Record

Аналогом объекта данного класса в SQL является одна запись коллекции и возвращаемого курсора.  Этот класс является родительским для элементов коллекции, которая может использоваться во from.

В данном классе:

  • recordFields – это ссылка на список полей (шапку) коллекции или курсора
  • массив fieldsValues – содержит значения полей записи
  • Имеются методы для получения по имени поля его значения, приведенного к Object, String, Integer, Long.
  • Имеются методы для сравнения записи с другой записью, по значению полей и для сравнения значений поля в двух записях
  • Имеются методы для получения hash кода записи и hash кода значения поля

 

Класс Provider

Этот класс расширяет класс Record и предназначен как демонстрационный, для использования во всех примерах from.

Большая часть этого класса помечена как экпериментальная часть функционала. Она нужна для демонстрация использования select(), с указанием  полей, через функциональные ссылки (см. ниже).

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

При этом, для каждой записи Record, необходимо указывать ссылку на шапку записи. Это делается в классе DB.

Класс DB

Этот класс также демонстрационный и организует наполнение данными коллекции Provider.

 

Реализация запроса select * from <одна коллекция > (OneTableBegin).

 

Здесь использованы как простейшие методы Stream – map, iterate, peek, filter, forEach, так и такие сложные как   collect и Collector.

Метод select:

  • имеет модификатор static и всегда стоит первым в цепочке вызовов.
  • создает объект класса OneTableBegin для поддержки выполнения запроса
  • начинает формирование текстового представления запроса в SQLText
  • фиксирует список имен полей, переданных при вызове метода
  • при trace==true логирует список имен полей формируемого курсора

Первый (основной) вариант select имеет входной параметр – массив String, в котором каждое поле указывается как <имя поля><пробелы><as><пробелы><имя алиаса>. Вспомогательное слово as, а также <имя алиаса> могут опускаться.

Если указан символ *, то он заменяется на все поля коллекции.

Поскольку коллекция в select еще не известна, а будет указана во from, то  здесь только фиксируется список имен полей, а создание полей (объектов Field) и раскрытие символа *, переносится на завершающий этап в метод setSelectFieldsAliases(  );.

Stream в методе  используется для получения из массива названий полей строки, в которой поля перечислены через запятую

  • Stream.of( fields ) – создается Stream с инструкцией по превращению массива String[] в Stream<String>. Аналог этого действия можно написать через цикл:

Но данный Stream.of( fields ) пока еще не выполняет никакой операции, даже не перебирает элементы массива, все это только в проекте и будет выполнено когда к Stream подключат инструкцию, которая предполагает реализацию Stream (терминальную инструкцию).

  • .collect(Collectors.joining( “, “)) – инcтрукция собирает строки в одну строку, разделяя их запятой и реализует Stream, так как collect это терминальная инструкция. Вообще говоря, Stream может как выполнять некоторые действия, перебирая элементы, так и возвращать результат. В данном случае Stream возвращает строчку, но может возвращать и другие объекты – коллекции, числа и др.. Механизм получения результата определяется, в данном случае методом collect и передаваемым в него Collector. Этот коллектор создается вызовом Collectors.joining( “, “), который имеется в стандарной библиотеке java, но можно написать и свой Collector и мы обсудим это чуть позже.

Второй вариант select (далее не поддерживаемый) получает на входе массив лямбд и указывать их можно в особой форме  записи лямбды – ссылке на метод.  Реализация этого механизма требует наличия в объекте коллекций (Provider) статических (в данной реализации) методов для каждого поля типа getProvider_id(). В этом случае поле указывается как <имя класса в коллекции><::><имя статического метода>, возвращающего значение поля в записи коллекции. Например

Select(Provider::getProvider_id) from lM, где Provider – класс объектов коллекции lM.

Такой синтаксис весьма похож на систаксис SQL запроса select <алиас><.><имя поля>. Проблема может возникнуть если во from использовать несколько раз одну и ту же коллекцию  с разными алиасами. У них один класс, а надо как-то указать от какой коллекции поле. Кроме того, необходимо как-то указывать алиас поля (в примере используется автоматическое создание алиасов поля).

Метод from

  • Фиксируется коллекция, переданная при вызове метода
  • Логируется информация о переданной коллекции
  • Инициируется создание cursorStream. Первая инструкция – это перебор всех записей коллекции, указанной во from

Метод setSQLName

Приписывает запросу имя которое используется только для трассировки, чтобы понимать к чему относится информационное сообщение.

Метод getCursor

Как уже упоминалось, запрос (Select) должен возвращать коллекцию. Необходимо реализовать набор инструкций, собранных в Stream cursorStream. Для этого используется терминальная инструкция метода collect(Collectors.toList()).

Предварительно в Stream добавляются все отложенные инструкции formCorsorStream( );

Метод formCursorStream

В методе formCursorStream:

  • В cursorStream добавляется новая инструкция по преобразованию (RecordFactory) записей коллекции from, в записи курсора (создаются новые объекты), на основании списка полей курсора.
  • Затем вызывается метод setSelectFieldsAliases, где происходит формирование полей курсора.

Интересно, что для указания преобразования записей в методе RecordFactory используется еще не сформированный список полей  selectFieldsAliases.fields. Это оказывается возможным, потому что cursorStream еще не реализован и только наполняется инструкциями, в которых используется ссылка на инициализированную (но не заполненную) коллекцию selectFieldsAliases.fields. Важно наполнить эту коллекцию (делается в методе setSelectFieldsAliases) перед реализацией cursorStream.

Метод setSelectFieldsAliases

Метод setSelectFieldsAliases

  • Организует Stream по перебору всех имен полей указанных в select():
  • Если в имени указан символ <*>, то заменяем его на поля объекта, используемого в коллекции, указанной во from. Каждое поле добавляем в список selectFieldsAliases.fields, а в связанный список lambda добавляем зависимость (<поле><лямбда для вычисления значения поля>). Лямбда, фактически представляет собой получение по имени поля значения из массива полей записи Record fieldsValues.
  • Если указано имя поля (с алиасом или без), то отделяем имя поля и имя алиаса и создаем поле курсора с именем алиаса и с лямбда, использующей имя поля.
  • Проводится проверка на уникальность имен (в selectFieldsAliases.fields). Для этого, получаем множество полей курсора

Проводим группировку по именам полей. Эта операция полностью аналогична группировке (group by) в select SQL. Она выполняется с помощью инструкции Stream

и отфильтровываем вхождения, в которых коллекция состоит только из одной записи (то-есть отфильтровываем уникальные)

Прошедшие фильтр (неуникальные) поля курсора объединяем в одну строчку

Если длина полученной строки больше нуля, то поднимаем ошибку “В курсоре задублированы имена алиасов”+<список задублированных полей>.

Метод RecordFactory

В cursorStream добавляется новая инструкция по преобразованию (RecordFactory) записей коллекции from, в записи курсора (создаются новые объекты), на основании списка полей курсора.

Метод RecordFactory организует Stream, аналогичный  for( int i=0; i< fieldCount ; i++ )

И для каждого поля курсора выполняет лямбду для вычисления значения поля в данной записи курсора

После заполнения массива данных полей курсора формируется новая запись курсора.

Метод printCursor

Данный метод, в зависимости от значения входного параметра может печатать результат запроса:

  1. в виде таблицы (приятный вывод)
  2. в виде <имя поля1>=<значение поля1>; … <имя поляn>=<значение поляn> для каждой записи курсора (простой вывод)

Рассмотрим сначала второй вариант, как более простой:

Производится окончательное оформление Stream tableStream

formCorsorStream ( );

Затем выводится каждая запись на консоль, для этого используется метод Record.toString()

System.out::println – это запись лямбды record -> System.out.println(record), через ссылку на метод.

 

Теперь рассмотрим вариант вывода курсора в виде таблицы.

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

Вывод курсора в виде таблицы, таким образом, складывается из следующих этапов:

  1. Реализация Stream и получение курсора (List<Record>)
  2. Инициация массива ширины колонок, значениями ширины имен полей

Здесь интересна терминальная инструкция toArray(Integer[]::new). Она создает новый массив типа Integer и заполняет его значениями, которые ему передает Stream.

  1. Коррекция ширины колонок исходя из значений полей во всех записях курсора. При переборе полей курсора мы организуем Stream по номеру полей (а не Stream по полям – элементам списка r.recordFields.fields), так как нам надо обращаться и к массиву ширины полей и списку полей курсора и мы для этого используем общий индекс i – maxSize[i] и r.recordFields.fields.get(i).

Создание формата вывода записи курсора. Здесь мы используем инструкцию reduce. Это терминальная инструкция и она возвращает некоторое значение (в данном случае типа String). В инструкцию передаются два параметра – начальное значение строки (в данном случае “|”) и Function которая определяет прибавление на каждом шаге – это будет формат вывода значения поля. В данном случае (s,s1)->s+”%-“+ s1+”.”+ s1+”s | “, здесь s – это результирующий элемент, s1 – очередной элемент Stream. В результате получается формат для вывода строки на консоль типа “| %-<ширина колонки1>.<ширина колонки1>s | %-<ширина колонки2>.<ширина колонки2>s |\n”

  1. Формирование горизонтальной линии разделителя между записями курсора. В принципе здесь также можно было обойтись инструкцией reduce. Но мы используем более сложную инструкцию – collect, передав в нее свой Collector. Если простейшие инструкции Stream – map, peek, forEach, filter достаточно легко осваиваются новичками, то рассказ о Collector может ввести в ступор. Между тем, ничего сложного здесь нет. Необходимо понять следующее – Stream может возвращать значение, а может не возвращать значения (скажем если терминальной операцией является forEach). Одной из терминальных инструкций, возвращающей значение, является инструкция collect, в которую передается объект Collector. Но какое значение возвратит Stream? Это может быть объект любого типа – число, строка, коллекция – что угодно. Что именно – определит объект типа Collector. Объект должен указать 4 действия:
    1. что делать перед началом перебора элементов Stream (создаем объект StringBuilder – StringBuilder::new)
    2. что делать на каждом шаге перебора Stream (формируем добавок в созданный объект класса StringBuilder )
    3. (необходим только для параллельной работы Stream) Как объединить результаты работы каждого потока Stream в единый результат(работа в параллельном режиме не предусмотрена, поэтому генерим ошибку).
    4. Что делать после перебора элементов Stream (окончательное преобразование результата – в нашем случае StringBuilder::toString)
  2. Вывод шапки таблицы и тела таблицы (курсора)

Остальные части класса OneTableBegin не представляют интереса с точки зрения Stream и  SQL и  поэтому здесь подробно не описаны

Резюме по данному пункту.

В рамках задачи данного пункта синтаксис, разрабатываемый в java для коллекций, практически совпадает с синтаксисом SQL запросов для таблиц БД

Java запрос

SQL запрос

По данному пункту соответствие синтаксиса java и sql можно оценить как 5 с минусом.

 

Добавляем калькулируемые поля (OneTableCalc)

Перечисление FieldType

Данное перечисление имеет значения Simple (простое поле) и Calc (калькулируемое поле). В дальнейшем будет добавлен еще тип Agregate (агрегатное поле).

Внутренний статический класс FieldU

Данный класс используется для поддержки лямбд разного типа:

  • для расчета значения поля записи простого и для расчета значения поля записи калькулируемого типа
  • в дальнейшем, для расчета значения поля записи агрегируемого типа

В классе имеются поля:

Field f – поле записи

FieldType fieldType – тип поля – простое или калькулируемое

Function<? extends Record, T> lambda;  – лямбда для расчета поля простого типа и поля калькулируемого типа.

 Метод select()

Поскольку теперь поля могут быть как простые, так и калькулируемые, то входной параметр теперь массив не String[], а Object[] и элементы  в нем могут быть как  String, так и FieldU.

В диагностике и текстовом представлении запроса SQLText для элементов входного параметра типа String и FieldU мы иначе определяем имя поля.

Метод setSelectFieldsAliases( )

В связи с тем, что в select() передается теперь не массив строк, а массив Object[], то надо выявить класс элемента массива (это может быть String или FieldU) и для каждого класса методика создания и учета поля и лямбды для вычисления значения отличаются.

Метод RecordFactory()

Значение поля вычисляется в зависимости от типа – простое или калькулируемое. Для простых полей  используется лямбда, которая определяет значение поля в записи по его имени, для калькулируемых – лямбда калькуляции, указанная при вызове select().

Методы calcLong(), calcInt(), calcStr() и simple()

Первые три метода предназначены для более удобного синтаксиса задания калькулируемого поля при вызове select(). Они получают  на входе лямбду калькулируемого поля и имя алиаса и возвращают объект класса FieldU.

Метод simple() для общности интерфейса, он предназначен для задания простого поля и возвращает String с именем поля и алиасом. Вместо него можно использовать просто значение типа String, в формате <имя поля>< as ><имя алиаса>.

Данные методы статические. Напомним, что в java 8 статические методы можно вызывать (в других классах) без указания класса, если класс указан в import с опцией static. Например,

import static SQLLib.OneTableCalc.*;

Резюме по данному пункту.

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

В рамках задачи данного пункта синтаксис, разрабатываемый в java для коллекций, сложнее синтаксиса SQL запросов для таблиц БД. Усложнение заключается в следующем:

  • Необходимо использовать метод CalcXXX и передавать в него лямбду и алаиас ( в ORACLE надо только указать выражение и алиас можно опустить)
  • Лямбда имеет начальные r -> (в ORACLE этого нет)
  • Необходимо явно указывать преобразования типов (в ORACLE это делается автоматически)
  • Поле указывается как r.asXXX( <имя поля> ) ( в ORACLE только имя поля)

Java запрос

 SQL запрос

По данному пункту соответствие синтаксиса java и sql можно оценить как 4 с минусом.

 Реализуем where, distinct, order by, многоэтажные запросы (OneTableWhere)

Метод where()

В where() мы передаем лямбду типа Predicate<Record>.  Здесь все как при реализации калькулируемых полей. Мы можем использовать любую функциональность java в where, при этом не прикладывая никаких усилий для реализации этой возможности.  Синтаксис весьма близок к синтаксису SQL. Вместо and используем &&, вместо or используем ||. Также имеется возможность использования скобок.

Выражения в where для java будут более громоздкими, за счет необходимости явного приведения типов и  использования конструкций типа r.asInt(<имя поля>), вместо просто <имя поля>.

Метод orderBy()

В orderBy() мы только фиксируем строку полей (иногда с признаком desc), перечисленных через запятую.  Реализуем эту строку мы в методе getCursor(). Наша задача создать Comparator, сравнивающий строки по значениям указанных полей (иногда с признаком desc). Для этого, мы сначала создаем Stream Comparator попроще, которые сравнивают записи (Record) на основании одного поля. Для этого, используем  метод класса Record compareTo(String fieldName, Record r). После этого, все Comparator объединяем в один, с помощью инструкции Stream reduce. Полученный Comparator добавляем в cursorStream как еще одну инструкцию.

Метод distinct()

В distinct() мы только фиксируем необходимость применения опции distinct, а реализуем ее в formCorsorStream(), добавив в cursorStream инструкцию distict(). Как видим, в Stream  distinct реализован, однако есть ньюанас. Distinct будет сравнивать записи используя методы hashCode()  и equals(Object obj). Если в Record не переопределить эти методы, то они будут браться от Object и будут сравниваться ссылки на Record, которые, понятно, у всех объектов коллекции будут скорее всего разные. Это не то сравнение, которое нас интересует. Нам надо сравнение записей по значениям полей. Поэтому в Record вышеупомянутые методы переопределены.

Многоэтажные запросы и подзапросы

Что касается многоэтажных запросов, то для этого ничего дополнительного не надо делать, см. запрос с именем  <многоэтажный select> в классе OneTableWhere.

Также можно, применять подзапросы в where, см. запрос именем  <Запрос с использованием в where подзапроса>.

Резюме по данному пункту.

Добавление возможности использования в select where, distinct, order by, многоэтажных запросов не потребовало серьезных усложнений программы, так в java8 вся необходимая функциональность уже реализована и мы имеем дело с некиим конструктором.

В части where синтаксис, разрабатываемый в java для коллекций, сложнее синтаксиса SQL запросов для таблиц БД. Усложнение заключается в следующем:

  • Лямбда имеет начальные r -> (в ORACLE этого нет)
  • Необходимо явно указывать преобразования типов (в ORACLE это делается автоматически)
  • Поле указывается как r.asXXX( <имя поля> ) ( в ORACLE только имя поля)

В части distinct, order by, многоэтажных запросов синтаксис в java практически совпадает с синтаксисом SQL.

Java запрос

 SQL запрос

По данному пункту соответствие синтаксиса java и sql можно оценить как 4 с минусом.

 

Добавляем group by, агрегатные поля, having, minus, union, intersection (OneTableGroupBy)

 

Метод groupBy()

В этот метод мы передаем список имен полей, по которым будут сгруппированы записи коллекций.

У нас имеется коллекция lM<Record>. В записях коллекции record имеются поля, по которым мы хотим сгруппировать. Таким образом, нам надо получить Map<Record, List<Record>>. Первый Record, упомянутый в Map (ключ) – это record, имеющий поля по которым происходит группировка. Значение  List<Record> – это записи изначальной коллекции lM, в которых значения полей группировки одинаковое и соответствуют значению полей в record ключе.

Задача эта уже реализована в java8 Stream. Необходимо использовать метод collect( Collectors.groupingBy( лямбда ключа )). Лямбда ключа – это создание record на основе полей группировки.

Сначала мы создаем шапку полей на основе полей группировки, так как для создания record на основе полей группировки нам нужна шапка.

Затем на основе this.cursorStream мы создаем this.groupByStream.

Мы не можем продолжить строительство cursorStream и вынуждены его реализовать. В результате группировки cursorStream tableStream будет реализован в объект класса Map, и мы дальнейшие действия (having и агрегированные поля) будем реализовывать с Stream<Map.Entry<Record,List<Record>>> groupByStream. Однако, на заключительном этапе (в методе formCorsorStream()) мы опять перейдем  к tableStream<Record>.

Методы агрегирования sum(), count(), max(), min()

При вызове этих методов указываются имя поля агрегирования и  алиас поля.

Методы применяются к полученному при группировке Stream<Map.Entry<Record,List<Record>>>. Для каждого входжения (Entry) считается сумма (если мы говорим о методе sum()) по всем записям List<Record>.

Каждый метод имеет свою лямбду для вычисления значения поля на основании записей в List<Record>. Эта лямбда реализуется при переходе от записей исходной коллекции (указанной во from) к записи курсора, а также при вычислении значения предиката, указанного в having().

Методы предназначены для передачи их в вызов метода select() и в предикате при вызове having() (см. ниже).

Метод having()

Having – это аналог where с тем лишь отличием, что в предикате having могут использоваться поля группировки и агрегатные поля. В нашем случае, с помощью статических методов sum, count, max, min, реализованы соответствующие агрегатные поля.

Метод minus()

Эта опция реализована в SQL примерно так

У нас синтаксис другой – minus( a, b ).

В minus передаются OneTableGroupBy otFrom и OneTableGroupBy otMinus.

Допустим otFrom может быть select( “*”).from( a ), а otMinus select( “*”).from( a ).where( p ).

В результате должен получиться Stream, который при реализации даст записи, которые есть в otFrom, но которых нет в otMinus.

Сначала прийдется реализовать otMinus и получить коллекцию List<Integer>

Затем в otFrom.cursorStream добавляем distinct() и filter() с предикатом, который проверяет наличие элемента otFrom.cursorStream в списке, который дал otMinus.

В результате действий метода получаем модифицированный, но не реализованный otFrom.cursorStream!

Stream otMinus.cursorStream мы реализовали и больше им не сможем воспользоваться, а Stream otFrom.cursorStream еще не реализован терминальной функцией и мы можем продолжать им пользоваться!

Метод intersection()

Здесь все как в minus(), только в фильтре используется другой предикат

 В результате действий метода получаем модифицированный, но не реализованный otFrom.cursorStream!

Метод union()

В этот метод может быть передано не два, а массив объектов OneTableGroupBy, результаты, которых нужно соединить.

Для этого, используем инструкцию reduce() и Stream.concat(o0, o1). Последний метод возвращает нереализованный Stream() (не список List!).

В результате действий метода получаем модифицированный, но не реализованный ot[0].cursorStream!

Резюме по данному пункту

Добавление возможности использования в select group by, having, агрегированных полей, minus, union, intersection  не потребовало серьезных усложнений программы. Практически все методы имеются в стандартной библиотеке java8.

В части having и  агрегированных полей синтаксис, разрабатываемый в java для коллекций, сложнее синтаксиса SQL запросов для таблиц БД. Усложнение заключается в следующем:

  • Для указания агрегатного поля в select надо вызывать метод и указывать в нем имя поля и алиас
  • Лямбда имеет начальные r -> (в ORACLE этого нет)
  • Необходимо явно указывать преобразования типов (в ORACLE это делается автоматически)
  • Поле указывается как r.asXXX( <имя поля> ) ( в ORACLE только имя поля)

В части group by синтаксис в java практически совпадает с синтаксисом SQL.

В части minus, union, intersection синтаксис, хоть и отличается, но это не критично.

Java запрос

 SQL запрос

по данному пункту соответствие синтаксиса java и sql можно оценить как 4 с минусом.

 

Примеры

В финальном классе OneTableGroupBy реализованы все опции запросов, которые мы планировали реализовать. Данный класс, вместе с классами Field, TableFields, Record составляет некую библиотеку для обеспечения возможности написания SQL запросов select к java коллекции. Все эти классы размещены в пакете SQLLib.

В пакете main размещены классы Provider и DB, которые были  описаны ранее и класс OneTableTest. Эти классы предназначены для демонстрации написания запросов к java коллекции.

Вы можете скачать все эти классы и попробовать самостоятельно писать запросы.

Скачать “StreamSQL” StreamSQL.zip – Загружено 330 раз – 46 КБ

Необходимо разархивировать файл StreamSQL.zip в директорию, на которую смотрит workspace Eclipse и после этого File->Import->General->Existance  Projects into Workspace->выбрать директорию StreamSQL.

Напомним основные требования к демонстрационным классам.

  1. Элементами коллекции, должны быть классы, расширяющие класс Record и имеющие конструктор

(см. например Provider)

Cписок полей – Integer provider_id, String provider_name, Integer provider_rating, String provider_city – конечно же может быть другим.

  1. При создании нового объекта коллекции, надо использовать вышеуказанный конструктор, с указанием на предварительно созданный объект класса TableFields, как это сделано в классе DB

 

  1. Необходима версия java не ниже 1.8.

Финальное резюме

  1. В java8 в стандартных библиотеках уже реализована вся функциональность, необходимая для реализации SQL запросов к java-коллекциям.

В данной статье, разработана программа, позволяющая использовать синтаксис SQL запросов select к java коллекциям и которая несет в себе лишь функции некоего конструктора, соединяющего элементы этой функциональности, в соответствии с тем, что указывается при вызове методов  select(), from(), where(), orderBy(), groupBy и др.

При этом, реализуется практически полный функционал SQL запросов (в данной части статьи во from только 1 коллекция) select.

  1. Синтаксис, реализованный в java несколько сложнее, чем синтаксис SQL запросов select. Эти отличия в основном определяются тем, что вместо имени поля используется метод, в который передается имя поля и необходимостью явного приведения типов. Однако, представляется, что эти отличия приемлемы.
  2. Реализация SQL запросов в java имеет, системный недостаток – использование лямбд не дает возможности анализировать их содержание и проводить на основе такого анализа оптимизацию выполнения запросов.

Литература

 

  • Лямбда-выражения в Java 8 (https://habrahabr.ru/post/224593/)
  • Шпаргалка Java программиста 4. Java Stream API (https://habrahabr.ru/company/luxoft/blog/270383/)
  • Мартин Грубер. Понимание SQL.
  • Крис Дж. Дейт. SQL и реляционная теория. Как грамотно писать код на SQL

 

 

 

 

 

 

 

  

Добавить комментарий