Статьи

Java 8 пятница: Java 8 революционизирует доступ к базе данных

В Data Geekery мы любим Java. И так как мы действительно входим в свободный API jOOQ и запросы DSL , мы абсолютно взволнованы тем, что Java 8 принесет в нашу экосистему. Для нашей серии Java 8 для нас большая честь принимать очень важную гостевую статью доктора Минг-Йи Иу .

Д-р Минг-Йи Ю получил степень доктора философии по запросам к базам данных на Java в EPFL . Он создал проект с открытым исходным кодом Jinq, чтобы продемонстрировать некоторые новые методы поддержки запросов к базе данных в Java.

Наша редакционная заметка:

С тех пор, как Эрик Мейер ввел LINQ в экосистему .NET, нам, Java-пользователям, стало интересно, можем ли мы иметь то же самое. Мы уже писали об этой теме пару раз:

Хотя большинство API LINQesque в экосистеме Java работают как внутренние доменно-ориентированные языки, такие как jOOQ , некоторые пытаются решить проблему интеграции на уровне байт-кода, например JaQu .

JINQ формализует преобразования байт-кода во время выполнения с помощью того, что доктор Минг-Йиу называет символическим выполнением . Мы находим это очень интересным, и нам интересно, стоит ли нам начинать создавать JINQ-провайдер JINQ-to-JOOQ, где выразительная мощь Java 8 Streams API может сочетаться с нашими великолепными функциями стандартизации и преобразования SQL… ?

Убедите себя:

Java 8 Goodie: Java 8 революционизирует доступ к базе данных

Java 8 наконец-то здесь! После многих лет ожидания программисты на Java наконец получат поддержку функционального программирования на Java. Поддержка функционального программирования помогает оптимизировать существующий код, предоставляя новые мощные возможности для языка Java. Одна из областей, которая будет нарушена этими новыми функциями, — это то, как программисты работают с базами данных в Java. Поддержка функционального программирования открывает новые захватывающие возможности для более простых, но более мощных API баз данных. Java 8 обеспечит новые способы доступа к базам данных, которые конкурируют с базами данных других языков программирования, таких как LINQ в C #.

Функциональный способ работы с данными

Java 8 не только добавляет функциональную поддержку к языку Java, но и расширяет классы коллекции Java новыми функциональными способами работы с данными. Традиционно работа с большими объемами данных в Java требует большого количества циклов и итераторов.

Например, предположим, что у вас есть коллекция объектов Customer :

1
Collection<Customer> customers;

Если бы вас интересовали только клиенты из Бельгии, вам пришлось бы перебирать всех клиентов и сохранять тех, кого вы хотели.

1
2
3
4
5
Collection<Customer> belgians = new ArrayList<>();
for (Customer c : customers) {
    if (c.getCountry().equals("Belgium"))
        belgians.add(c);
}

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

С Java 8 вы можете написать один и тот же код в одну строку. Благодаря поддержке функционального программирования Java 8 позволяет написать функцию, в которой говорится, какие клиенты вас интересуют (из Бельгии), а затем фильтровать коллекции с помощью этой функции. Java 8 имеет новый API-интерфейс Streams, который позволяет вам это делать.

1
2
3
customers.stream().filter(
    c -> c.getCountry().equals("Belgium")
);

Мало того, что версия Java 8 кода короче, но и код легче понять. Там практически нет шаблонов. Код вызывает метод filter() , поэтому ясно, что этот код используется для фильтрации клиентов. Вам не нужно тратить время на расшифровку кода в цикле, чтобы понять, что он делает со своими данными.

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

1
2
3
customers.parallelStream().filter(
    c -> c.getCountry().equals("Belgium")
);

Еще более увлекательно то, что этот функциональный стиль кода работает и с базами данных!

Функциональный способ работы с базами данных

Традиционно программистам приходилось использовать специальные языки запросов к базам данных для доступа к данным в базах данных. Например, ниже приведен код JDBC для поиска всех клиентов из Бельгии:

1
2
3
4
5
6
PreparedStatement s = con.prepareStatement(
      "SELECT * "
    + "FROM Customer C "
    + "WHERE C.Country = ? ");
s.setString(1, "Belgium");
ResultSet rs = s.executeQuery();

Большая часть кода представлена ​​в виде строки, которую компилятор не может проверить на наличие ошибок и которая может привести к проблемам с безопасностью из-за небрежного кодирования. Существует также много стандартного кода, который делает написание кода доступа к базе данных довольно утомительным. Такие инструменты, как jOOQ, решают проблему проверки ошибок и безопасности, предоставляя язык запросов к базе данных, который может быть написан с использованием специальных библиотек Java. Или вы можете использовать такие инструменты, как объектно-реляционные средства отображения, чтобы скрыть много скучного кода базы данных для общих шаблонов доступа, но если вам нужно написать нетривиальные запросы к базе данных, вам все равно придется снова использовать специальный язык запросов к базе данных.

В Java 8 можно писать запросы к базе данных, используя тот же функциональный стиль, который используется при работе с Streams API. Например, Jinq — это проект с открытым исходным кодом, который исследует, как будущие API баз данных могут использовать функциональное программирование. Вот запрос к базе данных, написанный с использованием Jinq:

1
2
3
customers.where(
    c -> c.getCountry().equals("Belgium")
);

Этот код практически идентичен коду, использующему Streams API. Фактически, будущие версии Jinq позволят вам писать запросы напрямую, используя Streams API. Когда код будет запущен, Jinq автоматически переведет код в запрос к базе данных, как и запрос JDBC, показанный ранее.

Таким образом, без необходимости изучать новый язык запросов к базе данных, вы можете создавать эффективные запросы к базе данных. Вы можете использовать тот же стиль кода, который вы использовали бы для коллекций Java. Вам также не нужен специальный компилятор Java или виртуальная машина. Весь этот код компилируется и выполняется с использованием обычного Java 8 JDK. Если в вашем коде есть ошибки, компилятор найдет их и сообщит вам о них, как обычный код Java.

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

1
2
3
4
5
6
7
8
customers
    .where( c -> c.getCountry().equals("Belgium") )
    .where( c -> {
        if (c.getSalary() < 100000)
            return c.getSalary() < c.getDebt();
        else
            return c.getSalary() < 2 * c.getDebt();
        } );

Как видите, поддержка функционального программирования в Java 8 хорошо подходит для написания запросов к базе данных. Запросы компактны, и сложные запросы поддерживаются.

Внутренние работы

Но как это все работает? Как обычный компилятор Java может транслировать код Java в запросы к базе данных? Есть ли что-то особенное в Java 8, что делает это возможным?

Ключом к поддержке этих новых API-интерфейсов баз данных функционального стиля является тип анализа байт-кода, называемый символьным выполнением. Хотя ваш код компилируется обычным компилятором Java и выполняется на обычной виртуальной машине Java, Jinq может анализировать ваш скомпилированный код Java, когда он выполняется, и создавать запросы к базе данных из них. Символьное выполнение лучше всего работает при анализе небольших функций, которые часто встречаются при использовании Java 8 Streams API.

Самый простой способ понять, как работает это символическое выполнение, — на примере. Давайте рассмотрим, как Jinq преобразует следующий запрос в язык запросов SQL:

1
2
customers
    .where( c -> c.getCountry().equals("Belgium") )

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

1
2
SELECT *
  FROM Customers C

Затем вызывается метод where() и ему передается функция. В этом методе where() Jinq открывает файл .class функции и получает скомпилированный байт-код для функции, которую нужно проанализировать. В этом примере, вместо использования реального байт-кода, давайте просто использовать несколько простых инструкций для представления байт-кода функции:

  1. d = c.getCountry ()
  2. е = «Бельгия»
  3. e = d.equals (e)
  4. вернуть е

Здесь мы притворяемся, что функция была скомпилирована компилятором Java в четыре инструкции. Это то, что видит Jinq, когда вызывается метод where() . Как Jinq может понять этот код?

Jinq анализирует код, выполняя его. Jinq не запускает код напрямую, хотя. Он запускает код «абстрактно». Вместо того чтобы использовать реальные переменные и реальные значения, Jinq использует символы для представления всех значений при выполнении кода. Вот почему анализ называется символическим исполнением .

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

Пример символического исполнения

Пример символического исполнения

На диаграмме видно, как после выполнения первой инструкции Jinq обнаруживает два побочных эффекта: переменная d изменилась и был вызван метод Customer.getCountry() . При символическом выполнении переменной d не присваивается действительное значение, например «США» или «Дания». Ему присваивается символическое значение c.getCountry() .

После того, как все инструкции были выполнены символически, Jinq сокращает побочные эффекты. Поскольку переменные d и e являются локальными переменными, любые изменения в них отбрасываются после выхода из функции, поэтому эти побочные эффекты можно игнорировать. Jinq также знает, что методы Customer.getCountry() и String.equals() не изменяют никакие переменные и не показывают никакого вывода, поэтому эти вызовы методов также можно игнорировать. Исходя из этого, Jinq может заключить, что выполнение функции производит только один эффект: она возвращает c.getCountry().equals("Belgium") .

Как только Jinq понял, что функция, переданная ему в методе where() , он может объединить эти знания с запросом к базе данных, лежащим в основе коллекции customers для создания нового запроса к базе данных.

Генерация запроса к базе данных

Генерация запроса к базе данных

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

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

Захватывающее будущее

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

  • Чтобы поиграть с прототипом для этих новых типов запросов, вы можете посетить http://www.jinq.org